Unity URP Tessellation

Unity URP Tessellation

I'm finally not like me


Read Note:

1 Surface subdivision and tessellation

Surface subdivision or subdivision surface (Subdivision surface) refers to a technology that refines a rough geometric mesh through a recursive algorithm. Tessellation is a specific means of surface subdivision, which can divide the vertex set of geometric objects in the scene into suitable rendering structures, such as triangles. In some cases, "tessellation" is also referred to as "tessellation".

Through surface subdivision, we can maintain a low model in memory, and then dynamically increase the triangle mesh according to the demand, so as to save resources. We can also implement LOD at the GPU level, and adjust the degree of surface subdivision through some factors (such as: camera distance, etc.), so that high-mode is present in the near area and low-mode in the distance.

Before Direct3D 11, in order to achieve this kind of operation, the grid can only be refined in the CPU stage, and then passed to the GPU, which is relatively inefficient. Therefore, the tessellation stage (Tessllation Stage) was introduced in Direct3D 11, and the task was handed over to the GPU: the
insert image description here
tessellation stage is divided into three stages: Hull Shader, Tessellator and domain coloring The domain shader stage (Domain Shader).

1.1 Hull Shader

Hull Shader is actually composed of two phases (Phase): Constant Hull Shader and Control point hull shader.

Constant Hull Shader will process each patch, and its main task is to output the Tessellation Factor of the mesh , which is used to guide the number of subdivisions of the mesh. Here we take the triangle patch as an example:

// 三角面片
struct PatchTess {
    
      
    float edgeFactor[3] : SV_TESSFACTOR;
    float insideFactor  : SV_INSIDETESSFACTOR;
};

PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
     
    PatchTess o;
    o.edgeFactor[0] = 4;
    o.edgeFactor[1] = 4; 
    o.edgeFactor[2] = 4;
    o.insideFactor  = 4;
    return o;
}

The constant hull shader will take all the vertices (or control points) of the patch as input, InputPatch<VertexOut,3>which VertexOutis the structure output by the vertex shader, and the number 3 behind it represents the three vertex data that are passed into the triangle patch (and so on, If you pass in a quadrilateral patch, then this should be InputPatch<VertexOut,4>).

The code also contains three semantics:

  • SV_PrimitiveID: Provide the ID value of the incoming patch. The parameters passed in here patchIDcan be manipulated according to specific needs.
  • SV_TESSFACTOR: The subdivision factor used to identify the corresponding edge
  • SV_INSIDETESSFACTOR: subdivision factor used to identify the interior

Since what we pass in is a triangular patch, it will naturally output 3 edge subdivision factors and 1 internal subdivision factor. If the input is a four-corner patch, then the structure of PatchTess is as follows:

// 四角面片
struct PatchTess {
    
      
    float edgeFactor[4] : SV_TESSFACTOR; // 分别对应四角面片的四个边
    float insideFactor[2]  : SV_INSIDETESSFACTOR; // 分别对应内部细分的列数与行数
};

Here, we fix all the subdivision factors to 4, but in actual operation, the size of the factors can be flexibly adjusted according to different strategies to achieve the effect of LOD. For example, the adjustment of the factor size based on the camera position will be mentioned later.

Control Point Hull Shader can be used to change the position of each output vertex and other information, such as changing a triangle into a 3-time Bezier triangle patch.

[domain("tri")]    
[partitioning("integer")]    
[outputtopology("triangle_cw")]   
[patchconstantfunc("PatchConstant")]   
[outputcontrolpoints(3)]             
[maxtessfactor(64.0f)]        
HullOut ControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
    
      
    HullOut o;
    o.positionOS = patch[id].positionOS;
    o.texcoord = patch[id].texcoord; 
    return o;
}

However, for the convenience of explanation, the function does not do too much here, it just passes the value. Let's take a step-by-step look at this complex way of writing:

  • domain: Patch type. Parameters include triangular surface tri , quadrangular surface quad , isoline isoline

  • partitioning: Tessellation mode. The parameters are integer , fractional_even , fractional_odd .

    integer

    It means that the addition of new vertices depends only on the integer part of the subdivision, and the corresponding mode in Opengl is equal_spacing.
    insert image description here

    Since this mode only takes integers, when the subdivision level is changed, the graphics will have an obvious mutation (pop). For example: when you rely on the meridian, suddenly change from a cube to a sphere, etc.

    fractional_even

    Take the nearest even number n up, and cut the whole segment into n-2 parts of equal length, and shorter parts at both ends.
    insert image description here

    fractional_odd

    Take the nearest odd n up, and cut the whole segment into n-2 parts of equal length, and shorter parts at both ends.
    insert image description here

  • outputtopology: The order of triangular patches created by subdivision. The parameters are clockwise triangle_cw , counterclockwise triangle_ccw

  • patchconstantfunc: Specifies the function name of the constant hull shader.

  • outputcontrolpoints: The number of executions of the shell shader, each execution will generate a control point. This number does not have to be consistent with the number of input control points, for example, inputting 4 control points can output 16 control points.

  • maxtessfactor: The maximum subdivision factor that the program will use. Direct3D 11 supports a maximum tessellation factor of 64.

  • SV_OutputControlPointID: This semantic identifies the control point index ID currently being operated.

