012-Direct3D 12渲染流水线

Direct3D 12渲染流水线

渲染流水线是现代图形处理的核心,它将3D场景转换为屏幕上的2D图像。在DirectX 12中,这个过程更加精细可控,允许开发者优化性能并实现各种高级视觉效果。本章将详细介绍DirectX 12渲染流水线的各个阶段及其工作原理。

5.1 3D 视觉即错觉?

人类视觉系统能够感知3D世界,但我们的显示设备(如显示器、手机屏幕)只能显示2D图像。3D图形学的核心挑战就是在2D平面上创造出3D深度的错觉。

这种错觉主要通过以下视觉线索实现:

  1. 透视投影:远处的物体看起来更小
  2. 光照与阴影:提供物体形状和表面细节的视觉线索
  3. 遮挡:前面的物体会遮挡后面的物体
  4. 纹理和细节:增加表面真实感
  5. 运动视差:观察角度变化时,物体间相对位置的变化

渲染流水线的目标就是模拟这些视觉线索,创造出令人信服的3D环境。在计算机图形学中,我们通过数学模型来表示3D世界,并通过一系列转换将其投影到2D屏幕上。

cpp

// 简单的透视投影示例
float fov = 45.0f * (XM_PI / 180.0f);  // 视场角(角度转弧度)
float aspectRatio = (float)screenWidth / (float)screenHeight;
float nearPlane = 0.1f;
float farPlane = 1000.0f;

// 创建透视投影矩阵
XMMATRIX projectionMatrix = XMMatrixPerspectiveFovLH(
    fov, aspectRatio, nearPlane, farPlane
);

5.2 模型的表示

在3D图形中,模型通常由以下几个基本要素组成:

  1. 顶点数据:定义模型的几何形状

    • 位置坐标
    • 法线向量
    • 纹理坐标
    • 切线和副切线
    • 顶点颜色
    • 蒙皮权重(用于动画)
  2. 索引数据:定义如何将顶点连接成三角形

  3. 材质信息:定义表面的外观特性

    • 漫反射颜色
    • 镜面反射颜色
    • 粗糙度/光泽度
    • 法线贴图
    • 其他特殊贴图

以下是一个简单的顶点结构和模型数据的例子:

cpp

// 顶点结构定义
struct Vertex {
    XMFLOAT3 Position;    // 位置坐标
    XMFLOAT3 Normal;      // 法线向量
    XMFLOAT2 TexCoord;    // 纹理坐标
};

