Visual LabINTERACTIVE LEARNING
OpenGL 与 3D第 5 / 5 章

曲线路径与相机矩阵

用贝塞尔曲线组织路径,再从线性代数理解视图矩阵。

Three.jsUV相机
专题预计 46 分钟查看源文 ↗

贝塞尔曲线路径绘制

使用贝塞尔曲线绘制有宽度的路面

 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就是把世界空间的坐标转换到相机空间中去,那就是视图矩阵