4、几何阶段(二):曲面细分着色器与几何着色器
好,咱们接着聊几何阶段。上一章我们把顶点着色器聊透了,这一章要啃两块硬骨头——曲面细分着色器和几何着色器。说实话,这两个东西在早期的渲染管线里是没有的,是后来才加进来的。我刚开始接触它们的时候,也觉得有点绕,但用熟了之后会发现,它们能解决很多传统管线搞不定的问题。
曲面细分着色器(Tessellation Shader)
曲面细分,说白了就是“把粗糙的模型变精细”。你想想看,一个三角形只有三个顶点,但如果我想在它上面画出一个凹凸不平的山丘,光靠这三个点肯定不够。曲面细分就是干这个的——它把一个大三角形拆成很多个小三角形,然后让这些小三角形的顶点动起来,形成更复杂的形状。
三个阶段:Hull Shader、Tessellator、Domain Shader
曲面细分着色器其实不是单个着色器,而是三个阶段的组合。我个人习惯把它们理解成“决策者”、“执行者”和“修正者”。
| 阶段 | 名称 | 作用 |
|---|---|---|
| 第一阶段 | Hull Shader(外壳着色器) | 决定怎么细分,输出细分因子 |
| 第二阶段 | Tessellator(固定功能单元) | 根据细分因子,生成新的顶点坐标 |
| 第三阶段 | Domain Shader(域着色器) | 把新顶点映射到最终位置 |
嗯,这里要注意:Tessellator 是固定功能的硬件单元,你不能写代码控制它,只能通过 Hull Shader 告诉它“你要细分多少”。
Hull Shader 的输入输出
Hull Shader 的输入是控制点(Control Points),输出是细分因子(Tessellation Factors)。细分因子决定了每个三角形边被分成几段。
// 一个简单的 Hull Shader 示例
struct HullInput {
float3 position : POSITION;
};
struct HullOutput {
float3 position : POSITION;
};
// 细分因子输出
struct TessFactors {
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
// 主函数
TessFactors HullShaderMain(InputPatch<HullInput, 3> patch, uint patchID : SV_PrimitiveID) {
TessFactors tf;
tf.edge[0] = 4.0f; // 第一条边分成4段
tf.edge[1] = 4.0f; // 第二条边分成4段
tf.edge[2] = 4.0f; // 第三条边分成4段
tf.inside = 4.0f; // 内部也分成4段
return tf;
}
我在项目中遇到过一个问题:细分因子设得太大,结果一个三角形被拆成了几万个微小的三角形,帧率直接掉到个位数。后来我学乖了,根据相机距离动态调整细分因子——远处的物体细分少一点,近处的多一点。这样既保证了质量,又控制了性能。
Domain Shader 的典型用法
Domain Shader 接收 Tessellator 生成的新顶点坐标(通常是重心坐标),然后计算出这些顶点在三维空间中的实际位置。最常见的用法是做置换贴图(Displacement Mapping)。
// Domain Shader 示例:根据高度图做置换
struct DomainOutput {
float4 position : SV_POSITION;
float3 normal : NORMAL;
};
[domain("tri")]
DomainOutput DomainShaderMain(
TessFactors tf,
const OutputPatch<HullOutput, 3> patch,
float3 bary : SV_DomainLocation) {
// 重心坐标插值
float3 pos = patch[0].position * bary.x +
patch[1].position * bary.y +
patch[2].position * bary.z;
// 采样高度图,沿法线方向偏移
float height = tex2D(heightMap, uv).r;
pos += normal * height * 0.1f;
DomainOutput output;
output.position = mul(projMatrix, float4(pos, 1.0f));
return output;
}
几何着色器(Geometry Shader)
几何着色器,它比曲面细分更灵活。曲面细分只能把三角形拆成更小的三角形,但几何着色器可以干更多事——它可以增删顶点,甚至改变图元类型。说白了,它让你在渲染管线的中间阶段,对几何体做“手术”。
输入输出类型
几何着色器的输入是一个完整的图元(点、线、三角形),输出可以是零个或多个图元。这个“零个”很有意思——你可以用它来做剔除,比如把背面的三角形直接扔掉。
| 输入类型 | 输出类型 | 典型用途 |
|---|---|---|
| point | point / line / triangle | 粒子系统、点精灵扩展 |
| line | line / triangle | 毛发渲染、线条加粗 |
| triangle | point / line / triangle | 爆炸效果、轮廓线提取 |
典型用法:法线可视化
我记得有一次调试一个复杂的模型,法线方向总是搞不对。用几何着色器把法线可视化出来,问题一目了然。
// 几何着色器:把法线画成线段
[maxvertexcount(6)]
void GS_Main(triangle VertexOutput input[3], inout LineStream<VertexOutput> stream) {
// 计算三角形中心
float3 center = (input[0].pos + input[1].pos + input[2].pos) / 3.0f;
float3 normal = (input[0].normal + input[1].normal + input[2].normal) / 3.0f;
VertexOutput v1, v2;
v1.pos = mul(projMatrix, float4(center, 1.0f));
v1.color = float4(0, 0, 1, 1); // 蓝色起点
v2.pos = mul(projMatrix, float4(center + normal * 0.5f, 1.0f));
v2.color = float4(1, 0, 0, 1); // 红色终点
stream.Append(v1);
stream.Append(v2);
}
几何着色器的局限性
说实话,几何着色器在现在的渲染管线里有点“尴尬”。为什么?因为它的性能表现不太稳定。它在 CPU 和 GPU 之间引入了额外的同步点,而且输出数量有限。我个人的建议是:
- 如果你只是想做简单的图元扩展(比如点变四边形),几何着色器很合适
- 如果你需要大量生成几何体,用计算着色器 + 顶点着色器的方式更高效
- 曲面细分和几何着色器不要混用,容易出奇怪的 bug
避坑指南:我曾经在一个项目里同时启用了曲面细分和几何着色器,结果发现几何着色器处理的是细分后的三角形,而不是原始三角形。这意味着我原本以为只处理 100 个三角形,实际上处理了 10000 个。性能直接崩了。所以,记住:几何着色器是在曲面细分之后执行的。
总结一下
曲面细分着色器和几何着色器,都是给开发者更多“几何控制权”的工具。曲面细分擅长把粗糙模型变精细,适合做地形、角色细节;几何着色器擅长改变图元结构,适合做特效、调试可视化。但两者都有性能代价,用之前一定要想清楚:我真的需要这个吗?
下一章,我们会进入光栅化阶段。那才是真正决定像素命运的地方。