简介一份基于Direct3D 9.0的太阳系三维模拟工程用Visual C 10构建实现行星轨道、自转与公转的实时可视化。项目面向3D图形编程初学者及游戏开发爱好者重点演示摄像机动态控制、开普勒椭圆轨道、纹理贴图与天空盒背景等核心技术可作为计算机图形学课程设计或D3D入门实践的参考。压缩包共109个文件包含C源文件.cpp/.h、可执行程序.exe、纹理素材.jpg以及完整工程配置.sln/.vcxproj等整体大小14.2MB已有547人学习下载。代码结构清晰涵盖窗口创建、D3D初始化、几何体绘制与相机交互等模块工程可直接编译运行便于对照研究行星运动模拟的数学建模与渲染管线的衔接。1. 先把太阳系装进D3D场景一个轨道自转公转的C演示提到“太阳系”可视化多数人先想到Unity或Three.js。这个D3D项目选了最底层的路线用C和Direct3D 11不碰引擎从零把太阳和八颗行星渲染成可交互的三维场景。核心功能就三件——行星沿轨道绕太阳公转、每颗行星绕自身轴自转、相机在轨道内外自由漫游。此外所有天体的半径、轨道半径、公转/自转速度、纹理都是参数化配置改一行数据就能把地球公转周期拉长十倍。项目适合刚学完D3D 11、想找完整案例对照的图形学入门者也适合需要课设底稿、想快速改成自己交互逻辑的开发者。代码量不大但D3D里最常踩坑的环节它都实实在在走了一遍。2. 技术栈与整体架构从D3D11初始化到帧循环2.1 技术选型为什么是D3D11而不是引擎或D3D9Direct3D 11相比9有几个实打实的优势。第一着色器模型升级到5.0行星纹理采样、颜色输出这些效果写起来更直接第二调试工具链完整VS自带的图形诊断能逐帧看每个常量值排查黑屏比D3D9时代舒服得多第三不再依赖早已废弃的D3DX辅助库纹理加载、数学运算都由自己显式引入代码更透明出了问题能顺着调用链摸。为什么不直接用Unity因为引擎把设备创建、资源管理、渲染状态全封装成了黑匣子拖个材质球行星就“自己转起来”了但你对D3D管线本身的理解仍然是空的。这个项目的价值恰恰在于把“设备—资源—着色器—绘制调用”这条链路完整露出来。至于D3D12性能上限更高但线程安全和资源状态管理复杂度远超入门阶段的目标。D3D11用单一设备上下文驱动帮你处理大部分状态过渡是图形学教学阶段那个“压得住的复杂度”。2.2 工程目录与模块划分一份典型的D3D11太阳系工程会按这样的方式组织文件D3DSolarSystem/ ├── src/ │ ├── D3DApp.h/.cpp # 设备、交换链、深度缓冲的创建与封装 │ ├── Camera.h/.cpp # 相机漫游、鼠标/键盘控制 │ ├── Planet.h/.cpp # 行星数据、公转/自转更新、渲染 │ ├── SolarSystem.h/.cpp # 管理所有行星实例驱动Update和Render │ └── main.cpp # Win32窗口、消息循环、帧循环 ├── shaders/ │ ├── PlanetVS.hlsl # 顶点着色器世界矩阵视图投影 │ ├── PlanetPS.hlsl # 像素着色器纹理采样 │ └── SunPS.hlsl # 太阳的自发光效果 └── textures/ # 每颗行星一张表面贴图模块职责就三句话。D3DApp把环境配好是典型的样板代码Planet是核心一颗行星一个实例SolarSystem是调度者逐帧让每个Planet先Update再Render。Camera独立成类是因为交互逻辑WASD移动、右键拖拽转向和渲染逻辑解耦之后你再想加“锁定行星跟随”功能就很轻松。2.3 设备创建与交换链配置D3DApp里最开始那几十行代码决定了后面所有渲染能否正常跑。我一般会按以下配置来创建设备和交换链这也是项目里最可能采用的方式// D3DApp.cpp —— 设备与交换链创建核心节选 HRESULT CreateDeviceAndSwapChain(HWND hWnd, UINT width, UINT height) { DXGI_SWAP_CHAIN_DESC sd {}; sd.BufferCount 2; // 双缓冲避免画面撕裂 sd.BufferDesc.Width width; sd.BufferDesc.Height height; sd.BufferDesc.Format DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferUsage DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow hWnd; sd.SampleDesc.Count 1; // 不开启MSAA sd.SampleDesc.Quality 0; sd.Windowed TRUE; D3D_FEATURE_LEVEL levels[] { D3D_FEATURE_LEVEL_11_0 }; HRESULT hr D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels, 1, D3D11_SDK_VERSION, sd, m_swapChain, m_device, m_featureLevel, m_context ); return hr; }这里几个参数是有讲究的。BufferCount 2表示双缓冲游戏循环里Present后前一个缓冲区正在显示后一个给GPU渲染不会出现撕裂感如果设为3画面延迟会更明显但帧率波动更平滑。SampleDesc的Count和Quality是MSAA开关Count1代表不做抗锯齿。太阳系里的球体边缘有锯齿可以接受因为MSAA意味着渲染目标和深度缓冲的格式都要换复杂度会整体上升。Feature Level选了D3D_FEATURE_LEVEL_11_0约束了着色器模型为5.0同时兼容近十年内的主流显卡。初始化完成之后还需要创建ID3D11RenderTargetView和ID3D11DepthStencilView前者决定颜色写到哪里后者供深度测试使用后续渲染的每一帧都要同时绑定它们。2.4 帧循环与渲染命令的先后main.cpp的循环不用消息驱动而是事件驱动——没有Windows消息需要处理时立即进入渲染帧。这是D3D程序的典型写法// main.cpp —— 帧循环节选 while (msg.message ! WM_QUIT) { if (PeekMessage(msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } else { float dt m_timer.DeltaSeconds(); // 上一帧到这一帧的时间单位秒 m_camera-Update(dt); // 先更新相机再更新行星 m_solarSystem-Update(dt); RenderFrame(); // 真正的绘制调用都在这里 } }DeltaSeconds()返回的是秒而不是毫秒这是个关键约定。有人把时间差直接当毫秒传进Update行星会转得停不下来这就是第5章要说的单位混用问题。帧循环的渲染顺序也有讲究清屏必须在所有绘制之前深度缓冲的Clear也不能省否则上一帧的深度值会污染这一帧的遮挡关系。渲染阶段大致是清屏 → 绑定渲染目标和深度模板 → 设置视图/投影矩阵 → 遍历每颗行星设置世界矩阵、绑定纹理、绘制 → 解除绑定 → Present。这个顺序在代码里非常直观之后调试黑屏时按这个顺序倒查能很快定位问题。3. 核心模块拆解行星类、轨道计算与HLSL渲染3.1 Planet类用参数描述一颗行星Planet类把所有天体的公共特质抽象成一组参数。这样做的直接好处是新增一颗行星不需要改渲染逻辑只需要加一个PlanetDesc实例。这是核心的类定义// Planet.h #include DirectXMath.h #include wrl/client.h struct PlanetDesc { float orbitRadius; // 公转轨道半径场景单位 float orbitSpeed; // 公转速度弧度/秒 float spinSpeed; // 自转速度弧度/秒 float radius; // 行星半径场景单位 const wchar_t* texturePath; // 表面纹理相对路径 }; class Planet { public: Planet(ID3D11Device* device, const PlanetDesc desc); void Update(float deltaTime); void Render(ID3D11DeviceContext* context, const DirectX::XMMATRIX viewProj); private: PlanetDesc m_desc; float m_orbitAngle 0.0f; // 当前公转角度 float m_spinAngle 0.0f; // 当前自转角度 DirectX::XMFLOAT4X4 m_world; // 每帧由Update计算 Microsoft::WRL::ComPtrID3D11Buffer m_vertexBuffer; Microsoft::WRL::ComPtrID3D11Buffer m_indexBuffer; Microsoft::WRL::ComPtrID3D11ShaderResourceView m_textureSRV; UINT m_indexCount 0; };构造函数里要做三件事保存desc副本、把行星几何体生成到顶点/索引缓冲区、加载纹理创建SRV。这些资源创建只要成功一次后面每帧渲染就是绑定与绘制的重复劳动。用ComPtr管理D3D对象可以避免手动Release在异常路径上泄漏资源。3.2 公转与自转的运动学计算这是项目名称里“轨道自转公转”最核心的实现。Update函数的内容决定了行星每一帧的位置和朝向// Planet.cpp —— 行星更新逻辑 #include Planet.h #include cmath void Planet::Update(float deltaTime) { // 1. 角度累积速度 × 时间 m_orbitAngle m_desc.orbitSpeed * deltaTime; m_spinAngle m_desc.spinSpeed * deltaTime; // 2. 归一化到 [0, 2π)避免长时间运行后浮点精度下降 if (m_orbitAngle DirectX::XM_2PI) m_orbitAngle - DirectX::XM_2PI; if (m_spinAngle DirectX::XM_2PI) m_spinAngle - DirectX::XM_2PI; // 3. 公转位置圆周运动轨道落在XZ平面 float x cosf(m_orbitAngle) * m_desc.orbitRadius; float z sinf(m_orbitAngle) * m_desc.orbitRadius; // 4. 世界矩阵先缩放 → 自转 → 平移到轨道位置 DirectX::XMMATRIX scale DirectX::XMMatrixScaling( m_desc.radius, m_desc.radius, m_desc.radius); DirectX::XMMATRIX rotation DirectX::XMMatrixRotationY(m_spinAngle); DirectX::XMMATRIX translation DirectX::XMMatrixTranslation(x, 0.0f, z); DirectX::XMStoreFloat4x4(m_world, scale * rotation * translation); }逻辑分四步。第一步用速度乘时间累积角度帧率不恒定时比如拖动窗口位置变化依然跟真实时间挂钩第二步把角度减回0到2π范围看起来无所谓但程序挂机两小时后浮点误差会显现cosf精度下降会导致轨道不再圆第三步用三角函数把角度映射到XZ平面坐标cos取X、sin取Z从Y轴向下俯视时行星做逆时针公转第四步的SRT顺序决定了“先缩放到实际大小再自转再放到轨道位置”。如果误把平移写在缩放之前行星会以原点太阳为中心进行缩放离太阳越远的球体越大画面完全错误。参数说明orbitSpeed和spinSpeed的单位是弧度/秒。想模拟真实公转周期传入2π / 周期秒数即可。项目里预设的数值不追求天文比例因为真实比例下内行星紧贴太阳、外行星跑出视野什么都看不见。视觉演示优先。3.3 球体网格与纹理采样行星的几何体不是模型文件导入的而是在代码里程序化生成。常见做法是用经纬度网格把球面切分再把每个四边形拆成两个三角形// Planet.cpp —— 生成球体顶点节选 struct Vertex { DirectX::XMFLOAT3 pos; DirectX::XMFLOAT2 uv; }; std::vectorVertex vertices; const UINT latSegments 32, lonSegments 64; for (UINT lat 0; lat latSegments; lat) { float v (float)lat / latSegments; // 纬度比例 0~1 float phi v * DirectX::XM_PI; // 纬度角 0~π for (UINT lon 0; lon lonSegments; lon) { float u (float)lon / lonSegments; // 经度比例 0~1 float theta u * DirectX::XM_2PI; // 经度角 0~2π Vertex vert; vert.pos DirectX::XMFLOAT3( sinf(phi) * cosf(theta), cosf(phi), sinf(phi) * sinf(theta) ); vert.uv DirectX::XMFLOAT2(u, 1.0f - v); vertices.push_back(vert); } } // 索引按每个四边形两个三角形生成此处省略sinf(phi)*cosf(theta)和sinf(phi)*sinf(theta)分别得到X和Z分量cosf(phi)得到Y分量。生成的是单位球半径1缩放交给世界矩阵完成。UV的v方向取1.0f - v是因为D3D纹理坐标原点在左上角而球体顶点是从北极开始生成的不翻转的话纹理在行星上会上下颠倒。3.4 着色器与绘制流程行星的最终视觉效果由HLSL着色器决定。这里最关键的是常量缓冲的使用方式——视图投影矩阵是全局的每帧只上传一次世界矩阵是每颗行星独有的在绘制每颗行星时单独上传。// PlanetVS.hlsl —— 行星顶点着色器 cbuffer FrameConstants : register(b0) { float4x4 gViewProjection; }; cbuffer ObjectConstants : register(b1) { float4x4 gWorld; }; struct VIn { float3 position : POSITION; float2 uv : TEXCOORD; }; struct VOut { float4 position : SV_POSITION; float2 uv : TEXCOORD; }; VOut VS_main(VIn input) { VOut output; // 顶点顺序模型 → 世界 → 视图 → 投影 float4 worldPos mul(float4(input.position, 1.0f), gWorld); output.position mul(worldPos, gViewProjection); output.uv input.uv; return output; }mul(float4(input.position, 1.0f), gWorld)把局部坐标转成世界坐标随后乘上gViewProjection变成裁剪空间坐标。HLSL的mul使用行主序矩阵时是向量在前跟C里XMMatrixMultiply的调用习惯要分清否则矩阵顺序反了行星会飞到奇怪位置。// PlanetPS.hlsl —— 行星像素着色器 Texture2D gPlanetTex : register(t0); SamplerState gSampler : register(s0); struct PIn { float4 position : SV_POSITION; float2 uv : TEXCOORD; }; float4 PS_main(PIn input) : SV_TARGET { return gPlanetTex.Sample(gSampler, input.uv); }绘制时的调用顺序固定VSSetShader绑定顶点着色器、PSSetShader绑定像素着色器、IASetInputLayout设置顶点布局、VSSetConstantBuffers上传常量和世界矩阵、PSSetShaderResources绑定纹理、最后DrawIndexed。这个顺序在代码里看起来重复但每步对应管线的一个阶段缺一步就画不出正确答案。4. 运行、配置与窗口适配把Demo调到顺滑状态4.1 编译与运行步骤拿到代码后我用VS打开解决方案第一件事是确认Windows SDK版本。如果工程配置选的SDK版本与本地不一致编译器会大片报错。操作路径是项目属性 → 常规 → Windows SDK版本选一个已安装的版本再把配置管理器的平台设为x64或Win32与工程原文匹配即可。接下来确认DirectXTK的引用方式。如果工程通过NuGet引入DirectXTK省事很多如果是手动源码拷贝要检查Include目录和Lib目录是否配对。程序里用到CreateWICTextureFromFile时还需要链接DirectXTK.lib并注意Debug/Release和x86/x64四种组合不能混搭。一切就绪后直接F5。启动后如果VS输出窗口没有任何报错画面中应该能看到发光的太阳和多颗行星在缓慢移动。操作方式围绕相机展开WASD控制前后左右位移鼠标右键拖拽旋转视角滚轮拉近拉远。4.2 行星参数调优轨道半径与速度对照真实比例的行星和轨道半径放进D3D是什么结果太阳太亮、内行星太挤、外行星离得太远。所以项目里用的是视觉比例。我习惯把参数配成下面这样数值供参考行星轨道半径行星半径公转速度(弧度/秒)自转速度(弧度/秒)水星120.40.91.0金星180.70.650.4地球240.80.51.2火星320.50.351.0木星482.00.221.8土星621.80.161.5天王星801.30.10.8海王星961.30.080.9这套数值的要点是内圈轨道半径差距小、外圈差距大视觉上更像层层递散的盘面而不是等间距圆环。公转速度递减让每颗行星有明显不同的快慢节奏。如果想模拟金星逆行自转把spinSpeed设为负值即可。4.3 窗口适配与垂直同步拖动窗口大小时最容易翻车的不是布局而是投影矩阵的宽高比没跟上。处理WM_SIZE消息时要做两件事更新Viewport的Width和Height以及重新计算XMMatrixPerspectiveFovLH的AspectRatio。另外注意垂直同步。IDXGISwapChain::Present(1, 0)表示锁60帧Present(0, 0)表示不锁帧。这个太阳系项目建议开启垂直同步否则低端显卡上旋转速度会变得极不稳定轨道角速度看起来时快时慢。5. 排查与避坑五个让你怀疑人生的D3D时刻5.1 定位问题的工具链面对黑屏和显示异常我一般按这个顺序排查。先在创建设备时加D3D11_CREATE_DEVICE_DEBUG标志让D3D调试层在错误发生处输出详细原因。这个标志打开后输出窗口会打印错误到哪一步以及涉及哪个API调用比对着代码干瞪眼快得多。再用VS的图形诊断开始诊断后运行程序、切换一帧可以在Graphics栏里看到该帧的绘制调用列表、每个阶段绑定的资源和常量缓冲内容。行星消失时第一步先看DrawIndexed有没有被调用再看绑定到相同槽位的常量缓冲是不是某个行星的世界矩阵。最后是朴素的日志法。在每帧的关键步骤输出HRESULT和资源状态。文件加载失败时会得到0x80070002文件不存在、0x80070003路径找不到这类错误码先解决文件问题再谈渲染。5.2 五条踩坑记录现象→原因→解决问题1行星显示为纯色纹理加载无效现象球体形状正常但表面是白色或黑色看不到贴图。 原因CreateDDSTextureFromFile只认DDS格式把JPG直接传进去会失败失败后的SRV是空指针采样结果为默认白色。 解决改用CreateWICTextureFromFile加载常见图片格式并确认textures目录在exe的工作目录下。文件找不到时先打印HRESULT看到0x80070002先查路径。问题2遮挡关系错乱远处的行星透穿过来现象位置判断没问题但后方行星叠到前方行星上场景没有层次。 原因只创建了渲染目标视图没创建深度模板缓冲。没有深度测试GPU按提交顺序绘制后画的直接盖住先画的。 解决创建深度模板纹理和DepthStencilView每帧开始时ClearDepthStencilView并在OMSetRenderTargets调用中把深度视图绑定到第二个参数槽位。问题3行星自转方向看起来反了现象同一颗行星纹理亮斑移动方向和公转方向相反。 原因D3D是左手坐标系绕Y轴正方向旋转在俯视视角下是顺时针跟右手坐标系的习惯相反。 解决自转速度统一设正值想逆行就把公转速度的方向反过来而不是在自转里取负号。问题4启动即黑屏找不到任何行星现象程序无报错窗口全黑怎么转相机都是黑屏。 原因相机初始位置在场景中心或lookAt方向没对准太阳另一种可能是光栅化状态里的背面剔除把球体正面剔掉了。 解决先把相机初始化为从斜上方看向原点例如eye(30, 25, 60)、lookAt(0,0,0)再把CullMode临时设为D3D11_CULL_NONE验证球体本身。问题5窗口尺寸变化后画面拉伸变形现象拖动窗口后行星全变成椭圆比例失调。 原因视口和投影矩阵的宽高比还停留在创建时的值没有跟着窗口尺寸更新。 解决在WM_SIZE里同步更新Viewport和XMMatrixPerspectiveFovLH的AspectRatio。把这两步封装成OnResize()在初始化末尾和WM_SIZE里各调用一次。6. 二次开发加天体、改轨道与做行星跟随想把这套D3D太阳系改成自己的演示核心工作量几乎都在数据和少量Update逻辑上不碰渲染管线。6.1 快速添加一颗新天体在SolarSystem的初始化列表里push一个Planet实例就能完成// SolarSystem.cpp —— 增加自定义天体 PlanetDesc comet {}; comet.orbitRadius 75.0f; // 插在木星和土星之间 comet.orbitSpeed 0.12f; comet.spinSpeed 0.5f; comet.radius 0.5f; comet.texturePath Ltextures/comet.jpg; m_planets.push_back(std::make_uniquePlanet(m_device, comet));PlanetDesc是值语义传入Planet构造后内部持有副本后续修改外部变量不影响已创建实例。新增的彗星会自动参与公转、自转和渲染不需要改任何别的代码。6.2 椭圆轨道与倾斜轨道把圆形轨道改成椭圆只需替换Update里的位置公式float x semiMajor * cosf(m_orbitAngle); // 半长轴 float z semiMinor * sinf(m_orbitAngle); // 半短轴再给轨道加倾角时在translation之前乘以旋转矩阵即可DirectX::XMMATRIX orbitTilt DirectX::XMMatrixRotationX(0.3f); DirectX::XMMATRIX translation DirectX::XMMatrixTranslation(x, 0.0f, z); DirectX::XMMATRIX world scale * rotation * orbitTilt * translation;0.3f弧度约等于17度轨道会稍微倾斜行星运行轨迹就不再局限在同一个平面内。6.3 行星跟随与轨道线绘制交互上我建议优先补两个能力。行星跟随在Camera中维护一个目标位置每帧把eye设为行星位置加固定偏移量lookAt设为行星位置就能获得“站在肩头看一颗行星”的视角切换效果。轨道线绘制用D3D11_PRIMITIVE_TOPOLOGY_LINELIST生成一圈(cosθ*R, 0, sinθ*R)顶点每帧绑定单位矩阵绘制观众就能直观看到每颗行星的轨道半径差演示效果提升明显。有一次我为了给土星加环折腾了大半夜最后发现是环的两面都没有正确朝向相机阴影全部投在了背面。从那以后我每次在D3D里加新的天体部件都会强制把坐标系的约定写进注释第一行并用一个简单的替代体先验证朝向再上正式模型。做D3D这种底层项目细节真是一人一个坑希望这篇拆解能帮你起步时少踩几个。本文还有配套的精品资源点击获取