讲真,上一篇计算着色器总有一种意犹未尽之感,可能是我对这个话题还不够熟悉,所以我觉得还是不要把上一篇当做计算着色器的教程来看了,做个简单了解还差不多。如果以后有机会我会重新回来说这个话题。今天,我们就进入流水管线的一个重要环节——曲面细分阶段。
曲面细分,从名字就能看出它的含义:将几何体细分为更小的三角形并偏移产生曲面,达到细节丰富的效果。
先定个调:easy!曲面细分可以说是非常简单且流水化的操作过程,对CPU端资源传输啥的都没有太大的要求(只需要指定图元拓扑关系),剩下的工作都在shader中,也就是进行格式的配置即可。
一、图元类型
启用了曲面细分,就要把点作为控制点来传输:
1.cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST);
2.opaquePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH;
以上就是在CPU端从设置PSO和绑定渲染流水线的控制点图元设置全过程。其中输入装配可以根据控制点个数具有不同的类型(修改格式而已,从1-32)

二、使用曲面细分
只要我们在PSO阶段给出了HS和DS(编译同VS等),曲面细分就开始了:
opaquePsoDesc.HS =
     {
         reinterpret_cast<BYTE*>(mShaders["tessHS"]->GetBufferPointer()),
         mShaders["tessHS"]->GetBufferSize()
     };
     opaquePsoDesc.DS =
     {
         reinterpret_cast<BYTE*>(mShaders["tessDS"]->GetBufferPointer()),
         mShaders["tessDS"]->GetBufferSize()
     };
开启曲面细分之后,顶点着色器处理对象就是单个控制点,可用于对控制点进行调整:
VertexOut VS(VertexIn vin)
 {
     VertexOut vout;
     
     vout.PosL = vin.PosL;
    return vout;
 }
出了顶点着色器,就进入外壳着色器。
1.外壳着色器
所谓外壳着色器就是一种规则的制定者,共分为两种:常量外壳着色器与控制点外壳着色器。
1).常量外壳着色器HS
如上,指定4个控制点,实际意义就是将4个控制点组成一个单个面片。常量HS的作用就是处理单个面片(处理一个面片就调用一次),输出该面片细分的因子:
struct PatchTess
 {
     float EdgeTess[4]   : SV_TessFactor;//四条边细分因子,几边形就是几条边
     float InsideTess[2] : SV_InsideTessFactor;//内部细分因子,三角形一个,四边形两个(横竖)
 };
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
 {
     PatchTess pt;
//各边均分为3等份,
    pt.EdgeTess[0] = 3;
     pt.EdgeTess[1] = 3;
     pt.EdgeTess[2] = 3;
     pt.EdgeTess[3] = 3;    
//四边形内部行列数
     pt.InsideTess[0] = 3;
     pt.InsideTess[1] = 3;    
     return pt;
 }
注意:
1.InputPatch<VertexOut, 4> patch表示是由顶点着色器输出的4个控制点构成的一个面片;
2.每个面片具有独立的图元索引:uint patchID : SV_PrimitiveID;
3.返回值 return pt;将细分因子返回至流水线以供调用。
2).控制点外壳着色器HS
以大量的控制点作为输入与输出,每输出一个控制点着色器被调用一次。
龙书对这个部分有大量的叙述,什么PN三角形法,N-patches方法啥的。个人感觉没太大感触,这个控制点外壳着色器,用我自己的话说就是细分规则的制定者。
来看看他都制定了什么规则:
struct HullOut
 {
     float3 PosL : POSITION;
 };
[domain("quad")]//面片类型(quad / tri / isoline)
 [partitioning("integer")]//剔除小数部分,使用细分因子整数部分,如果考虑小数(fractional_even / fractional_odd)
 [outputtopology("triangle_cw")]//细分生成的三角形绕序(自动组织定义正反面),对线段细分则:line
 [outputcontrolpoints(4)]//控制点个数,一个控制点HS执行一次
 [patchconstantfunc("ConstantHS")]//指定常量外壳着色器以获得细分因子
 [maxtessfactor(64.0f)]//钳制最大细分因子
 HullOut HS(InputPatch<VertexOut, 4> p, //同常量HS,从顶点着色器过来
            uint i : SV_OutputControlPointID,//控制点ID,单次执行用于索引点
            uint patchId : SV_PrimitiveID)
 {
     HullOut hout;
//更复杂的逻辑    
     hout.PosL = p[i].PosL;    
     return hout;
 }