// 模型数据(一个简单的立方体)
std::vector<Vertex> vertices = {
    // 前面
    { XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT2(0.0f, 1.0f) },
    { XMFLOAT3(-1.0f,  1.0f, -1.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT2(0.0f, 0.0f) },
    { XMFLOAT3( 1.0f,  1.0f, -1.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT2(1.0f, 0.0f) },
    { XMFLOAT3( 1.0f, -1.0f, -1.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT2(1.0f, 1.0f) },
    // 其余面的顶点...
};

// 索引数据
std::vector<uint16_t> indices = {
    // 前面
    0, 1, 2,
    0, 2, 3,
    // 其余面的索引...
};

在DirectX 12中,这些数据通常存储在缓冲区中,然后传递给渲染流水线:

cpp

// 创建顶点缓冲区
ComPtr<ID3D12Resource> vertexBuffer;
// ... 分配和填充顶点缓冲区 ...

// 创建顶点缓冲区视图
D3D12_VERTEX_BUFFER_VIEW vbv = {};
vbv.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vbv.SizeInBytes = vertexBufferSize;
vbv.StrideInBytes = sizeof(Vertex);

// 创建索引缓冲区和视图
// ... 类似的代码 ...

// 在绘制命令中使用
commandList->IASetVertexBuffers(0, 1, &vbv);
commandList->IASetIndexBuffer(&ibv);
commandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);

5.3 计算机色彩基础

颜色是渲染中的核心要素,理解计算机如何表示和处理颜色对于图形编程至关重要。

5.3.1 颜色运算

在计算机图形学中,颜色通常表示为RGB(红、绿、蓝)或RGBA(加上Alpha透明度)分量的组合。每个分量的值范围通常为0到1(浮点表示)或0到255(整数表示)。

cpp

// 颜色表示(浮点)
struct Color {
    float R, G, B, A;
};

// 常见颜色
Color red = { 1.0f, 0.0f, 0.0f, 1.0f };
Color green = { 0.0f, 1.0f, 0.0f, 1.0f };
Color blue = { 0.0f, 0.0f, 1.0f, 1.0f };
Color white = { 1.0f, 1.0f, 1.0f, 1.0f };
Color black = { 0.0f, 0.0f, 0.0f, 1.0f };

颜色的基本运算包括:

  1. 颜色混合:将两种颜色按一定比例混合

    cpp

    Color Lerp(Color a, Color b, float t) {
        return {
            a.R + (b.R - a.R) * t,
            a.G + (b.G - a.G) * t,
            a.B + (b.B - a.B) * t,
            a.A + (b.A - a.A) * t
        };
    }
    
  2. 颜色乘法:通常用于光照计算

    cpp

    Color Multiply(Color a, Color b) {
        return {
            a.R * b.R,
            a.G * b.G,
            a.B * b.B,
            a.A * b.A
        };
    }
    
  3. Alpha混合:考虑透明度的颜色混合

    cpp

    Color AlphaBlend(Color source, Color destination) {
        float a = source.A;
        return {
            source.R * a + destination.R * (1 - a),
            source.G * a + destination.G * (1 - a),
            source.B * a + destination.B * (1 - a),
            1.0f  // 结果是完全不透明的
        };
    }
    

5.3.2 128 位颜色

在某些高精度渲染场景中,每个颜色通道可能需要32位浮点数表示,形成128位颜色格式:

  • 每个通道使用32位IEEE浮点数
  • 提供极高的精度和动态范围
  • 适用于HDR渲染、色彩校正和图像处理
  • 在DirectX中对应DXGI_FORMAT_R32G32B32A32_FLOAT格式

cpp

// 128位浮点颜色
struct Color128 {
    float R, G, B, A;  // 每个分量是32位浮点数
};

// 在DirectX中使用128位颜色格式
D3D12_RESOURCE_DESC textureDesc = {};
textureDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
// ... 其他参数 ...

5.3.3 32 位颜色

32位颜色是最常用的颜色格式,通常分配为RGBA,每个通道8位:

  • 红色、绿色、蓝色和alpha各占8位
  • 可表示约1670万种颜色
  • 在DirectX中对应DXGI_FORMAT_R8G8B8A8_UNORM格式

cpp

// 32位整数颜色
struct Color32 {
    uint8_t R, G, B, A;  // 每个分量是8位整数
};

// 或使用单个32位值
uint32_t PackColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
    return (a << 24) | (b << 16) | (g << 8) | r;
}

// 在DirectX中使用32位颜色格式
D3D12_RESOURCE_DESC textureDesc = {};
textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// ... 其他参数 ...

5.4 渲染流水线概述

DirectX 12的渲染流水线由以下几个主要阶段组成:

  1. 输入装配器阶段:将顶点和索引数据组装成基本图元(如点、线、三角形)
  2. 顶点着色器阶段:处理每个顶点的位置、颜色等属性
  3. 曲面细分阶段(可选):增加模型细节
  4. 几何着色器阶段(可选):创建或修改图元
  5. 光栅化阶段:将3D图元转换为2D像素
  6. 像素着色器阶段:计算每个像素的颜色
  7. 输出合并阶段:执行深度测试、模板测试和混合操作

以下是一个简化的DirectX 12渲染流水线示例:

cpp

// 1. 设置输入布局
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

// 2. 创建图形管线状态对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.pRootSignature = rootSignature.Get();
psoDesc.VS = { vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize() };
psoDesc.PS = { pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize() };
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
psoDesc.SampleDesc.Count = 1;

// 3. 在绘制命令中设置渲染管线
commandList->SetPipelineState(pipelineState.Get());
commandList->SetGraphicsRootSignature(rootSignature.Get());
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->IASetIndexBuffer(&indexBufferView);
commandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);

5.5 输入装配器阶段

输入装配器(Input Assembler,IA)阶段是渲染流水线的第一个阶段,负责从内存中读取顶点数据并将其组装成基本图元。

5.5.1 顶点

顶点是3D模型的基本构建单元,通常包含以下属性:

  • 位置:通常是三维空间中的XYZ坐标
  • 法线:用于光照计算的表面法向量
  • 纹理坐标:用于从纹理中采样的UV坐标
  • 颜色:顶点的颜色信息
  • 切线和副切线:用于法线映射
  • 骨骼索引和权重:用于蒙皮动画

在DirectX 12中,通过定义输入布局来指定顶点结构:

cpp

// 定义顶点结构
struct Vertex {
    XMFLOAT3 position;
    XMFLOAT3 normal;
    XMFLOAT2 texCoord;
    XMFLOAT4 color;
};

// 定义输入布局
D3D12_INPUT_ELEMENT_DESC inputLayout[] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

5.5.2 图元拓扑