See Tessellation Stages for more details .
insert image description here
The constant hull shader and control point hull shader stages are run in parallel by the hardware. The Constant Hull Shader runs once for each patch and outputs information such as edge subdivision factors. The Control Point Hull Shader runs once for each control point and outputs the corresponding or derived control point.

1.2 Tessellator in the mosaic stage

We have no control over it at this stage, and the whole process is controlled by the hardware. At this stage, the hardware subdivides the patch according to the previous tessellation factor.

Let's take the triangle patch as an example, select the subdivision mode to integerincrease the edge subdivision factor, and you can see that each side of the triangle is divided into the corresponding number.
insert image description here

Increasing the inner subdivision factor, you can see that the rules of the inner tessellation are a bit unintuitive. Because the number of internal factors does not directly correspond to the number of internal triangles.
insert image description here

The number of inner triangular rings is almost half the number of subdivisions, and when the number of subdivisions is even, the innermost layer will be a vertex.

For specific rules on subdivision, please refer to the explanation Tessellation on the Opengl official website .

1.3 Domain Shader stage Domain Shader

A domain shader is equivalent to a "vertex shader" for each control.

insert image description here

Just like a normal vertex shader, we need to calculate the vertex position of each control point and other information.

struct DomainOut
{
    
    
    float4  positionCS      : SV_POSITION;
    float3  color           : TEXCOORD0; 
};


[domain("tri")]      
DomainOut DomainShader (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
    
    
    float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z; 
    float2 texcoord   = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;

    DomainOut output;
    output.positionCS = TransformObjectToHClip(positionOS);
    output.texcoord = texcoord;

    return output; 
} 

What is worth noting here is how we get the subdivided vertex positions. Taking the triangular patch as an example, we pass in the basic three control point patchinformation, and then use SV_DOMAINLOCATIONthe vertex parameter coordinates after semantic subdivision, here is a centroid coordinate (u, v, w). (If it is a four-corner patch, this value will be a two-dimensional coordinate (u,v))

With the centroid coordinates of the vertices, we can interpolate to obtain the corresponding vertex information.

After processing by the domain shader, the data will be passed to the geometry shader and fragment shader according to the pipeline.

2 concrete implementation

The above briefly introduces the responsible content of each part of the tessellation stage. Next, we put them into practice!

2.2 Different Segmentation Strategies

2.2.1 Flat Tessellation

We only need to assemble the above code to get the simplest form of plane tessellation.

Shader "Tessellation/Flat Tessellation"
{
    
    
    Properties
    {
    
    
        [NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
    
    }  
        
        [Header(Tess)][Space]
     
        [KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 0
        [KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
        _EdgeFactor ("EdgeFactor", Range(1,8)) = 4 
        _InsideFactor ("InsideFactor", Range(1,8)) = 4 
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
         
        Pass
        {
    
     
            HLSLPROGRAM
            #pragma target 4.6 
            #pragma vertex FlatTessVert
            #pragma fragment FlatTessFrag 
            #pragma hull FlatTessControlPoint
            #pragma domain FlatTessDomain
             
            #pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD 
            #pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW 
   
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 

            CBUFFER_START(UnityPerMaterial) 
            float _EdgeFactor; 
            float _InsideFactor; 
            CBUFFER_END

            TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); 

            struct Attributes
            {
    
    
                float3 positionOS   : POSITION; 
                float2 texcoord     : TEXCOORD0;
    
            };

            struct VertexOut{
    
    
                float3 positionOS : INTERNALTESSPOS; 
                float2 texcoord : TEXCOORD0;
            };
             
            struct PatchTess {
    
      
                float edgeFactor[3] : SV_TESSFACTOR;
                float insideFactor  : SV_INSIDETESSFACTOR;
            };

            struct HullOut{
    
    
                float3 positionOS : INTERNALTESSPOS; 
                float2 texcoord : TEXCOORD0;
            };

            struct DomainOut
            {
    
    
                float4  positionCS      : SV_POSITION;
                float2  texcoord        : TEXCOORD0; 
            };


            VertexOut FlatTessVert(Attributes input){
    
     
                VertexOut o;
                o.positionOS = input.positionOS; 
                o.texcoord   = input.texcoord;
                return o;
            }
   
   
            PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
     
                PatchTess o;
                o.edgeFactor[0] = _EdgeFactor;
                o.edgeFactor[1] = _EdgeFactor; 
                o.edgeFactor[2] = _EdgeFactor;
                o.insideFactor  = _InsideFactor;
                return o;
            }
 
            [domain("tri")]   
            #if _PARTITIONING_INTEGER
            [partitioning("integer")] 
            #elif _PARTITIONING_FRACTIONAL_EVEN
            [partitioning("fractional_even")] 
            #elif _PARTITIONING_FRACTIONAL_ODD
            [partitioning("fractional_odd")]    
            #endif 
 
            #if _OUTPUTTOPOLOGY_TRIANGLE_CW
            [outputtopology("triangle_cw")] 
            #elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
            [outputtopology("triangle_ccw")] 
            #endif

            [patchconstantfunc("PatchConstant")] 
            [outputcontrolpoints(3)]                 
            [maxtessfactor(64.0f)]                 
            HullOut FlatTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
    
      
                HullOut o;
                o.positionOS = patch[id].positionOS;
                o.texcoord = patch[id].texcoord; 
                return o;
            }
 
  
            [domain("tri")]      
            DomainOut FlatTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
            {
    
      
                float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z; 
	            float2 texcoord   = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
                   
                DomainOut output;
                output.positionCS = TransformObjectToHClip(positionOS);
                output.texcoord = texcoord;
                return output; 
            }
 

            half4 FlatTessFrag(DomainOut input) : SV_Target{
    
       
                half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
                return half4(color, 1.0); 
            }
              
            ENDHLSL
        }
    }
}

