You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在DirectX 12中绘制球体?

如何在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能访问的缓冲了:

  1. 计算缓冲大小:顶点缓冲大小是vertices.size() * sizeof(Vertex),索引缓冲是indices.size() * sizeof(uint32_t)
  2. 创建顶点缓冲资源,用UpdateSubresources把CPU上的顶点数据复制到GPU缓冲
  3. 同理创建索引缓冲,复制索引数据
  4. 设置输入布局,告诉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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 13:43:12