OpenGL 与 3D第 5 / 5 章
曲线路径与相机矩阵
用贝塞尔曲线组织路径,再从线性代数理解视图矩阵。
贝塞尔曲线路径绘制
使用贝塞尔曲线绘制有宽度的路面
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
let scene, camera, renderer, orbitControls;
let curvePath = new THREE.CurvePath();
let roadMesh;
let pointsGroup = new THREE.Group();
let transformControl;
const pointArr = []
const cubeArr =[]
const controlArr= []
const ROAD_WIDTH = 5;
const SMOOTHNESS = 20;
const POINT_SIZE = 0.5;
const CONTROL_POINT_OFFSET = 0.4;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, 50, 50);
camera.lookAt(0, 0, 0);
orbitControls = new OrbitControls(camera, renderer.domElement);
transformControl = new TransformControls(camera, renderer.domElement);
transformControl.addEventListener('dragging-changed', function (event) {
orbitControls.enabled = !event.value;
});
transformControl.addEventListener('objectChange', updateRoadFromPoints);
scene.add(transformControl);
const gridHelper = new THREE.GridHelper(100, 100);
scene.add(gridHelper);
scene.add(pointsGroup);
renderer.domElement.addEventListener('click', onMouseClick, false);
animate();
}
function onMouseClick(event) {
if (transformControl.object) return; // Don't add new points if we're moving one
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
addPointToRoad(intersectionPoint);
}
function addPointToRoad(point) {
pointArr.push(point) // 添加点
const cube = createPointCube(point);
pointsGroup.add(cube);
if(pointArr.length > 1){
const lastPoint = pointArr[pointArr.length-2]
const midPoint = new THREE.Vector3().addVectors(lastPoint, point).multiplyScalar(0.5);
curvePath.add(new THREE.QuadraticBezierCurve3(lastPoint, midPoint, point));
const newControlCube = createPointCube(midPoint, 0x00ff00);
controlArr.push(newControlCube)
pointsGroup.add(newControlCube);
if (curvePath.curves.length > 1) {
// 计算上一条曲线的控制点的偏移
const lastCurve = curvePath.curves[curvePath.curves.length - 2];
const tangent = lastCurve.getTangent(0.5)
const normal = new THREE.Vector3().crossVectors( new THREE.Vector3(0,1,0),tangent ).normalize()
// Calculate the direction and normal
const direction = new THREE.Vector3().subVectors(point, lastPoint).normalize()
const dot = direction.dot(normal)
if(dot > 0){
normal.negate()
}
// const normal = new THREE.Vector3(direction.z, 0, direction.x);
// Calculate control points
const distance = lastPoint.distanceTo(point);
const offset = normal.multiplyScalar(distance * CONTROL_POINT_OFFSET);
const lastControlPoint = new THREE.Vector3().addVectors(lastCurve.v1, offset);
// Adjust the last curve's control point
if (lastCurve instanceof THREE.QuadraticBezierCurve3) {
lastCurve.v1.copy(lastControlPoint);
updateControlPointVisual(curvePath.curves.length - 2);
}
}
}
updateRoadGeometry();
}
function createPointCube(position, color = 0xff0000) {
const geometry = new THREE.BoxGeometry(POINT_SIZE, POINT_SIZE, POINT_SIZE);
const material = new THREE.MeshBasicMaterial({ color: color });
const cube = new THREE.Mesh(geometry, material);
cube.position.copy(position);
cube.userData.isControlPoint = color === 0x00ff00;
cube.addEventListener('click', function(event) {
event.stopPropagation();
transformControl.attach(this);
scene.add(transformControl);
});
return cube;
}
function updateControlPointVisual(curveIndex) {
const curve = curvePath.curves[curveIndex];
if (curve instanceof THREE.QuadraticBezierCurve3) {
const controlPoint = controlArr[curveIndex]
if (controlPoint && controlPoint.userData.isControlPoint) {
controlPoint.position.copy(curve.v1);
}
}
}
function updateRoadFromPoints() {
curvePath = new THREE.CurvePath();
const points = pointsGroup.children;
for (let i = 0; i < points.length - 1; i += 2) {
const startPoint = points[i].position;
const endPoint = points[i + 2] ? points[i + 2].position : startPoint;
const controlPoint = points[i + 1].position;
curvePath.add(new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint));
}
updateRoadGeometry();
}
function updateRoadGeometry() {
if (roadMesh) {
scene.remove(roadMesh);
}
const roadGeometry = new THREE.BufferGeometry();
const positions = [];
const normals = [];
const uvs = [];
const points = curvePath.getPoints(SMOOTHNESS * curvePath.curves.length);
for (let i = 0; i < points.length; i++) {
const current = points[i];
const next = points[Math.min(i + 1, points.length - 1)];
const tangent = new THREE.Vector3().subVectors(next, current).normalize();
tangent.y = 0;
const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
const leftEdge = new THREE.Vector3().addVectors(current, normal.clone().multiplyScalar(ROAD_WIDTH / 2));
const rightEdge = new THREE.Vector3().addVectors(current, normal.clone().multiplyScalar(-ROAD_WIDTH / 2));
positions.push(leftEdge.x, leftEdge.y, leftEdge.z);
positions.push(rightEdge.x, rightEdge.y, rightEdge.z);
normals.push(0, 1, 0, 0, 1, 0);
uvs.push(0, i / (points.length - 1));
uvs.push(1, i / (points.length - 1));
}
const indices = [];
for (let i = 0; i < points.length - 1; i++) {
const baseIndex = i * 2;
indices.push(baseIndex, baseIndex + 2, baseIndex + 1);
indices.push(baseIndex + 2, baseIndex + 3, baseIndex + 1);
}
roadGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
roadGeometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
roadGeometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
roadGeometry.setIndex(indices);
const roadMaterial = new THREE.MeshBasicMaterial({ color: 0x808080 });
roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
scene.add(roadMesh);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
init();
为什么相机的matrixWorldInverse 就是视图矩阵 view matrix?
因为相机(或者任何物体的)的matrixWorld都是把本地空间的坐标转换到世界空间,其逆矩阵就是把世界空间的坐标转换到本地空间中去,所以具体到相机, matrixWorldInverse就是把世界空间的坐标转换到相机空间中去,那就是视图矩阵