The code specifies the hull shader and domain shader via #pragma hulland . #pragma domainAnd defines the macro definition of the enumeration, which is convenient for us to switch between these modes.
insert image description here

Planar tessellation is just linear interpolation of position information, and the subdivided pattern only has some more triangles than before, and it cannot smooth the model when used alone. It is usually used in conjunction with a displacement map (Displacement Map) to create a bumpy plane.

2.2.2 PN Tessellation

In previous attempts, we didn't design the Control Point stage of the hull shader too much. Here we can try a different control point strategy.

In the shell shader stage, convert a triangle patch (3 control points) into a 3-time Bezier triangle patch (Cubic Bezier Triangle Patch, a patch with 10 control points), this strategy is called For Curved Point-Normal Triangles (PN triangles). It is different from Flat Tessellation, even if there is no displacement map, it can change the shape of the model and smooth the outline.
insert image description here

Due to the increase of control points, each vertex needs to carry two more vertex information when Hull Shader outputs (the central control point b111 can be directly calculated), for example: b030 may need to carry the vertex information of b021 and b012.

Following this strategy, let's redesign the code.

struct HullOut{
    
    
    float3 positionOS : INTERNALTESSPOS;
    float3 normalOS   : NORMAL;
    float2 texcoord   : TEXCOORD0;
    float3 positionOS1 : TEXCOORD1;	// 三角片元每个顶点多携带两个顶点信息
    float3 positionOS2 : TEXCOORD2;
}; 

float3 ComputeCP(float3 pA, float3 pB, float3 nA){
    
    
    return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;
}

[domain("tri")]    
[partitioning("integer")]   
[outputtopology("triangle_cw")]   
[patchconstantfunc("PatchConstant")]     
[outputcontrolpoints(3)]                 
[maxtessfactor(64.0f)] 
HullOut PNTessControlPoint(InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
    
     
    HullOut output;
    const uint nextCPID = id < 2 ? id + 1 : 0;

    output.positionOS    = patch[id].positionOS;
    output.normalOS      = patch[id].normalOS;
    output.texcoord      = patch[id].texcoord;

    output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);
    output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);

    return output;
}

In the output structure HullOut, positionOS1 and positionOS2 are used to store extra control point information. Get the ID of the adjacent vertex through a simple calculation nextCPID- , with the current top line and the adjacent vertex, you can calculate the extra control point between the two points.
insert image description here

ComputeCPThe principle of function is actually a simple geometric relationship. Take the above picture as an example (picture from: CurvedPNTriangles ), then the following relationship exists before each point:
insert image description here

Let's look at the domain shader stage again:

[domain("tri")]      
DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
    
     
    float u = bary.x;
    float v = bary.y;
    float w = bary.z;

    float uu = u * u;
    float vv = v * v;
    float ww = w * w;
    float uu3 = 3 * uu;
    float vv3 = 3 * vv;
    float ww3 = 3 * ww;

    float3 b300 = patch[0].positionOS;
    float3 b210 = patch[0].positionOS1;
    float3 b120 = patch[0].positionOS2;
    float3 b030 = patch[1].positionOS;
    float3 b021 = patch[1].positionOS1;
    float3 b012 = patch[1].positionOS2;
    float3 b003 = patch[2].positionOS;
    float3 b102 = patch[2].positionOS1;
    float3 b201 = patch[2].positionOS2;  

    float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
    float3 V = (b003 + b030 + b300) / 3.0; 
    float3 b111 = E + (E - V) / 2.0f;  
	// 插值获得细分后的顶点位置
    float3 positionOS = b300 * uu * u + b030 * vv * v + b003 * ww * w 
        + b210 * uu3 * v 
        + b120 * vv3 * u
        + b021 * vv3 * w
        + b012 * ww3 * v
        + b102 * ww3 * u
        + b201 * uu3 * w
        + b111 * 6.0 * w * u * v;
	// 此处简化了法线的计算
    float3 normalOS = patch[0].normalOS * u 
        + patch[1].normalOS * v
        + patch[2].normalOS * w;
    normalOS = normalize(normalOS);

    float2 texcoord = patch[0].texcoord * u
        + patch[1].texcoord * v
        + patch[2].texcoord * w;

    DomainOut output; 
    output.positionCS = TransformObjectToHClip(positionOS);  
    output.normalWS = TransformObjectToWorldNormal(normalOS);
    output.uv = texcoord;
    return output; 
}

After the number of control points is increased to 10, the computational complexity of interpolation using centroid coordinates will naturally increase. Therefore, when dealing with normals, the simplest method of interpolation of three control points is used here.

Paste the entire code here.

Shader "Tessellation/PN Tri Tessellation + Cubic Bezier Triangle Patch"
{
    
    
    Properties
    {
    
    
        [NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
    
    }  
        
        [Header(Tess)][Space]
     
        [KeywordEnum(integer, fractional_even, fractional_odd )]_Partitioning ("Partitioning Mode", Float) = 2
        [KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
        _EdgeFactor ("EdgeFactor", Range(1, 8)) = 4 
        _InsideFactor ("InsideFactor", Range(1, 8)) = 4 
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
         
        Pass
        {
    
     
            HLSLPROGRAM
            #pragma target 4.6 
            #pragma vertex PNTessVert
            #pragma fragment PNTessFrag 
            #pragma hull PNTessControlPoint
            #pragma domain PNTessDomain
             
            #pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD   
            #pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW 
             
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            CBUFFER_START(UnityPerMaterial) 
            float _EdgeFactor; 
            float _InsideFactor; 
            CBUFFER_END

            TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); 


            struct Attributes
            {
    
    
                float3 positionOS   : POSITION; 
                float3 normalOS     : NORMAL;
                float2 texcoord     : TEXCOORD0;
    
            };

            struct VertexOut{
    
    
                float3 positionOS : INTERNALTESSPOS;
                float3 normalOS   : NORMAL;
                float2 texcoord   : TEXCOORD0; 
            }; 

            struct PatchTess {
    
      
                float edgeFactor[3] : SV_TESSFACTOR; 
                float insideFactor  : SV_INSIDETESSFACTOR; 
            };

            struct HullOut{
    
    
                float3 positionOS : INTERNALTESSPOS;
                float3 normalOS   : NORMAL;
                float2 texcoord   : TEXCOORD0;
                float3 positionOS1 : TEXCOORD1;
                float3 positionOS2 : TEXCOORD2;
            }; 


            struct DomainOut
            {
    
    
                float4 positionCS      : SV_POSITION;
                float3 normalWS        : TEXCOORD0; 
                float2 uv              : TEXCOORD1;
            };


            VertexOut PNTessVert(Attributes input){
    
     
                VertexOut o = (VertexOut)0;
                o.positionOS = input.positionOS; 
                o.normalOS   = input.normalOS;
                o.texcoord   = input.texcoord;
                return o;
            } 
            
            PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
    
 
                PatchTess o; 
     
                o.edgeFactor[0] = _EdgeFactor;
                o.edgeFactor[1] = _EdgeFactor; 
                o.edgeFactor[2] = _EdgeFactor;
                o.insideFactor  = _InsideFactor;
                return o;
            }

 
            float3 ComputeCP(float3 pA, float3 pB, float3 nA){
    
    
                return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;
            }
 
            [domain("tri")]   
            #if _PARTITIONING_INTEGER
            [partitioning("integer")] 
            #elif _PARTITIONING_FRACTIONAL_EVEN
            [partitioning("fractional_even")] 
            #elif _PARTITIONING_FRACTIONAL_ODD
            [partitioning("fractional_odd")]    
            #endif 
 
            #if _OUTPUTTOPOLOGY_TRIANGLE_CW
            [outputtopology("triangle_cw")] 
            #elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
            [outputtopology("triangle_ccw")] 
            #endif

            [patchconstantfunc("PatchConstant")]     
            [outputcontrolpoints(3)]                 
            [maxtessfactor(64.0f)] 

            HullOut PNTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
    
     
                HullOut output;
                const uint nextCPID = id < 2 ? id + 1 : 0;
    
                output.positionOS    = patch[id].positionOS;
                output.normalOS      = patch[id].normalOS;
                output.texcoord      = patch[id].texcoord;

                output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);
                output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);
      
                return output;
            }
   
 
            [domain("tri")]      
            DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
            {
    
     
                float u = bary.x;
                float v = bary.y;
                float w = bary.z;

                float uu = u * u;
                float vv = v * v;
                float ww = w * w;
                float uu3 = 3 * uu;
                float vv3 = 3 * vv;
                float ww3 = 3 * ww;

                float3 b300 = patch[0].positionOS;
                float3 b210 = patch[0].positionOS1;
                float3 b120 = patch[0].positionOS2;
                float3 b030 = patch[1].positionOS;
                float3 b021 = patch[1].positionOS1;
                float3 b012 = patch[1].positionOS2;
                float3 b003 = patch[2].positionOS;
                float3 b102 = patch[2].positionOS1;
                float3 b201 = patch[2].positionOS2;  

                float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
                float3 V = (b003 + b030 + b300) / 3.0; 
                float3 b111 = E + (E - V) / 2.0f;    
  
                float3 positionOS = b300 * uu * u + b030 * vv * v + b003 * ww * w 
                                + b210 * uu3 * v 
                                + b120 * vv3 * u
                                + b021 * vv3 * w
                                + b012 * ww3 * v
                                + b102 * ww3 * u
                                + b201 * uu3 * w
                                + b111 * 6.0 * w * u * v;
   
                float3 normalOS = patch[0].normalOS * u 
                                + patch[1].normalOS * v
                                + patch[2].normalOS * w;
                normalOS = normalize(normalOS);

                float2 texcoord = patch[0].texcoord * u
                                + patch[1].texcoord * v
                                + patch[2].texcoord * w;
       
                DomainOut output; 
                output.positionCS = TransformObjectToHClip(positionOS);  
                output.normalWS = TransformObjectToWorldNormal(normalOS);
                output.uv = texcoord;
                return output; 
            }
 
            half4 PNTessFrag(DomainOut input) : SV_Target{
    
      
     
                Light mainLight = GetMainLight();
                half3 baseColor = SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,input.uv).xyz;   
      
                half NdotL = saturate(dot(input.normalWS, mainLight.direction) * 0.5 + 0.5);
                half3 diffuseColor = mainLight.color * NdotL;

                return half4(diffuseColor * baseColor ,1.0); 
            } 
            ENDHLSL
        }
    }
}
 

