通过本节的学习可以得到如下的效果

将一个低模的模型

unity shadergraph打包_unity shadergraph打包

通过渲染管线的曲面细分功能

unity shadergraph打包_unity_02

得到一个高模的结果

unity shadergraph打包_ide_03


当我们开启渲染管线的tessellation开关时,我们传统意义上的顶点着色器功能就发生了改变。因为此时我们提交给IA阶段的内容不再被看作是有三个顶点(vertex)的三角面(triangle)(因为经过曲面细分之后才是三角面),而是看作一个有三个控制点(control point)的片(patch)。
这里的控制点,可以理解成贝塞尔曲线的控制点或者PhotoShop钢笔工具的控制点,是控制点组成了这个图元的原始信息。

Hull Shader

Hull Shader实际上由两部分组成

  1. Constant Hull Shader
  2. Control Point Hull Shader
constant 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<VertexOut,3> 以patch内的所有控制点作为输入。看渲染管线图可以知道,hull shader的输入来自于顶点着色器,因此VertexOut就是顶点着色器的输出。系统同时通过SV_PrimitiveID提供了一个称之为patch ID的变量,这个ID是patch在该次绘制中的唯一标识。constant hull shader必须以 细分因子作为输出。对于拓扑结构为三角面的模型,其细分因子的结构如上面代码所示。不同细分因子的结果如下所示

unity shadergraph打包_unity shadergraph打包_04


D3D11 支持的最大tessellation factor 为 64. 如果所有的tessellation factor都为0,则该patch会被从后续的渲染管线中剔除。这个功能可以让我们实现基于patch的视锥体剔除或者背面剔除等优化操作。

  • 如果一个patch在视锥体中不可见,我们可以从后续的处理中直接剔除这个patch(如果我们没有在tessellation阶段剔除视锥体内不可见的patch,细分的三角面会在三角面裁减(triangle clipping)阶段被剔除)
  • 如果一个patch位于看不到的背面,我们可以在tessellation阶段直接剔除这个patch(如果我们没有这么做,细分过的三角面会在光栅化阶段的背面剔除部分被剔除掉)

使用曲面细分是为了增加模型的细节,但是我们通常没必要在用户关注不到地方增加不必要的细节,因此我们会采取一些策略来得到动态的曲面细分因子:

  • 距离相机的远近:距离相机越远,需要越少的模型细节,而离相机越近,曲面细分的程度可以越高。
  • 屏幕空间的覆盖范围:我们可以估算出一个模型覆盖到的屏幕区域大小,覆盖区域越小,说明模型在屏幕上占比越小,此时可以使用低多边形模型,相反,覆盖区域越大,曲面细分的程度需要越高。
  • 三角面的朝向:三角面的朝向和观察方向的夹角越大,说明越靠近模型边缘(视觉上的轮廓边缘),而此时需要更高的模型精度,因此曲面细分程度要越高。
  • 表面粗糙度:粗糙表面相比平滑表面,需要更多的模型细节。例如视觉上粗糙不平的地面相比光滑的镜面,需要更多的模型细节来进行表现。

对于URP管线,在"Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl" 里有不同的动态计算细分因子的策略的示例。

关于曲面细分还有如下guide line需要参考:

  • 如果细分因子为1(此时曲面细分阶段从结果上来讲什么都没做),就不要打开曲面细分功能,否则会有额外开销
  • 出于性能考虑,不要过度应用曲面细分以至最总单个三角面在屏幕上的覆盖范围小于8个像素
  • 最好能够把开启曲面细分的对象放在一个或者临近批次渲染,因为在多个drawcall间开关曲面细分功能是有相当的开销的。
control point hull shader

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引入了一系列属性:

  • domain: patch 类型或者叫做模型图元的拓扑结构有效参数为tri,quad,isoline。tri表示三角面
  • partitioning:指定曲面细分的拆分模式
  • integer:新的顶点只在细分因子为整数时进行添加或删除,细分因子的小数部分被忽略,这种情况下对于动态曲面细分因子,会出现在某个点(整数点)模型精度突然提高或减低,会有肉眼可见的精度突变
  • fraction:新的顶点依然在位于整数细分因子时(1.0,2.0,3.0等)进行添加或删除,但是根据细分因子的小数部分进行平滑过度。fraction模式,包括fractional_even和fractional_odd
  • outputtopology:输出的三角面正面的环绕方式
  • triangle_cw:顶点顺时针排列代表正面
  • triangle_ccw:顶点逆时针排列代表正面
  • line:只针对line的细分
  • outputcontrolpoints:输出的control point 的数目,同时也是control point hull shader的执行次数,因为每输出一个control point ,都执行一次hull shader.SV_OutputControlPointID语义指定的索引值的总数,对应于这里的control point数目
  • patchconstantfunc:一个字符串指定的constant hull shader 函数的名字,既前面的constant hull shader
  • maxtessfactor:告诉硬件最大的细分因子,有了此上限值,硬件可以执行某些优化,例如可以预先知道需要多少资源来执行曲面细分。Direct3D 11支持的最大值为64.其他硬件支持的最大值可能为16,因此unity中使用通常使用16作为最大值上限。

the tessellation stage

作为程序员我们没法控制 tessellation stage的执行,该阶段的任务都是由硬件完成的,硬件根据constant hull shader输出的细分因子和control point,来决定如何对patch进行细分。

the domain shader

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
        }
    }
}