资讯动态

D3D太阳系实战:矩阵变换与固定管线实现3D自转公转

发布时间:2026/10/16 9:09:23 来源:尧图企业网站定制
简介一套基于Direct3D 9.0实现的太阳系三维可视化工程面向具备一定C基础的3D图形编程学习者演示了摄像机动态控制、行星自转公转、椭圆轨道、纹理贴图与天空盒渲染等核心技术的综合应用。压缩包共109个文件以cpp/h源码、DirectX工程配置与编译中间文件、可执行程序、纹理图片及调试符号为主整体体积约14.2MB便于直接运行与对照学习。工程基于Visual C 10开发源码模块结构清晰展示了如何将自转公转等物理规律转化为3D动画逻辑并包含完整可运行的exe程序。目前已有547人学习下载适合用于理解D3D9场景构建、动画控制及科学可视化的实践入门。1. D3D太阳系轨道自转公转一个几十行矩阵就能撑起来的 3D 演示第一次把这个 D3D 太阳系源码编译跑起来我原本以为它只是一个会转的彩色球套球结果按方向键拖拽视角时发现整个场景里每颗行星都在做两套独立运动绕太阳的公转和绕自身轴的倾斜自转Sun 本身还能闪烁。整套代码没用到一张物理引擎、没写模型文件球体全是运行时生成的网格轨道、公转、自转全靠矩阵乘法和时间系数在每一帧里拼出来。对刚接触 Direct3D 的人而言它是把「矩阵变换」这节最干的课变成看得见摸得着的活教材对想拿 3D 演示做 PPT 或教学工具的人来说它又是一个可以直接换成自己行星纹理再打包分发的轻量骨架。2. 项目架构与运行前提先搞清这份源码在管什么2.1 整个场景到底由哪些对象组成这个 D3D 太阳系从文件结构看走的是一条极简路线主程序负责创建窗口和轮询事件一个工具类封装 D3D 设备初始化剩下三个模块分别负责网格生成、天体参数和摄像机视角。拆开之后你看到的核心实体其实只有三个一颗固定在原点不断自旋的太阳它没有公转轨道世界变换就是缩放矩阵乘自转矩阵若干颗沿不同半径圆轨道公转的行星每颗行星有独立的轨道半径、公转速度、自转速度、行星半径和贴图路径一台可以被键盘拖拽的轨道摄像机它绕场景原点旋转让观察者能从任意角度看到各个行星的空间关系。比较容易被忽略的是这里的天体参数全部写死在一个常量表里并没有温度、引力、质量等物理属性。我拿到的这份源码行星的公转周期是按「看着舒服」来配的内侧行星转得快外侧行星转得慢并没有一比一还原真实太阳系的时间尺度。如果你想要真实比例改起来也很轻后面第五章我会直接给出替换参数的方法。2.2 设备初始化从 Win32 窗口到 Direct3D 9 的第一步项目选的是 Direct3D 9 的立即模式没有引入 HLSL 着色器文件视觉效果靠固定管线的光照、纹理和材质来完成。这种选择放在今天看反而适合教学你不需要理解一堆 Effect 状态块只要把设备创建出来设置好视图和投影矩阵就可以直接进入场景渲染循环。创建设备的回调函数在所有渲染发生之前执行常见做法是把它单独放在一个初始化函数里参数直接引用 Win32 窗口的句柄// d3d_app.cpp bool InitD3D(HWND hWnd, int width, int height) { g_pD3D Direct3DCreate9(D3D_SDK_VERSION); // 创建 D3D 对象 if (!g_pD3D) return false; D3DPRESENT_PARAMETERS pp { 0 }; pp.Windowed TRUE; pp.SwapEffect D3DSWAPEFFECT_DISCARD; // 丢弃后台缓冲区旧内容 pp.BackBufferWidth width; pp.BackBufferHeight height; pp.BackBufferFormat D3DFMT_A8R8G8B8; // 32 位 ARGB 后台缓冲 HRESULT hr g_pD3D-CreateDevice( D3DADAPTER_DEFAULT, // 默认显卡适配器 D3DDEVTYPE_HAL, // 硬件加速失败可降级 REF hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, // 软件顶点处理兼容性优先 pp, g_pDevice); if (FAILED(hr)) return false; D3DXMATRIX proj; D3DXMatrixPerspectiveFovLH(proj, D3DX_PI / 4.0f, (float)width / (float)height, 0.1f, 1000.0f); g_pDevice-SetTransform(D3DTS_PROJECTION, proj); return true; }这段代码里最值得留意的是D3DCREATE_SOFTWARE_VERTEXPROCESSING和D3DDEVTYPE_HAL的组合。许多老示例直接写硬件顶点处理结果在虚拟机、远程桌面或者一些老核显上直接创建失败强制软件顶点处理能让程序在兼容性最差的机器上也先跑起来代价仅仅是顶点计算慢一点点。对太阳系这种顶点数量只有几千个的场景这几乎不构成性能负担。投影矩阵用视场角 45 度、宽高比 4:3、近裁面 0.1、远裁面 1000这个远裁面正好覆盖太阳系轨道半径之和。如果你后期把轨道半径改大比如要让海王星跑到 200 单位外就需要同步调大远裁面否则行星会在飞入场景时瞬间消失这是一个非常经典的黑屏排查方向后面避坑章节还会再提。2.3 球体网格生成没有模型文件靠代码造星项目里所有行星和太阳都使用同一个运行时生成的球体网格。程序按经纬度切分球面把每一条经线、每一层纬线的顶点位置和贴图坐标算出来再用三角形索引把顶点连成曲面。单位球体的半径是 1实际大小交给世界矩阵里的缩放矩阵控制这样一颗球可以服务所有天体。// mesh_sphere.cpp bool CreateSphereMesh(int slices, int stacks, ID3DXMesh** outMesh) { // 顶点数 (经线数 1) * (纬线数 1) D3DVERTEXELEMENT9 decl[] { {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, {0, 12, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0}, D3DDECL_END() }; // 通常直接使用 D3DXCreateSphere 生成自定义网格是为了控制纹理接缝 return SUCCEEDED(D3DXCreateSphere(g_pDevice, 1.0f, slices, stacks, outMesh, NULL)); }如果只讲D3DXCreateSphere就有点过于省事因为D3DXCreateSphere生成的顶点包含顶点法线但没有分布足够的贴图坐标——它把球侧面展开成一张方形贴图但南北极会出现条纹挤压。为了贴上地球这类有经纬纹理的图片时不变形更稳妥的做法是自己构建顶点缓冲把纬度角映射到贴图的 V 轴把经度角映射到 U 轴并将纹理接缝放在球体背面。// mesh_sphere_custom.cpp struct PosTexVertex { D3DXVECTOR3 pos; D3DXVECTOR2 tex; }; void BuildSphereVertexBuffer(IDirect3DDevice9* dev, IDirect3DVertexBuffer9** vb, IDirect3DIndexBuffer9** ib, int slices, int stacks) { int vertCount (slices 1) * (stacks 1); int indexCount slices * stacks * 6; PosTexVertex* verts new PosTexVertex[vertCount]; int idx 0; for (int lat 0; lat stacks; lat) { float phi D3DX_PI * lat / stacks; // 纬度 0~PI float sinPhi sinf(phi); for (int lng 0; lng slices; lng) { float theta 2.0f * D3DX_PI * lng / slices; // 经度 0~2PI verts[idx].pos D3DXVECTOR3( sinPhi * cosf(theta), cosf(phi), sinPhi * sinf(theta)); verts[idx].tex D3DXVECTOR2((float)lng / slices, 1.0f - (float)lat / stacks); idx; } } // 索引连接这里省略了具体的三角连接赋值 // 它和普通 grid 建索引的方式完全一致每 4 个顶点组成两个三角形 }参数slices和stacks分别是经线和纬线分段数太阳可以取 48x48行星取 32x32肉眼已经分辨不出棱角。分段数再往上升顶点数按平方增长但视觉收益非常低还会让调试时三角形指向看不清。3. 核心渲染链路拆解一个请求从进入到返回的完整链路3.1 矩阵顺序自转、平移、公转到底应该谁先谁后太阳系的核心代码其实只有一段循环。每帧刷新时程序先拿当前时间算出公转角速度和自转角速度然后为每颗行星拼出一个世界矩阵。这里最容易翻车的是矩阵相乘顺序Direct3D 9 采用行向量约定顶点最后乘以矩阵第一个乘上去的矩阵作用在最里层。为了让行星先自转、再放到轨道上、最后绕太阳公转正确的写法是// render_scene.cpp D3DXMATRIX matScale, matSpin, matTrans, matOrbit, matWorld; float t timeGetTime() / 1000.0f; // 当前时间单位秒 for (int i 0; i PLANET_COUNT; i) { CelestialBody* body g_bodies[i]; D3DXMatrixScaling(matScale, body-size, body-size, body-size); D3DXMatrixRotationY(matSpin, body-spinSpeed * t); // 自转 D3DXMatrixTranslation(matTrans, body-orbitRadius, 0.0f, 0.0f); D3DXMatrixRotationY(matOrbit, body-orbitSpeed * t); // 公转 matWorld matScale * matSpin * matTrans * matOrbit; g_pDevice-SetTransform(D3DTS_WORLD, matWorld); g_pDevice-SetTexture(0, body-texture); g_pDevice-SetStreamSource(0, body-vertexBuffer, 0, sizeof(PosTexVertex)); g_pDevice-SetFVF(D3DFVF_XYZ | D3DFVF_TEX1); g_pDevice-DrawPrimitive(D3DPT_TRIANGLELIST, 0, body-triangleCount); }这段逻辑拆开解释matScale先把单位球体放大到行星实际尺寸matSpin让它在原地绕 Y 轴旋转这是自转matTrans把它从原点挪到离太阳orbitRadius的距离上最后matOrbit再绕 Y 轴旋转此时自转已经被带到了轨道位置整体形成绕太阳公转的效果。反过来如果把matOrbit放在matTrans前面行星就会在一个固定方向上来回摆而不是绕太阳划圈。spinSpeed和orbitSpeed是弧度每秒这里直接用时间乘角速度没有做积分修正所以只要机器帧率恒定运动就均匀。帧率抖动时行星运动会出现肉眼可感的顿挫改进办法是记录上一帧时间差用增量时间做更新而不是用全局时间做乘法。3.2 光照和材质为什么球看起来像一个均匀的色块如果直接把带纹理的球画出来不开光照整个球体会像一张平淡的贴图纸糊在球面上没有立体感。这份源码的做法是打开固定管线光照同时给天体设置一个白色反射材质保留贴图原来的颜色。// render_scene.cpp 中设置光照的片段 D3DMATERIAL9 mat; ZeroMemory(mat, sizeof(mat)); mat.Diffuse D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); mat.Ambient D3DXCOLOR(0.4f, 0.4f, 0.4f, 1.0f); mat.Specular D3DXCOLOR(0.2f, 0.2f, 0.2f, 1.0f); mat.Emissive D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f); g_pDevice-SetMaterial(mat); D3DLIGHT9 light; ZeroMemory(light, sizeof(light)); light.Type D3DLIGHT_DIRECTIONAL; // 方向光模拟太阳 light.Diffuse D3DXCOLOR(0.9f, 0.9f, 0.8f, 1.0f); light.Direction D3DXVECTOR3(0.3f, -0.6f, 0.8f); // 朝右下后方照射 D3DXNormalize((D3DXVECTOR3*)light.Direction); g_pDevice-SetLight(0, light); g_pDevice-LightEnable(0, TRUE); g_pDevice-SetRenderState(D3DRS_LIGHTING, TRUE);注意上面SetMaterial的Diffuse设为白色这意味着贴图颜色原样输出如果你想把某些行星调暗可以降低 Diffuse 的三个分量。light.Direction是一个指向光源的向量方向不是光源坐标方向方向向量反过来会导致背光面全部变成黑色这是固定管线光照最容易搞错的地方。如果你拿到的版本里D3DRS_LIGHTING是 FALSE球体看起来就像一张没有裁剪效果的贴图观感差一大截。改成 TRUE 之后需要确认球体顶点包含法线上面基于经纬度自己构建的球体不包含法线必须额外生成顶点法线。最简单的做法是把法线取值为顶点坐标本身的方向因为单位球上顶点位置向量恰好就是法线方向。3.3 摄像机和输入响应用键盘拖出来的观察自由度这个项目的另一个可玩点是摄像机控制。它没有采用 D3DX 里的带目标点摄像机而是写了一个简化轨道相机摄像机绕太阳中心旋转鼠标滚轮控制观察半径左右方向键控制方位角。方位角以弧度保存每帧根据键盘状态累积再换算成相机位置。// camera.cpp void OrbitCamera::Update(float deltaTime) { if (GetAsyncKeyState(VK_LEFT) 0x8000) m_angleY - 1.5f * deltaTime; if (GetAsyncKeyState(VK_RIGHT) 0x8000) m_angleY 1.5f * deltaTime; if (GetAsyncKeyState(VK_UP) 0x8000) m_radius * 0.98f; if (GetAsyncKeyState(VK_DOWN) 0x8000) m_radius * 1.02f; if (m_radius 5.0f) m_radius 5.0f; // 最小观察距离防止穿模 if (m_radius 300.0f) m_radius 300.0f; D3DXVECTOR3 eye( m_radius * sinf(m_angleY), 15.0f, // 固定俯视角度 m_radius * cosf(m_angleY)); D3DXMatrixLookAtLH(m_view, eye, D3DXVECTOR3(0, 0, 0), // 目标点始终是太阳 D3DXVECTOR3(0, 1, 0)); g_pDevice-SetTransform(D3DTS_VIEW, m_view); }这里GetAsyncKeyState的按位与操作是实时读取键盘状态的关键不能在窗口的消息循环里用等待键盘消息的方式因为那样只有窗口获得焦点且持续按住时才能触发。把摄像机逻辑拆成独立模块是为了在二次开发时能快速替换成自由飞行摄像机或者第一人称摄像机只需改掉 eye 坐标的生成规则即可。4. 避坑笔记建链、光照、纹理三处最常见的翻车现场4.1 创建设备失败RDP 远程桌面和虚拟机里的无头问题我在这套代码上遇到的第一问题就是标题党式报错程序在本地电脑上运行正常放到远程桌面或者 VMware 虚拟机里弹窗提示创建 D3D 设备失败整个窗口黑屏退出。原因是D3DDEVTYPE_HAL在远程会话中拿不到可用硬件设备而代码没有做降级处理。解决办法是在CreateDevice失败后把设备类型改成D3DDEVTYPE_REF重试。参考设备虽然慢但能渲染出正确画面对调试阶段足够。g_pDevice nullptr; HRESULT hr g_pD3D-CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, pp, g_pDevice); if (FAILED(hr)) { // 退而求其次用软件参考光栅化保证能看到画面 hr g_pD3D-CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, pp, g_pDevice); }参考设备渲染三角形速度很慢在 1280x720 下能坚持 30 帧左右用来排查渲染逻辑完全够用。如果你非要在虚拟机里持续演示更彻底的办法是给 VM 开启 3D 加速并把显卡切换成宿主机的硬件能力。4.2 行星背面全黑方向光和法线方向打架运行调试时我一度怀疑纹理变了因为行星朝向光源的那一面亮得过度背光面却整片漆黑连纹理内容都看不到。排查到最后发现网格生成时法线没有跟随顶点位置重新计算一个单位球体却带上了创建立方体的法线数据。方向光的明暗取决于法线与光线方向的点积法线指向内部那面就永远黑。解决方式很简单如果使用自定义顶点缓冲在生成完顶点位置后为每个顶点重新计算法线然后调用D3DXComputeNormals或者直接手工计算三角面相邻法线平均值。对单位球体直接把顶点位置归一化后作为法线写入即可因为球体表面任意一点的切面法线方向就是该点位置向量方向。4.3 行星变多边形球分段数不足和高光闪烁把行星放到太阳近处仔细观察球体边缘有肉眼可见的棱角看着就像一颗低模钻石。原因单纯是球体分段数给得太低slices16, stacks16时远看还行拉近到行星表面就露馅了。分段数从 16 提到 32 之后视觉改善很明显顶点数量从 289 涨到 1089对显卡来说还是零头。这里另一个隐蔽细节是光照中的高光闪烁。如果材质的Specular功率设得偏高顶点颜色会出现在三角形边界上跳动的高光点这是逐顶点光照的通病。在 D3D9 固定管线下一个绕开它的土办法是把行星的高光部分关掉把 Specular 分量设为零只留环境光和漫反射。太阳本身在炫技时刻意保留一点高光效果其余行星都采用哑光材质画面反而更干净。4.4 纹理撕裂D3DX 加载压缩贴图后 UV 坐标对不上换成一张网上找的高清木星贴图后球体贴图在赤道附近出现斜向撕裂好像一圈被拉开的口子。这多半是贴图宽高不是 2 的幂次而老设备通过D3DXCreateTextureFromFile加载非 2 次幂纹理时内部会做缩放缩放算法在极点产生像素错位。解决手段是把所有行星贴图尺寸统一切到 1024x1024 或 512x512并重新检查球体 UV 在极点是否收敛到同一点。在纬度最上层所有顶点贴图 V 值都设为 1.0且 U 值取 0.5 放在纹理中线才能让南极形成一个针眼而不是一条撕裂线。5. 二次开发指南往这个太阳系里加行星、加土星环最后想分享的是怎么把这份源码改造成自己的作品而不是停在原样跑通。最容易上手的改造是替换行星参数打开天体常量表新增一行数据指定orbitRadius和size再放一张贴图到textures/目录工程编译新的行星就会自动出现。更带感的是给土星增加一个草帽状的光环。这里用D3DXCreateTorus生成一个扁平圆环再把圆环的 Y 轴方向压缩到原来的十分之一让环面紧贴赤道。渲染时关掉深度写入开启 Alpha 混合让环看起来透光。环的世界矩阵可以与土星本体共享同一个公转矩阵但自转角度独立设置这样土星本体和环就能以不同速度旋转产生真实感更强的视觉效果。如果你想模拟椭圆轨道而不是正圆轨道只要把公转矩阵从旋转矩阵换成旋转和平移的组合先做一个绕 Y 轴的旋转再做一个绕 Z 轴的小角度旋转让轨道平面倾角也变成可调参数。行星位置从orbitRadius单独作用改为(orbitRadius * cos(angle), orbitRadius * sin(angle))手动计算后填充进平移矩阵代码量会增加十行左右但呈现出来的轨道形状会从理想圆变成有近日点和远日点的椭圆。每次改完轨道参数我都会做两个验证动作一是编译后先让程序以 4 倍速度旋转跑 30 秒确认没有行星穿越太阳或者飞进摄像机裁剪面二是切换成参考设备渲染一帧对比软件设备和硬件设备的输出是否一致。固定管线里这两个结果如果出现界面差异基本可以确定是矩阵乘法顺序写反了。从那以后我每次修改天体常量都会强制把世界矩阵链从头推一遍缩放、自转、平移、公转一步一步确认哪个在内层哪个在外层这样才不会再犯最低级的翻车。D3D 太阳系这种小而全的项目正是练这套思考习惯的最佳沙盒希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