图元拓扑定义了如何将顶点连接成几何图元。DirectX 12支持以下主要拓扑类型:

  1. 点列表D3D_PRIMITIVE_TOPOLOGY_POINTLIST):每个顶点表示一个点
  2. 线列表D3D_PRIMITIVE_TOPOLOGY_LINELIST):每两个顶点构成一条线
  3. 线带D3D_PRIMITIVE_TOPOLOGY_LINESTRIP):连续的顶点形成连接的线段
  4. 三角形列表D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST):每三个顶点构成一个三角形
  5. 三角形带D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP):连续的顶点形成相邻的三角形

cpp

// 设置图元拓扑
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

三角形列表是最常用的拓扑类型,其工作原理如下:

  • 每三个顶点形成一个三角形
  • 顶点0, 1, 2形成第一个三角形
  • 顶点3, 4, 5形成第二个三角形
  • 以此类推

gherkin

顶点索引:  0----1    3----4
            |   /|    |   /|
            |  / |    |  / |
            | /  |    | /  |
            2    /    5    /

三角形带则更节省顶点:

  • 第一个三角形使用顶点0, 1, 2
  • 第二个三角形使用顶点2, 1, 3(注意顺序)
  • 第三个三角形使用顶点2, 3, 4
  • 以此类推

顶点索引:  1----3----5
            |\ /|\ /|
            | X | X |
            |/ \|/ \|
            0----2----4

5.5.3 索引

索引允许多个图元共享顶点,节省内存并提高性能。索引缓冲区存储顶点数组的索引,而不是重复存储顶点数据。

cpp

// 定义索引数据(三角形列表)
uint16_t indices[] = {
    0, 1, 2,  // 三角形1
    0, 2, 3,  // 三角形2
    4, 5, 6,  // 三角形3
    4, 6, 7   // 三角形4
};

// 创建索引缓冲区
ComPtr<ID3D12Resource> indexBuffer;
// ... 分配和填充索引缓冲区 ...

// 创建索引缓冲区视图
D3D12_INDEX_BUFFER_VIEW indexBufferView = {};
indexBufferView.BufferLocation = indexBuffer->GetGPUVirtualAddress();
indexBufferView.Format = DXGI_FORMAT_R16_UINT;  // 16位索引
indexBufferView.SizeInBytes = sizeof(indices);

// 设置索引缓冲区并绘制
commandList->IASetIndexBuffer(&indexBufferView);
commandList->DrawIndexedInstanced(12, 1, 0, 0, 0);  // 绘制12个索引(4个三角形)

索引的优势:

  1. 内存效率:共享顶点减少数据重复
  2. 性能提升:减少顶点着色器的处理量
  3. 顶点缓存优化:GPU可以缓存最近处理的顶点

5.6 顶点着色器阶段

顶点着色器(Vertex Shader)是完全可编程的阶段,负责处理每个顶点的计算。它的主要任务包括:

  1. 坐标变换:将顶点从模型空间转换到裁剪空间
  2. 顶点属性计算:计算或传递颜色、纹理坐标等属性
  3. 特殊效果准备:如顶点动画、变形等

典型的顶点着色器HLSL代码示例:

hlsl

// 常量缓冲区定义
cbuffer ModelViewProjectionConstantBuffer : register(b0) {
    matrix model;
    matrix view;
    matrix projection;
};

// 顶点着色器输入结构
struct VertexInput {
    float3 position : POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
    float4 color : COLOR;
};

// 顶点着色器输出结构(传递给像素着色器)
struct VertexOutput {
    float4 position : SV_POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
    float4 color : COLOR;
    float3 worldPosition : TEXCOORD1;
};

// 顶点着色器主函数
VertexOutput main(VertexInput input) {
    VertexOutput output;
    
    // 坐标变换:模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间
    float4 modelPosition = float4(input.position, 1.0f);
    float4 worldPosition = mul(modelPosition, model);
    float4 viewPosition = mul(worldPosition, view);
    output.position = mul(viewPosition, projection);
    
    // 法线变换:从模型空间到世界空间
    // 注意:法线需要使用模型矩阵的逆转置
    output.normal = normalize(mul(input.normal, (float3x3)model));
    
    // 传递纹理坐标和颜色
    output.texCoord = input.texCoord;
    output.color = input.color;
    
    // 保存世界空间位置(用于光照计算)
    output.worldPosition = worldPosition.xyz;
    
    return output;
}

5.6.1 局部空间和世界空间

在3D图形中,坐标转换是通过一系列空间变换完成的:

  1. 局部空间(模型空间)

    • 模型的原始坐标系
    • 以模型自身为中心
    • 独立于场景中的位置和方向
  2. 世界空间

    • 整个3D场景的共同坐标系
    • 所有模型都放置在这个统一空间中
    • 通过模型矩阵(Model Matrix)将模型从局部空间变换到世界空间

cpp

