Unity URP 曲面细分学习笔记
学百人时遇到了曲面着色器的内容,有点糊里糊涂,于是上知乎找到了两篇大佬的文章 Unity URP 曲面细分 和 Unity曲面细分笔记,本文只是自己做学习记录使用
1.曲面细分与镶嵌
曲面细分或细分曲面(Subdivision surface)是指一种通过递归算法将一个粗糙的几何网格细化的技术。镶嵌(Tessellation)则是实现曲面细分的具体手段,它能将场景中的几何物体顶点集划分为合适的渲染结构。
曲面细分分为三个阶段:外壳着色器(Hull Shader)、镶嵌器(Tessellator)、域着色器(Domain Shader)。
1.1 外壳着色器 Hull Shader
Hull shader实际上是两个阶段(Phase)组成:常量外壳着色器(Constant Hull Shader)和 控制点外壳着色器(Control point hull shader),两个阶段并行运行。
- Constant Hull Shader 会对每一个面片进行处理,主要任务是输出网格的曲面细分因子(Tessellation Factor),曲面细分因子用于指导面片细分数。
- 假如要传入的面片是三角形,那么对于三角形就会有三个边曲面细分因子,所以
edgeFactor[3]
,对于内部曲面细分因子,因为三角形是最小的图元所以内部是一个因子(个人理解)insideFactor
,如果是矩形的话就是四个边因子和两个内部因子 - 常量外壳着色器会以面片的所有顶点(或控制点)为输入,所以有三个顶点
InputPatch<VertexOut, 3> patch
,数字为3 - SV_PrimitiveID 提供传入面片的ID值,可以用来区分不同的Patch,这样你就可以根据Patch的ID来为每个Patch设置不同的细分因子,或者执行其他依赖于Patch ID的操作。它对于每个图元都是不同的
- 假如要传入的面片是三角形,那么对于三角形就会有三个边曲面细分因子,所以
struct PatchTess{
float edgeFactor[3];
float insideFactor;
};
PatchTess PatchConstant(InputPatch<VertexOut, 3> patch, uint patchID : SV_PrimitiveID)
{
PatchTess o;
o.edgeFactor[0] = 4;
o.edgeFactor[1] = 4;
o.edgeFactor[2] = 4;
o.insideFactor = 4;
return o;
}
- Control Point Hull Shader 用来改变每个输出顶点的位置信息,例如将一个三角形变为三次贝塞尔三角面片
- domain:面片类型,参数有三角面tri、四角面片quad、等值线isoline
- partitioning:曲面细分方式,参数有integer、fractional_even、fractional_odd
- integer,指新顶点的增加指取决于细分的整数部分(等分),图形可能会出现图片
- fractional_even,向上取最近的偶数n,将整段切割为n-2个相等长度的部分,和两端较短的部分
- fractional_odd,向上取最近的奇数n,将整段切割为n-2个相等长度的部分,和两端较短的部分
- outputtopology:细分创建的三角形面片的绕序,参数有顺时针triangle_cw、逆时针triangle_ccw
- patchconstantfunc:常量外壳着色器的函数名
- outputcontrolpoints:外壳着色器的执行次数,即生成的控制点个数
- maxtessfactor:程序会使用到最大的细分因子
- SV_OutputControlPointID:当前正在操作的控制点索引ID
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut ControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionOS = patch[id].positionOS;
o.texcoord = patch[id].texcoord;
return o;
}
常量外壳着色器对每个片元执行一次,输出细分因子等信息;控制点外壳着色器对每个控制点执行一次,并输出对应或衍生的控制点。两个阶段并行运行。
1.2 镶嵌器阶段 Tessellator
这一阶段我们无法对其做出任何控制,全程由硬件控制。在这一阶段,硬件根据之前曲面细分因子对面片做出细分操作。
1.3 域着色器阶段 Domain Shader
就和普通的顶点着色器要做的差不多,我们需要计算每一个控制点的顶点位置等信息。
- 功能
- 生成细分顶点,这些顶点是由外壳着色器确定的细分因子和细分模式所生成的
- 插值属性,根据控制点的属性进行插值,得到细分顶点的属性
- 输出顶点,域着色器生成最终的顶点数据,并传递给后面的着色器
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
[domain("tri")]
DomainOut FlatTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionOS = patch[0].positionOS * bary.x +
patch[1].positionOS * bary.y +
patch[2].positionOS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x +
patch[1].texcoord * bary.y +
patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.texcoord = texcoord;
return output;
}
- 域着色器输入:接受一个域(Domain)输入,代表了细分后顶点在原始补丁内的位置,这个位置通常用参数空间坐标表示,例如重心坐标(Barycentric Coordinates)(这里的重心坐标,是由硬件在细分过程的一个中间阶段 Tessellator 自动计算得出的)
- 属性插值:域着色器使用这些参数空间坐标来插值原始控制点的属性。
- 顶点输出:域着色器输出一个顶点,这个顶点包含计算后的位置和其他属性(如颜色、纹理坐标、法线等)
2.具体实现
2.1 不同的细分策略
2.1.1 平面镶嵌 Flat Tessellation
平面镶嵌只是线性插值位置信息,细分后的图案只比之前多了一些三角面片,单独使用并不能平滑模型。通常和置换贴图配合使用,创建凹凸不平的平面。
Shader "Tessellation/Flat Tessellation"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 0
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1,8)) = 4
_InsideFactor ("InsideFactor", Range(1,8)) = 4
}
SubShader
{
Tags {