以上就是规则的定义,此处依旧可以实现对控制点的调整。
3.镶嵌化
通过两个HS,我们似乎只是制定了曲面划分的规则,但是控制点还是那些控制点,没有任何变化。那我们的规则到底谁来执行呢?答案就是镶嵌化。
镶嵌化会根据我们指定的规则:
1.ConstantHS部分细分因子以SV_TessFactor,SV_InsideTessFactor标记的内容被系统识别;
2.HS部分通过[]指定了一堆规则
通过硬件在两个控制点之间进行插值,并给出在控制点构成的面片空间下的uv(w)坐标,表示插值生成的新点位置。提供给域着色器使用。
4.域着色器
struct DomainOut
 {
     float4 PosH : SV_POSITION;//此时才产生真正用于场景顶点的坐标,代替原顶点着色器部分
 };
[domain("quad")]//面片类型,4个控制点的,注意全过程的对应
 DomainOut DS(PatchTess patchTess, //虽然这里有曲面细分但是似乎没使用过
              float2 uv : SV_DomainLocation, //由镶嵌化对控制点间使用细分因子插值得到的uv(w)
              const OutputPatch<HullOut, 4> quad)//从HS穿过来的控制点
 {
     DomainOut dout;
     // 使用uv双线性插值.对于三角形就是uvw的中心坐标插值
     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); 
     // 偏移使之成为曲面,丰富细节
     p.y = 0.3f*( p.z*sin(p.x) + p.x*cos(p.z) );
   
float4 posW = mul(float4(p, 1.0f), gWorld);
 dout.PosH = mul(posW, gViewProj);  
     return dout;
 }
之后交给PS,就好像VS交给PS数据一样。
三、基于视点距离的曲面细分例子
这里只分享shader部分代码,因为CPU段顶点配置是非常easy的任务。
struct VertexIn
 {
     float3 PosL    : POSITION;
 };
struct VertexOut
 {
     float3 PosL    : POSITION;
 };
VertexOut VS(VertexIn vin)
 {
     VertexOut vout;
     
     vout.PosL = vin.PosL;
    return vout;
 }
  
 struct PatchTess
 {
     float EdgeTess[4]   : SV_TessFactor;
     float InsideTess[2] : SV_InsideTessFactor;
 };
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
 {
     PatchTess pt;
     
     float3 centerL = 0.25f*(patch[0].PosL + patch[1].PosL + patch[2].PosL + patch[3].PosL);
     float3 centerW = mul(float4(centerL, 1.0f), gWorld).xyz;
     
     float d = distance(centerW, gEyePosW);
    // Tessellate the patch based on distance from the eye such that
     // the tessellation is 0 if d >= d1 and 64 if d <= d0.  The interval
     // [d0, d1] defines the range we tessellate in.
     
     const float d0 = 20.0f;
     const float d1 = 100.0f;
//注意等于1表示不细分,0表示剔除
     float tess = max(64.0f*saturate( (d1-d)/(d1-d0) ),1.0);
// Uniformly tessellate the patch.
    pt.EdgeTess[0] = tess;
     pt.EdgeTess[1] = tess;
     pt.EdgeTess[2] = tess;
     pt.EdgeTess[3] = tess;
     
     pt.InsideTess[0] = tess;
     pt.InsideTess[1] = tess;
     
     return pt;
 }
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;
 }
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); 
     
     // Displacement mapping
     p.y = 0.3f*( p.z*sin(p.x) + p.x*cos(p.z) );
     
     float4 posW = mul(float4(p, 1.0f), gWorld);
     dout.PosH = mul(posW, gViewProj);
     
     return dout;
 }
float4 PS(DomainOut pin) : SV_Target
 {
     return float4(1.0f, 1.0f, 1.0f, 1.0f);
 }
虽然龙书还有贝塞尔相关的部分,但是那个部分我觉得比较数学,这个部分的逻辑也主要是在DS阶段对顶点的偏移,和我们的细分阶段大流程框架关系不大,所以不赘述。这种数学问题还是以后我们如果有机会讲解几何算法的时候来研究吧。
OK,细分阶段,这个细节摸鱼怪!










