1357 字
7 分钟
DirectX 11 3D 核心原理与着色器

一. 3D 空间矩阵变换#

之前提到了 MVP 变换,这里需要深入讲解为什么以及怎么做。这是 3D 引擎的数学心脏。

1 仿射变换 (Affine Transformation)#

在 3D 图形学中,物体的运动(位移、旋转、缩放)都是通过4x4 矩阵来实现的。

  • 齐次坐标 (Homogeneous Coordinates):为了让平移(加法)也能用矩阵乘法表示,我们将 3D 向量 (x,y,z)(x,y,z) 扩展为 4D 向量 (x,y,z,1)(x,y,z,1)
  • 变换顺序:矩阵乘法不满足交换律。标准的变换顺序是 (Scale\timeRotation×Translation)(Scale \time Rotation \times Translation)** (SRT)**。
  • 原理:先缩放(改变大小),再自转(改变朝向),最后平移(放到世界中的位置)。
  • 如果先平移再缩放,物体会一边移动一边变大,产生错误的位移偏差。

2 摄像机 (The View Matrix)#

在计算机图形学中,摄像机是不存在的

  • 相对运动原理:当你向左移动摄像机时,数学上等价于整个世界向右移动
  • LookAt 矩阵:你需要提供三个向量——眼位置 (Eye)、目标点 (At/Focus)、头顶朝向 (Up)。DirectXMath 库会通过向量叉乘(Cross Product)构建出一个正交基,算出一个矩阵,将世界里的所有物体搬到摄像机面前。

3 透视投影 (Perspective Projection)#

为什么会有“近大远小”?这是由投影矩阵决定的。

  • 视锥体 (Frustum):摄像机能看到的空间是一个被切掉尖顶的金字塔形状。
  • FOV (Field of View):视野角度。
  • 非线性深度:投影后的 Z 值不是均匀分布的。近处的精度极高,远处的精度极低。这就是为什么要设置 NearZ (0.01) 和 FarZ (100.0)。如果 NearZ 设为 0,会导致 Z-Fighting(深度冲突,画面闪烁)。

二. CPU 与 GPU 的通信 —— 常量缓冲区#

C++ 代码(CPU)怎么告诉 Shader(GPU)这一帧的内容?

1 Constant Buffer (常量缓冲区)#

  • 定义:这是一块 CPU 写、GPU 读的显存区域。
  • 特点
  • 高频更新:通常每一帧、每个物体都会更新一次(UpdateSubresource)。
  • 小容量:不像顶点缓冲存几万个点,常量缓冲通常只存几个矩阵和光照参数。

2 16 字节对齐 (16-Byte Alignment)#

GPU 的寄存器是 SIMD(单指令多数据)架构,每个寄存器宽 128 位(16 字节,即 float4)。

  • HLSL 规则:如果你的变量跨越了 16 字节的边界,它会被强制推到下一个寄存器。
  • 后果:如果 C++ 的结构体没有手动填充(Padding)对齐,C++ 传过去的数据会和 Shader 里读到的数据错位
  • 规范:在 C++ 结构体中,尽量使用 DirectX::XMMATRIXXMFLOAT4,确保数据紧凑且对齐。

三. 纹理采样与过滤#

如何把一张 2D 的纹理图贴在 3D 的球面上?

1 UV 坐标系#

  • 归一化:无论纹理图片是 1024x1024 还是 256x256,UV 坐标永远是 0.01.00.0\rightarrow 1.0
  • 寻址模式 (Address Mode)
  • 如果 UV 超过了 1.0 怎么办?
  • Wrap (重复):像铺地砖一样重复纹理。
  • Clamp (截断):超过部分强制取边缘颜色。

2 纹理过滤 (Texture Filtering)#

当 3D 物体离摄像机很近或很远时,一个屏幕像素可能对应纹理上的 0.1 个像素,也可能对应 100 个像素。

  • Minification (缩小):物体很远。多个纹素挤在一个像素里。如果不处理,会产生摩尔纹 (Moiré pattern) 和闪烁。
  • Magnification (放大):物体很近。一个纹素要覆盖多个像素。如果不处理,会看到马赛克
  • 双线性插值 (Bilinear):取周围 4 个纹素的加权平均值。解决马赛克问题,使画面模糊变平滑。
  • 各向异性过滤 (Anisotropic):解决侧视物体(如地面)纹理模糊的问题。

四. 深度测试 —— 遮挡剔除#

显卡如何保证不会发生透视,透过前面的物体看到后面的模型?

1 Z-Buffer (深度缓冲)#

  • 这是一张和屏幕分辨率一样大的“隐形图片”。
  • 它不存颜色,只存每个像素当前的深度值(0.0 代表最近,1.0 代表最远)。

2 深度测试流程#

当像素着色器算出一个像素的颜色和深度 ZnewZ_{new} 时:

  1. 比较:读取深度缓冲中该位置已有的深度 ZoldZ_{old}
  2. 判断
  • 如果 Znew<ZoldZ_{new}<Z_{old}:说明新像素离镜头更近。写入颜色,并更新深度缓冲
  • 如果 Znew>ZoldZ_{new}>Z_{old}:说明新像素被前面的物体挡住了。丢弃 (Discard),什么都不做。

这也是为什么每帧开始要 ClearDepthStencilView,把深度全部重置为 1.0(无限远)。


五. 着色器编程 (HLSL)#

High Level Shading Language (HLSL) 是运行在显卡上的 C 语言变体。

1 语义 (Semantics)#

在 HLSL 中,变量名不重要,重要的是冒号后面的标签

  • float4 Pos : POSITION:告诉 GPU,这个变量存放的是顶点位置。
  • float2 Tex : TEXCOORD:告诉 GPU,这个变量存放的是纹理坐标。
  • float4 Pos : SV_POSITIONSystem Value。这是 Vertex Shader 的必须输出。它告诉光栅化器:“这就是我要你在屏幕上画的位置”。

2 插值器 (Interpolator)#

从 Vertex Shader 输出的数据,传到 Pixel Shader 时,会发生线性插值

  • 如果你在 VS 中输出:顶点A是红色,顶点B是绿色。
  • 在 PS 中:A 和 B 连线中间的点,会自动变成渐变的黄色
  • 这种机制构成了现代图形学平滑着色的基础。
DirectX 11 3D 核心原理与着色器
https://www.m4doka.xyz/posts/dx11/dx11-2/
作者
m4doka
发布于
2026-01-22
许可协议
CC BY-NC-SA 4.0