简介:这份资源是一个基于Direct3D 9.0的太阳系三维模拟工程,适合计算机图形学初学者及游戏开发入门者学习,完整展示了如何用D3D9搭建包含摄像机、行星自转、公转轨道、纹理贴图与天空盒的交互式3D场景。压缩包共109个文件,约14.2MB,其中包含C++源文件(.cpp/.h)、Visual Studio工程文件(.sln/.vcxproj)、大量jpg纹理素材以及可直接运行的exe程序,既能直接观看运行效果,也能在VC++10环境中打开工程修改调试。已有546人学习下载,具有一定参考价值。源码中实现了摄像机的动态控制、行星按椭圆轨道公转和自转,并应用了纹理映射来表现行星表面,天空盒营造宇宙背景;通过研读代码可以掌握D3D9的初始化流程、三维坐标变换、相机矩阵设置和动画循环等核心技术,为后续游戏开发或科学可视化项目打下扎实基础。 年初给自己定了个任务:用D3D从零写一个太阳系模拟,让几颗行星各自绕太阳公转、同时保持自转,轨道线也画出来。折腾了一个多月,代码写了两千多行,踩了大大小小十几个坑,其中“D3D device being lost”这个错差点让我把键盘拍碎。这篇文章就把整个项目的选型逻辑、数学原理、实现细节和踩坑记录完整写下来。适合刚开始学Direct3D、想找个完整练手项目的开发者,也适合那些正在做图形学作业、需要快速把场景跑起来的同学。
1. 为什么用D3D写太阳系:选型动机与项目架构
1.1 图形API之争:D3D还是OpenGL
在这个项目开始前,我认真对比过Direct3D和OpenGL。如果只是追求“能跑”,OpenGL的上手曲线确实更平缓,资料多、代码简洁,教程几百篇随便挑。但我最终选择了D3D,原因有三个:第一,项目主战场是Windows,D3D在Windows上的一等公民地位不用多说,驱动兼容性和调试工具链都比OpenGL更省心;第二,D3D的API设计更贴近现代GPU的抽象方式,特别是资源绑定和状态管理,对理解硬件管线有直接帮助;第三,后续如果要做游戏开发或图形学方向的工作,D3D的技能栈在工业界的覆盖面非常广。
当然,选择D3D也意味着要面对它的坏脾气——比如那个让人崩溃的设备丢失问题,后面我会单独开一节讲。这不是说D3D不好,恰恰相反,正是因为它的状态管理和资源机制更复杂,踩的坑才更有训练价值。
1.2 太阳系是练手“全家桶”
很多人会小看太阳系模拟,觉得不就是几个球转圈吗?实际做下来我才意识到,它几乎是图形学基础能力的一个微型检阅场。
要显示一个球体,你得手动生成网格顶点、索引缓冲、法线和纹理坐标;要让球体有立体感,你得实现光照;要让不同行星有不同外观,你得懂纹理采样;要让多个物体各自独立运动,你要处理大量的矩阵变换;要让画面不闪烁、不穿插,你得正确设置深度缓冲和渲染状态。更别提轨道线绘制、相机控制、多物体批量管理这些工程问题。
换句话说,这个项目做完,你就把D3D从初始化到最终呈现的整条链路完整走了一遍,没有任何一个环节是能跳过去的。这也是我推荐大家拿它当练手项目的原因——它足够小,一个人能掌控;又足够全,不会让你纸上谈兵。
1.3 整体架构与模块划分
动手写代码前,我把项目拆成五个模块:
- 初始化模块:创建窗口、设备、交换链、渲染目标视图、深度模板视图
- 资源模块:生成球体网格、加载纹理、创建着色器与常量缓冲区
- 场景模块:管理行星的属性(轨道半径、自转速度、公转速度、相位),负责每帧更新运动状态
- 渲染模块:处理每帧绘制,设置矩阵常量、绑定资源、发出DrawCall
- 异常处理模块:检测设备丢失等运行时错误,尝试恢复而不是直接崩溃
这个分层不够学术,但对一个人维护来说足够了。关键是每个模块的职责清晰,后续哪一环出问题,能迅速锁定范围。
2. 运动的核心数学:坐标系统与矩阵变换
2.1 从模型到屏幕:坐标系统的流转
D3D里的每个物体都要经历一个坐标变换流水线:模型自己的局部坐标,经过世界变换进入世界坐标,再经过视图变换变成相机视角下的坐标,最后经过投影变换落到屏幕上。太阳系这个项目里,行星的“局部坐标”就是球体网格原始生成时的坐标,而世界变换就是我们每帧计算出来的自转+公转矩阵。
如果不理解这套流转,后面代码就是跟着抄。我当初犯过一个典型错误:把行星的世界矩阵和视图矩阵顺序搞反,结果画面里所有球体跟着相机一起乱飞。后来想通了,就把整个变换链写在纸上:v_screen = v_local × worldMatrix × viewMatrix × projectionMatrix。计算机不知道“行星应该在哪”,它只认矩阵,所以坐标系统的流转必须时刻清晰。
2.2 自转与公转的矩阵组合
太阳系的核心运动就是两件事:自转和公转。自转是行星绕自身轴(这里简化为Y轴)旋转,公转是行星绕太阳旋转。
我使用D3D标准的行向量约定,每帧更新世界矩阵时这样计算:
XMMATRIX rotationSelf = XMMatrixRotationY(selfAngle); // 自转角度 XMMATRIX rotationOrbit = XMMatrixRotationY(orbitAngle); // 公转角度 XMMATRIX translateOrbit = XMMatrixTranslation(orbitRadius, 0.0f, 0.0f); // 关键一步:先平移到轨道位置,再绕太阳公转,最后自转 XMMATRIX worldMatrix = rotationSelf * rotationOrbit * translateOrbit;具体顺序上,由于D3D的行向量约定是“右边的矩阵先作用”,这里其实是先做平移(把球体挪到轨道半径处),再绕Y轴公转,最后做自转。这样写出来的效果就是行星始终保持正确朝向,绕太阳转圈的同时也在自我旋转。
2.3 为什么矩阵顺序不能乱
这是整个项目里最容易出问题的地方。矩阵乘法不满足交换律,先自转再公转和先公转再自转完全不是一回事。
打个比方:假设你的手机放在桌子上,先把手机向右转90度,再绕桌子中心转180度,和先绕桌子中心转180度再向右转90度,最终手机朝向和位置完全不同。图形学里没有“大概、差不多”,矩阵顺序错了,画面上的行星要么翻跟头,要么像喝醉了一样飘走。
我有一次调试轨道线,发现轨道是歪的,查了半天才发现是公转矩阵和世界矩阵的乘法顺序写反了。后来我把这个写成了项目里的一个调试备忘:无论多简单的矩阵组合,都在注释里明确标出每个矩阵代表哪个空间变换,以及行向量约定下的作用顺序。谁改代码谁看得懂,就不会再犯低级错误。
3. 从空窗口到旋转星球:场景搭建实战
3.1 设备与交换链的创建
一切渲染都从创建D3D设备开始。D3D11中,设备、设备上下文和交换链三者通常一起创建。我的初始化代码是这样的:
UINT flags = 0; #if defined(_DEBUG) flags |= D3D11_CREATE_DEVICE_DEBUG; #endif 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; sd.Windowed = TRUE; sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 }; ID3D11Device* device = nullptr; ID3D11DeviceContext* context = nullptr; IDXGISwapChain* swapChain = nullptr; HRESULT hr = D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, &sd, &swapChain, &device, nullptr, &context);注意一个容易忽略的点:调试标志。如果你已经遇到设备丢失问题但找不到原因,打开D3D调试层能输出很多关键错误信息,默认配置下这些错误会被静默吞掉。Debug模式下开启调试层,是排查渲染问题最有效的第一动作。
3.2 用代码生成球体网格
我不打算依赖外部模型文件,因为一个UV球体的生成逻辑并不复杂,还更可控。思路是把球体按经纬度切分:经度方向切segments段,纬度方向切stacks层,然后根据球坐标公式计算顶点位置、法线和纹理坐标。
顶点结构我定义成位置+法线+纹理坐标:
struct Vertex { XMFLOAT3 position; XMFLOAT3 normal; XMFLOAT2 texcoord; };对每一格生成两个三角形,填入索引缓冲。这里有一个容易出错的地方:球体顶部的极点(北极、南极)位置所有顶点重合,纹理坐标却不同,处理不当会导致顶部出现不自然的纹理扭曲。我的经验是写一个独立的addVertex函数,每次插入顶点时先去重,如果没有这个去重逻辑,索引缓冲会白白多出一倍冗余数据,而且顶部渲染会出现闪烁。
3.3 纹理、光照和着色器
球体生成好后,要让它“看起来像个行星”,就得做纹理映射和光照。纹理我用的是网上找的太阳和行星六面贴图,D3D里加载纹理一般用DirectXTex库,简单封装一下:
#include <DirectXTex.h> ScratchImage image; LoadFromWICFile(L"mars.jpg", WIC_FLAGS_NONE, nullptr, image); ID3D11ShaderResourceView* textureView = nullptr; CreateShaderResourceView(device, image.GetImages(), image.GetImageCount(), image.GetMetadata(), &textureView);光照方面,为了突出立体感,我在顶点着色器里对法线做世界变换,在像素着色器里用方向光(太阳方向)做一个简单的兰伯特漫反射。其实太阳系这种场景最适合的是一盏点光源放在太阳中心,但为了性能和控制方便,我用方向光近似,效果并不差。
着色器的核心逻辑,像素着色器就几行:
float4 diffuse = textureColor * sunColor * max(dot(normalWorld, lightDir), 0.0f);这样的光照模型在保证实时性的同时,已经能让行星有清晰的明暗过渡。
4. 轨道系统落地:自转公转与相机控制
4.1 轨道参数与运动更新
项目里每个行星都用一个结构体表示:
struct Planet { float orbitRadius; // 轨道半径 float orbitSpeed; // 公转速度(弧度每秒) float selfSpeed; // 自转速度(弧度每秒) float initialPhase; // 初始相位 float currentOrbitAngle; float currentSelfAngle; ID3D11ShaderResourceView* texture; };更新逻辑放在每帧调用一次:
void UpdatePlanet(Planet& planet, float deltaTime) { planet.currentOrbitAngle += planet.orbitSpeed * deltaTime; planet.currentSelfAngle += planet.selfSpeed * deltaTime; if (planet.currentOrbitAngle > XM_2PI) planet.currentOrbitAngle -= XM_2PI; }加上initialPhase就能让每颗行星从不同位置开始,避免大家整齐划一。数值上,真实太阳系的行星轨道半径和周期差距极大,但项目里如果完全按照真实比例,水星和木星的尺寸差距会超过屏幕分辨率的表达能力。我的做法是保留相对距离的比例感,但压缩到可视范围:轨道半径从5到20,公转速度做非线性映射,让视觉上看起来舒服。
4.2 避免帧率依赖的时钟系统
新手最容易犯的错误就是把运动增量直接写成angle += speed,这样60帧和120帧下行星转速差一倍。解决办法是引入时间步长:
float deltaTime = timer.DeltaSeconds(); planet.currentOrbitAngle += planet.orbitSpeed * deltaTime;更严谨的做法是固定时间步长,每帧按固定步长多次update,保证物理行为稳定。不过对太阳系这种简单场景,普通deltaTime就够用,只要保证所有速度单位是“弧度/秒”而不是“弧度/帧”。
我在项目里额外加了一个时间倍率,用来加速或减速整个模拟。任何增量和倍率相乘时,都要记得倍率是纯量,放到deltaTime的那一侧,否则换算会乱。这个小问题也折磨了我一个晚上。
4.3 相机控制与观察视角
光有行星在转,如果相机不能动,整个项目会很无聊。我加了一个简单的环绕相机:按住鼠标左键拖拽旋转视角,滚轮缩放距离。
实现方式是维护yaw、pitch和orbitDistance三个变量,每次更新时根据它们重新计算相机位置和LookAt矩阵:
XMMATRIX view = XMMatrixLookAtLH( XMVectorSet( cosf(yaw) * cosf(pitch) * orbitDistance, sinf(pitch) * orbitDistance, sinf(yaw) * cosf(pitch) * orbitDistance, 1.0f), XMVectorZero(), XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f));如果整个场景只有行星和轨道,把相机LookAt盯着太阳中心就够了。后续如果你想加卫星,可以再扩展一个嵌套的相机目标点,但那就属于进阶玩法了。
5. 设备丢失危机:遇到“d3d device being lost”怎么办
5.1 设备丢失到底是怎么回事
开发过程中,我最先遇到的是弹窗提示“unreal engine is exiting due to d3d device being lost”。当时我很奇怪:我又没用Unreal,为什么报这个错?后来才明白,这是因为我的D3D程序里某些操作触发了设备丢失,而系统全局环境被识别成了某个Unreal窗口。
设备丢失,本质上是GPU设备与CPU之间的连接被中断了。常见触发场景包括:显卡驱动因GPU命令卡死而超时重置(TDR)、显存耗尽导致设备无法继续工作、系统从睡眠唤醒后驱动重置、远程桌面连接切断、显卡超频或供电不稳等等。
D3D设备丢失后的表现大多是:Present调用返回DXGI_ERROR_DEVICE_REMOVED或DXGI_ERROR_DEVICE_RESET。如果不处理,渲染管线的所有后续调用都会失败,最终窗口黑屏甚至崩溃。
5.2 为什么Unreal也逃不过这个报错
这个错误在Unreal引擎里特别有名,因为Unreal的游戏多为重负载3D应用,最容易触碰到GPU超时阈值。玩家显卡压力过大、驱动抽风、显存不足,最终都汇聚成这句提示。它不是一个“Unreal专属问题”,而是所有D3D应用运行时都会面临的底层风险。
太阳系项目虽然轻量,但如果我在Debug模式下反复创建和销毁纹理、又开着垂直同步跑高DPI窗口,同样可能触发设备丢失。第一次遇到时,我的小程序直接弹出错误框然后崩溃退出。那一刻我意识到:任何严格的D3D应用,都必须把设备丢失当成运行时正常分支来处理,而不是当成永远不发生的例外。
5.3 正确的恢复处理链路
处理设备丢失的标准做法分四步:
第一步,检查返回码。Present调用或者任一D3D调用返回设备移除/重置错误码时,记录现场:
HRESULT hr = swapChain->Present(1, 0); if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { HRESULT reason = device->GetDeviceRemovedReason(); // 输出 reason,辅助定位原因 RecoverDevice(); return; }第二步,释放所有和设备相关的资源。这一步不能偷懒,谁都不行。视口、渲染目标视图、深度模板视图、着色器、常量缓冲区、顶点/索引缓冲、纹理资源,全部释放干净。
第三步,重新创建设备和交换链。注意重新创建时必须用当前窗口尺寸,如果窗口前后发生了大小变化,要同步更新交换链BufferDesc的宽高。
第四步,重新创建所有资源并重新设置管线状态。为了让恢复过程不那么痛苦,我建议把所有资源创建封装成一个函数CreateAllResources(),初始化时调用一次,设备丢失恢复时再调用一次,复用到极致。
这个恢复逻辑我专门做了压力测试:快速切换窗口大小、拔插显示器信号、休眠唤醒,让设备丢失的概率尽量出现。实测下来,只要恢复逻辑完整,程序基本不会真的崩溃,最多画面顿一下。
5.4 降低设备丢失概率的工程习惯
设备丢失不可能完全避免,但我们可以降低发生频率。在太阳系项目里,我养成了几个习惯:
- 避免CPU端每帧分配大量显存资源。行星纹理一次性加载,不要每帧重新创建。
- 绘制调用前检查设备上下文状态是否合法,出错立即返回而不是继续渲染。
- 关闭GPU调试图层时报错,Debug模式不意味着可以慢,但排查问题时机要匹配。在发布构建里去掉调试层,减少额外的运行时开销。
- 长时间高负载时,注意电源管理和散热。驱动超时重置最常见的原因是卡顿时间过长,适当优化渲染效率就是对稳定性的投资。
另外说个细节:真机上用GPU工具(如PIX)抓帧时,也会经常碰到设备丢失。这不一定是bug,可能是PIX注入导致的状态变化。遇到这种情况不要慌,把PIX附加关掉再跑一下,如果程序正常,那就是调试工具引起的,不是项目问题。
写在最后的收尾心得
整个项目做下来,最大的感悟是:太阳系模拟的难点不在“让球转起来”,而在如何用严谨的工程方式,把坐标变换、资源管理、异常恢复这些底层问题梳理清楚。D3D不会帮你记住任何状态,所有细节都要你亲力亲为,但这恰恰是它最能锻炼人的地方。
如果你也想复刻这个项目,我建议按我写的顺序来:先把设备的创建和清屏跑通,再生成球体网格,再上纹理光照,最后加轨道与相机控制。设备丢失处理一定要从一开始就留好接口,否则做到后面再补,改动成本会大得多。
另外,轨道线的绘制、卫星系统、时间加速器这些扩展功能,都在我自己的TODO列表里躺着。等磨完这些坑,也许我会再写一篇更进阶的版本,但前提是先把这套基础打牢——毕竟,能把公转自转稳定跑起来,已经是一个合格的D3D入门作品了。
本文还有配套的精品资源,点击获取