three.js太阳系模拟实战:从零搭建三维动画场景 简介这是一份Three.js入门到实战的资源围绕宇宙模拟与太阳系场景讲解纹理贴图、行星自转公转、灯光布置、坐标定位以及星系随机生成等Web三维核心知识适合有JavaScript和Vue基础、正在学习三维前端的开发者。包内含2000个文件以1675个JS源码为主另有198个Markdown说明文档、116个JSON数据文件及少量Vue/HTML/CSS等整体57.66MB目录按功能模块划分便于从代码到原理逐步查阅。项目采用Vue3、Element Plus与Three.js搭建使用真实尺寸比例生成星空背景、恒星、黑洞和星河完整演示了太阳系从建模到动画渲染的可运行流程。通过对照源码与笔记读者可以掌握Three.js工程化组织方式并直接扩展出自己的三维宇宙交互场景。目前已有49人浏览学习。1. 用 three.js 做宇宙模拟为什么太阳系是三维基础框架里最好的练手项目三维 web 前端的基础框架里最容易被新手问住的一句是three.js 的官网示例看完了下一步到底做什么我的建议一直是用太阳系来磨这一个项目。宇宙模拟三维动画看起来是个花活实际上把 three.js 最核心的五块能力全占了——场景与相机框架、纹理贴图、自转公转动画、灯光定位、轨道与星系的程序化生成恰好是三维 web 前端日常开发里复用率最高的技术点。这篇内容适合刚跑通 three.js 快速创建项目的前端新手也适合可视化大屏做到一半、想补三维基本功的从业者。照下面的步骤做你能得到一个能转能看、能继续加功能的太阳系而不是一个只在文档里跑过的立方体。2. 搭建三维场景基础框架从空页面到第一个能转起来的太阳系2.1 场景、相机、渲染器的最小配置想用 three.js 快速创建项目常见做法是直接用 Vite 的 vanilla 模板而不是手搓一个 index.html 再去引脚本。Vite 自带开发服务器和热更新后面处理纹理、模块拆分会省很多事。新建项目的命令如下npm create vitelatest solar-system -- --template vanilla cd solar-system npm install three第一行命令里的solar-system是项目名--template vanilla表示用纯净的 JavaScript 模板不引入 React 或 Vue。第二行进入目录第三行安装 three。装完之后用npm run dev启动后面所有代码都写在src/main.js里。打开main.js先把 three.js 的三件套初始化出来场景Scene、相机Camera、渲染器Renderer。这是所有 three.js 应用的起点太阳系也不例外。import * as THREE from three; const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000); camera.position.set(0, 80, 150); camera.lookAt(0, 0, 0); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);这三行代码的逻辑不多Scene是一个容器所有模型、灯光、轨道线都挂到它下面PerspectiveCamera是透视相机近大远小符合人眼观察习惯它的四个参数依次是视野角度fov、宽高比aspect、近裁剪面near、远裁剪面far。视野 60 度比较舒服near 设 0.1far 设 2000保证球体在轨道远端不会被裁剪掉。相机位置放在(0, 80, 150)从斜上方俯视太阳系lookAt让它盯着原点。WebGLRenderer是真正的绘图引擎参数antialias: true开启抗锯齿否则球体边缘会出现明显的锯齿。最后一行把渲染器的画布挂到页面上。到这里页面还是空的因为还没放任何模型。先临时加一个太阳球体进去确认渲染链路是通的const sunGeometry new THREE.SphereGeometry(10, 48, 48); const sunMaterial new THREE.MeshBasicMaterial({ color: 0xffaa33 }); const sun new THREE.Mesh(sunGeometry, sunMaterial); scene.add(sun); renderer.render(scene, camera);SphereGeometry(10, 48, 48)表示半径 10 的球体横向和纵向分段数都是 48分段越多球体越圆代价是顶点数增加。太阳用的是MeshBasicMaterial它不受灯光影响永远显示自己本身的颜色后面会解释为什么太阳不能和行星用同一种材质。2.2 动画循环requestAnimationFrame 与 setAnimationLoop 怎么选静态渲染只有一帧行星不会自己转。要让画面持续更新需要进入动画循环。很多老教程写的是requestAnimationFrame递归调用这在 three.js 里完全能用但 newer 版本提供了一个更适合动画场景的方法renderer.setAnimationLoop。function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate();上面是经典写法requestAnimationFrame会按屏幕刷新率通常 60fps循环调用animate每次调用都重新渲染一帧。这个写法的问题在于如果在页面切到后台后没有做暂停处理它会一直空转消耗 CPU。three.js 从 r92 开始提供setAnimationLoop用法更直接const clock new THREE.Clock(); renderer.setAnimationLoop(() { const delta clock.getDelta(); renderer.render(scene, camera); });setAnimationLoop内部帮你管理了帧回调在普通浏览器环境中等价于requestAnimationFrame在 WebXR 环境里会自动切换到 XR 的帧循环所以做三维可视化项目我一般直接用这个省得以后接 VR 再改一遍。clock.getDelta()返回上一帧到当前帧的秒数这个值在后面做自转公转动画时非常关键用它乘速度就能保证动画在不同帧率下快慢一致。这里有个小坑要提前说setAnimationLoop传入的回调不是立刻执行的有些浏览器需要先手动调用一次renderer.render让渲染器完成初始化否则首帧可能黑屏。代码里在循环之前已经调过一次renderer.render(scene, camera)正好解决了这个问题。2.3 项目目录与资源加载纹理、模型与脚本怎么摆项目体积一大资源文件怎么放直接决定你后面要不要返工。用 Vite 时有个常见的文件组织方式我一般长这样路径作用public/textures/放星球纹理图、星空背景图用绝对路径引用src/main.js入口脚本负责场景初始化和动画循环src/planets.js行星配置与生成逻辑src/controls.js相机控制和交互逻辑纹理文件放public目录是关键。public里的文件会被原样复制到构建产物根目录运行时路径就是/textures/earth.jpg。如果你把图片放在src/assets里然后直接写相对路径开发环境可能正常打包上线后路径一变贴图就显示不出来了。至于浏览器怎么知道纹理文件的地址靠的是TextureLoader它是异步加载的下面这段代码就是加载一张纹理的标准姿势const loader new THREE.TextureLoader(); const earthTexture loader.load(/textures/earth.jpg);loader.load第一个参数是路径它内部用ImageLoader去拉图片图片加载完成后纹理数据才会更新。问题在于这句代码执行完后纹理可能还没到位如果你紧接着就把纹理赋给材质去渲染画面上看到的仍然是一个没贴图的灰球。后面讲贴图排查时会专门处理这件事。3. 生成太阳、行星与星系纹理贴图与几何体的一次到位3.1 用 SphereGeometry 和纹理图生成地球先做一颗完整的地球它是最标准的三维模型流程几何体 纹理图 受光材质。把上一章的临时太阳代码往下扩展在场景里加入地球。const loader new THREE.TextureLoader(); const earthTexture loader.load(/textures/earth.jpg); earthTexture.colorSpace THREE.SRGBColorSpace; const earthGeometry new THREE.SphereGeometry(2, 64, 64); const earthMaterial new THREE.MeshStandardMaterial({ map: earthTexture, roughness: 0.6, metalness: 0.1, }); const earth new THREE.Mesh(earthGeometry, earthMaterial); earth.position.x 24; scene.add(earth);earthTexture.colorSpace THREE.SRGBColorSpace这一行是新版 three.js 里特别容易被忽略的配置。纹理图本身是 sRGB 颜色空间如果不告诉渲染器颜色会发灰、偏淡很多教程的星球看起来灰蒙蒙的第一个原因就是没设这行。MeshStandardMaterial是 three.js 的物理材质能配合灯光产生真实的光影关系roughness控制表面粗糙度metalness控制金属感。地球表面岩石和海洋都不反光粗糙度给 0.6金属感给 0.1。SphereGeometry(2, 64, 64)的 2 是半径64 是分段数。行星在屏幕里通常只占几十个像素64 段足够圆了。分段数不是越高越好后面八颗行星每颗 64 段顶点数量会非常可观。这里补一张材质选型表后面生成所有行星都用得上材质是否需要灯光特点适合对象MeshBasicMaterial否自发光颜色直接显示太阳、轨道线、星空粒子MeshPhongMaterial是传统光照模型性能好低配项目里的大星球MeshStandardMaterial是PBR 物理材质光影真实行星主体推荐默认选它MeshPhysicalMaterial是效果最强支持透明折射等需要做大气层或水面的星球3.2 贴图开始不显示时按这三步排查“three.js 贴图开始不显示”是新手翻车率最高的场景。球体建好了材质也赋了map但画面里就是一个灰球或白球。常见的原因有三个按顺序查基本都能定位。第一个是路径问题。用 Vite 时图片放public目录就用/textures/earth.jpg这种绝对路径不要用../assets/earth.jpg。以下是正确的加载姿势const loader new THREE.TextureLoader(); const earthTexture loader.load(/textures/earth.jpg, (texture) { earthMaterial.map texture; earthMaterial.needsUpdate true; }, undefined, (err) { console.error(纹理加载失败检查路径与跨域设置, err); });这段代码把纹理赋值挪到了onLoad回调里确保图片字节已经下载完才交给材质。needsUpdate true是告诉材质“你的贴图换了请重新编译着色器”这是一个很多人不知道的黑匣子不加它有时候改了map也不生效。第二个原因是 UV 映射缺失。SphereGeometry自带一套正确的 UV 坐标所以贴图能顺利包在球面上。但如果你从建模软件导入一个自定义球体或者用几个平面拼出几何体UV 可能是空的或乱的贴图自然显示不出来。排查办法把材质临时换成MeshBasicMaterial并加一个wireframe看网格确认几何体没有破面在BufferGeometry上检查是否存在uv属性console.log(earthGeometry.attributes.uv);uv属性不存在说明几何体就没 UV 数据需要手动展开或者改用SphereGeometry、BoxGeometry等 three.js 原生几何体。第三个原因是跨域。纹理放在 CDN 或第三方图床上时浏览器会拦截跨域图片。看到Tainted canvases may not be loaded报错就去图片服务商的响应头里加Access-Control-Allow-Origin。自建项目把纹理放到自己的服务器或public目录里就没有这个问题。3.3 批量生成八颗行星并加入背景星系地球只有一颗太阳系里还有水星、金星、火星这些。如果手工复制八份地球代码后面要调速度、调大小会改到怀疑人生。我一般把行星数据抽成配置数组用一个循环生成这样后面加“第九行星”或者调参数只动配置就行。const planetConfigs [ { name: 水星, radius: 16, size: 0.8, speed: 0.015, texture: /textures/mercury.jpg }, { name: 金星, radius: 22, size: 1.2, speed: 0.010, texture: /textures/venus.jpg }, { name: 地球, radius: 30, size: 1.5, speed: 0.008, texture: /textures/earth.jpg }, { name: 火星, radius: 38, size: 1.1, speed: 0.007, texture: /textures/mars.jpg }, { name: 木星, radius: 52, size: 4.0, speed: 0.005, texture: /textures/jupiter.jpg }, { name: 土星, radius: 66, size: 3.5, speed: 0.004, texture: /textures/saturn.jpg }, { name: 天王星, radius: 80, size: 2.5, speed: 0.003, texture: /textures/uranus.jpg }, { name: 海王星, radius: 92, size: 2.4, speed: 0.002, texture: /textures/neptune.jpg }, ]; const planetMeshes []; const orbitGroups []; planetConfigs.forEach((cfg) { const geometry new THREE.SphereGeometry(cfg.size, 32, 32); const material new THREE.MeshStandardMaterial({ map: loader.load(cfg.texture), roughness: 0.7, metalness: 0.05, }); const orbitGroup new THREE.Object3D(); scene.add(orbitGroup); const mesh new THREE.Mesh(geometry, material); mesh.position.x cfg.radius; orbitGroup.add(mesh); planetMeshes.push(mesh); orbitGroups.push(orbitGroup); });这里做了一个关键的结构决定每个行星外面包了一层Object3D叫orbitGroup。行星本体挂在orbitGroup下面位置是(cfg.radius, 0, 0)也就是偏移到轨道半径上。后面要让行星公转只需要旋转orbitGroup行星就会跟着绕原点转要让行星自转旋转mesh本身就行。自转和公转从此互不干扰这是太阳系动画里最优雅的解法。几何体的分段数这里降到了 32。行星数量一多每颗球 64 段会带来大量顶点计算屏幕上看不出差别性能却能差出一倍。如果你用的纹理单张在 2048 以上建议先做纹理压缩把主纹理压成 jpg 或 webp控制单张在 2048 以内绝大多数场景肉眼看不出细节损失。正式上线对显存敏感时可以接 three 自带的 KTX2 加载器用 GPU 压缩纹理但那属于进阶优化开发阶段先别折腾。背景星系也在这个阶段一起做掉。用Points生成几千颗随机分布的星星比一张星空贴图在视觉上更干净const starGeometry new THREE.BufferGeometry(); const starCount 2000; const positions new Float32Array(starCount * 3); for (let i 0; i starCount; i) { positions[i * 3] (Math.random() - 0.5) * 800; positions[i * 3 1] (Math.random() - 0.5) * 800; positions[i * 3 2] (Math.random() - 0.5) * 800; } starGeometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)); const stars new THREE.Points(starGeometry, new THREE.PointsMaterial({ color: 0xffffff, size: 0.6, })); scene.add(stars);BufferGeometry是 three.js 中更底层的几何体容器直接把顶点坐标放进一个Float32Array然后用setAttribute绑定为位置属性。2000 个点对 GPU 来说几乎没有压力。PointsMaterial的size是像素大小给 0.6 刚好是远处星星的效果太大就变成雪花噪点了。4. 自转公转动画与轨道定位把八颗行星的动势调对4.1 为什么用 Object3D 层级结构而不是直接算坐标上一章生成的orbitGroup结构就是自转公转动画的地基。如果不做这层封装常见的翻车写法是直接改mesh.position.x和mesh.position.z用三角函数去算公转位置// 不推荐的写法 const angle Date.now() * 0.001 * speed; mesh.position.x Math.cos(angle) * radius; mesh.position.z Math.sin(angle) * radius;这个写法的问题在于自转要旋转mesh.rotation.y公转也在改mesh.position一旦后面要加“行星跟着太阳在银河系里移动”这种更大的层级所有坐标都要重算十分痛苦。推荐的实现是在动画循环里同时旋转两个对象const selfRotationSpeeds [0.02, 0.015, 0.01, 0.009, 0.02, 0.022, 0.015, 0.01]; renderer.setAnimationLoop(() { const delta clock.getDelta(); for (let i 0; i planetMeshes.length; i) { planetMeshes[i].rotation.y selfRotationSpeeds[i] * delta * 10; orbitGroups[i].rotation.y planetConfigs[i].speed * delta * 10; } renderer.render(scene, camera); });planetMeshes[i].rotation.y负责自转它绕的是行星自身中心orbitGroups[i].rotation.y负责公转它绕的是场景原点也就是太阳的位置。乘上delta之后再放大 10 倍是为了让转速匹配实际可看的节奏。这里的delta是上一帧到这一帧的秒数如果没乘它同一台电脑在高刷新率屏幕和普通屏幕上的动画速度会不一样。有个细节要注意旋转顺序在 three.js 里默认按XYZ欧拉角计算。对于球体自转轴正好是 Y 轴不存在万向锁问题。但如果以后做地轴倾斜比如让地球的自转轴偏 23.5 度就不能直接改rotation.x正确做法是把行星再包一层Object3D让外层负责倾斜内层负责自转。这也是层级结构比直接算坐标更利于扩展的原因。4.2 轨道参数表与动画公式转速与半径怎么定行星轨道的半径和速度需要一张配置表来统一管理。上面planetConfigs里的数值不是真实的物理比例真实太阳系里海王星轨道半径约是地球的三十倍如果按真实比例内行星会被压成一个小点。模拟动画里我只取了“视觉舒适”的比例轨道半径从 16 到 92公转速度从 0.015 到 0.002外圈行星明显更慢符合直觉。行星轨道半径公转速度相对值自转速度相对值初始相位水星160.0150.0200.0金星220.0100.0151.2地球300.0080.0102.4火星380.0070.0093.6木星520.0050.0204.8土星660.0040.0221.8天王星800.0030.0150.6海王星920.0020.0103.0表中的初始相位是每个orbitGroup初始的rotation.y值。如果不加初始相位所有行星一开始都在同一条直线上排队观感非常假。在配置里增加一个phase字段生成时赋值// planetConfigs 里每项加 phase: 1.2 orbitGroup.rotation.y cfg.phase;相位只是初始角度不影响速度。给行星做动画时转速永远用相对值而不是绝对值这样你调整整体节奏时只需要把所有speed乘同一个系数比如场景需要慢镜头时用一个全局的timeScale去缩放所有转速即可。4.3 轨道线绘制与相机定位先画出轨道再决定怎么看有轨道线比没有轨道线直观得多尤其当你想验证每颗行星的轨道半径是否正确时。用LineLoop画一个圆形轨道代码量很小function createOrbitLine(radius, color 0x445566) { const points []; const segments 128; for (let i 0; i segments; i) { const angle (i / segments) * Math.PI * 2; points.push(new THREE.Vector3( Math.cos(angle) * radius, 0, Math.sin(angle) * radius )); } const geometry new THREE.BufferGeometry().setFromPoints(points); return new THREE.LineLoop(geometry, new THREE.LineBasicMaterial({ color })); } planetConfigs.forEach((cfg) { scene.add(createOrbitLine(cfg.radius)); });BufferGeometry.setFromPoints会把一个三维点数组转成几何体LineLoop会自动把最后一个点和第一个点连起来形成闭环。128 个点的圆已经是平滑的圆。轨道线挂在scene下面而不是挂在orbitGroup下面所以它不会跟着公转旋转看起来是恒星背景里静止的圆圈。相机定位是另一个影响观感的环节。如果固定用一个视角看整个太阳系会错失很多细节。我通常在交互里保留两套视角全局俯视视角和行星跟随视角。全局视角就是初始化时的camera.position.set(0, 80, 150)适合看整体结构行星跟随视角需要拿到行星的世界坐标再把相机移过去function followPlanet(index) { const target new THREE.Vector3(); planetMeshes[index].getWorldPosition(target); camera.position.copy(target).add(new THREE.Vector3(8, 4, 8)); camera.lookAt(target); }getWorldPosition会返回行星在场景中的世界坐标即使它已经随着轨道组转到了远处也能正确取到位置。相机放在行星附近偏移(8, 4, 8)的位置再lookAt行星本体就能看到一颗星球悬浮在星空背景里的完整效果。做这个切换时要注意先手动调用scene.updateMatrixWorld()确保层级矩阵是最新的否则取到的坐标可能是上一帧的。5. 灯光配置与渲染排障宇宙场景发黑、贴图不显示的常见问题5.1 太阳点光源参数强度、距离与衰减怎么设才不爆白太阳系里最自然的光源就是太阳本体。用PointLight放在原点点光源向四面八方发光正好模拟恒星的光照效果。但光照参数的坑比想象中多新版 three.js 从 r155 开始默认启用物理光照模式点光源的强度单位是坎德拉老教程里的intensity: 1在新版本里几乎等于没开灯。const sunLight new THREE.PointLight(0xfff5e6, 20000, 500, 1.6); sunLight.position.set(0, 0, 0); scene.add(sunLight);四个参数依次是颜色、强度、衰减距离、衰减指数。颜色0xfff5e6是偏暖的太阳光强度 20000 是适配这个场景尺寸的经验值衰减距离 500 表示只在 500 个单位内生效衰减指数 1.6 让光在近距离稍亮、远距离迅速变暗。如果发现最近的行星被照得惨白优先把强度降到 15000 试试而不是去动衰减距离。调节顺序记住一条先定太阳光的强度和衰减让最远的行星刚好能被照亮再谈环境光。5.2 环境光与半球光配合不让暗部死黑的补救办法纯点光源的物理结果是行星背对太阳的一面全黑。真实宇宙里暗部确实黑得彻底但用户会觉得你的模型没做完。这里加一点淡淡的环境光做兜底视觉效果提升明显但又不能把场景整体洗灰。const ambientLight new THREE.AmbientLight(0x404060, 0.3); scene.add(ambientLight); const hemisphereLight new THREE.HemisphereLight(0x88aaff, 0x332211, 0.2); scene.add(hemisphereLight);AmbientLight是各方向均匀照亮强度 0.3 已经很低用来保证完全背阴的面还能看到轮廓。HemisphereLight模拟天空和地面的双色光照第一个颜色是天空色偏蓝第二个是地面色偏棕强度 0.2。这样行星暗部会带一点蓝紫色的环境反射更像太空的冷色调氛围。我习惯先加环境光再加半球光每加一个就刷新一次画面看效果强度宁低勿高。5.3 高频故障排查记录这五个坑我基本每做一次都会撞一个坑一贴图开始不显示球体是默认的白色。现象map赋值了代码也没报错但球体表面没有任何图案。 原因八成是纹理异步加载的问题。loader.load返回的纹理对象是空的图片还在下载中渲染器已经画完第一帧之后纹理到达但材质没有收到更新通知。另一个原因是路径写错纹理请求返回 404。 解决按 3.2 节的做法把map赋值放到onLoad回调里同时加needsUpdate true。检查浏览器 Network 面板里的纹理请求确认返回状态是 200。坑二场景全黑球体完全看不见。现象代码明明添加了行星和灯光但画面是一整块黑色。 原因最常见的是相机位置在球体内部或者球体坐标与相机重合其次是材质用了MeshStandardMaterial但没有加任何灯光标准材质没有光照就是纯黑。 解决先把光照全部移除材质临时改成MeshBasicMaterial如果球体能看见说明问题在灯光。加灯之后再逐个恢复材质。相机那边检查camera.position的坐标值不要设成(0, 0, 0)那会钻进太阳肚子里。坑三开了阴影后星球表面阴影闪烁。现象给行星开启了castShadow太阳直射下阴影边缘像被踩碎的饼干一跳一跳的。 原因阴影贴图分辨率太低。默认的shadow.mapSize只有 512太阳系场景范围很大阴影贴图覆盖整个场景后每个像素对应的世界尺寸太大。 解决把shadow.mapSize明确设到 2048并手动收缩阴影相机视锥范围让阴影贴图只覆盖行星所在区域renderer.shadowMap.enabled true; sunLight.castShadow true; sunLight.shadow.mapSize.set(2048, 2048); sunLight.shadow.camera.near 1; sunLight.shadow.camera.far 130;shadow.camera.near和far从太阳出发覆盖到海王星轨道就够了。范围越大阴影贴图分辨率被摊薄得越厉害。另外多颗行星同时开阴影在低端设备上是性能杀手开发期可以全开演示期我一般只给地球开。坑四行星自转时公转轨迹出现“追尾”错乱。现象明明只改了一颗行星的速度整条轨道都歪了。 原因把行星网格直接挂在了scene下又同时改它的position和rotation两者叠加导致轨道偏移。我在最早做这个项目时也犯过行星一边翻跟头一边偏离轨道飞出去。 解决不要直接改scene级行星的位置一律通过orbitGroup.rotation.y控制公转通过mesh.rotation.y控制自转两者解耦。坑五帧率骤降旋转时卡顿明显。现象行星一转起来就卡GPU 占用飙升。 原因八颗 64 段球体加 4096 纹理加上setPixelRatio取了设备像素比画面渲染像素数是屏幕物理像素的四倍。特别是带阴影之后每帧都要重新计算多张阴影贴图。 解决分段降到 32纹理压到 2048再补上这个最容易被忽略的渲染器参数renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));devicePixelRatio在 Retina 屏上是 2 或更高直接乘到渲染尺寸上会把像素数拉爆。限制到 2 是画质和性能的常用平衡点。6. 进阶技巧让太阳系变成可交互的“可玩”场景一套静态演示的太阳系只算完成了一半加交互才能让观看者真正把玩起来。第一个必加的是轨道控制器three.js 自带的OrbitControls让用户可以用鼠标旋转视角、缩放远近代码只要几行import { OrbitControls } from three/addons/controls/OrbitControls.js; const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.08; controls.minDistance 5; controls.maxDistance 300;enableDamping打开惯性效果旋转时不会突然停下dampingFactor是阻尼系数越小惯性越明显。minDistance和maxDistance限制缩放范围防止用户把相机怼进行星内部或拉出太阳系外。注意要在动画循环里调用controls.update()阻尼效果才会实时生效renderer.setAnimationLoop(() { controls.update(); // ...原有动画逻辑 renderer.render(scene, camera); });交互的第二层是速度控制。教学演示或大屏展示时经常需要暂停讲解或快进。我给项目加过一组键盘快捷键空格暂停上下方向键调速。核心是一个全局时间缩放因子timeScalelet timeScale 1; let running true; window.addEventListener(keydown, (e) { if (e.code Space) running !running; if (e.key ArrowUp) timeScale Math.min(timeScale * 2, 20); if (e.key ArrowDown) timeScale Math.max(timeScale / 2, 0); });在动画循环里让所有角度增量都乘上timeScale并且只在running为真时累加时间。这个设计比直接改每个行星的速度干净得多调试时能一键回到慢速观察状态。验证动画性能时我习惯加一个帧率统计面板。three.js 的Stats组件能显示当前 FPS、渲染时间等数据直接引用即可import Stats from three/addons/libs/stats.module.js; const stats new Stats(); document.body.appendChild(stats.dom);然后在动画循环里调用stats.update()。注意这个面板默认放在左上角上线时要移除或隐藏。回顾我在这套太阳系上踩过的最大一个坑最早我没有用orbitGroup层级结构而是把所有行星直接摆在scene下靠三角函数算位置结果每次自转都会轻微改变公转轨迹调了整整一个晚上最后老老实实按“轨道组套行星”的方式重写十分钟解决。这个“外层对象管运动里层对象管自身旋转”的思路后来做可视化大屏时在二三维联动、模型定位上一直沿用。你先把这个项目做熟再往里面加更多东西时就会少走很大一段弯路。希望帮到你。本文还有配套的精品资源点击获取