Here I casually broke the normal of the triangular surface, and the Shader operation effect is as follows:
insert image description here

However, the current approach is flawed. When facing some models with different normals at the same position, the edge of the model will be discontinuous after subdivision, forming cracks (Crack) .
insert image description here

In order to solve this problem, NVIDIA has adopted an improved strategy PN-AEN (Point-Normal Triangles Using Adjacent Edge Normals). Here are related links:

This strategy generates an index buffer with adjacency information during data preprocessing. When subdividing, you can eliminate the cracks through adjacent vertex information.
insert image description here

However, it seems that it is not convenient to generate the index buffer of adjacency information in Unity. To generate this data, it may have to be manually operated. I saw that someone in the Asset Store has implemented PN-AEN Crack-Free Tessellation Displacement . If you are interested, you can take a look. (Or directly make the art hard concave!)

2.2.3 Phone Tessellation

Phone Tessellation is similar to PN Tessellation, and it is also for the purpose of smoothing the outline of the model. It's just that Phone Tessellation can achieve a similar effect with less calculations. For more content, it is recommended to read Phone Tessellation .

The core idea is that the generated vertex PPP is projected onto the tangent plane of the three vertices, and then these projection points are interpolated with the centroid coordinates, and finally the vertexP ∗ P^*P
insert image description here

In the code, we need to modify the domain shader, the entire code is posted below:

Shader "Tessellation/Flat Tri Tessellation + Phone Tess"
{
    
    
    Properties
    {
    
    
        [NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
    
    }  
        
        [Header(Tess)][Space]
     
        [KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 2
        [KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
        _EdgeFactor ("EdgeFactor", Range(1,16)) = 4 
        _InsideFactor ("InsideFactor", Range(1,16)) = 4  
        _PhoneShape ("PhoneShape", Range(0, 1)) = 0.5
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
         
        Pass
        {
    
     
            HLSLPROGRAM
            #pragma target 4.6 
            #pragma vertex PhoneTriTessVert
            #pragma fragment PhoneTriTessFrag 
            #pragma hull PhoneTriTessControlPoint
            #pragma domain PhoneTriTessDomain
             
            #pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD 
            #pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW 

 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl"

            CBUFFER_START(UnityPerMaterial) 
            float _EdgeFactor;  
            float _InsideFactor; 
            float _PhoneShape;
            CBUFFER_END

            TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); 

            struct Attributes
            {
    
    
                float3 positionOS   : POSITION; 
                float3 normalOS     : NORMAL;
                float2 texcoord     : TEXCOORD0;
    
            };

            struct VertexOut{
    
    
                float3 positionWS : INTERNALTESSPOS; 
                float2 texcoord : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
            };
             
            struct PatchTess {
    
      
                float edgeFactor[3] : SV_TESSFACTOR;
                float insideFactor  : SV_INSIDETESSFACTOR;
            };

            struct HullOut{
    
    
                float3 positionWS : INTERNALTESSPOS; 
                float2 texcoord : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
            };

            struct DomainOut
            {
    
    
                float4  positionCS      : SV_POSITION;
                float2  texcoord        : TEXCOORD0; 
            };


            VertexOut PhoneTriTessVert(Attributes input){
    
     
                VertexOut o;
                o.positionWS = TransformObjectToWorld(input.positionOS);  
                o.normalWS   = TransformObjectToWorldNormal(input.normalOS);
                o.texcoord   = input.texcoord;
                return o;
            }
   
   
            PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
     
                PatchTess o;
                o.edgeFactor[0] = _EdgeFactor;
                o.edgeFactor[1] = _EdgeFactor;
                o.edgeFactor[2] = _EdgeFactor;

                o.insideFactor  = _InsideFactor;
                return o;
            }
 
            [domain("tri")]   
            #if _PARTITIONING_INTEGER
            [partitioning("integer")] 
            #elif _PARTITIONING_FRACTIONAL_EVEN
            [partitioning("fractional_even")] 
            #elif _PARTITIONING_FRACTIONAL_ODD
            [partitioning("fractional_odd")]    
            #endif 
 
            #if _OUTPUTTOPOLOGY_TRIANGLE_CW
            [outputtopology("triangle_cw")] 
            #elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
            [outputtopology("triangle_ccw")] 
            #endif

            [patchconstantfunc("PatchConstant")] 
            [outputcontrolpoints(3)]                 
            [maxtessfactor(64.0f)]                 
            HullOut PhoneTriTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
    
      
                HullOut o;
                o.positionWS = patch[id].positionWS;
                o.texcoord = patch[id].texcoord; 
                o.normalWS = patch[id].normalWS;
                return o;
            }
 
  
            [domain("tri")]      
            DomainOut PhoneTriTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
            {
    
      
                float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z; 
                positionWS = PhongTessellation(positionWS, patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, patch[0].normalWS, patch[1].normalWS, patch[2].normalWS, bary, _PhoneShape);

                float2 texcoord   = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
    
                DomainOut output;
                output.positionCS = TransformWorldToHClip(positionWS);
                output.texcoord = texcoord;
    
                return output; 
            }
 

            half4 PhoneTriTessFrag(DomainOut input) : SV_Target{
    
     
                half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
                return half4(color, 1.0); 
            } 


            ENDHLSL
        }
    }
}

PhongTessellationThe content of the function is as follows:

// ===================== GeometricTools.hlsl ======================

float3 ProjectPointOnPlane(float3 position, float3 planePosition, float3 planeNormal)
{
    
    
    return position - (dot(position - planePosition, planeNormal) * planeNormal);
}


// ===================== Tessellation.hlsl =========================

// p0, p1, p2 triangle world position
// p0, p1, p2 triangle world vertex normal
real3 PhongTessellation(real3 positionWS, real3 p0, real3 p1, real3 p2, real3 n0, real3 n1, real3 n2, real3 baryCoords, real shape)
{
    
    
    // 分别计算三个切平面的投影点
    real3 c0 = ProjectPointOnPlane(positionWS, p0, n0);
    real3 c1 = ProjectPointOnPlane(positionWS, p1, n1);
    real3 c2 = ProjectPointOnPlane(positionWS, p2, n2);
	
    // 利用质心坐标插值得到最终顶点位置
    real3 phongPositionWS = baryCoords.x * c0 + baryCoords.y * c1 + baryCoords.z * c2;
	
    // 通过shape 控制平滑程度
    return lerp(positionWS, phongPositionWS, shape);
}

The operation effect of Shader is as follows:
insert image description here

Similarly, Phone Tessellation, like PN Tessellation, uses the normal of the model to smooth the surface. If a vertex position has multiple normal directions, cracks will also occur.

2.3 Different Subdivision Factors

So far, our subdivision factors are all controlled by the Shader panel. Next, we will use the algorithm to flexibly adjust the subdivision factor.

2.3.1 Based on camera distance

