如何在DirectX 12中绘制球体?
嘿,我来帮你搞定在DirectX 12里用顶点/像素着色器画球体的事儿!你选的思路完全正确——生成顶点和索引缓冲来绘制,而且把基础球设成半径1、中心在原点的单位球,后续在顶点着色器里做缩放平移简直不要太方便,不用每次改大小位置都重新生成缓冲,省超多事儿。
核心思路:先做单位球,再靠着色器变换
我们先生成一个单位球体(半径1、中心固定在(0,0,0)),所有的细分控制都在生成顶点/索引的时候搞定。等要调整球体的大小、位置或者旋转时,直接在顶点着色器里用矩阵变换顶点坐标就行,不用碰缓冲数据,效率拉满。
用细分参数控制平滑度
细分程度直接决定了球体的圆润度——参数越高,顶点和索引数量越多,球体越光滑,但GPU的绘制开销也会变大。最容易上手的是经纬度细分法,用两个参数控制:
- 纬度分段数(比如
latDivisions):控制从上极点到下极点有多少层横向的线 - 经度分段数(比如
lonDivisions):控制绕球体一周有多少条纵向的线
你可以根据需求调这俩数,比如做个低精度的占位球用10×10,做高质量渲染就用30×30甚至更高。
生成顶点缓冲数据
单位球的顶点坐标可以用球面坐标系转笛卡尔坐标系的公式计算,而且单位球的顶点位置本身就是法线方向,省得额外计算法线:
- 纬度角度θ从0到π(从上极点到下极点)
- 经度角度φ从0到2π(绕z轴一周)
- 坐标公式:
x = sinθ * cosφ,y = cosθ,z = sinθ * sinφ
给你一段C++的生成代码参考:
#include <vector> #include <DirectXMath.h> using namespace DirectX; struct Vertex { XMFLOAT3 position; XMFLOAT3 normal; }; void GenerateUnitSphere(int latDivisions, int lonDivisions, std::vector<Vertex>& vertices, std::vector<uint32_t>& indices) { vertices.clear(); indices.clear(); // 添加上极点顶点 vertices.push_back({ XMFLOAT3(0.0f, 1.0f, 0.0f), XMFLOAT3(0.0f, 1.0f, 0.0f) }); // 生成中间的纬度层顶点 for (int lat = 1; lat < latDivisions; ++lat) { float theta = XM_PI * static_cast<float>(lat) / static_cast<float>(latDivisions); float sinTheta = sinf(theta); float cosTheta = cosf(theta); for (int lon = 0; lon < lonDivisions; ++lon) { float phi = 2.0f * XM_PI * static_cast<float>(lon) / static_cast<float>(lonDivisions); float sinPhi = sinf(phi); float cosPhi = cosf(phi); XMFLOAT3 pos = { sinTheta * cosPhi, cosTheta, sinTheta * sinPhi }; vertices.push_back({ pos, pos }); // 单位球位置即法线 } } // 添加下极点顶点 vertices.push_back({ XMFLOAT3(0.0f, -1.0f, 0.0f), XMFLOAT3(0.0f, -1.0f, 0.0f) }); // 生成上极点周围的三角形索引 for (int lon = 0; lon < lonDivisions; ++lon) { int nextLon = (lon + 1) % lonDivisions; indices.push_back(0); indices.push_back(nextLon + 1); indices.push_back(lon + 1); } // 生成中间层四边形的索引(每个四边形拆成两个三角形) int baseIndex = 1; for (int lat = 0; lat < latDivisions - 2; ++lat) { for (int lon = 0; lon < lonDivisions; ++lon) { int nextLon = (lon + 1) % lonDivisions; int current = baseIndex + lat * lonDivisions + lon; int nextLonCurrent = baseIndex + lat * lonDivisions + nextLon; int currentNextLat = baseIndex + (lat + 1) * lonDivisions + lon; int nextLonNextLat = baseIndex + (lat + 1) * lonDivisions + nextLon; // 第一个三角形 indices.push_back(current); indices.push_back(nextLonCurrent); indices.push_back(currentNextLat); // 第二个三角形 indices.push_back(nextLonCurrent); indices.push_back(nextLonNextLat); indices.push_back(currentNextLat); } } // 生成下极点周围的三角形索引 int bottomPoleIndex = static_cast<int>(vertices.size()) - 1; int lastLatBase = baseIndex + (latDivisions - 2) * lonDivisions; for (int lon = 0; lon < lonDivisions; ++lon) { int nextLon = (lon + 1) % lonDivisions; indices.push_back(bottomPoleIndex); indices.push_back(lastLatBase + lon); indices.push_back(lastLatBase + nextLon); } }
在DirectX 12中创建缓冲
生成顶点和索引数据后,就可以创建GPU能访问的缓冲了:
- 计算缓冲大小:顶点缓冲大小是
vertices.size() * sizeof(Vertex),索引缓冲是indices.size() * sizeof(uint32_t) - 创建顶点缓冲资源,用
UpdateSubresources把CPU上的顶点数据复制到GPU缓冲 - 同理创建索引缓冲,复制索引数据
- 设置输入布局,告诉GPU顶点数据的格式(比如位置是
DXGI_FORMAT_R32G32B32_FLOAT,法线也是同格式)
顶点着色器里做缩放平移
既然是单位球,要调整大小和位置直接在顶点着色器里用矩阵变换就行。比如我们可以用一个常量缓冲传递世界矩阵(包含缩放、旋转、平移),把顶点位置从模型空间转到世界空间,再转到裁剪空间:
struct VSInput { float3 position : POSITION; float3 normal : NORMAL; }; struct PSInput { float4 position : SV_POSITION; float3 normal : NORMAL; }; cbuffer MatrixBuffer : register(b0) { float4x4 worldViewProj; // 世界*视图*投影矩阵 float4x4 world; // 世界矩阵(缩放、旋转、平移) }; PSInput VS(VSInput input) { PSInput output; // 把单位球顶点变换到世界空间 float4 worldPos = mul(float4(input.position, 1.0f), world); // 变换到裁剪空间,供GPU光栅化 output.position = mul(worldPos, worldViewProj); // 法线也要做世界变换,用逆转置矩阵避免缩放影响法线方向 output.normal = mul(input.normal, (float3x3)transpose(inverse(world))); return output; }
像素着色器就简单了,比如做个基础漫反射效果:
struct PSInput { float4 position : SV_POSITION; float3 normal : NORMAL; }; float4 PS(PSInput input) : SV_TARGET { // 模拟一个方向光 float3 lightDir = normalize(float3(1.0f, 1.0f, 1.0f)); // 计算漫反射强度 float diffuse = max(dot(input.normal, lightDir), 0.0f); return float4(diffuse, diffuse, diffuse, 1.0f); }
最后一步:绘制
设置好顶点缓冲视图和索引缓冲视图后,调用DrawIndexedInstanced就行,参数用索引的数量,实例数设为1,起始索引和起始顶点都设为0。
小提醒
如果觉得经纬度法在极点附近的三角形太密(浪费性能),可以试试正二十面体细分法,生成的三角形大小更均匀,但实现起来会复杂一点,适合对渲染质量要求高的场景。
内容来源于stack exchange

