1354 字
7 分钟
DirectX 11 基础光照模型与管线状态管理

一、 光照模型基础:从颜色到立体感#

在 3D 渲染中,仅仅贴上纹理会让物体看起来像平面的纸模。为了体现物体的体积感和材质感,我们需要模拟光线与物体表面的交互。

1. 法线(Normal)的重要性#

光照计算的核心在于角度。我们需要知道光线是从哪里射入的,以及物体表面是朝向哪里的。

  • 顶点法线:在输入布局(Input Layout)中,除了位置(Position)和纹理坐标(UV),通常还需要引入法线(Normal)。
  • 插值:顶点法线在光栅化阶段会被线性插值传递给像素着色器(Pixel Shader),这就是为什么平滑的球体表面能呈现连续的光照变化。

2. 冯氏光照模型 (Phong Reflection Model)#

这是最经典的局部光照模型,它将光照分解为三个分量:

A. 环境光 (Ambient)#

  • 物理含义:模拟光的散射,即那些经过多次反射后充满整个环境的基础亮度。
  • 计算全局环境色 * 材质反射率
  • 效果:保证物体在没有直接光照的背面也不会完全变成死黑。

B. 漫反射 (Diffuse)#

  • 物理含义:模拟粗糙表面的光线均匀散射(朗伯余弦定律)。
  • 计算:取决于光线方向 (L)法线方向 (N) 的夹角余弦值。
  • 公式
  • 点积原理:当光线垂直射向表面时(夹角0度,点积1),最亮;当光线与表面平行或从背面射入时(夹角>=90度,点积<=0),无光照。

C. 镜面光 (Specular)#

  • 物理含义:模拟光滑表面的高光反射。
  • 计算:取决于反射光方向 (R)观察者视线方向 (V) 的夹角。
  • 公式
  • 高光指数 (Shininess):指数越大,光斑越小且锐利(如金属);指数越小,光斑越散(如塑料)。

3. HLSL 实现逻辑#

在像素着色器中,我们通常在一个统一的空间(如世界空间 World Space)进行计算:

// 伪代码示例
float3 ambient = globalAmbient * materialColor;
float3 diffuse = saturate(dot(normal, lightDir)) * lightColor * materialColor;
// Blinn-Phong 改进:使用半程向量 (Half Vector) 替代反射向量计算,性能更好
float3 halfVector = normalize(lightDir + viewDir);
float3 specular = pow(saturate(dot(normal, halfVector)), shininess) * lightColor;
return float4(ambient + diffuse + specular, 1.0f);

二、 CPU 与 GPU 的通信桥梁:常量缓冲区 (Constant Buffer)#

在渲染每一帧时,世界矩阵(World Matrix)、光照位置、相机位置等数据都在不断变化。cbuffer 是 DirectX 11 专门用于传输这类“每帧更新”数据的机制。

1. 16字节对齐规则 (16-Byte Alignment)#

这是初学者最容易踩的坑。GPU 读取常量缓冲区时,是按 4个浮点数(float4,即 16字节) 为一个寄存器单位(Register)读取的。

  • HLSL 端:变量会自动跨越寄存器边界,但如果 CPU 端的数据结构没对齐,数据就会错位。
  • C++ 端:结构体大小必须是 16 的倍数。

错误示例

struct CBuffer {
XMFLOAT3 pos; // 12 bytes
float time; // 4 bytes (刚好凑齐16,没问题)
XMFLOAT3 dir; // 12 bytes
// --- 这里结束,总共 28 bytes ---
// 下一个结构体开始时,GPU 会期待从 32 byte 处开始读取,导致错位。
};

正确做法:使用填充变量(Padding)或 DirectXMath 的对齐类型。

struct CBuffer {
XMMATRIX world; // 64 bytes (OK)
XMFLOAT3 pos; // 12 bytes
float padding; // 4 bytes (手动补齐到 16)
};

2. 更新策略:Map/Unmap vs UpdateSubresource#

  • UpdateSubresource:简单直接,驱动程序会处理内存复制。适合更新频率较低或数据量极小的情况。
  • Map (Write Discard):对于每帧都要更新的缓冲区(如变换矩阵),使用 D3D11_MAP_WRITE_DISCARD。这告诉 GPU:“旧的数据我不要了,给我一块新内存写”。这是实现动态缓冲区高性能更新的标准模式,可以避免 GPU 等待 CPU 的同步阻塞。

三、 输出合并阶段 (Output Merger Stage)#

这是图形管线的最后一环,决定了像素着色器计算出的颜色最终如何写入渲染目标(Render Target)。

1. 深度测试 (Depth Testing / Z-Buffering)#

在 3D 空间中,物体有前后遮挡关系。

  • 原理:每个像素除了存储颜色,还存储一个深度值(0.0 ~ 1.0)。

  • 逻辑:当新像素要写入时,比较它的深度值与缓冲区中已有的深度值。

  • 如果新像素更近(Depth < OldDepth),则通过测试,覆盖颜色并更新深度。

  • 如果新像素更远,则丢弃(Discard)。

  • Early-Z:现代 GPU 会在像素着色器运行之前进行一次粗略的深度测试,如果注定被遮挡,就不跑昂贵的着色器代码了,极大地节省了性能。

2. 混合状态 (Blending)#

用于实现半透明效果(如玻璃、水、特效)。

  • 公式

  • Src (Source):当前像素着色器输出的颜色。

  • Dest (Destination):渲染目标上原本存在的颜色。

  • 常用组合

  • 标准透明:SrcFactor = SRC_ALPHA, DestFactor = INV_SRC_ALPHA

  • 叠加模式(Add):SrcFactor = ONE, DestFactor = ONE(常用于火焰、光效)。

3. 光栅化状态 (Rasterizer State)#

虽然从流程上属于 RS 阶段,但常与 OM 阶段一起配置。

  • 剔除 (Culling):决定背面是否渲染。通常设置为 D3D11_CULL_BACK(逆时针为正面)以提高性能。
  • 填充模式 (Fill Mode):可以选择 SOLID(实体填充)或 WIREFRAME(线框模式,常用于调试几何体)。
DirectX 11 基础光照模型与管线状态管理
https://www.m4doka.xyz/posts/dx11/dx11-3/
作者
m4doka
发布于
2026-01-28
许可协议
CC BY-NC-SA 4.0