通过本节的学习可以得到如下的效果
将一个低模的模型
通过渲染管线的曲面细分功能
得到一个高模的结果
当我们开启渲染管线的tessellation开关时,我们传统意义上的顶点着色器功能就发生了改变。因为此时我们提交给IA阶段的内容不再被看作是有三个顶点(vertex)的三角面(triangle)(因为经过曲面细分之后才是三角面),而是看作一个有三个控制点(control point)的片(patch)。
这里的控制点,可以理解成贝塞尔曲线的控制点或者PhotoShop钢笔工具的控制点,是控制点组成了这个图元的原始信息。
Hull Shader实际上由两部分组成
对于每一个patch(原始三角片) 都会执行一次这个constant hull shader,其功能是用来输出所谓的细分因子(tessellation factor).细分因子用于在tessellation 阶段告诉硬件如何对patch进行细分。
struct PatchTess
{
float EdgeTess[3]:SV_TessFactor;
float InsideTess:SV_InsideTessFactor;
};
PatchTess constantHS(InputPatch<VertexOut,3> patch,uint patchID:SV_PrimitiveID)
{
PatchTess pt;
pt.EdgeTess[0]=3;
pt.EdgeTess[1]=3;
pt.EdgeTess[2]=3;
pt.InsideTess=3;
}
constant hull shader 通过InputPatch
D3D11 支持的最大tessellation factor 为 64. 如果所有的tessellation factor都为0,则该patch会被从后续的渲染管线中剔除。这个功能可以让我们实现基于patch的视锥体剔除或者背面剔除等优化操作。
使用曲面细分是为了增加模型的细节,但是我们通常没必要在用户关注不到地方增加不必要的细节,因此我们会采取一些策略来得到动态的曲面细分因子:
对于URP管线,在"Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl" 里有不同的动态计算细分因子的策略的示例。
关于曲面细分还有如下guide line需要参考:
control point hull shader使用多个控制点作为输入(原始模型顶点),并且输出多个控制点。每输出一个控制点都会调用一次 control point hull shader。通常在hull shader阶段输出的控制点数目和输入的控制点数目一致,除非我们要改变模型的几何结构,例如把一个三角面输出为一个三阶贝塞尔曲面。真真正的曲面细分实在下一个tessellation stage完成的。
struct HullOut
{
float3 PosL:TEXCOORD0;
};
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
HullOut HS(InputPatch<VertexOut,3> p,uint i:SV_OutputControlPointID)
{
HullOut hout;
hout.PosL=p[i].PosL;
return hout;
}
前面提到 control point hull shader是每一个输出的control point 都要执行一次,因此这里引入了SV_OutputControlPointID语义修饰的参数i,表示当前hull shader正在处理的那个 control point 的索引。例子中中输入的controlpoint和输出的contorl point数目一致,但是实践中输出的control point数目可以多于输入的control point的数目,多出来的control point的信息,可以根据算法以及输入的control point进行计算。 control point hull shader引入了一系列属性:
作为程序员我们没法控制 tessellation stage的执行,该阶段的任务都是由硬件完成的,硬件根据constant hull shader输出的细分因子和control point,来决定如何对patch进行细分。
tessellation stage输出我们新创建的所有顶点。 对于每一个tessellation stage输出的顶点都会调用一次domain shader.
当开启曲面细分时,vertex shader的功能是处理每一个control point,而domain shader才是实际上的处理细分的patch的顶点着色器。使用中,我们通常在这里把细分过的顶点坐标,投影到齐次裁减空间,包括顶点法线,切线,UV的处理都在这里执行。在domain shader中,以 constant hull shader输出的细分因子和control point hull shader 输出的control point,以及和细分过的顶点位置相关的参数化的(u,v,w)坐标作为输入,使用这个和实际顶点位置一一对应的参数化的(u,v,w)坐标以及其他输入参数,我们可以计算得到实际的顶点坐标。
对于拓扑结构为三角面的图元,这里的(uvw)三维坐标是重心点坐标。对于其他拓扑结构例如四边形quad,只需要二维(uv)即可描述细分坐标(类似纹理uv)。
struct DomainOut
{
float4 PosH:SV_POSITION;
};
[domain("tri")]
DomainOut DS(PatchTess patchTess,float3 baryCoords:SV_DomainLocation,const OutputPatch<HullOut,3> triangles)
{
DomainOut dout;
float3 p=triangles[0].PosL*baryCoords.x+triangles[1].PosL*baryCoords.y+triangles[2].PosL*baryCoords.z;
dout.PosH=TransformObjectToHClip(p.xyz);
return dout;
}
可以看到这里的输出DomainOut里的SV_Position就是没有开启曲面细分时顶点着色器的输出,同时也是光栅化阶段的输入。
完整代码如下
Shader "tutorial/chapter_2/water"
{
Properties
{
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex vert
#pragma hull HS
#pragma domain DS
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct app_data
{
float4 positionOS:POSITION;
};
struct VertexOut
{
float3 PosL:TEXCOORD0;
};
VertexOut vert(app_data IN)
{
VertexOut o;
o.PosL=IN.positionOS.xyz;
return o;
}
struct PatchTess
{
float EdgeTess[3]:SV_TessFactor;
float InsideTess:SV_InsideTessFactor;
};
PatchTess ConstantHS(InputPatch<VertexOut,3> patch,uint patchID:SV_PrimitiveID)
{
PatchTess pt;
pt.EdgeTess[0]=15;
pt.EdgeTess[1]=15;
pt.EdgeTess[2]=15;
pt.InsideTess=15;
return pt;
}
struct HullOut
{
float3 PosL:TEXCOORD0;
};
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
HullOut HS(InputPatch<VertexOut,3> p,uint i:SV_OutputControlPointID)
{
HullOut hout;
hout.PosL=p[i].PosL;
return hout;
}
struct DomainOut
{
float4 PosH:SV_POSITION;
};
[domain("tri")]
DomainOut DS(PatchTess patchTess,float3 baryCoords:SV_DomainLocation,const OutputPatch<HullOut,3> triangles)
{
DomainOut dout;
float3 p=triangles[0].PosL*baryCoords.x+triangles[1].PosL*baryCoords.y+triangles[2].PosL*baryCoords.z;
dout.PosH=TransformObjectToHClip(p.xyz);
return dout;
}
half4 frag(DomainOut IN):SV_Target
{
return half4(1,1,1,1);
}
ENDHLSL
}
}
}