In order to make the position closer to the camera more subdivided, the code of the constant shell shader is now adjusted as follows:

   
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
     
    PatchTess o;
    float3 cameraPosWS = GetCameraPositionWS();
    real3 triVectexFactors =  GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);

    float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
    o.edgeFactor[0] = max(1.0, tessFactors.x);
    o.edgeFactor[1] = max(1.0, tessFactors.y);
    o.edgeFactor[2] = max(1.0, tessFactors.z);

    o.insideFactor  = max(1.0, tessFactors.w);
    return o;
}

Here the position of the camera is obtained, and the world space coordinates of the three vertices are passed into GetDistanceBasedTessFactorthe function:

// Tessellation.hlsl
real3 GetDistanceBasedTessFactor(real3 p0, real3 p1, real3 p2, real3 cameraPosWS, real tessMinDist, real tessMaxDist)
{
    
    
    real3 edgePosition0 = 0.5 * (p1 + p2);
    real3 edgePosition1 = 0.5 * (p0 + p2);
    real3 edgePosition2 = 0.5 * (p0 + p1);

    // In case camera-relative rendering is enabled, 'cameraPosWS' is statically known to be 0,
    // so the compiler will be able to optimize distance() to length().
    real dist0 = distance(edgePosition0, cameraPosWS);
    real dist1 = distance(edgePosition1, cameraPosWS);
    real dist2 = distance(edgePosition2, cameraPosWS);

    // The saturate will handle the produced NaN in case min == max
    real fadeDist = tessMaxDist - tessMinDist;
    real3 tessFactor;
    tessFactor.x = saturate(1.0 - (dist0 - tessMinDist) / fadeDist);
    tessFactor.y = saturate(1.0 - (dist1 - tessMinDist) / fadeDist);
    tessFactor.z = saturate(1.0 - (dist2 - tessMinDist) / fadeDist);

    return tessFactor;
}

The function takes the distance between the midpoint of each side and the camera, starting from the minimum subdivision distance tessMinDistto the furthest subdivision distance tessMaxDist, and the subdivision factor gradually decays to 0.

After obtaining the edge subdivision factor, the internal subdivision factor is simply processed as the average of the three sides.

// Tessellation.hlsl
real4 CalcTriTessFactorsFromEdgeTessFactors(real3 triVertexFactors)
{
    
    
    real4 tess;
    tess.x = triVertexFactors.x;
    tess.y = triVertexFactors.y;
    tess.z = triVertexFactors.z;
    tess.w = (triVertexFactors.x + triVertexFactors.y + triVertexFactors.z) / 3.0;

    return tess;
}

The complete code is posted below:

Shader "Tessellation/Flat Tri Tessellation + Distance-Based"
{
    
    
    Properties
    {
    
    
        [NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
    
    }  
        
        [Header(Tess)][Space]
     
        [KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 2
        [KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
        [IntRange]_EdgeFactor ("EdgeFactor", Range(1,8)) = 4
        _TessMinDist ("TessMinDist", Range(0,10)) = 10.0
        _FadeDist ("FadeDist", Range(1,20)) = 15.0
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
         
        Pass
        {
    
     
            HLSLPROGRAM
            #pragma target 4.6 
            #pragma vertex DistanceBasedTessVert
            #pragma fragment DistanceBasedTessFrag 
            #pragma hull DistanceBasedTessControlPoint
            #pragma domain DistanceBasedTessDomain
             
            #pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD 
            #pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW 

 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl"

            CBUFFER_START(UnityPerMaterial) 
            float _EdgeFactor;  
            float _TessMinDist;
            float _FadeDist;
            CBUFFER_END

            TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); 

            struct Attributes
            {
    
    
                float3 positionOS   : POSITION; 
                float2 texcoord     : TEXCOORD0;
    
            };

            struct VertexOut{
    
    
                float3 positionWS : INTERNALTESSPOS; 
                float2 texcoord : TEXCOORD0;
            };
             
            struct PatchTess {
    
      
                float edgeFactor[3] : SV_TESSFACTOR;
                float insideFactor  : SV_INSIDETESSFACTOR;
            };

            struct HullOut{
    
    
                float3 positionWS : INTERNALTESSPOS; 
                float2 texcoord : TEXCOORD0;
            };

            struct DomainOut
            {
    
    
                float4  positionCS      : SV_POSITION;
                float2  texcoord        : TEXCOORD0; 
            };


            VertexOut DistanceBasedTessVert(Attributes input){
    
     
                VertexOut o;
                o.positionWS = TransformObjectToWorld(input.positionOS);  
                o.texcoord   = input.texcoord;
                return o;
            }
   
   
            PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
     
                PatchTess o;
                float3 cameraPosWS = GetCameraPositionWS();
                real3 triVectexFactors =  GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);
 
                float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
                o.edgeFactor[0] = max(1.0, tessFactors.x);
                o.edgeFactor[1] = max(1.0, tessFactors.y);
                o.edgeFactor[2] = max(1.0, tessFactors.z);

                o.insideFactor  = max(1.0, tessFactors.w);
                return o;
            }
 
            [domain("tri")]   
            #if _PARTITIONING_INTEGER
            [partitioning("integer")] 
            #elif _PARTITIONING_FRACTIONAL_EVEN
            [partitioning("fractional_even")] 
            #elif _PARTITIONING_FRACTIONAL_ODD
            [partitioning("fractional_odd")]    
            #endif 
 
            #if _OUTPUTTOPOLOGY_TRIANGLE_CW
            [outputtopology("triangle_cw")] 
            #elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
            [outputtopology("triangle_ccw")] 
            #endif

            [patchconstantfunc("PatchConstant")] 
            [outputcontrolpoints(3)]                 
            [maxtessfactor(64.0f)]                 
            HullOut DistanceBasedTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
    
      
                HullOut o;
                o.positionWS = patch[id].positionWS;
                o.texcoord = patch[id].texcoord; 
                return o;
            }
 
  
            [domain("tri")]      
            DomainOut DistanceBasedTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
            {
    
      
                float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z; 
	            float2 texcoord   = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;

                DomainOut output;
                output.positionCS = TransformWorldToHClip(positionWS);
                output.texcoord = texcoord;
    
                return output; 
            }
 

            half4 DistanceBasedTessFrag(DomainOut input) : SV_Target{
    
       
                half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
                return half4(color, 1.0); 
            }


            ENDHLSL
        }
    }
}

