尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【three.js教程】场景图:用“父子关系“让月亮自动绕地球转

【three.js教程】场景图:用“父子关系“让月亮自动绕地球转 Three.js 场景图用父子关系让月亮自动绕地球转原文出处Three.js Manual – Scene Graph本文基于 three.js 官方手册场景图章节整理用通俗的方式带你理解 three.js 里最容易被忽视、却最强大的概念——场景图Scene Graph。写在前面前面我们学会了搭场景、放物体、设材质。但有个底层概念一直没细说而它才是 three.js 真正的基础——场景图Scene Graph。如果你一直把每个物体都直接scene.add()到场景里各自算各自的坐标一旦物体多了、还要互相联动比如坦克履带跟着车体走、炮塔跟着坦克转数学会复杂到爆炸。这一篇就用太阳-地球-月亮这个经典例子把场景图的思路讲透。理解了它你才真正算会用 three.js。一、场景图到底是什么一句话场景图就是一棵节点树每个节点代表一个局部空间。子节点跟着父节点一起移动、旋转、缩放而且子节点用的是相对于父节点的坐标。听起来抽象用太阳系来理解最直观太阳在原点地球绕太阳转月亮绕地球转从月亮的视角看它只管绕地球的局部空间转圈完全不用关心地球在绕太阳怎么跑。但站在太阳视角月亮的轨迹其实是一条超级复杂的螺旋线spirograph。这就像你走在地球上根本不用管地球在自转、在绕太阳公转——你只管在地球的局部空间里走路、开车。three.js 的场景图就是把这种嵌套关系用代码表达出来。二、从只有太阳开始先搭一个最低配的太阳系只有一个太阳放在原点。// 所有要旋转的物体都丢这个数组里constobjects[];// 所有球共用同一个低面数几何体方便看清旋转constradius1;constwidthSegments6;constheightSegments6;constsphereGeometrynewTHREE.SphereGeometry(radius,widthSegments,heightSegments);constsunMaterialnewTHREE.MeshPhongMaterial({emissive:0xffff00});// 自发光黄色constsunMeshnewTHREE.Mesh(sphereGeometry,sunMaterial);sunMesh.scale.set(5,5,5);// 太阳放大 5 倍scene.add(sunMesh);objects.push(sunMesh);几个小细节用了超低面数球体赤道才 6 段这样旋转时肉眼能看清它在转。emissive设成黄色意思是即使没有光照到也显示这个颜色。光会再叠加在它上面。这样太阳看起来像自己在发光。所有物体共用一个sphereGeometry通过scale区分大小省内存。再加一个点光源放在中心太阳位置让其他物体能被照亮{constcolor0xffffff;constintensity500;constlightnewTHREE.PointLight(color,intensity);scene.add(light);}相机放在正上方往下看最直观。注意这里要告诉相机哪个方向算上——因为是正俯视得把 Z 轴设成 upconstcameranewTHREE.PerspectiveCamera(fov,aspect,near,far);camera.position.set(0,50,0);camera.up.set(0,0,1);// 正下方俯视Z 轴朝上camera.lookAt(0,0,0);渲染循环里让数组里所有物体绕 Y 轴转objects.forEach((obj){obj.rotation.ytime;});三、加地球加地球很简单蓝材质、放太阳左边 10 个单位。constearthMaterialnewTHREE.MeshPhongMaterial({color:0x2233ff,emissive:0x112244});constearthMeshnewTHREE.Mesh(sphereGeometry,earthMaterial);earthMesh.position.x10;scene.add(earthMesh);objects.push(earthMesh);但问题来了地球虽然在转却没有绕着太阳转。它只是原地自转。要让地球绕太阳转直觉是让地球成为太阳的子节点-sunMesh.add(earthMesh);// 取代 scene.add(earthMesh)结果却翻车了——地球变得和太阳一样大还飘得老远相机得从 50 拉到 150 才能看见。原因很关键sunMesh.scale.set(5, 5, 5)让太阳的局部空间放大了 5 倍。地球一旦成为它的子节点所有属性都会被乘 5——包括大小5×和距离10×550。这就是为什么地球又大又远。四、用空节点Object3D修复缩放问题解决办法引入一个没有形状、只代表局部空间的空节点Object3D让太阳和地球都成为它的子节点。constsolarSystemnewTHREE.Object3D();// 空节点代表太阳系中心scene.add(solarSystem);objects.push(solarSystem);constsunMeshnewTHREE.Mesh(sphereGeometry,sunMaterial);sunMesh.scale.set(5,5,5);solarSystem.add(sunMesh);// 太阳挂到 solarSystem 下objects.push(sunMesh);constearthMeshnewTHREE.Mesh(sphereGeometry,earthMaterial);earthMesh.position.x10;solarSystem.add(earthMesh);// 地球也挂到 solarSystem 下objects.push(earthMesh);Object3D和Mesh一样都是场景图节点但它没有材质也没有几何体纯粹用来组织层级关系。现在太阳的 5× 缩放只影响自己地球不再被牵连。地球绕着solarSystem转大小也正常了。五、继续加月亮月球要绕地球转那就再套一层。关键是地球的位置偏移交给一个叫earthOrbit的空节点来承担constearthOrbitnewTHREE.Object3D();earthOrbit.position.x10;solarSystem.add(earthOrbit);objects.push(earthOrbit);constearthMeshnewTHREE.Mesh(sphereGeometry,earthMaterial);earthOrbit.add(earthMesh);// 地球挂到 earthOrbit 下不再自己设 position.xobjects.push(earthMesh);constmoonOrbitnewTHREE.Object3D();moonOrbit.position.x2;// 月球离地球 2 个单位earthOrbit.add(moonOrbit);constmoonMaterialnewTHREE.MeshPhongMaterial({color:0x888888,emissive:0x222222});constmoonMeshnewTHREE.Mesh(sphereGeometry,moonMaterial);moonMesh.scale.set(0.5,0.5,0.5);moonOrbit.add(moonMesh);objects.push(moonMesh);现在层级是solarSystem → earthOrbit → earthMesh → moonOrbit → moonMesh。神奇的地方来了你完全不用手动算月亮相对于太阳的轨迹只要把层级摆对、让每个节点转自己的 Y 轴月亮自然就走出那条复杂的螺旋线。场景图帮你把复杂数学自动化了。六、可视化辅助工具AxesHelper 和 GridHelper调试层级关系时最好能看见每个节点的局部坐标系。three.js 提供了两个帮手AxesHelper画三条轴红X、绿Y、蓝ZGridHelper在 XZ 平面画一张网格constaxesnewTHREE.AxesHelper();axes.material.depthTestfalse;// 不被挡住永远可见axes.renderOrder1;// 后绘制盖在球体上面node.add(axes);depthTest false保证坐标轴即使藏在球体内部也能看见renderOrder 1保证它最后画不会被球体盖住。手册里还演示了用 lil-gui 给每个节点做个开关一键显示/隐藏它的坐标轴和网格。核心是一个AxisGridHelper类classAxisGridHelper{constructor(node,units10){constaxesnewTHREE.AxesHelper();axes.material.depthTestfalse;axes.renderOrder2;node.add(axes);constgridnewTHREE.GridHelper(units,units);grid.material.depthTestfalse;grid.renderOrder1;node.add(grid);this.gridgrid;this.axesaxes;this.visiblefalse;}getvisible(){returnthis._visible;}setvisible(v){this._visiblev;this.grid.visiblev;this.axes.visiblev;}}用 getter/setter 把网格可见和坐标轴可见绑在一个visible属性上lil-gui 就能用一个复选框同时控制两个。这是个很实用的小模式。七、更复杂的场景图长什么样场景图能表达的东西远超太阳系。手册举了几个例子汽车车体移动轮子跟着动。想让车身和轮子独立弹跳就再加一个frame节点车体和轮子都挂它下面。人体头、手臂、手指、脚趾、下巴……每个关节都是一个节点层级能深到吓人光手指就 28 个节点。坦克手册里最复杂的示例车体沿样条曲线行驶炮塔自动用lookAt瞄准目标6 个轮子同步转还挂了 4 个摄像机在不同位置切换。坦克示例里有个重要技巧用getWorldPosition()拿到子节点在世界坐标里的真实位置再去lookAt它。因为子节点的坐标都是相对父节点的直接拿position会出错必须用世界坐标。consttargetPositionnewTHREE.Vector3();targetMesh.getWorldPosition(targetPosition);// 拿到世界坐标turretPivot.lookAt(targetPosition);// 炮塔瞄准目标回顾一下场景图是 three.js 的骨架核心就三点父子关系 局部空间套娃子节点跟着父节点动坐标用相对父节点的。善用空节点Object3D只为了组织层级、避免被父节点的 scale 牵连。太阳放大 5 倍却不能让地球也变大靠的就是中间垫一个空节点。复杂联动交给层级别手算月亮绕地球的螺旋轨迹、坦克炮塔瞄准都不用算数学摆好树结构让 three.js 自动处理。下次做有联动效果的东西车辆、机器人、骨骼、UI 跟随先别急着算坐标先想想这个场景图该怎么搭延伸阅读Three.js Object3D 文档Three.js AxesHelper 文档Three.js GridHelper 文档lil-gui GitHub
返回列表