- 细分着色器分为:曲面细分着色器(Unity在指定平台硬件支持)、细分计算着色器。使用片面来描述一个物体形状,并增加顶点和片面数量,使模型外观开起来更平滑。
- 作用:动态细分三角面片,提升模型细节(如草地、地形渲染),优化大场景性能 。
- 这是一个可选阶段。它在顶点着色器之后运行,负责根据设定的细分因子 (Tessellation Factor) 将输入的图元(通常是三角形面片)动态细分为更小的三角面片,生成更多顶点。
【从UnityURP开始探索游戏渲染】专栏-直达
三个子阶段:
Hull Shader:定义每条边和内部分割因子(Tessellation Factor)。
- 接收原始控制点数据并定义细分因子(Tessellation Factor)
- 通过
patch constant function
计算每条边和内部的细分等级 - 支持分数细分模式(fractional_odd/fractional_even)实现平滑过渡
目标:
- 定义原始控制点数据并计算细分因子(Tessellation Factor)
- 确定每个patch的边和内部细分等级
输入输出:
- 输入:原始控制点数据(位置、法线、UV等)
- 输出:
- 细分因子(
SV_TessFactor
标记边,SV_InsideTessFactor
标记内部) - 处理后的控制点数据(通过
INTERNALTESSPOS
语义标记)
- 细分因子(
实现关键:
glsl
hlsl
struct TessFactors {
float edge[3] : SV_TessFactor; // 三条边的细分因子
float inside : SV_InsideTessFactor; // 内部细分因子
};
Tessellation Primitive Generator 固定功能:硬件根据 Hull Shader 输出的因子实际执行细分操作。
- GPU固定功能阶段,根据Hull Shader输出的细分因子生成新顶点拓扑
目标:
- 根据Hull Shader输出的细分因子生成新顶点拓扑
- 将原始patch细分为更密集的三角网格
输入输出:
-
输入:Hull Shader输出的细分因子和控制点
glslhlsl // 来自Hull Shader的输出 struct TessControlPoint { float4 positionOS : INTERNALTESSPOS; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; // 细分因子定义 struct TessFactors { float edge[3] : SV_TessFactor; // 每条边的细分等级 float inside : SV_InsideTessFactor; // 内部细分等级 };
-
输出:细分后的顶点UV坐标和拓扑关系
-
生成的重心坐标数据:
glslhlsl float3 baryCoords : SV_DomainLocation; // 新顶点的重心坐标
-
输出拓扑类型由Hull Shader的
[outputtopology]
属性定义(如triangle_cw)
-
特性:
- GPU自动执行,无需开发者编码
- 支持分数细分模式实现平滑过渡
实现原理
细分算法
-
对原始三角面片进行递归细分,采用Delaunay三角剖分原则
GPU使用的Delaunay三角剖分原则是一种优化网格拓扑的数学方法
-
Delaunay三角剖分原则
- 空圆特性:任意三角形的外接圆内不包含其他顶点
- 最大化最小角:避免出现尖锐三角形,提高网格质量
- 局部优化:通过边翻转(edge flip)逐步优化三角网格
-
URP中的实现特点:
- 在Tessellation Primitive Generator阶段自动应用
- 对细分后的新顶点进行拓扑优化
- 保持与原网格的平滑过渡
-
具体示例:
原始三角面片(控制点A/B/C)经过细分后:
-
计算细分因子为3时:
原始三角形: A
/
C---B细分后拓扑:
A
/|
/ |
C--D--B
\ | /
|/
E -
其中D/E是新生成的顶点,所有三角形都满足:
- ∠ADB + ∠AEB ≈ 180°
- 外接圆不包含其他顶点
-
-
URP中的实际应用:
- 当使用
[partitioning("fractional_odd")]
时:- 会在过渡区域自动应用Delaunay优化
- 确保不同细分等级间的平滑连接
- 位移贴图处理时:
- 新顶点根据Delaunay规则分布
- 位移后的法线计算更准确
- 当使用
-
这种剖分方式使得:
- 细分后的网格更适应曲面变形
- 避免渲染时的褶皱现象
- 提升位移贴图的视觉效果
-
-
根据分数细分模式(fractional_odd/even)处理过渡区域
坐标转换 Pnew=P0⋅u+P1⋅v+P2⋅w
- 将参数空间坐标(u,v,w)转换为新的顶点位置
性能优化
- 采用并行计算处理多个patch
- 自动剔除屏幕空间不可见的细分结果
Domain Shader:
将细分后位于重心坐标系 (重心坐标内容后续单开一篇讲解)中的新顶点位置转换到目标空间(如世界空间或裁剪空间)。
- 将细分后的UV坐标映射到3D空间
- 执行顶点位移等后期处理
目标:
- 将细分后的UV坐标映射到3D空间
- 执行顶点位移等后期处理
输入输出:
- 输入:细分后的UV坐标和原始控制点数据
- 输出:最终顶点位置(
SV_POSITION
)和其他顶点属性
实现示例:
glsl
hlsl
[domain("tri")] // 声明处理三角形patch
Varyings domain(TessFactors factors, OutputPatch<DomainAttributes, 3> patch, float3 baryCoords : SV_DomainLocation) {
Varyings OUT;
// 插值计算新顶点属性
OUT.positionWS = TransformObjectToWorld(patch[0].positionOS * baryCoords.x + ...);
return OUT;
}
动态控制技巧:
-
通过摄像机距离调整细分因子:
glslhlsl float CalcTessFactor(float3 worldPos) { return lerp(_MaxTess, _MinTess, saturate(distance(_WorldSpaceCameraPos, worldPos) / _TessRange)); }
-
结合高度图实现位移效果
配置:
- 需显式启用(HLSL 中声明
hull
和domain
函数)。 - 必须声明
#pragma target 4.6
以启用DX11/OpenGL Core特性3 - 需手动实现Hull/Domain Shader,URP不提供表面着色器的简化写法
- 建议结合摄像机距离动态控制细分因子以优化性能
URP中的具体实现步骤:
声明编译目标为4.6以上:
glsl
hlsl
#pragma target 4.6
定义三个关键程序:
glsl
hlsl
#pragma vertex BeforeTessVert
#pragma hull HullProgram
#pragma domain DomainProgram
控制点数据结构需包含顶点位置、法线等基础属性
动态细分控制样例:
- 通过距离或屏幕空间尺寸自动调整细分因子:
glsl
hlsl
// 根据摄像机距离动态计算细分因子
float CalcTessFactor(float3 worldPos) {
float dist = distance(_WorldSpaceCameraPos, worldPos);
return lerp(_MaxTess, _MinTess, saturate(dist / _TessRange));
}
常见应用场景:
- 地形动态LOD:根据视角距离细分地面网格3
- 曲面平滑:将低模转换为高模曲面2
- 动态位移:结合高度图实现实时凹凸效果6
注意事项:
- 仅支持DX11/OpenGL Core等现代图形API6
- 细分过度会导致性能下降,需合理设置上限5
- URP中需手动实现Hull/Domain Shader,不同于内置管线的表面着色器简化写法
Unity URP中完整的曲面细分着色器示例,包含顶点位移效果和动态细分控制
示例实现功能:
- 动态细分控制:根据摄像机距离自动调整细分因子
- 顶点位移效果:通过高度图(_DispTex)驱动顶点偏移
- 分数细分模式:使用fractional_odd实现平滑过渡
- 完整渲染管线:包含顶点/细分/片元全阶段处理
- URP兼容性:使用URP的ShaderLibrary核心函数
使用说明:
- 创建材质球并应用此着色器
- 为_DispTex指定高度图纹理
- 调整_TessFactor控制细分密度
- 通过_Displacement参数控制位移强度
TessellationExample.shader
glsl
// HLSL
Shader "Custom/TessellationExample"
{
Properties
{
_MainTex ("Base Texture", 2D) = "white" {}
_TessFactor ("Tessellation Factor", Range(1, 64)) = 4
_Displacement ("Displacement", Range(0, 1.0)) = 0.3
_DispTex ("Displacement Texture", 2D) = "gray" {}
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" }
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};
struct TessControlPoint
{
float4 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};
struct TessFactors
{
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
struct DomainOutput
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalWS : NORMAL;
};
ENDHLSL
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma hull hull
#pragma domain domain
#pragma fragment frag
#pragma target 4.6
sampler2D _MainTex;
sampler2D _DispTex;
float _TessFactor;
float _Displacement;
TessControlPoint vert(Attributes v)
{
TessControlPoint o;
o.positionOS = v.positionOS;
o.normalOS = v.normalOS;
o.uv = v.uv;
return o;
}
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("patchConstantFunc")]
TessControlPoint hull(InputPatch<TessControlPoint, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
TessFactors patchConstantFunc(InputPatch<TessControlPoint, 3> patch)
{
TessFactors f;
float avgTess = _TessFactor * (1 - saturate(length(_WorldSpaceCameraPos - TransformObjectToWorld(patch[0].positionOS.xyz)) / 20));
f.edge[0] = f.edge[1] = f.edge[2] = avgTess;
f.inside = avgTess;
return f;
}
[domain("tri")]
DomainOutput domain(TessFactors factors, OutputPatch<TessControlPoint, 3> patch, float3 baryCoords : SV_DomainLocation)
{
DomainOutput o;
// 插值计算基础属性
float3 positionOS = patch[0].positionOS.xyz * baryCoords.x +
patch[1].positionOS.xyz * baryCoords.y +
patch[2].positionOS.xyz * baryCoords.z;
float2 uv = patch[0].uv * baryCoords.x +
patch[1].uv * baryCoords.y +
patch[2].uv * baryCoords.z;
// 从高度图获取位移值
float disp = tex2Dlod(_DispTex, float4(uv, 0, 0)).r * _Displacement;
positionOS += normalize(patch[0].normalOS) * disp;
// 转换到裁剪空间
o.positionCS = TransformObjectToHClip(positionOS);
o.uv = uv;
o.normalWS = TransformObjectToWorldNormal(patch[0].normalOS);
return o;
}
half4 frag(DomainOutput i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}
【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)