简介一套基于Three.js构建的太阳系八大行星公转自转三维模拟程序以网页形式呈现行星绕太阳运动的轨道与自身旋转效果适用于前端三维开发学习者、天文科普教育及个人Web项目参考。包内共13个文件包含十张行星与太阳纹理贴图、两个脚本文件核心三维引擎与轨道交互控制插件以及一个网页入口文档压缩后仅一点三九兆字节非常轻量。目前已有四百三十人学习或下载。运行入口文档即可打开完整场景支持鼠标拖拽旋转视角与滚轮缩放方便从任意角度观察各行星运动纹理图片与逻辑代码分离便于理解球体建模、贴图映射、自转轴设置及公转轨道参数计算等关键环节也可作为二次开发或教学演示的基础模板。1. 用 Three.js 把太阳系搬进浏览器先别急着写代码想清楚这三件事如果你试过在三维引擎里放几个球体然后转起来多半遇到过这种情况行星是转了但转得像个陀螺仪校准现场水星和冥王星跑到了同一圈上土星环看着贴在球面上而不是悬在赤道外围。这不是 Three.js 的锅而是把现实世界的轨道数据直接塞进引擎时单位、比例和坐标系三件事没理顺。本文要解决的是用 Three.js 模拟太阳系八大行星公转及自转三维模型让行星既绕太阳公转自身也在自转并且转向、倾角、轨道形状都有据可依。这个方向适合两类人一类是做天文科普可视化、需要把轨道和自转数据真实呈现的前端工程师另一类是刚接触 Three.js想找一个复杂度适中、但能覆盖场景管理、动画循环、光照、贴图和性能调优的完整案例。前者要精度后者要路径。本文两种都会给到但更偏重后者——把一个可行的方案拆开讲清楚。做这个题目标题里没有说、但绕不开的三件事先给结论第一坐标系必须用真实轨道参数做底不要手工调位置第二尺度压缩要同时处理距离和半径否则屏幕上看不到任何东西第三自转和公转要分成两套更新逻辑不能写进同一个旋转变量里。下面每一章都在为这三件事服务。2. 从真实轨道数据到 Three.js 场景选型、数据准备和尺度压缩2.1 为什么选择 Three.js 而不是直接手写 WebGL 或使用其他引擎标题把技术栈限定成了 Three.js但值得先说清楚它在这个项目里的不可替代点在哪里。太阳系场景需要处理的东西包括一个发光光源太阳、八个不同轨道半径和速度的行星、至少两个环状结构土星环和天王星的轴倾角导致的环、以及一个不断更新的相机视角。手写 WebGL 的话光是轨道椭圆的顶点生成和自转矩阵的逐帧更新就够写几百行矩阵代码而 Three.js 把这些封装成了OrbitControls、EllipseCurve和Object3D.rotation核心逻辑可以集中到数据层而不是渲染层。对比其他常见方案Babylon.js 的粒子系统和物理引擎很强但这个场景没有碰撞需求反而它的场景复杂度会拖慢上手速度Cesium 专精地理空间但定位是地球级别做太阳系需要额外处理天体尺度的坐标精度Cesium 本身的浮点精度方案在这里属于杀鸡用牛刀。Three.js 的优势在于生态成熟、示例多、社区里能搜到大量轨道模拟和纹理贴图的现成方案同时 r150 版本对浮点精度和远距离相机的支持已经足够用。一句话你的目标不是在引擎层面解决渲染性能问题而是把天文数据组织好。Three.js 就是那个不做渲染以外干预、让你能直接处理数据组织的引擎。它不限制你的数据来源也基本不干预你的动画循环写法。这点在这个项目里比什么都重要因为轨道数据的处理大概率会占用你一半以上的开发时间。2.2 真实轨道数据的获取与参数提取从 TLE 到桌面数据模拟太阳系不能凭记忆填半径和周期否则做出来的东西连科普都算不上。常见的做法是使用经典的两组数据源一组是 NASA 的行星事实表Planetary Fact Sheet它给出了每个行星的轨道半长轴以 AU 为单位、轨道离心率、轨道倾角、自转周期、自转轴倾角等基础参数另一组是 JPL 的 HORIZONS 系统它可以按任意时间点导出精确的日心黄道坐标适合需要高精度的验证场景。如果你希望做实时模拟而不引入外部数据文件推荐用 Kepler 轨道参数而不是笛卡尔坐标。Kepler 参数半长轴 a、离心率 e、倾角 i、升交点经度 Ω、近地点幅角 ω、平近点角 M可以直接驱动椭圆轨道方程避免每帧存储大数组。这里给一个我常用的核心参数清单注意单位换算参数水星地球木星半长轴 (AU)0.3871.0005.204轨道离心率0.2060.0170.049轨道倾角 (度)7.000.001.31自转周期 (小时)1407.623.99.9自转轴倾角 (度)0.0323.43.1注意水星的自转周期比公转周期还长公转约 88 天自转约 58.6 天这种看似反常的数据最容易写错。也正因如此模拟时常有人发现水星不动——因为自转速度设在公转速度的数值级里几乎看不出差异要真实模拟就得把时间尺度拉开。拿到这些参数后光照方向也是要补齐的一环。如果你的行星模型想按真实光照展示至少需要让行星上的光照方向正确这就涉及黄道坐标系下太阳光源的位置以及相机视角的初始方向。这部分不是难点但很多初学者会漏掉光源的castShadow配置导致行星看起来像纸片。2.3 尺度压缩为什么直接按真实距离建模会什么都看不到这是新人最容易翻车的一步。真实太阳系中太阳半径约 69.6 万公里地球轨道半径约 1.5 亿公里两者差了约 215 倍。如果按真实比例建模太阳会小到看不见或者地球轨道远到相机需要在几亿公里的尺度上才能看到全貌且深度缓冲精度不够会引发闪烁。常见的做法是使用“双尺度压缩”一是距离尺度压缩。把轨道半径按对数或分段线性方式压缩到可视化尺度比如把 1 AU 映射到 100 个引擎单位海王星约 30 AU就是 3000 个单位。二是半径尺度独立压缩。行星半径不能跟随距离比例那样地球会是 0.0005 个单位需要单独映射到 0.41.5 个单位的范围。实际建模时我会在初始化阶段使用这样的逻辑// 轨道半径映射函数对数压缩保留轨道间距差异 function orbitRadiusToScene(au, minAU, maxAU) { const logMin Math.log(minAU); const logMax Math.log(maxAU); const scale 120; // 最高到场景 120 单位 return scale * (Math.log(au) - logMin) / (logMax - logMin); } // 行星半径映射线性表 const radiusMap { mercury: 0.4, venus: 0.6, earth: 0.65, mars: 0.5, jupiter: 1.8, saturn: 1.5, uranus: 1.0, neptune: 0.95 }; // 示例地球轨道半径 1 AU → 场景单位 const earthOrbitSceneUnits orbitRadiusToScene(1.0, 0.3, 30); console.log(earthOrbitSceneUnits); // 约 40.5 单位这里的对数压缩不是唯一方案但能保证内侧轨道水星到火星不至于挤成一条线外侧轨道木星到海王星不至于拉出上万个单位。关键参数是scale——它决定了场景整体尺寸直接关系相机远裁剪面far的设置。场景 120 单位相机 far 一般设 1000 以上否则木星轨道约 85 单位后面的土星会消失。有人会问能不能直接用线性压缩如果只做演示问题不大但水星和金星两条轨道间距只有 0.18 AU线性压缩后只有 1 个单位的间隔行星模型本身半径就 0.4两条轨道几乎重合。你需要在 0.4 和 0.55 之间放两个半径 0.4 的球体很容易穿模。对数压缩能拉开内圈间距这是经验之谈。2.4 纹理贴图与表面质感让行星“看”起来对很多模拟器能看出假不是因为轨道错了而是行星表面没有纹理、光照方向混乱。常见的做法是使用 Solar System Scope 等公开纹理集注意授权每张贴图包含颜色、法线、云层等通道。Three.js 加载贴图的标准路径是import { TextureLoader, MeshStandardMaterial } from three; const loader new TextureLoader(); const material new MeshStandardMaterial({ map: loader.load(/textures/earth_diffuse.jpg), bumpMap: loader.load(/textures/earth_bump.jpg), // 表面凹凸 bumpScale: 0.05, roughnessMap: loader.load(/textures/earth_rough.jpg), roughness: 0.8 });几个容易出错的细节木星和土星有云带如果只贴漫反射贴图旋转时会有很明显的 “方位角跳变”需要开启各向异性过滤——texture.anisotropy renderer.capabilities.getMaxAnisotropy()。土星环不能贴在球体上需要单独用一个RingGeometry并设置双面材质将环的贴图 alpha 通道作为透明遮罩。天王星的环极薄且几乎垂直于轨道面很多人会把它的环贴成和土星一样平躺这是典型的天文常识 bug——天王星的自转轴倾角约 98 度它的环几乎是“竖着”绕的。3. 实现公转椭圆轨道与开普勒方程别把圆周当成默认3.1 为什么不能直接用匀速圆周运动代替公转不少教学 Demo 会直接给每个行星绑定一个角度、然后按固定角速度旋转。这在视觉上“看起来像那么回事”但和真实公转的差距在行星轨道是椭圆离心率不是 0行星在近地点移动快、远地点移动慢开普勒第二定律如果匀速圆周水星和火星会在视觉上出现明显的相位偏移。如果你只是做“演示效果”圆周近似可以接受但如果标题里写的是“模拟”这里默认你需要遵循轨道力学的基本规律否则每转一圈误差都在累积——未来如果要加入“日期输入”功能跳到某年某月某日看行星位置匀速圆周会成为最大的误差来源甚至比尺度压缩的问题还大。此外真实轨道还具有空间倾角。水星轨道倾角 7 度土星 2.5 度如果不加倾角俯视视角下所有行星都在一个平面上如果加了倾角但轨道节点方向升交点经度不对行星会出现在错误的方位角。新手常犯的错误是只设置rotation.x或rotation.z来“倾斜轨道”结果是轨道相对屏幕倾斜而不是相对黄道面倾斜。3.2 用 Kepler 方程计算行星位置每帧更新还是预计算要在 Three.js 中让行星沿椭圆轨道运动最直接的路径是每帧调用开普勒方程求解。给定平近点角 $M$需要迭代求偏近点角 $E$再通过 $E$ 计算真实位置。代码实现不长// 开普勒方程迭代给定平近角 M 和离心率 e返回偏近角 E function solveKepler(M, e, tolerance 1e-6) { let E M; for (let i 0; i 100; i) { const dE (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); E - dE; if (Math.abs(dE) tolerance) break; } return E; } // 从轨道根数计算日心黄道坐标简化为 2D 椭圆投影3D 需加倾角旋转 function orbitalPosition(a, e, M, inclination, ascendingNode) { const E solveKepler(M, e); const x a * (Math.cos(E) - e); const y a * Math.sqrt(1 - e * e) * Math.sin(E); // 应用轨道倾角和升交点经度绕 X 轴和 Z 轴的旋转 const cosOmega Math.cos(ascendingNode), sinOmega Math.sin(ascendingNode); const cosI Math.cos(inclination), sinI Math.sin(inclination); const x1 x * cosOmega - y * sinOmega * cosI; const y1 x * sinOmega y * cosOmega * cosI; const z1 y * sinI; return [x1, z1, y1]; // 转为 Three.js 坐标Y 轴向上 }这里把轨道倾角inclination和升交点经度ascendingNode直接编码进了坐标变换比group.rotation.x更精确也更可控。注意函数中[x1, z1, y1]的返回顺序Three.js 默认 Y 轴向上而天文坐标系中通常黄道面法线为 Z 轴这里做了轴序映射否则行星会在错误平面上运行。每帧对八个行星分别做迭代求解是可行的——每帧最多 8 次每次迭代少于 20 次耗时远小于 1ms。但如果你要支持“倍速”、“暂停”和时间跳转更稳妥的方案是预计算轨道位置表。比如把每个行星的公转周期切分成 720 个点预先算好全部位置存为Float32Array每帧按索引插值。这个方案的好处是跳转时间时不需要重新解方程坏处是会让行星轨道变成折线但如果点足够密视觉上察觉不出来。我一般选择混合方案动画循环里直接解 Kepler 方程代码简单、精度无上限但在需要“跳到某时刻”时用缓存插值。如果你不想引入任何预计算直接在动画循环里调solveKepler即可。3.3 轨道可视化EllipseCurve 与 BufferGeometry在三维场景中轨道线不仅增强视觉可信度还能帮你调试轨道参数是否正确。Three.js 有现成的EllipseCurve但它生成的是二维曲线需要手动转到三维平面。行星轨道因为有倾角必须先把二维椭圆曲线变换到三维空间。标准做法是生成轨道点之后用Object3D做旋转变换import { EllipseCurve, BufferGeometry, LineLoop, LineBasicMaterial, Object3D } from three; function createOrbitLine(a, e, inclination, ascendingNode, segments 256) { const curve new EllipseCurve(0, 0, a, a * Math.sqrt(1 - e * e), 0, 2 * Math.PI); const points2D curve.getPoints(segments); const pivot new Object3D(); // 绕 X 轴旋转轨道倾角绕 Z 轴旋转升交点经度 pivot.rotation.x inclination; pivot.rotation.z ascendingNode; const points3D points2D.map(p { const v new THREE.Vector3(p.x, p.y, 0); return v.applyMatrix4(pivot.matrix); }); const geometry new BufferGeometry().setFromPoints(points3D); return new LineLoop(geometry, new LineBasicMaterial({ color: 0x446688, transparent: true, opacity: 0.35 })); }这里的pivot.matrix是在调用updateMatrix()后才有效的但上面代码中我给的是rotation.x和rotation.z分别设置后的矩阵使用前需要手动pivot.updateMatrix()一次。这个细节很多教程跳过了结果是轨道倾角不生效。轨道线用LineLoop而不是Line首尾自动闭合避免在 0 度附近出现一条横穿的线段。3.4 公转速度的时间尺度控制真实公转周期跨度巨大水星 88 天海王星 165 年。如果按真实时间渲染水星转一圈需要几秒钟海王星几乎静止。常见的做法是引入“时间尺度因子”把 1 秒引擎时间映射为 N 个地球日。比如timeScale 5表示每秒模拟 5 个地球日。const dayInSeconds 86400; const timeScale 10; // 每秒模拟 10 个地球日水星公转一圈约 8.8 秒 let simulatedTime 0; function updateOrbits(delta) { simulatedTime delta * timeScale * dayInSeconds; planets.forEach(planet { const n 2 * Math.PI / planet.orbitalPeriodInDays; // 平均角速度弧度/天 const M n * simulatedTime planet.phaseAngle; // 加上初始相位 const pos orbitalPosition(planet.a, planet.e, M, planet.inclination, planet.node); planet.mesh.position.set(pos[0], pos[1], pos[2]); }); }注意phaseAngle初始平近角必须在初始化时给每个行星设一个不同的值否则所有行星都会从同一方向出发视觉上像开运动会。这个值可以随便给但建议至少让木星和土星区分明显因为它们的轨道周期接近 12 年和 29 年如果初始相位一致很长一段时间内它们会一前一后地“绑在一起”。4. 自转与公转的分离轴倾角、坐标层级与渲染循环4.1 层级结构决定了自转是否正确Three.js 中实现“行星自转的同时绕太阳公转”最自然的方式是嵌套 Object3D太阳位于根节点行星的公转轨道环绕根节点每个行星网格本身作为公转节点的子节点自转只修改子节点的本地旋转。这样公转和自转天然分离不需要额外矩阵运算。// 每个行星对应一个 “orbitGroup” 和一个 “mesh” const planetGroup new THREE.Group(); scene.add(planetGroup); const mesh new THREE.Mesh(geometry, material); mesh.position.x orbitalPosition(...); // 仅在初始化时定位 planetGroup.add(mesh);但注意如果你的 orbitGroup 本身不移动而只是被用来承载轨道倾角那么行星的位置应该设置到 orbitGroup 内部的某个坐标上如果 orbitGroup 本身在旋转行星又需要被加到 group 的外部再通过位置关联。这里有个常见的坑公转和自转都在同一个对象上执行rotation那就完全是混乱的。正确的简化方案是——公转移动的是轨道节点的位置比如把mesh放在轨道组下、轨道组绕太阳中心旋转自转只对mesh执行rotation.y speed。实际操作中更直观的分层方法是建一个公共原点太阳位置每个行星建两个节点——外层节点orbitNode控制位置沿椭圆轨迹移动内层mesh作为其子节点控制自转。这样自转永远作用在模型本地坐标系不受公转位置影响。4.2 自转轴倾角与方向别把自转方向搞反行星自转方向大多与公转方向相同太阳系中除金星外都是逆时针你只需要在更新时对mesh.rotation.y做正方向增量即可。但是有两个特例金星是逆行自转自转轴几乎倒转——原本的北极指向下自转方向是顺时针的天王星的自转轴几乎平躺倾斜到轨道面以上 98 度。如果你在这两个行星上没有单独设置自转轴偏向会得出和科普资料不一致的效果。具体做法是每次更新自转时不要直接mesh.rotation.y speed而是让mesh位于一个“轴支架”内部支架的旋转用于表达自转轴倾角// axisTilter承载自转轴倾角 const axisTilter new THREE.Group(); const mesh new THREE.Mesh(geometry, material); // 先倾斜轴行星黄道坐标系绕 X 轴转 inclination 度 axisTilter.rotation.x THREE.MathUtils.degToRad(planet.axisTilt); axisTilter.add(mesh); orbitNode.add(axisTilter); // 自转更新只旋转 mesh 自身 function updateRotation(mesh, speed) { mesh.rotation.y speed * delta; }对于金星额外加一个axisTilter.rotation.z Math.PI来翻转极点方向然后自转增量取负。对于天王星只需把轴倾角设为 97.77 度。特别注意不要把这些特殊设置复制到其他行星身上——那会在可视化上很“猎奇”。4.3 相机、光照与渲染循环里容易被忽略的细节相机要同时容纳水星轨道和远处的海王星需要合理的视野和远裁剪面。如果far太大超过 100000深度冲突会让近处的行星边缘闪烁如果far太小外侧轨道会被剪裁。我习惯的做法是camera PerspectiveCamera(60, aspect, 0.1, 5000)然后把场景总半径控制在 1200 以内。你可以选定“当前追踪行星”动态调整相机位置但初始阶段用全局视角预览全貌更好调参数。光源方面太阳应使用PointLight并放置在场景原点同时添加一个小球体作为太阳可见表面。为了让行星有立体感建议给太阳光加适度强度比如 2.5再加一个微弱的AmbientLight强度 0.050.1否则行星背光面会完全消失。PointLight的distance参数要设 0无限远否则外侧行星收到的光照衰减过强每颗行星会一半黑一半亮。渲染循环中需要做两件事更新公转位置、更新自转角度。这里有一个容易忽略的问题requestAnimationFrame的delta时间如果太大例如切后台回来行星位置会瞬移看起来跳变。解法是限制单帧最大deltaconst clock new THREE.Clock(); function animate() { const delta Math.min(clock.getDelta(), 0.05); // 防跳变限制最大 50ms updateOrbits(delta); updateRotations(delta); controls.update(); renderer.render(scene, camera); requestAnimationFrame(animate); }设置 0.05 秒上限还有一个额外好处避免用户切换浏览器标签页回来后时间戳差异让行星整体飞到不可知的位置。这在做展示项目时尤其重要比任何相机控制都关键。5. 必踩的坑与排查从轨道消失到贴图发黑5.1 场景里行星看不见排查相机裁剪面、轨道缩放与焦距现象运行代码后屏幕上只有星星背景或一片空白控制台没有报错。原因排序通常是相机 far 太小把行星裁剪了行星轨道半径太大比如没压缩的时候到几万单位或者相机初始方向背对太阳系。解决方式是先输出所有行星的世界坐标// 在 render 前输出木星位置确认是否在视野中 console.log(Jupiter, planetJupiter.getWorldPosition(new THREE.Vector3())); console.log(Camera, camera.position);如果坐标值上万说明尺度压缩没生效如果坐标正常但看不到把相机拉远到(300, 200, 300)并lookAt(0,0,0)。还可以在循环里临时加一个gridHelper或axesHelper一眼看出行星在哪、轨道绕哪个方向。5.2 轨道线不闭合、乱飞或位置与行星不符现象轨道线画出来了但行星不在轨道上或者轨道线首尾有断点。轨道线断点在 LineLoop 场景下意味着点首尾有一个巨大的跳变——原因基本是使用getPoints()时aStartAngle和aEndAngle中间有某个位置经过了椭圆中心半长轴和半短轴差太远。排查方法是检查segments是否过少用 128 或 256一般不会出问题。位置不符则检查轨道参数中的a和行星位置中orbitalPosition使用的a是否一致以及是否有scale的不同应用位置。注意轨道线直接使用场景单位而行星位置也要在orbitalPosition后乘同样的尺度因子否则轨道线对齐 120 单位、行星位置对齐 40 单位永远套不上。5.3 自转轴看起来像翻滚而不是自转现象土星或地球的自转轴在旋转过程中不断摆动行星像在漂移。这是典型的层级结构错误——你把自转增量应用到了承载轴倾角的 group 上而不是 mesh 本身。自查方法临时删掉axisTilter.rotation.x如果自转恢复正常说明层级分工错了。解决就是让自转作用在最内层 mesh 上轴倾角作用在外层 group 上。另一个现象是天王星的自转轴看起来是垂直的像公转一样平躺旋转这种状态下单独检查axisTilter的角度是否设置为 98 度并且方向向量不是 Y 轴而是接近轨道面方向。如果你在axisTilter.rotation.x设置 97 度后视觉上看着像“朝天旋转”可能是在加载模型时模型本身的坐标轴方向和 Three.js 默认坐标系不一致需要旋转模型根节点。5.4 行星自转速度设置成 0 或方向反向水星的自转周期 1407.6 小时是很大的坑有人会填成 58.6那是天数于是结果等同“不转”。土星、木星自转很快它们的日周期不到 10 小时在动画中表现为快速转圈但如果你每次delta很小而转速值却很大有可能超过浮点精度视觉表现为闪烁。设一个安全上限行星每帧自转不超过 2π/8 弧度超过就按 2π/8 算。5.5 性能问题贴图加载慢和阴影开销大八个行星全部开启阴影会导致shadowMap每帧绘制大量深度性能和显存都会吃紧。经验方案只对木星和土星开启阴影其他小行星不设castShadow光源的shadow.mapSize控制在 1024。贴图方面单张纹理 2K 即可8 张贴图加起来约 20MB加载时可以统一走LoadingManager并配合进度条避免首屏长时间白屏。如果你发现加载后页面卡顿先检查renderer.setPixelRatio(Math.min(devicePixelRatio, 2))——高分屏上默认 3x 会让像素数量翻 9 倍。6. 把模拟变成天文工具加入日期跳转、轨道线显隐与实时 DPR 控制6.1 用日期驱动所有行星位置做一个跳转面板如果你想要一个比“动画演示”更进一步的东西可以把整个模拟从时间驱动改成日期驱动。即设定一个基准日期比如 2024 年 1 月 1 日每个行星的平近角是该日期与轨道周期计算的结果。这样用户可以在界面输入任意日期看到当天的真实行星排列。关键实现是在初始化时给每个行星存一个“纪元平近角”epoch mean anomaly随后任意时间点的平近角 纪元平近角 2π * 经过天数 / 公转周期。这个数据可以从 JPL 下载或按近似公式计算。你需要把日期转成儒略日避免手动算每年天数出错。我习惯用以下函数function dateToJulianDay(year, month, day) { const y year, m month, d day; const jd (1461 * (y 4800 Math.floor((m - 14) / 12))) / 4 (367 * (m - 2 - 12 * Math.floor((m - 14) / 12))) / 12 - (3 * Math.floor((y 4900 Math.floor((m - 14) / 12)) / 100)) / 4 d - 32075; return Math.floor(jd); }然后在更新逻辑里用 JD 和基准 JD 的差值替代simulatedTime。注意这组公式是适用于格里高利历的处理公元前日期会出错但行星模拟一般只关心近现代够用。用日期驱动后动画的倍速控制就变成了“前进一天/后退一天”展示维度比单纯旋转高一个量级。6.2 轨道线的显示切换用 Raycaster 还是简单按钮当行星数量多时全局轨道线会让画面杂乱。常见的做法是加一个“显示轨道线”开关默认隐藏除当前选中行星外的轨道。用LineLoop.visible控制即可不需要复杂交互。如果你想支持点击行星选中并拉进视角Raycaster 是标准方案const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); window.addEventListener(click, (event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const hits raycaster.intersectObjects(planetMeshes); if (hits.length 0) { const name hits[0].object.userData.planetName; selectPlanet(name); } });选中行星后可以使用controls.target.copy(planet.position)让相机围绕目标行星旋转而不是继续围绕太阳。这里的坑是OrbitControls的 target 在每帧更新位置后会失效如果不持续跟随相机视角会一直飘回太阳中心。解决方式是用一个跟随标志位function selectPlanet(name) { followingName name; } // 在 animate 循环中 if (followingName) { const p planets[followingName].mesh; controls.target.copy(p.position); }6.3 开放一个“时间尺度”滑块并让不同行星的自转速度同步如果你把timeScale暴露成 UI 参数注意公转速度的timeScale与自转速度的timeScale必须保持一致否则会出现“自转和公转脱节”——比如地球一小时内自转了好几圈但公转位置没动多少视觉上看起来很诡异。我常在updateRotations里读取同一个timeScale而不是传一个写死的自转速度。关于设备适配最后提一个高频经验在手机端高分辨率纹理很容易导致内存溢出建议在移动端限制纹理最大尺寸比如 1024并通过renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))控制渲染尺寸。窗口 resize 事件里记得同步更新 camera 纵横比和 renderer 尺寸否则画面会拉伸。写到这里这个太阳系模拟器已经从静态场景进化成可交互的天文演示工具。真正把模拟做完后你会发现最有价值的不只是视觉效果而是调试轨道参数时被迫重新理解开普勒方程、自转轴倾角、以及坐标系转换的整个过程。以后再做带轨道运动的项目比如卫星轨迹、双星系统、空间站绕行这些经验都能直接迁移。最后提醒一句把轨道根数单独存成一份数据文件、是唯一能让你在不同项目里复用同一套太阳系方案的方式。希望这些文字对你做这个项目有帮助。本文还有配套的精品资源点击获取