简介:基于微软三维图形接口与可视化C++开发环境打造的太阳系模拟工程,以轨道、自转、公转三大天文运动为主线,实现了摄像机视角控制、行星自转与绕日公转、椭圆轨道绘制、表面纹理映射以及宇宙天空盒渲染等完整功能,面向三维图形编程学习者和游戏开发入门者。工程包共有109个文件,包含C++源代码、可执行演示程序、纹理图片以及Visual Studio工程配置,整体压缩后仅14.2MB,便于下载后在本地直接运行与对照调试。已有547人学习浏览,代码按摄像机、几何体绘制、轨道计算等模块清晰组织,并结合可执行程序阅读,能够帮助理解三维场景构建、矩阵变换、交互控制以及天文运动模拟的具体实现,是一份兼具教学示范和二次开发价值的实用样例。整体功能完整、结构清晰,很适合作为入门实践的综合性参考。
1. D3D太阳系:轨道、自转、公转是同一套坐标变换
地理课上老师转着那个电动的太阳系模型,说“地球绕太阳转的同时自己也在转”,台下没几个同学真的看懂。直到你把套 D3D 太阳系(轨道自转公转)工程跑起来,摄像机拉到斜上方,看火星一边沿椭圆轨道公转、一边缓慢翻转地表纹路,你才会意识到:公转和自转不是两个动画,是同一个坐标变换系统里先后施加的两步操作。这份资源是一份基于 Direct3D 的 3D 太阳系模拟程序,行星沿轨道公转,自身绕轴自转,相机视角可自由拖拽。它适合刚学完 D3D 初始化和网格绘制的人拿来练手,适合做课程演示素材的同行,也适合想弄明白“自转叠加公转”矩阵顺序的从业者。下面我按数学建模、D3D 场景搭建、动画时钟、避坑排查、性能验证的顺序拆这套工程。
2. 把天体运动写成矩阵:公转椭圆、自转轴与尺度归一化的工程选择
很多资料把太阳系模拟写成“画八个圆,转起来就完事”。但真要让轨道看起来像回事,得回答三个问题:轨道为什么不是正圆,自转轴朝向哪,真实天文数字怎么塞进 D3D 的 float。这三个问题全落在矩阵和单位换算上,任何一个偷懒,最后画面都会露馅。
2.1 公转轨道:用半长轴与离心率描述椭圆轨迹
行星的真实轨道是椭圆,太阳在椭圆一个焦点上。直接套圆轨道模拟也不是不行,只是所有行星的远近点变化会消失,演示效果会平。工程里常见做法是用椭圆极坐标方程,输入公转角,输出行星相对太阳的位置:
// 行星公转位置计算,angle 为当前公转角(弧度) XMFLOAT3 OrbitPosition(float angle, float semiMajorAxis, float eccentricity, float orbitTilt) { // 椭圆极坐标方程:r = a(1 - e^2) / (1 + e * cos(theta)) float r = semiMajorAxis * (1.0f - eccentricity * eccentricity) / (1.0f + eccentricity * cosf(angle)); XMFLOAT3 pos; pos.x = r * cosf(angle); pos.z = r * sinf(angle); pos.y = 0.0f; // 轨道平面倾斜:绕 X 轴旋转 orbitTilt float cosTilt = cosf(orbitTilt); float sinTilt = sinf(orbitTilt); float oldZ = pos.z; pos.z = oldZ * cosTilt; pos.y = -oldZ * sinTilt; return pos; }这段代码的逻辑核心在极坐标方程:公转角angle随时间累加,而r会随着 angle 变化,行星在近日点跑得快、远日点跑得慢的视觉效果,就差这一步。轨道参数里,semiMajorAxis是半长轴,控制轨道整体大小;eccentricity是离心率,为 0 就是正圆,越大椭圆越扁;orbitTilt是轨道平面与基准面的夹角。我一般把半长轴放在 5 到 80 的范围内,离心率调在 0.02 到 0.2 之间。想要真实感,把地球的 e 设成 0.0167,肉眼几乎看不出来;想给学生展示“轨道是椭圆”,调到 0.1 到 0.25 比较直观,这是演示项目里很常见的取舍。
调用方只需要把 angle 累加:
orbitAngle += omega * dt; // omega 为公转角速度,单位弧度/秒 XMFLOAT3 planetPos = OrbitPosition(orbitAngle, a, e, orbitTilt);这里的omega建议由“公转一圈多少秒”反推出来:omega = 2π / yearInSeconds。直接写死一个角度增量会出现一个隐蔽问题:不同帧率下运转速度不一致,这点在第 5 章还会专门展开。
2.2 自转叠加公转:矩阵乘法顺序决定行星是自转还是翻滚
自转的数学难度不在转动本身,而在它和公转怎么组合。D3D 用行向量左乘矩阵,顶点变换的顺序是“最左边的矩阵先作用”。因此一套稳妥的世界矩阵链是:先缩放,再自转,最后平移到公转位置。
// 行星世界矩阵:半径缩放 -> 自转 -> 平移到公转位置 XMMATRIX world = XMMatrixScaling(radius, radius, radius); world = XMMatrixMultiply(world, XMMatrixRotationY(selfAngle)); world = XMMatrixMultiply(world, XMMatrixTranslation(pos.x, pos.y, pos.z));这里每一步都不能换位置。如果把平移放到最前面,相当于把行星先挪到轨道上再绕世界坐标系的 Y 轴旋转,那行星就不是自转,而是绕着太阳系中心点翻滚,整个动画会变成“陀螺式乱转”。这个错误我见过不止一次,而且画面特别容易误导人:单颗行星原地高速翻滚,轨道却完全正确,新手经常检查半天调不回来。
如果还想模拟真实行星的自转轴倾斜,比如地球的 23.44 度倾斜,做法是在旋转矩阵链里先做一次轴倾斜旋转,再做绕自身 Y 轴的自转:
world = XMMatrixMultiply(world, XMMatrixRotationX(axisTilt)); // 轴倾斜 world = XMMatrixMultiply(world, XMMatrixRotationY(selfAngle)); // 自转参数上,selfAngle每秒增加的角度也不该拍脑袋。自转周期如果是 24 小时,压缩到演示场景里一般是 4 到 60 秒转一圈,超过这个范围,人眼就会分不清“自转”还是“抖动”。把自转周期和公转周期单独存在参数表里,比散落在代码里好维护得多。
2.3 尺度归一化:真实天文数字放进去会直接黑屏
太阳系的真实数据单位是千米和秒,直接代入 D3D 必然翻车。太阳到水星约 5800 万公里,行星半径几千公里,这个比例下,所有行星在屏幕上连一个像素都不到,相机近裁面稍微设置错,画面就是纯黑。工程上必须做尺度归一化,把轨道距离和半径都压到成百上千的显示单位,时间维度再单独压缩。
| 物理量 | 真实值示例 | 工程折算目标 |
|---|---|---|
| 轨道距离 | 水星 0.39 AU(约 5800 万公里) | 6~12 内部单位 |
| 行星半径 | 岩石行星几千公里 | 0.3~2.0 内部单位 |
| 公转周期 | 地球 365.25 天 | 压缩到 5~20 秒一圈 |
| 自转周期 | 约 24 小时 | 压缩到 3~60 秒一圈 |
同一套代码里,轨道距离和行星半径必须遵循同一个内部单位系统。我习惯把“1 内部单位 = 约 0.1 AU”,然后把行星半径乘一个 10 到 50 的放大系数,让球体在轨道上至少占 3 到 5 个像素。时间压缩用时间倍率timeScale统一控,比如默认 1 表示 1 秒模拟半天,按住某个键临时放大到 10 倍,这样演示时既能看细节也能看全局。以上准备都是纯逻辑层的,跑不出画面,真正的 D3D 渲染搭建从相机和球体开始。
3. 搭起 D3D 场景:透视相机、球体网格与光照材质的默认参数
这一章解决“怎么让行星在屏幕里长得像个球”。D3D 场景搭建本身不算难,但太阳系场景的特殊性在于:空间跨度很大,相机要既能全局俯视又能拉近看单颗行星,球体网格要兼顾顶点数和贴图精度,光照还要让太阳“自己发光”而不是被自己照成黑球。
3.1 相机与投影:近远裁剪面怎么给才能不丢行星
太阳系相机最常出问题的地方是投影矩阵的近裁剪面和远裁剪面。远裁面给小了,外圈行星到某个角度突然消失;近裁面太近,深度缓冲精度会浪费在相机附近,导致远景星星闪烁。下面是一组我常用来跑通全局视角的参数:
// 环绕相机的视点:azimuth 水平角,elevation 仰角 float eyeDist = 120.0f; float eyeX = eyeDist * sinf(azimuth) * cosf(elevation); float eyeY = eyeDist * sinf(elevation); float eyeZ = eyeDist * cosf(azimuth) * cosf(elevation); XMFLOAT3 eye(eyeX, eyeY, eyeZ); XMFLOAT3 target(0.0f, 0.0f, 0.0f); // 看向太阳位置 XMMATRIX view = XMMatrixLookAtLH( XMLoadFloat3(&eye), XMLoadFloat3(&target), XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f) ); XMMATRIX proj = XMMatrixPerspectiveFovLH( XMConvertToRadians(60.0f), aspectRatio, 0.1f, // 近裁剪面 5000.0f // 远裁剪面 );eyeDist决定相机离太阳多远,120 在轨道最外圈 80 的情况下正好能把整个黄道面收入视野。elevation建议 25 到 45 度之间,太低了行星互相遮挡,太高了又看不出公转平面。FOV 60 度是 D3D 项目最常见的默认值,aspectRatio用窗口宽高比。近裁剪面 0.1 是为了拉近到某颗行星表面时不把球体切掉;远裁剪面 5000 是为了当外圈行星转到远侧时不会突然消失。
提示:近裁剪面和远裁剪面的比值不要超过 10000。在这个场景下如果还出现远处的星星闪烁,优先把近裁剪面调到 0.5,而不是盲目调大远裁剪面。
3.2 球体生成:经纬网格、单位法线和纹理坐标一起算
行星网格我一般直接用代码生成球体,不加载外部模型,因为球体在数学上太规整了。用经纬分段生成顶点,顺带把单位法线和 UV 坐标全算好:
void BuildSphere(float radius, int segX, int segY, std::vector<VertexPNT>& vertices, std::vector<uint16_t>& indices) { for (int y = 0; y <= segY; ++y) { float phi = XM_PI * (float)y / segY; // 纬度 0~PI float sp = sinf(phi), cp = cosf(phi); for (int x = 0; x < segX; ++x) { float theta = XM_2PI * (float)x / segX; // 经度 0~2PI VertexPNT v; v.pos = { radius * sp * cosf(theta), radius * cp, radius * sp * sinf(theta) }; v.normal = { sp * cosf(theta), cp, sp * sinf(theta) }; v.uv = { (float)x / segX, 1.0f - (float)y / segY }; vertices.push_back(v); } } for (int y = 0; y < segY; ++y) { for (int x = 0; x < segX; ++x) { int i0 = y * segX + x; int i1 = y * segX + (x + 1) % segX; // 经度方向绕回 int i2 = (y + 1) * segX + x; int i3 = (y + 1) * segX + (x + 1) % segX; indices.push_back(i0); indices.push_back(i1); indices.push_back(i2); indices.push_back(i1); indices.push_back(i3); indices.push_back(i2); } } }顶点法线直接用单位位置向量是球体专属福利:球心在原点时,位置向量归一化就是法线。这样省掉了从三角面插值法线的整套计算,也不容易出现硬边。UV 里我把 V 方向做了翻转,因为 D3D 纹理坐标的 V 轴方向跟图像上下是反的。segX = 64、segY = 32时顶点约 2000 个,集成显卡跑几十个球都不吃力;想贴高分辨率纹理时再提高到 128×64。
3.3 光照与材质:让太阳自己发光,让行星被太阳照亮
太阳系场景的光照有个天然矛盾:太阳既是光源又是被渲染的物体。如果太阳也走标准光照计算,它会被自己的点光源“照亮”,结果可能是黑色或者过曝。常见做法是两步走:太阳用自发光材质,不参与光照;其他行星用方向光加环境光模拟太阳照明。
// 像素着色器思路(HLSL 伪代码) float3 worldNormal = normalize(IN.normal); float diffuse = saturate(dot(worldNormal, SunDirection)); float3 finalColor = baseColor.rgb * (ambientColor + sunColor * diffuse);SunDirection建议从太阳位置指向行星表面,而不是用一个全场景统一的方向光。每颗行星的像素着色器拿世界坐标算normalize(sunPos - worldPos),光照方向会更准确。环境光强度 0.2 到 0.4,太阳光强度 0.6 到 0.8,这样背光面不至于死黑。另一个容易被忽略的参数是 Material 的高光强度:岩石行星高光设为 0,气态巨行星给一点点高光就能看出大气感,但这属于观感微调,放在所有轨道跑通之后再做。
4. 让转速稳定下来:时钟累积器、场景节点与行星参数表
画面能出球之后,就要让行星动起来。这一章的坑不在 D3D,而在“时间”本身。你写angle += speed跑一帧确实转了,但换个刷新率、掉个帧,速度立刻乱掉。行星之间的挂载关系也直接用世界坐标硬算的话,等你想加月球的时候会想骂人。
4.1 帧率无关的模拟时钟:为什么不能每帧直接累加角度
游戏主循环里常见的错误是直接在每帧渲染时更新公转角:angle += 0.01f。这在 60Hz 显示器上看起来正常,换到 144Hz 高刷屏就快了一倍多;帧率波动时行星忽快忽慢。正确做法是用真实流逝时间驱动模拟,并用固定步长累积器把物理更新和渲染帧率解耦:
float elapsed = 0.0f; while (running) { float dt = frameTimer.MeasureSeconds(); // 本帧真实流逝时间,单位秒 elapsed += dt; const float step = 1.0f / 60.0f; int steps = 0; // 把累积时间切成一格格固定步长,最多补 4 帧,防止卡顿后追帧爆炸 while (elapsed >= step && steps < 4) { UpdateSimulation(step); elapsed -= step; ++steps; } if (steps >= 4) elapsed = 0.0f; // 掉帧严重时放弃旧时间,避免螺旋式追帧 Render(); }逻辑说明:MeasureSeconds()返回真实秒数,elapsed累积后按固定步长消费。这样无论显示器是 60Hz 还是 144Hz,物理更新始终按照 1/60 秒步长执行,行星公转速度只跟UpdateSimulation(step)里的秒数有关。steps上限设 4,是为了防止程序卡住几秒后恢复时,while 循环疯狂补齐几十帧模拟,直接让所有行星瞬移一大截。掉帧后的旧时间直接丢弃,视觉上损失有限,逻辑上干净利落。
4.2 场景节点:把月球挂在地球上,而不是直接挂在太阳上
太阳系是一个天然的树状结构:行星绕着太阳转,卫星绕着行星转。如果所有天体都用世界坐标算位置,月球就得写成地球位置加月球轨道偏移,一旦地球轨道参数变了,月球位置要跟着改,用不了几天代码就成了一坨不能维护的算术。正确做法是利用矩阵嵌套,让月球的世界矩阵从地球的世界矩阵继承下来:
// 地球世界矩阵:半径缩放 -> 自转 -> 平移到公转位置 XMMATRIX earthWorld = XMMatrixScaling(EARTH_R, EARTH_R, EARTH_R); earthWorld = XMMatrixMultiply(earthWorld, XMMatrixRotationY(earthSpin)); earthWorld = XMMatrixMultiply(earthWorld, XMMatrixTranslation(earthX, 0.0f, earthZ)); // 月球本地坐标:先做一个小距离平移,再乘地球世界矩阵 XMMATRIX moonWorld = XMMatrixScaling(MOON_R, MOON_R, MOON_R); moonWorld = XMMatrixMultiply(moonWorld, XMMatrixRotationY(moonSpin)); moonWorld = XMMatrixMultiply(moonWorld, XMMatrixTranslation(MOON_ORBIT_R, 0.0f, 0.0f)); moonWorld = XMMatrixMultiply(moonWorld, earthWorld);关键在于最后一行:moonWorld先构造出“相对地球的本地平移”,再左乘earthWorld。这样月球会继承地球的公转位置、自转姿态和缩放。MOON_ORBIT_R是月球相对地球的轨道半径,这个值只跟地月系统有关系,地球轨道放大缩小都不影响月球挂载。真实月球被潮汐锁定,自转周期等于公转周期,演示时直接把moonSpin设成 0,就自带潮汐锁定效果。如果你把乘法顺序写成earthWorld * moonWorld,D3D 行主序下月球会被先挪到地球位置再执行本地平移,结果完全不对,这是从 DX9 时代就流传下来的一个经典错位。
4.3 行星参数表:用数据驱动替代十份相似逻辑
三四个行星时,每个行星写一段if还能忍;八个行星加卫星之后,重复代码会淹没真正的数学逻辑。我习惯把所有天体的轨道和外观参数集中到一张静态表里,用一份 Update 循环驱动:
struct PlanetDesc { const char* name; // 用于调试日志 float orbitRadius; // 内部单位,轨道半长轴 float eccentricity; // 离心率,0 为正圆 float yearInSeconds; // 公转一圈的演示时间 float dayInSeconds; // 自转一圈的演示时间 float radiusScale; // 渲染半径(已放大) }; static PlanetDesc planetTable[] = { { "水星", 7.0f, 0.206f, 6.0f, 90.0f, 0.8f }, { "金星", 10.0f, 0.007f, 9.0f, 240.0f, 1.1f }, { "地球", 14.0f, 0.017f, 12.0f, 10.0f, 1.0f }, { "火星", 19.0f, 0.093f, 18.0f, 11.0f, 0.7f } // 外圈行星按轨道距离递增继续补 };参数表驱动的好处是:公转周期、自转速度、轨道半径三者独立可调,不需要在代码里翻if分支。全局时间倍率可以统一放在UpdateSimulation(step)里:
void UpdateSimulation(float step) { float realStep = step * timeScale; // timeScale 默认 1.0 for (int i = 0; i < planetCount; ++i) { float omega = XM_2PI / planetTable[i].yearInSeconds; orbitAngle[i] += omega * realStep; float spinOmega = XM_2PI / planetTable[i].dayInSeconds; spinAngle[i] += spinOmega * realStep; // 更新轨道位置与世界矩阵,绘制时使用 } }这里timeScale是全局时间倍率,按住空格临时放大到 8 倍,松开恢复 1,用来快速看外圈行星的轨道形态。注意realStep由 step 推导,不要在每帧渲染处单独乘timeScale,否则帧率相关的抖动问题会重新出现。
5. D3D 太阳系避坑:五个跑出黑屏和乱转的常见原因
这个工程劝退人的地方不在 D3D 初始化,而在几个特别隐蔽的细节。我自己调试早期版本时,把下面五类问题全部踩了一遍。每条按现象、原因、解决来写,方便你在项目里直接对照排查。
5.1 行星全部消失或只有太阳一闪而过
现象:程序跑起来,窗口一片漆黑,偶尔能看见一个高亮的圆形物体掠过,但八颗行星一颗都找不到。输出日志发现行星坐标动辄几千万,相机却站在 120 的位置。
原因:直接把真实天文数字当内部单位用。水星轨道半长轴 5800 万公里,相机近裁剪面 0.1、远裁剪面 1000 的情况下,整个太阳系都被裁剪掉了;就算把相机拉远到能容纳轨道,行星半径几千公里在几千万公里的距离边上连一个像素都占不到。
解决:统一按 2.3 节的尺度归一化方案:轨道距离 5 到 80,行星半径 0.3 到 2.0,近裁剪面 0.1,远裁剪面 5000。排查阶段先把纹理和光照全去掉,用纯白色球体跑,能在画面上看到八个白球,再逐步加贴图。这个排障习惯能帮你区分“位置错误”和“视觉问题”。
5.2 换了一台机器,公转速度全变
现象:在自己电脑上地球 12 秒绕一圈,拿到高刷新率显示器上变成 8 秒圈,再拿到一台老笔记本上又慢得离谱。
原因:公转角度在每帧的Update里直接写死了增量,比如orbitAngle += 0.01f。同样 12 秒,60Hz 下累计 720 帧,144Hz 下累计 1728 帧,转的总角度差了 2.4 倍。用dt乘以固定速度也不能完全解决问题,因为dt本身波动时,行星会一卡一顿。
解决:用 4.1 节的固定步长累积器,物理更新只认step,不认渲染帧率。UpdateSimulation(step)里所有角速度都写成“弧度/秒”,再乘realStep。一次排查还要顺手检查:有没有既在累积器里更新了一次轨道角,又在渲染处为了特效额外加了一次小角度旋转,双重叠加很容易漏在代码审查里。
5.3 自转方向正常,公转方向反了
现象:行星自身从左往右转是对的,但所有轨道环绕方向和预期相反。从 Y 轴正方向俯视,本该逆时针公转的系统在顺时针转。
原因:D3D 的左手坐标系和数学课上常用的右手坐标系在旋转正方向上定义相反;或者公转角度增量写成了angle -= omega * dt,两个问题导致的画面现象一样。还有一种情况:移植自右手坐标系代码时,只改了相机矩阵没改旋转逻辑。
解决:先在场景里放一颗内圈行星,轨道半径设到 10 左右,人为把omega设成很小,比如 0.2 弧度/秒。从 Y 轴正方向向 Y 轴负方向俯视,期望的转动方向是逆时针。如果整体反了,统一把omega取负值,不要只改某几颗行星,否则轨道交叉时视觉上会非常奇怪。更稳妥的做法是给轨道画一条轨迹线,直接看行星在轨迹上走向,别凭感觉判断。
5.4 球体表面一条经线亮缝,怎么调都消不掉
现象:行星表面从北极到南极有一条明显的亮线或颜色断层,球体旋转到某个角度时尤其扎眼。换纹理、换过滤方式都还在。
原因:UV 接缝问题。球体在经度 0 度和 360 度处是同一个空间位置,但纹理坐标一个是 0,一个是 1。如果网格生成时让这两个顶点共用同一个顶点缓冲项,光栅化会在这条接缝处插入错误纹理坐标,产生一条跨度极大的三角带。D3D 的 Wrap 采样器有时能遮住,但 mipmap 层级一变化,问题就又冒出来。
解决:最简单的是用 3.2 节那种x < segX的循环,让每行最后一个顶点 U 值落在 0.98 而不是 1.0,并用 Wrap 采样。如果纹理精度要求高,需要在接缝处复制顶点:同一空间位置,一个顶点 UV 设为 0,另一个设为 1,这两组顶点用不同的索引访问,把接缝两侧的三角形分开。我通常默认用第一种,只有贴图纹理边缘带着明显色标时才改成复制顶点方案。
5.5 光照模式下行星一半纯黑,高光闪烁
现象:开启方向光后,行星表面出现一道明显的明暗分界,暗面死黑;旋转视角时高光点跳动,像锯齿一样。
原因:顶点法线没有归一化,或者法线从模型空间变换到世界空间时用错了矩阵。球体网格如果带了非均匀缩放,比如 X 轴拉伸成椭球,直接拿世界矩阵乘法线会导致法线方向扭曲;D3D 光照模式下,单位法线是光计算的默认前提,长度不对,漫反射就会在saturate(dot())附近抖动。
解决:球体模型如果没有非均匀缩放,直接把原法线归一化再乘旋转矩阵就行;如果带了压扁椭球建模,必须用法线矩阵,即世界矩阵的逆转置。着色器里再安全一道:
worldNormal = normalize(mul(normal, (float3x3)gWorldInvTranspose));光照方向也要保证是单位向量,最好在 CPU 端就XMVector3Normalize好再传进常量缓冲。排查时把环境光临时调到 1.0,如果明暗断层消失,那基本可以锁定法线变换问题。
6. 进阶验证与性能调优:轨道曲线、秒表测周期与 Draw Call 优化
轨道和自转都正常之后,下一步是证明它们真的正确,而不是“看起来在转”。这部分我习惯加两条验证路径:一条画轨道轨迹线,一条拿秒表测公转周期。两者都跑通了,再去做性能优化也不迟。
轨道轨迹线在 debug 阶段帮了大忙。把行星每帧位置追加到一个点数组里,用 LineStrip 绘制出来,椭圆轨道和太阳焦点的关系一眼就能看出来:
// 启用轨道追踪时,每帧把行星位置推进轨迹缓冲 void CollectOrbitTrace(XMFLOAT3 planetPos) { if (!orbitTraceEnabled) return; tracePoints.push_back(planetPos); if (tracePoints.size() > kMaxTracePoints) { tracePoints.erase(tracePoints.begin()); } }kMaxTracePoints建议 1024 到 2048,行星跑完一圈后轨迹线会自动形成完整椭圆。内侧行星走完一圈时,外侧行星的轨迹还没闭合,缓存太小会截断轨迹。用秒表验证周期也很直接:把某颗行星的yearInSeconds设为整数 10 秒,等它经过轨道上一个固定标记点时按下秒表,跑完一圈再按一次,读数应在 10 秒正负 0.1 秒内。如果偏差大,先检查timeScale是否被意外修改,再看公转角有没有在累积器里重复累加。
验证通过后,下一步值得做的优化是把八颗行星的绘制合并成一次实例化调用。所有行星共用同一个球体网格,每颗行星只是世界矩阵、纹理和颜色不同,非常适合DrawIndexedInstanced:
// 每个行星一条实例记录:世界矩阵 + 颜色乘数 struct PlanetInstance { XMFLOAT4X4 World; XMFLOAT4 Color; }; // 填充 instanceBuffer 后,一次绘制提交所有行星 // ctx->DrawIndexedInstanced(sphereIndexCount, planetCount, 0, 0, 0);实例化之后,行星数量从 8 个变成 8 个实例,Draw Call 从“背景 1 + 行星 8”降到 2 次,CPU 开销大幅减少。要注意的是:行星离相机特别近时,盖上所有行星用一个网格精度会浪费性能,这种场景可以单独给最近的行星再画一个高细分网格,其余行星继续实例化。优化完建议留一张检查表自查:
| 检查项 | 参考目标 | 超标处理 |
|---|---|---|
| Draw Call 数量 | 低于 20 | 行星改实例化渲染 |
| 单个球体顶点数 | 8000~20000 | 降低经纬分段数 |
| 纹理内存总量 | 64MB 以内 | 转 DDS 压缩纹理 |
| CPU 每帧矩阵运算 | <0.5ms | 轨道位置预计算成查询表 |
这套验证流程走完,剩下的就是手感上的微调:自转速度往哪个方向拧看起来更自然、高光强度多少不刺眼。从那以后,我每次拿到 3D 模拟类源码,不管看上去多简单,都会强制先关掉纹理和光照,用白色网格把坐标空间和轨道线打出来,确认尺度、旋转方向都没问题,再逐层把纹理、光照和动画加上去。这个习惯看着慢,实际省掉的是反复翻车的时间。希望帮到你——把这套 D3D 太阳系源码下载下来跑一遍,按上面的清单检查,轨道和自转会比你想象中更早稳定下来。
本文还有配套的精品资源,点击获取