基于three.js的三维空间曲线轨迹运动

引言

我们在做项目的时候,有时候会遇到物体或者相机需要做复杂轨迹运动的情况,往往没法简单的通过修改位置来达成我们想要的运动效果。
这时候可以通过引入多段曲线去拟合我们想要的运动轨迹,再获取曲线的参数去控制物体做相应轨迹的运动。

目录

  • 1、创建关键空间点数组
  • 2、根据点数组绘制曲线
  • 3、获取曲线上特定位置的点,修改物体位置
  • 4、获取曲线上特定位置的切线,修改物体朝向
  • 5、随时间实时改变物体位置和朝向
  • 6、添加修改曲线功能
  • 7、引入模型模拟应用场景

1、创建关键空间点数组

首先我们可以先找出运动轨迹上几个特定的点。
假设给定的点是(1,1,-1),(1,0,1),(-1,0,1),(-1,0,-1)
这里在每个点放了一个实体方块用于示意点的位置,同时为后面的调整功能做准备

        const initialPoints = [
            { x: 1, y: 1, z: -1 },
            { x: 1, y: 0, z: 1 },
            { x: -1, y: 0, z: 1 },
            { x: -1, y: 0, z: -1 }
        ];

        const addCube = (pos) => {
            const geometry = new THREE.BoxBufferGeometry(0.1, 0.1, 0.1);
            const material = new THREE.MeshBasicMaterial(0xffffff);
            const cube = new THREE.Mesh(geometry, material);
            cube.position.copy(pos);
            scene.add(cube);
        }

        const cubeList = initialPoints.map(pos => {
            return this.addCube(pos);
        });
绘制曲线的关键空间点

2、根据点数组绘制曲线

three.js 提供了好几种方法绘制曲线,这里采用的是 CatmullRom 插值的方法绘制曲线。

CatmullRom 插值的曲线一定会经过所有给定的点,所以这种方法会更适合用作轨迹曲线的绘制。

        const curve = new THREE.CatmullRomCurve3(
            cubeList.map((cube) => cube.position) // 直接绑定方块的position以便后续用方块调整曲线
        );
        curve.curveType = 'chordal'; // 曲线类型
        curve.closed = true; // 曲线是否闭合

        const points = curve.getPoints(50); // 50等分获取曲线点数组
        const line = new THREE.LineLoop(
            new THREE.BufferGeometry().setFromPoints(points),
            new THREE.LineBasicMaterial({ color: 0x00ff00 })
        ); // 绘制实体线条,仅用于示意曲线,后面的向量线条同理,相关代码就省略了

        scene.add(line);
绘制曲线

3、获取曲线上特定位置的点,修改物体位置

有了曲线之后,可以通过 getPointAt 函数获取曲线上特定位置的点向量,然后复制给物体的 position

        function changePosition (t) {
            const position = curve.getPointAt(t); // t: 当前点在线条上的位置百分比,后面计算
            mesh.position.copy(position);
        }

为了直观表现下图采用 30 等分取点把位置向量绘制出来了,后面的图片也采用一样的方式展现向量


获取曲线上的点向量

4、获取曲线上特定位置的切线,修改物体朝向

现在物体的位置对上了,但是朝向却是固定的,不符合生活经验。一般来说物体在运动的时候,正面总是朝向轨迹的切线方向的。

现在我们通过 getTangentAt 函数获取曲线上特定位置的切线向量,根据该切线向量和点的位置向量计算物体朝向的点向量,传入物体的 lookAt 函数

        function changeLookAt (t) {
            const tangent = curve.getTangentAt(t);
            const lookAtVec = tangent.add(position); // 位置向量和切线向量相加即为所需朝向的点向量
            mesh.lookAt(lookAtVec);
        }
获取切线向量(黄色线条)

注意上图示的切线(黄线)实际起点为原点(0,0,0),这里为了示意切线在曲线上的位置,平移到了点所在位置上

向量相加得到朝向的点向量(蓝色线条)

