


前言
曲面细分着色器在手机端的应用还比较少,几何着色器已经大规模的应用了,比如原神用来大规模渲染草。
弱弱地吐槽一句:半小时的课没几天时间估计拿不下来。
这一节课会分为三部分,曲面着色器一部分,几何着色器一部分,最后的综合练习一部分
曲面细分着色器的原理

曲面细分着色器的输入和输出
输入=Patch:Patch可以认为是多个顶点的集合,它包含每个顶点的属性,可以指定一个Patch包含的定点数和属性。
输出=细分后的顶点
Input Assembler Stage
当我们使用曲面细分的时候,我们不再从显存读取几何数据,而是提交包含数个控制点的Patch。
三角形可以认为是有三个控制点的patch,所以我们依然可以提交常规的三角面网格。四边形(四个控制点)也可以被提交,不过在曲面细分阶段这些patch会被细分成三角面。
当然也可以在一个Patch中添加更多的控制点。
⬇
Vertex Shader
控制点也会经过顶点着色器的处理。这里我们可以做一些针对控制点的计算。
⬇
Hull Shader
经过VS处理的点会传到HS,HS又分为Const Hull Shader和Control Point Hull Shader。Const Hull用于设定Patch的细分参数,Control Point Hull Shader则是对Patch中的参数进行改变。
下文会详细解释Hull Shader。
⬇
Tessellation Stage
曲面细分阶段,这个阶段是由硬件完成的,会根据ConstHS将patch分割成多个三角面或者线段。这些顶点会被传给下一个阶段DS,以供插值。
⬇
Domain Shader
对于每个细分后的顶点,都会调用一次DS,计算顶点坐标,然后将顶点转换到齐次裁剪空间。
⬇
Geometry Shader
可以在几何着色器里对顶点进行进一步的处理
⬇
Fragment Shader
最后经过光栅化后就转化到了片元着色器。
Hull Shader详解
ConstHS
struct PatchTess
{
float EdgeTess[4] : SV_TessFactor;
float InsideTess[2] : SV_InsideTessFactor;
// Additional info you want associated per patch.
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
{
PatchTess pt;
// Uniformly tessellate the patch 3 times.
pt.EdgeTess[0] = 3; // Left edge
pt.EdgeTess[1] = 3; // Top edge
pt.EdgeTess[2] = 3; // Right edge
pt.EdgeTess[3] = 3; // Bottom edge
pt.InsideTess[0] = 3; // u-axis (columns)
pt.InsideTess[1] = 3; // v-axis (rows)
return pt;
} SV_TessFactor代表细分度,指定几条边各被分为多少段,SV_InsideTessFactor代表内部细分度,指定内部在几个维度上各分为多少段,详情下面解释。
VertexOut:HS需要将InputPatch作为参数进行处理,所以要添加InputPatch参数,VertexOut则是指定数据格式,表示Vertex shader的输出参数。
"4":处理三角形时每个patch包含3个顶点,四边形则是4个顶点,4是InputPatch的第二个参数。
SV_Primitive:Hull程序的工作是将所需的顶点数据传递到细分阶段。patchID代表Patch的编号
ControlPointHS
struct HullOut
{
float3 PosL : POSITION;
};
[domain(“quad”)]
[partitioning(“integer”)]
[outputtopology(“triangle_cw”)]
[outputcontrolpoints(4)]
[patchconstantfunc(“ConstantHS”)]
[maxtessfactor(64.0f)]
HullOut HS(InputPatch<VertexOut, 4> p,uint i : SV_OutputControlPointID, uint patchId : SV_PrimitiveID)
{
HullOut hout;
hout.PosL = p[i].PosL;
return hout;
} domain: 指定patch的类型,可选的有:tri(三角形)、quad(四边形)、isoline(线段,苹果的metal api不支持:2018/8/21)。不同的patch类型,细分的方式也有差别,后面会详细介绍。
partitioning:分割模式,有三种:integer,fractional_even,fractional_odd。这三种分割模式如下图。
outputtopology:输出拓扑结构,人话就是几个顶点的顺序。有三种:triangle_cw(顺时针环绕三角形)、triangle_ccw(逆时针环绕三角形)、line(线段)。
outputcontrolpoints:输出的控制点的数量(每个图元),不一定与输入数量相同,也可以新增控制点。
patchconstantfunc:指定ConstHS。
maxtessfactor:最大细分度,告知驱动程序shader用到的最大细分度,硬件可能会针对这个做出优化。Direct3D 11和OpenGL Core都至少支持64。
InputPatch:输入的patch,尖括号第二个参数,代表输入的控制点的数量(“quad”为4个),由API设置,并且ConstHS里对应的数值要与这里相同。
SV_OutputControlPointID:给出控制点在patch中的ID,就是处理第几个点,与outputcontrolpoints对应,例如outputcontrolpoints为4,那么i的取值就是[0,4)的整数。为什么要给出ID,因为但该函数一次仅应输出一个顶点。Patch中的每个顶点都会调用一次它。
SV_PrimitiveID:给出patch的ID。

integer,整数划分

fractional_even,偶数划分

fractional_odd,奇数划分
下面着重展示一下SV_TessFactor和SV_InsideTessFactor在不同path类型上是如何细分的。 对于三种不同的patch类型。
SV_TessFactor和SV_InsideTessFactor
- Quad

SV_TessFactor:4,2,9,3 SV_InsideTessFactor:6,7
SV_TessFactor的长度为4,指定四条边各被分为多少段。比如上图,外面的四条边分别是4,2,9,3段。SV_InsideTessFactor长度为2,指定内部在横向和纵向上各被分为多少段,比如上图横向一共有6段,纵向一共有7段。
- Tri
SV_TessFactor的长度为3,指定三条边各被分为多少段,SV_InsideTessFactor的长度为1,指定内部有多少个点。


对于三角形的内部顶点,将他们的临边分割,然后在分割点上做垂线,得到的交点即为内部点。

- Isoline
SV_TessFactor长度为2,第0个元素指定线段的个数,第1个元素指定线段被分为多少段,SV_InsideTessFactor会被忽略。

SV_TessFactor:6,2

SV_TessFactor:3,4
Tessellation Stage详解
曲面细分阶段,这个阶段是由硬件完成的,会根据ConstHS将patch分割成多个三角面或者线段。这些顶点会被传给下一个阶段DS,以供插值。
对于不同的patch,这些顶点的形式也不一样。
Quad
顶点以UV坐标的形式传给DS。

Tri
顶点以重心坐标(Barycentric coordinates)的形式(u, v, w)传给DS。

Iso
顶点以UV坐标的形式传给DS。

Domain Shader详解
从曲面细分阶段获取新创建的顶点和三角面(或线段)之后,便进入了DS(对应于OpenGL Core的TES,Tessellation Evaluation Shader)阶段。对于每个顶点,都会调用一次DS。一般来讲,这里会涉及到大量的计算,所有的顶点信息都会在这里重新计算,最后会将顶点坐标转换到投影空间。
struct DomainOut
{
float4 PosH : SV_POSITION;
};
// The domain shader is called for every vertex created by the tessellator.
// It is like the vertex shader after tessellation.
[domain(“quad”)]
DomainOut DS(PatchTess patchTess, float2 uv : SV_DomainLocation, const OutputPatch<HullOut, 4> quad)
{
DomainOut dout;
// Bilinear interpolation.
float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
float3 p = lerp(v1, v2, uv.y);
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}
参数
PatchTess:ConstHS的输出参数,即细分参数。
SV_DomainLocation:由曲面细分阶段阶段传入的顶点位置信息。
OutputPatch:由HS传入的patch数据,尖括号的第二个参数与HS中的outputcontrolpoints对应。
如果Patch类型是Tri:
[domain(“tri”)]
DomainOut DS(PatchTess patchTess,
float3 uvw : SV_DomainLocation, const OutputPatch<HullOut, 3> tri)
{
DomainOut dout;
// Bilinear interpolation.
float3 p = tri[0] * uvw.x + tri[1] * uvw.y + tri[2] * uvw.z;
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
} 上面没有做曲面细分,就单纯计算了顶点的坐标。
在了解了曲面细分着色器后,我们现在可以来看一看教程中的案例。
曲面细分案例详解
案例1
Shader "Unlit/Tessellation"
{
Properties
{
_TessellationUniform("TessellationUniform",Range(1,64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
// 定义曲面细分函数
#pragma hull hullProgram
// 定义Domain函数
#pragma domain ds
// 定义顶点着色器
#pragma vertex tessvert
#pragma fragment frag
#include "UnityCG.cginc"
// 包含曲面细分的头文件
#include "Tessellation.cginc"
#pragma target 5.0
// 定义顶点着色器的输入结构体
struct VertexInput
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
// Domain函数的输出结构体,即片元着色器的输入结构体
struct VertexOutput
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
// 定义Domain里坐标转换的函数
VertexOutput vert (VertexInput v)
{
VertexOutput o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.tangent = v.tangent;
o.normal = v.normal;
return o;
}
// 判断是否能曲面细分
#ifdef UNITY_CAN_COMPILE_TESSELLATION
// 曲面着色器输入结构体
struct TessVertex{
float4 vertex : INTERNALTESSPOS;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
// 曲面着色器ConstHull的输出结构体
// 用来定义TessellationFactor和InsideTessellationFactor
// 不同的几何图形的参数不一样
struct OutputPatchConstant {
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};
// 顶点着色器
// 这里可以做一些顶点动画之类的,但是演示Demo里没必要
TessVertex tessvert (VertexInput v){
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}
float _TessellationUniform;
// 定义ConstHull的函数,即曲面细分的参数是什么
OutputPatchConstant hsconst (InputPatch<TessVertex,3> patch){
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}
// 定义HullShader的函数
[UNITY_domain("tri")]//确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")]//拆分edge的规则,equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]//顶点的顺序,顺时针,逆时针等,会影响显示哦
// 这个函数是规定Patch进行曲面细分的属性
[UNITY_patchconstantfunc("hsconst")]//一个patch一共有三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] //定义有几个控制点
// inputPatch为TessVertex的输入单位,id表示第几个控制点
TessVertex hullProgram (InputPatch<TessVertex,3> patch,uint id : SV_OutputControlPointID){
//定义hullshaderV函数
return patch[id];
}
[UNITY_domain("tri")]//同样需要定义图元
// patch中的3个顶点为上面定义的3个控制点
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)
//bary:细分后的某个点的重心坐标
{
VertexInput v;
v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;
// 将模型空间的坐标转换为齐次裁剪空间
VertexOutput o = vert (v);
return o;
}
#endif
float4 frag (VertexOutput i) : SV_Target
{
return float4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
案例2 置换贴图
置换贴图需要在顶点函数和片元着色器里进行一系列的转化。
关键代码如下:
VertexOutput vert (VertexInput v)
//这个函数应用在domain函数中,用来空间转换的函数
{
VertexOutput o;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
//Displacement
//由于并不是在Fragnent shader中读取图片,GPU无法获取mipmap信息,因此需要使用tex2Dlod来读取图片,使用第四坐标作为mipmap的level,这里取了0
float Displacement = tex2Dlod(_DisplacementMap,float4(o.uv.xy,0.0,0.0)).g;
Displacement = (Displacement-0.5)*_DisplacementStrength;
v.normal = normalize(v.normal);
v.vertex.xyz += v.normal * Displacement;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
//计算切线空间转换矩阵
half3 vNormal = UnityObjectToWorldNormal(v.normal);
half3 vTangent = UnityObjectToWorldDir(v.tangent.xyz);
//compute bitangent from cross product of normal and tangent
half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
half3 vBitangent = cross(vNormal,vTangent)*tangentSign;
//output the tangent space matrix
o.tspace0 = half3(vTangent.x,vBitangent.x,vNormal.x);
o.tspace1 = half3(vTangent.y,vBitangent.y,vNormal.y);
o.tspace2 = half3(vTangent.z,vBitangent.z,vNormal.z);
return o;
}
float4 frag (VertexOutput i) : SV_Target
{
float3 lightDir =_WorldSpaceLightPos0.xyz;
float3 tnormal = UnpackNormal (tex2D (_DisplacementMap, i.uv));
half3 worldNormal;
worldNormal.x=dot(i.tspace0,tnormal);
worldNormal.y= dot (i.tspace1, tnormal);
worldNormal.z=dot (i.tspace2, tnormal);
float3 albedo=tex2D (_MainTex, i.uv). rgb;
float3 lightColor = _LightColor0.rgb;
float3 diffuse = albedo * lightColor * DotClamped(lightDir,worldNormal);
float3 viewDir = normalize (_WorldSpaceCameraPos. xyz-i. worldPos. xyz);
float3 halfVector = normalize(lightDir + viewDir);
float3 specular = albedo * pow (DotClamped (halfVector, worldNormal), _Smoothness * 100);
float3 result = specular + diffuse;
return float4(result, 1.0);
return float4(result,1.0);
}