1/3
2/3
3/3
[笔记]TA百人计划-图形3.3 曲面细分着色器
艺术菌毯
2022年01月26日 21:11
收录于文集
共26篇
  • 前言

        曲面细分着色器在手机端的应用还比较少,几何着色器已经大规模的应用了,比如原神用来大规模渲染草。

        弱弱地吐槽一句:半小时的课没几天时间估计拿不下来。

        

        这一节课会分为三部分,曲面着色器一部分,几何着色器一部分,最后的综合练习一部分

  • 曲面细分着色器的原理

曲面细分着色器的输入和输出

        输入=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

代码块
JavaScript
自动换行
复制代码
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&#​34;:处理三角形时每个patch包含3个顶点,四边形则是4个顶点,4是InputPatch的第二个参数。

        SV_Primitive:Hull程序的工作是将所需的顶点数据传递到细分阶段。patchID代表Patch的编号

        

ControlPointHS

代码块
JavaScript
自动换行
复制代码
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。一般来讲,这里会涉及到大量的计算,所有的顶点信息都会在这里重新计算,最后会将顶点坐标转换到投影空间。

代码块
JavaScript
自动换行
复制代码
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:

代码块
JavaScript
自动换行
复制代码
[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

代码块
JavaScript
自动换行
复制代码
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 置换贴图

        置换贴图需要在顶点函数和片元着色器里进行一系列的转化。

        关键代码如下:

代码块
JavaScript
自动换行
复制代码
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;
            }
复制成功

代码块
JavaScript
自动换行
复制代码
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);
            }
复制成功