因为 lookAt 实际上是指向某个点向量,如果直接传切线向量会导致物体朝向下图 A 点,需要和位置向量相加后才能得到所需的点向量(蓝线)即 C 点

image.png

5、随时间实时改变物体位置和朝向

现在轨迹上单一点的位置和朝向都可以获取到了,剩下的就是在渲染函数中实时修改了。

根据时间计算当前点在曲线上的位置百分比,传入第 3、4 步中

        const loopTime = 10 * 1000; // loopTime: 循环一圈的时间

        // 在渲染函数中获取当前时间
        const render = () => {
            let time = Date.now();
            let t = (time % loopTime) / loopTime; // 计算当前时间进度百分比

            changePosition(t);
            changeLookAt(t);

            requestAnimationFrame(render);
            renderer.render(scene, camera);
        }

        requestAnimationFrame(render);
物体曲线轨迹运动

相机曲线轨迹运动

6、添加修改曲线功能

到这里曲线运动的效果是做出来了,但是如果我们想调整曲线,就得修改最初的点数组,既不直观也很繁琐。

参考 three.js 官网的 demo 发现可以通过 TransformControls 控制方块位置,实时修改曲线。同时因为前面的 curve 是通过方块的 position 生成的,所以方块位置的修改可以直接反映到 curve 上

        import { TransformControls } from 'TransformControls.js'; // 引入模块

        const control = new TransformControls(camera, renderer.domElement);

        // 获取点击位置
        const mouse = new THREE.Vector2();
        renderer.domElement.addEventListener(
            'click',
            (event) => {
                mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
            },
            false
        );

        // 方块点击检测
        const rayCaster = new THREE.Raycaster();
        rayCaster.setFromCamera(mouse, camera);
        const intersects = rayCaster.intersectObjects(cubeList);
        if (intersects.length) {
            const target = intersects[0].object;
            control.attach(target); // 绑定controls和方块
            scene.add(control);
        }

        // 修改曲线后同步修改实体线条
        control.addEventListener('dragging-changed', (event) => {
            if (!event.value) {
                const points = curve.getPoints(50);
                line.geometry.setFromPoints(points);
            }
        });
实时修改曲线

7、引入模型模拟应用场景

经过前面的步骤现在有了一个比较抽象的场景,现在可以考虑通过模型让应用场景更具象化。这里采用和场景契合度较高的过山车模型。

车模型的处理方式和方块基本没区别这里就不放相关代码了,轨道是通过一小段的轨道模型不断重复的方式去模拟。

        // 轨道分段数
        let railNum = 50;

        // 导入模型
        const loader = new GLTFLoader().setPath('model/');
        loader.load('scene.gltf', (gltf) => {
            // 轨道容器
            const railway = new THREE.Object3D();
            let position = new THREE.Vector3();
            let tangent = new THREE.Vector3();

            for (let i = 0; i < railNum; i++) {
                // 复制多段轨道模型
                let model = gltf.scene.clone();
                railway.add(model);
                
                // 这里和前面一样通过获取位置和切线向量去计算每段轨道的朝向
                position = curve.getPointAt(i / railNum);
                tangent = curve.getTangentAt(i / railNum);
                model.position.copy(position);
                model.lookAt(tangent.add(position));
            }
            scene.add(railway);
        });
导入模型

不过这样的方式相当于用多段直线拼出来的曲线,整体会比较生硬。如果把曲线调整的过长也会出现轨道接不上的问题。

three.js 官网的 examples 里有一个过山车 demo,轨道不是使用模型而是通过代码建模去模拟轨道,效果会自然很多。详见:https://threejs.org/examples/?q=roller#webxr_vr_rollercoaster

Demo 地址

http://demo.treedom.cn/threejs.curve_animation.wyl/

参考

https://threejs.org/examples/?q=curve#webgl_modifier_curve

https://threejs.org/examples/?q=spli#webgl_geometry_extrude_splines

了解更多

原文来源: 基于three.js的三维空间曲线轨迹运动

你可能感兴趣的:(基于three.js的三维空间曲线轨迹运动)