// 创建模型矩阵(局部空间->世界空间)
XMMATRIX modelMatrix = XMMatrixIdentity();
modelMatrix *= XMMatrixScaling(scale.x, scale.y, scale.z);                        // 缩放
modelMatrix *= XMMatrixRotationQuaternion(XMLoadFloat4(&rotation));                // 旋转
modelMatrix *= XMMatrixTranslation(position.x, position.y, position.z);            // 平移

变换顺序很重要:

  • 先缩放
  • 再旋转
  • 最后平移

在顶点着色器中应用模型矩阵:

hlsl

// 将顶点从局部空间变换到世界空间
float4 worldPosition = mul(float4(localPosition, 1.0f), modelMatrix);

5.6.2 观察空间

观察空间(又称摄像机空间或视图空间)是以摄像机为中心的坐标系:

  • 摄像机位于原点(0,0,0)
  • 摄像机沿负Z轴观察
  • Y轴通常指向上方

通过视图矩阵(View Matrix)将世界空间转换为观察空间:

cpp

// 创建视图矩阵(世界空间->观察空间)
XMVECTOR eyePosition = XMLoadFloat3(&cameraPosition);
XMVECTOR focusPoint = XMLoadFloat3(&cameraTarget);
XMVECTOR upDirection = XMLoadFloat3(&cameraUp);

XMMATRIX viewMatrix = XMMatrixLookAtLH(eyePosition, focusPoint, upDirection);

在顶点着色器中应用视图矩阵:

hlsl

// 将顶点从世界空间变换到观察空间
float4 viewPosition = mul(worldPosition, viewMatrix);

5.6.3 投影和齐次裁剪空间

投影变换将观察空间中的3D坐标转换为标准化设备坐标(NDC)空间,这个过程包含两个步骤:

  1. 投影变换:通过投影矩阵将观察空间转换为齐次裁剪空间
  2. 透视除法:将齐次坐标(x,y,z,w)转换为NDC坐标(x/w, y/w, z/w)

DirectX中使用两种主要的投影方式:

  1. 透视投影

    • 模拟现实中的透视效果(远小近大)
    • 适用于大多数3D场景

    cpp

    // 创建透视投影矩阵
    float fovY = 45.0f * XM_PI / 180.0f;  // 垂直视场角(度转弧度)
    float aspectRatio = (float)width / height;
    float nearZ = 0.1f;
    float farZ = 1000.0f;
    
    XMMATRIX projectionMatrix = XMMatrixPerspectiveFovLH(
        fovY, aspectRatio, nearZ, farZ
    );
    
  2. 正交投影

    • 不考虑透视效果(远近物体大小相同)
    • 适用于2D UI、工程图纸等

    cpp

    // 创建正交投影矩阵
    float width = 10.0f;
    float height = 8.0f;
    float nearZ = 0.1f;
    float farZ = 100.0f;
    
    XMMATRIX projectionMatrix = XMMatrixOrthographicLH(
        width, height, nearZ, farZ
    );
    

在顶点着色器中应用投影矩阵:

hlsl

// 将顶点从观察空间变换到裁剪空间
float4 clipPosition = mul(viewPosition, projectionMatrix);

// 整个变换链可以合并为一行
output.position = mul(mul(mul(float4(input.position, 1.0f), model), view), projection);

完成投影变换后,顶点位于裁剪空间中。位于单位立方体(-w ≤ x,y,z ≤ w)外的顶点将被裁剪。经过透视除法后,顶点将位于[-1,1]³的标准化设备坐标系中。

5.7 曲面细分阶段

曲面细分(Tessellation)是一个可选的流水线阶段,允许动态增加模型的几何细节。它由三个子阶段组成:

  1. 外壳着色器(Hull Shader):准备控制点和细分因子
  2. 曲面细分器(Tessellator):固定功能,执行细分操作
  3. 域着色器(Domain Shader):计算细分后的新顶点属性

曲面细分的主要应用:

  • 随距离动态调整网格细节(LOD)
  • 曲面位移映射
  • 水面和地形渲染
  • 程序化几何生成

简化的曲面细分HLSL示例:

hlsl

// 补丁常量函数(决定细分因子)
struct PatchConstantOutput {
    float edges[3]    : SV_TessFactor;
    float inside      : SV_InsideTessFactor;
};

PatchConstantOutput PatchConstantFunction(InputPatch<VertexOutput, 3> patch) {
    PatchConstantOutput output;
    
    // 基于摄像机距离计算细分因子
    float distance = length(CameraPosition - GetPatchCenter(patch));
    float tessAmount = saturate((MaxDistance - distance) / MaxDistance);
    
    // 设置细分因子(1到MaxTessFactor)
    float factor = lerp(1.0f, MaxTessFactor, tessAmount);
    
    output.edges[0] 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小宝哥Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值