基于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的三维空间曲线轨迹运动

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Threejs 为什么? webGL太难用,太复杂! 但是现代浏览器都支持 WebGL 这样我们就不必使用 Fla...
    强某某阅读 6,003评论 1 21
  • 照相机 此处所说照相机与现实有所差别, 由于threejs创建的场景是三维的,人眼要看出三维效果就需要有透视点。 ...
    风铭阅读 1,928评论 0 1
  • Threejs中文文档 郭隆邦技术博客 2018-09-21 20:40:17 关注 Three.js中文文档 今...
    情人波阅读 13,968评论 0 7
  • Three.js是构建web3d场景非常流行的框架,利用three.js我们可以更优雅地创建出三维场景和三维动画,...
    YoneChen阅读 9,751评论 3 13
  • 1.重用Material和Geometry 2.不在render()中实例化或是赋值操作 3.粒子系统代替粒子 4...
    bbh123阅读 4,597评论 1 3