简介基于 Three.js 构建的太阳系八大行星公转与自转三维模拟项目面向 Web 前端开发者和 3D 可视化初学者帮助理解 Three.js 场景搭建、球体模型、纹理贴图以及轨道动画的基本方法。压缩包内共 13 个文件核心为 1 个 HTML 入口页面、2 个 JavaScript 脚本Three.js 主库与 OrbitControls 交互控制器和 10 张行星太阳 PNG 纹理图片整体大小仅 1.39MB可快速下载并直接运行。项目通过修改几何体旋转属性实现各行星自转并按照椭圆轨道计算公转轨迹同时利用 OrbitControls 支持鼠标拖拽旋转、缩放和平移从任意角度观察整个太阳系行星纹理与轨道参数均可替换调整便于二次开发。目前已有 430 人学习/下载适合用作用户端 Three.js 入门实践、天文科普演示或课程作业参考完整源码与素材均包含在内方便对照学习与分析。1. 从二维平面到三维空间threejs 重建太阳系公转自转的第一公里做 3d threejs 太阳系模拟最大的门槛不是写代码而是理解三维空间里“运动”和“视觉”是两套系统。很多初学者把公转做成“行星绕太阳转”结果发现相机一转轨道全乱了还有人把自转做成“球体原地转”但转过 180 度后发现纹理方向反了。这套 threejs 太阳系八大行星公转及自转三维模型把轨道、公转、自转、纹理、光照拆成独立模块每一块都是直接可跑的代码。它解决的问题很明确给你一套“从零搭建到可交互”的完整流程包含轨道几何体、帧循环动画、纹理贴图、径向模糊光源处理等关键环节。适合已经会 basic scene 搭建、但想在运动机制和渲染细节上做得更专业的前端开发者和三维可视化爱好者。2. 三维场景搭建轨道、相机与光照的基础配置2.1 场景初始化与轨道平面绘制——用 LineLoop 画半径等差轨道太阳系模拟的第一件事是把“平面感”建立起来。八大行星的轨道虽然在真实空间里有微小倾角但做可视化时通常简化为同一个平面这样用户视角拉平后能一眼看清内外行星的分布关系。轨道本身不需要真实几何体用 LineLoop 画圆环就够了数据量小且不会遮挡行星。// 创建轨道线每条轨道用 128 个点拟合圆 function createOrbit(radius, color 0x444466) { 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); const material new THREE.LineBasicMaterial({ color }); return new THREE.LineLoop(geometry, material); } // 为每颗行星创建轨道并加入场景 planetData.forEach(item { const orbit createOrbit(item.distance); scene.add(orbit); });这里的 radius 是行星到太阳的距离单位可以随意定义但关键是“比例感”。如果完全按真实天文单位缩放内行星会挤成一团外行星又远到看不见。我一般把水星轨道半径设为 8金星 12地球 16火星 20木星 34土星 44天王星 58海王星 68这样既有层次感又不会让内行星距离太阳太近导致相机碰撞。LineLoop 比 Line 多了一个自动闭合首尾顶点的能力画圆轨道时刚好合适。颜色建议统一用低饱和度色避免抢了行星纹理的视觉权重。2.2 相机与控制器参数视角、阻尼和缩放边界相机是用户在三维空间里的“眼睛”它的参数直接决定了太阳系看起来是“宏大”还是“拥挤”。这里用的是 PerspectiveCamera视野角FOV设为 45 度比较接近人眼感知近裁剪面 0.1远裁剪面 1000否则离太阳太近时表面会被裁掉。const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 40, 80); camera.lookAt(0, 0, 0); const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.08; controls.minDistance 5; controls.maxDistance 300; controls.maxPolarAngle Math.PI / 2;OrbitControls 里最容易被忽略的是 enableDamping。如果关闭它鼠标拖动后视角会立刻停止手感非常“脆”打开阻尼后视角会带一点惯性滑动专业感立刻上来。maxPolarAngle 限制俯仰角防止用户转到太阳正下方导致视野全部被行星遮挡。minDistance 设置为 5 是为了防止相机穿进水星轨道内部后迷失方向。如果不设置这些边界用户用滚轮一路缩到太阳中心画面会变成一片亮白这一般是新手玩太阳系模拟时最常遇到的操作事故。2.3 光源选型点光源模拟太阳环境光解决背光面死黑太阳系里唯一的真实光源是太阳本身所以主光必须是 PointLight位置固定在原点。但问题在于点光源照射下行星背光面会完全黑掉看起来像剪影缺乏立体感。解决办法是加一个低强度的 AmbientLight 做全局提亮再加一个 DirectionalLight 从相机方向补光。const sunLight new THREE.PointLight(0xffffff, 2, 0, 0); sunLight.position.set(0, 0, 0); scene.add(sunLight); const ambientLight new THREE.AmbientLight(0x404060, 0.5); scene.add(ambientLight); const dirLight new THREE.DirectionalLight(0xffffff, 0.3); dirLight.position.set(10, 20, 10); scene.add(dirLight);这里 PointLight 的 intensity 设为 2是为了让木星、土星这类远轨道行星依然能被照到。AmbientLight 颜色用偏蓝的 0x404060 而不是纯白可以让暗部带一点冷色视觉上更像太空。DirectionalLight 的强度一定要低否则会出现两个影子方向破坏真实感。这个光源组合是太阳系可视化里的标配既保证主体受光明确又保留暗部细节是“像不像太空”的关键一步。3. 公转与自转机制从欧拉到矩阵的旋转实现3.1 公转速度与半径参数表真实比例的等比缩放问题公转动画的核心不是“转起来”而是“转得对”。真实太阳系里行星公转周期和轨道半径满足开普勒第三定律半径越大周期越长。但直接按真实秒数来跑水星一年 88 天海王星 165 年海王星根本看不出在动。所以要做时间尺度压缩。const planetData [ { name: 水星, distance: 8, period: 0.24, rotationSpeed: 0.005 }, { name: 金星, distance: 12, period: 0.62, rotationSpeed: -0.002 }, { name: 地球, distance: 16, period: 1.0, rotationSpeed: 0.02 }, { name: 火星, distance: 20, period: 1.88, rotationSpeed: 0.018 }, { name: 木星, distance: 34, period: 11.86, rotationSpeed: 0.04 }, { name: 土星, distance: 44, period: 29.46, rotationSpeed: 0.038 }, { name: 天王星, distance: 58, period: 84.01, rotationSpeed: 0.03 }, { name: 海王星, distance: 68, period: 164.8, rotationSpeed: 0.028 }, ];这张表里的 period 是地球年rotationSpeed 是自转弧度增量。公转角速度用 2π / (period * scale) 计算其中 scale 是一个全局时间缩放系数比如 1 表示“地球一秒转一圈”不太合适实际我用 scale 0.1让地球 10 秒完成一圈公转视觉效果最舒服。金星的自转方向是反向的所以 rotationSpeed 给了负数这是个容易藏彩蛋也容易忘掉的细节。3.2 自转轴的倾斜与方向rotation.z 与 Quaternion 的差别行星自转不是简单地绕 Y 轴转。真实行星的自转轴各有倾角地球约 23.5 度天王星约 98 度几乎是躺着转的。在 Three.js 里如果只改 rotation.y自转轴永远是垂直的无法表现“倾斜自转”。正确做法是先绕 Z 轴倾斜一次再在动画里累加 Y 轴旋转。// 创建行星网格时设置自转轴倾角 const mesh new THREE.Mesh(geometry, material); mesh.rotation.z planetData[i].axisTilt * Math.PI / 180; scene.add(mesh); // 帧循环里累加自转角度 mesh.rotation.y delta * planetData[i].rotationSpeed * 0.5;这里要注意顺序Three.js 默认按 XYZ 顺序应用欧拉旋转先设置 rotation.z 会让整个球体先倾斜再旋转 Y 轴时实际上是绕着“已经倾斜后的局部 Y 轴”在转视觉上就是行星歪着头自转效果正确。但如果用 Quaternion 做旋转需要先构造一个表示自转轴方向的矢量再乘旋转角反而更麻烦。网上很多实现看起来“公转自转都正常但就是不够真实”十有八九是少了 axisTilt 这一步。天王星的 98 度倾角在数据里一眼就能看出来把它的 rotation.z 设大之后整个太阳系的立体感会明显不同。3.3 帧循环里的 deltaTime决定你动画会不会“一会快一会慢”如果直接在 requestAnimationFrame 里写angle 0.01帧率高的设备上行星会转得飞快帧率低的设备上又慢得像蜗牛。这个“平台差异”不是玄学而是没有使用 deltaTime。标准做法是在每次渲染时用时钟计算出两帧间隔再乘以速度系数。const clock new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta clock.getDelta(); planetGroup.children.forEach((planet, index) { if (!planet.userData.isSun) { // 公转角度累加 planet.userData.orbitAngle delta * (2 * Math.PI / (planet.userData.period * 10)); // 根据公转角度设置位置 planet.position.x Math.cos(planet.userData.orbitAngle) * planet.userData.distance; planet.position.z Math.sin(planet.userData.orbitAngle) * planet.userData.distance; // 自转 planet.rotation.y delta * planet.userData.rotationSpeed; } }); controls.update(); renderer.render(scene, camera); } animate();这里把行星全部挂在 planetGroup 下方便统一遍历。userData 是 Three.js 里挂自定义数据的标准位置比额外维护一个数组更不容易下标错位。公转角度用三角函数换算成 x/z 坐标比用 rotateY 旋转父节点更可控因为父节点旋转会影响所有子对象而直接算位置只影响当前行星。deltaTime 方案唯一需要注意的是首次调用 getDelta 时会返回从 Clock 创建到现在的总时长所以动画启动瞬间会有一次跳变但通常小于 50ms视觉上感知不到。4. 纹理与材质让行星从“白球”变成“木星”的关键细节4.1 六张纹理贴图与材质参数map、bumpMap、specularMap 怎么配行星表面光靠纯色是骗不了人的。木星的条纹、地球的大陆、土星的环都需要纹理贴图来完成。下载资源里如果带的是 DDS 格式注意 WebGL 支持度不如 JPG/PNG建议统一转成 JPG。材质方面用 MeshPhongMaterial 或 MeshStandardMaterial 都可以标准材质效果更好但开销更大。const textureLoader new THREE.TextureLoader(); // 以地球为例 const earthMap textureLoader.load(textures/earth.jpg); const earthBump textureLoader.load(textures/earth_bump.jpg); const earthSpec textureLoader.load(textures/earth_specular.jpg); const earthMaterial new THREE.MeshPhongMaterial({ map: earthMap, bumpMap: earthBump, bumpScale: 0.05, specularMap: earthSpec, specular: new THREE.Color(0x333333), shininess: 25 });bumpMap 是凹凸贴图它不改变几何体顶点只是在光照计算时改变法线方向让山地区域看起来有起伏。bumpScale 推荐 0.03~0.08太高会出现“搓衣板”效果太低又看不出来。specular 颜色设置成深灰色而不是白色否则海洋区域会像镜子一样反光。木星、土星这类气态行星不需要 bumpMap用 map 加轻微 emissive 提亮就行。火星和月球这类表面有明显地貌的bumpMap 的收益最高。4.2 行星环与卫星的简单实现环形几何体与父节点挂载土星环是太阳系模拟里“看起来难、做起来简单”的部分。用 RingGeometry 然后贴上带透明通道的环纹理再把环作为土星的子对象就能实现“土星转环也跟着转”的效果。// 创建土星环 const ringGeo new THREE.RingGeometry(1.4, 2.2, 64); const ringMat new THREE.MeshBasicMaterial({ map: textureLoader.load(textures/saturn_ring.png), side: THREE.DoubleSide, transparent: true, depthWrite: false }); const ring new THREE.Mesh(ringGeo, ringMat); ring.rotation.x Math.PI / 2; // 让环平面朝上 saturnMesh.add(ring);RingGeometry 的参数是内半径和外半径比例根据土星体大小动态调整。depthWrite: false 这一步很多人会漏掉它决定了环在挡在土星前后时半透明部分不会遮挡其他行星产生错误的深度排序。同理如果想加木星的伽利略卫星只需要在木星位置加一个小球体并设置它的公转半径挂在木星的子节点下即可这样木星动卫星跟着动不需要额外计算绝对坐标。4.3 轨道标签的视口大小保持CSS2DRenderer 的使用边界给行星加名字标签时如果用 Sprite 或 CSS3DRenderer标签要么是贴在三维空间里会随距离缩放要么是 DOM 元素无法跟随旋转。CSS2DRenderer 是这里的最佳解把 HTML 标签投射到三维坐标上但始终面向相机且保持视口大小不变。import { CSS2DRenderer, CSS2DObject } from three/addons/renderers/CSS2DRenderer.js; const labelRenderer new CSS2DRenderer(); labelRenderer.setSize(window.innerWidth, window.innerHeight); labelRenderer.domElement.style.position absolute; labelRenderer.domElement.style.top 0px; labelRenderer.domElement.style.pointerEvents none; document.body.appendChild(labelRenderer.domElement); // 为每颗行星创建标签 function createLabel(text) { const div document.createElement(div); div.textContent text; div.style.color #fff; div.style.fontSize 12px; div.style.fontFamily sans-serif; return new CSS2DObject(div); }CSS2DRenderer 的边界在于它生成的 DOM 节点不受 Three.js 的 z-buffer 控制当行星在太阳背后时标签依然可见。这个问题在“标签穿透”场景很难受。我一般会在行星不朝向相机时设置 label.visible false或者在行星与太阳之间做射线检测再决定是否显示。另一个注意点是 CSS2DRenderer 必须在渲染循环里单独调用 render它和 WebGLRenderer 是两个独立的画布。5. 避坑指南太阳系模拟最容易翻车的五个细节5.1 现象行星纹理方向不对赤道附近出现“接缝撕裂”原因纹理贴图在 UV 展开时边缘像素没有正确包裹。尤其是 JPG 格式的非无缝纹理在最右侧边缘和最左侧边缘颜色不一致时接缝就会明显。解决在 TextureLoader 加载后设置 texture.wrapS THREE.RepeatWrapping并将 repeat.x 设为 1.0让纹理首尾采样连续。如果贴图本身是标准等距柱状投影Equirectangular这一招基本能消除接缝。设置后要重新调用 material.needsUpdate true 才会生效。5.2 现象公转速度在帧率高的电脑上明显更快原因直接把角度增量写在 requestAnimationFrame 里没有用 deltaTime 做归一化。解决统一使用 Clock.getDelta() 作为时间增量所有角速度都以“弧度/秒”为单位而不是“弧度/帧”。这是开篇第 3.3 节的方案实测在 60Hz 和 144Hz 显示器上动画速度完全一致。5.3 现象太阳光点光源位置正确但行星背光面全黑原因PointLight 的 intensity 对远距离物体的衰减过于剧烈外圈行星收到的光几乎为零。解决把 PointLight 的 distance 设为 0表示无限远decay 设为 0 或 1。注意 Three.js r150 之后 decay 默认是 2物理衰减会让光强按距离平方衰减太阳系这种大尺度场景必须手动调低。也可以像 2.3 节那样增加环境光和方向光做辅助照明。5.4 现象轨道线在相机拉远后闪烁或消失原因LineBasicMaterial 默认启用了 depthTest轨道线和行星距离过近时深度冲突表现为闪烁。解决给轨道材质设置 depthTest: false 和 transparent: true让轨道始终渲染在行星之下。同时渲染顺序设 renderOrder -1避免轨道覆盖行星主体。这个方法也适用于 UI 辅助线。5.5 现象自转轴设置后公转轨迹开始歪斜原因把自转轴倾斜写在了行星的父级 Group 上父 Group 的 rotation.z 影响了子对象的位置计算导致公转平面被带歪。解决自转轴倾斜必须写在 Mesh 本身上公转位置永远放在 Group 或直接放在场景坐标里。两者分离才能避免“自转轴影响公转轨道”的脏数据污染。这也是 3.2 节里强调父子层级管理的核心原因。6. 进阶调试dat.GUI 控制面板与 LOD 动态降面当太阳系跑起来之后下一步是把它变成“可操作”的演示项目。dat.GUI 是最常用的控制面板库用它来调节公转速度、自转速度、轨道显示开关、相机跟随目标等比改代码刷新浏览器高效得多。import * as dat from dat.gui; const gui new dat.GUI(); const params { speedScale: 0.1, showOrbits: true, followPlanet: none }; gui.add(params, speedScale, 0, 2, 0.01).name(公转速度系数); gui.add(params, showOrbits).name(显示轨道).onChange(v { orbitGroup.visible v; }); gui.add(params, followPlanet, [none, 地球, 火星, 木星]).name(相机跟随);在动画循环里读入 params 覆盖原有速度参数然后当 params.followPlanet 不是 none 时让相机每帧 lookAt 对应行星的位置。注意相机跟随行星时必须把 camera.position 也一起移动否则只 lookAt 会导致视角“扭曲”——位置没变、视角在追画面会像被拉扯一样。性能优化方面考虑 LODLevel of Detail。行星在远处观察时64 段球体高精度几何体完全浪费 GPU。用 THREE.LOD 对象在视距小于 30 时用 32 段球体大于 30 用 16 段球体能稳掉 30% 以上帧率开销。function createPlanetWithLOD(radius, detailHigh, detailLow) { const lod new THREE.LOD(); const highGeo new THREE.SphereGeometry(radius, 64, 64); const lowGeo new THREE.SphereGeometry(radius, 16, 16); lod.addLevel(new THREE.Mesh(highGeo, material), 0); lod.addLevel(new THREE.Mesh(lowGeo, material), 30); return lod; }这个太阳系模拟项目的资源结构里textures 目录下已经按行星名分好了贴图代码里也预留了 planetData 数组作为增删行星的入口。我第一次跑通时最大的教训就是“先调光照再调纹理”——光照不对时纹理再高清都是废的。从那以后我每次做三维可视化都强制走一遍“光源三件套、轨道辅助线、相机边界、LOD 开销”四个检查点五分钟内就能定位问题出在场景层还是渲染层。希望帮到你。本文还有配套的精品资源点击获取