Shader application effect is as follows:
insert image description here

2.3.2 Based on screen footprint

Modify the constant hull shader as follows:

PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
    
     
    PatchTess o;
    real3 triVectexFactors =  GetScreenSpaceTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, GetWorldToHClipMatrix() , _ScreenParams, _TriangleSize);
    float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
    o.edgeFactor[0] = tessFactors.x;
    o.edgeFactor[1] = tessFactors.y;
    o.edgeFactor[2] = tessFactors.z;

    o.insideFactor  = tessFactors.w;
    return o;
}

GetScreenSpaceTessFactorThe function is as follows:

// Tessellation.hlsl
// Reference: http://twvideo01.ubm-us.net/o1/vault/gdc10/slides/Bilodeau_Bill_Direct3D11TutorialTessellation.pdf

// Compute both screen and distance based adaptation - return factor between 0 and 1
real3 GetScreenSpaceTessFactor(real3 p0, real3 p1, real3 p2, real4x4 viewProjectionMatrix, real4 screenSize, real triangleSize)
{
    
    
    // Get screen space adaptive scale factor
    real2 edgeScreenPosition0 = ComputeNormalizedDeviceCoordinates(p0, viewProjectionMatrix) * screenSize.xy;
    real2 edgeScreenPosition1 = ComputeNormalizedDeviceCoordinates(p1, viewProjectionMatrix) * screenSize.xy;
    real2 edgeScreenPosition2 = ComputeNormalizedDeviceCoordinates(p2, viewProjectionMatrix) * screenSize.xy;

    real EdgeScale = 1.0 / triangleSize; // Edge size in reality, but name is simpler
    real3 tessFactor;
    tessFactor.x = saturate(distance(edgeScreenPosition1, edgeScreenPosition2) * EdgeScale);
    tessFactor.y = saturate(distance(edgeScreenPosition0, edgeScreenPosition2) * EdgeScale);
    tessFactor.z = saturate(distance(edgeScreenPosition0, edgeScreenPosition1) * EdgeScale);

    return tessFactor;
}

The general idea is that ComputeNormalizedDeviceCoordinatesthe screen coordinates are obtained through function calculations, and then multiplied by the screen size screenSizeto obtain the screen position of the vertex. If the length of the top edge of the screen is less than triangleSize, then its subdivision factor is attenuated.
insert image description here

2.3.3 Others

In "DirectX12 3D Game Development Practical Combat", there are also two measurement criteria for calculating subdivision factors:

  • According to the orientation of the triangle: for example, the triangle located around the outline (Sihouette edge) should have more details than other positions. We can judge whether it is near the contour line through the dot product of the patch normal and the viewing direction.
    insert image description here

    Read more about Real-time linear silhouette enhancement .

  • Depending on roughness: Rough, uneven surfaces require more detailed tessellation than smooth surfaces. The roughness data can be obtained through the texture to determine the number of tessellations.

3 References


The level is limited, please bear with me if there are mistakes (〃'▽'〃)

Guess you like

Origin blog.csdn.net/zigzagbomb/article/details/128438875
Recommended