• Unity地面交互效果——3、曲面细分基础知识


    回到目录
      大家好,我是阿赵。
      之前介绍了使用动态法线贴图混合的方式模拟轨迹的凹凸感,这次来讲一下更真实的凹凸感制作。不过在说这个内容之前,这一篇先要介绍一下曲面细分着色器(Tessellation Shader)的用法。

    一、为什么要做曲面细分

      之前通过法线贴图模拟了凹凸的感觉:
    在这里插入图片描述

      法线贴图不会真的产生凹凸,它只是改变了这个平面上面的法线方向。所以,只有通过光照模型,通过法线方向和灯光方向进行点乘,才会计算出不同的光照角度,让我们产生一定的凹凸感觉。
      但如果想做到这样的效果,法线贴图是不行的:
    在这里插入图片描述

      这种效果,球是真的陷进去地面了。很明显,这些都是需要偏移顶点让网格产生真实的变形,才能做到。
      不过这里有一个问题,如果地面的网格面数并不是很高,那么就算我们有能力去偏移顶点,也产生不了这样好的效果。
      比如一般的地面网格的面数都很低,只有这样的水平:
    在这里插入图片描述

      这个时候,球所在的地方,根本就没有顶点,所以也偏移不了。就算再稍微多一点面,这样的地面网格面数算比较高了,仍然产生不了很好的凹凸效果:
    在这里插入图片描述

      所以这里有一个很严重的问题,我们难道需要用几十万甚至几百万面,去做一个地面的模型,才能产生真实的凹凸感吗?
      这是不可能的,实际的情况是:
    在这里插入图片描述

      在需要到很精确的顶点控制的一个小局部,才需要把面数变高,其他的地方,面数很是很低的。具体可以看看这个视频:

    Unity引擎动态曲面细分

      而这里用到的局部增加面数的技术,就是曲面细分(Tessellation)了。

    二、曲面细分的过程

      在Unity里面写顶点片段着色器的Shader,我们一般只会注意到需要些Vertex顶点程序,和fragment片段程序,因为在大多数情况下,其他的渲染管线流程都不是我们可以控制的,而我们能控制顶点程序改变模型的形状,控制片段程序来改变模型的颜色。
      但在顶点程序和片段程序中间,其实还有一个曲面细分(tessellate)的过程,这个过程有2个程序是我们可以控制的
    1、hullProgram
      这个程序会接受每个多边形各个顶点的信息,记录下来,然后通过指定一个Patch Constant Function,去设置细分的数量,这个过程是针对多边形的每一条边,还有多边形的内部,分别设置拆分的数量的。
    2、domainProgram
      在前面的hullProgram里面,其实只是设置了顶点信息和拆分数量,并没有真正的生成新的网格。而在这个domainProgram里面,拆分后的顶点信息已经产生了,所以可以对拆分后的顶线进行操作,可以计算他们的位置、法线、uv等。
      为了避免难以理解,也不说太多,只要知道,需要做曲面细分的时候,需要添加2个程序过程,一个过程设置了拆分的数量和其他参数,另外一个过程就得到了顶点,可以进行实际操作,这样就行了。

    三、曲面细分在Unity引擎的实现

    1、Surface类型着色器

      Surface类型的Shader提供了很多Unity封装好的方法,也包括提供了对应曲面细分着色器的方法。
    使用很简单:
    1.#include “Tessellation.cginc”
    2.指定曲面细分的方法:tessellate:tessFunction
    3.指定target 4.6

    在这里插入图片描述

    看到这里有target 4.6的声明了,没错Unity官方的说明也是这样的:

    When you use tessellation, the shader is automatically compiled into
    the Shader Model 4.6 target, which prevents support for running on
    older graphics targets.

      这里着重说一下曲面细分方法。
      由于Surface的曲面细分方法是Unity封装好的,所以我们不需要走正常的渲染流程,不需要指定hullProgram、Patch Constant Function和domainProgram,只需要指定一个tessellate处理方法。这个方法实际是返回一个曲面细分的值,来决定某个面具体要细分成多少个网格。
    而在Unity提供的方法里面,对于怎样细分曲面,提供了3种选择:

    1.Fixed固定数量细分

      这种方式细分,在tessFunction里面直接返回一个数值,然后全部面就按照统一的数值去细分。
    在这里插入图片描述

    unity官方文档里面的例子是这样

    Shader "Tessellation Sample" {
            Properties {
                _Tess ("Tessellation", Range(1,32)) = 4
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
                
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
                #pragma target 4.6
    
                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };
    
                float _Tess;
    
                float4 tessFixed()
                {
                    return _Tess;
                }
    
                sampler2D _DispTex;
                float _Displacement;
    
                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }
    
                struct Input {
                    float2 uv_MainTex;
                };
    
                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;
    
                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    其中曲面细分方法是直接返回了一个指定的值

    float4 tessFixed()
    {
        return _Tess;
    }
    
    • 1
    • 2
    • 3
    • 4

    2.根据距离细分

      这里的距离,指的是和摄像机的距离。根据离摄像机不同的距离,设置一个范围来细分
    在这里插入图片描述

    unity官方文档里面的例子是这样:

       Shader "Tessellation Sample" {
            Properties {
                _Tess ("Tessellation", Range(1,32)) = 4
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
                
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
                #pragma target 4.6
                #include "Tessellation.cginc"
    
                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };
    
                float _Tess;
    
                float4 tessDistance (appdata v0, appdata v1, appdata v2) {
                    float minDist = 10.0;
                    float maxDist = 25.0;
                    return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
                }
    
                sampler2D _DispTex;
                float _Displacement;
    
                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }
    
                struct Input {
                    float2 uv_MainTex;
                };
    
                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;
    
                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    其中曲面细分方法是传入了最小距离、最大距离和一个控制值

    float4 tessDistance (appdata v0, appdata v1, appdata v2) {
    	 float minDist = 10.0;
    	float maxDist = 25.0;
    	return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    UnityDistanceBasedTess就是Unity提供的根据距离计算细分值的方法。

    3.根据边的长度细分

      这个根据边的长度,指的是多边形的边,在屏幕里面渲染的大小。
    在这里插入图片描述

      所以从左图可以看出,越近屏幕的边,渲染的长度越大,所以细分得越多,而离屏幕越远的边,渲染的长度越小,细分得也越少。
      从右图可以看出,同一个模型,如果通过缩放把边拉长,它的细分程度也会随着模型拉长而变大,最后保持着一个比较固定的细分密度。
    unity官方文档里面的例子是这样的:

        Shader "Tessellation Sample" {
            Properties {
                _EdgeLength ("Edge length", Range(2,50)) = 15
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DispTex ("Disp Texture", 2D) = "gray" {}
                _NormalMap ("Normalmap", 2D) = "bump" {}
                _Displacement ("Displacement", Range(0, 1.0)) = 0.3
                _Color ("Color", color) = (1,1,1,0)
                _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 300
                
                CGPROGRAM
                #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
                #pragma target 4.6
                #include "Tessellation.cginc"
    
                struct appdata {
                    float4 vertex : POSITION;
                    float4 tangent : TANGENT;
                    float3 normal : NORMAL;
                    float2 texcoord : TEXCOORD0;
                };
    
                float _EdgeLength;
    
                float4 tessEdge (appdata v0, appdata v1, appdata v2)
                {
                    return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
                }
    
                sampler2D _DispTex;
                float _Displacement;
    
                void disp (inout appdata v)
                {
                    float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                    v.vertex.xyz += v.normal * d;
                }
    
                struct Input {
                    float2 uv_MainTex;
                };
    
                sampler2D _MainTex;
                sampler2D _NormalMap;
                fixed4 _Color;
    
                void surf (Input IN, inout SurfaceOutput o) {
                    half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    o.Specular = 0.2;
                    o.Gloss = 1.0;
                    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
                }
                ENDCG
            }
            FallBack "Diffuse"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    其中曲面细分程序传入一个指定的值,需要注意的是,这个值越小,细分得越多

    float4 tessEdge (appdata v0, appdata v1, appdata v2)
    {
    	return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
    }
    
    • 1
    • 2
    • 3
    • 4

    UnityEdgeLengthBasedTess 是Unity提供的根据边长细分的方法

    2、顶点片段程序实现曲面细分

      如果不使用Surface类型的Shader,而用传统的顶点片段程序着色器,实现曲面细分就只有一种方式,就是正常的添加hullProgram、Patch Constant Function和domainProgram,然后逐条边和多边形内部指定细分的数量。我这里提供一个最简单的Shader来说明一下写法:

    Shader "azhao/TessVF"
    {
        Properties
        {
    		_MainTex("Texture", 2D) = "white" {}
    		_Color("Color", Color) = (1,1,1,1)
    		_EditFactor("edgeFactor", Float) = 15
    		_InsideFactor("insideFactor",FLoat)  =15
    
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" }
            LOD 100
    
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
    			//在正常的vertex和fragment之间还需要hull和domain,所以在这里加上声明
    			#pragma hull hullProgram
    			#pragma domain domainProgram
                #pragma fragment frag
    
    
                #include "UnityCG.cginc"
    			sampler2D _MainTex;
    			float4 _MainTex_ST;
    			fixed4 _Color;
    			uniform float _EditFactor;
    			uniform float _InsideFactor;
    			struct a2v
    			{
    				float4 pos	: POSITION;
    				float2 uv  : TEXCOORD0;
    			};
    
    			struct v2t
    			{
    				float4 worldPos	: TEXCOORD0;
    				float2 uv  : TEXCOORD1;
    			};
    			struct t2f
    			{
    				float4 clipPos:SV_POSITION;
    				float2 uv: TEXCOORD0;
    				float4 worldPos:TEXCOORD1;
    				
    			};
    
    			struct TessOut
    			{
    				float2 uv  : TEXCOORD0;
    				float4 worldPos	: TEXCOORD1;
    				
    			};
    			struct TessParam
    			{
    				float EdgeTess[3]	: SV_TessFactor;//各边细分数
    				float InsideTess : SV_InsideTessFactor;//内部点细分数
    			};
    
    			v2t vert(a2v i)
    			{
    				v2t o;
    				o.worldPos = mul(unity_ObjectToWorld,i.pos);
    				o.uv = i.uv;
    				return o;
    			}
    			//在hullProgram之前必须设置这些参数,不然会报错
    			[domain("tri")]//图元类型,可选类型有 "tri", "quad", "isoline"
    			[partitioning("integer")]//曲面细分的过渡方式是整数还是小数
    			[outputtopology("triangle_cw")]//三角面正方向是顺时针还是逆时针
    			[outputcontrolpoints(3)]//输出的控制点数
    			[patchconstantfunc("ConstantHS")]//对应之前的细分因子配置阶段的方法名
    			[maxtessfactor(64.0)]//最大可能的细分段数
    
    			//vert顶点程序之后调用,计算细分前的三角形顶点信息
    			TessOut hullProgram(InputPatch<v2t, 3> i, uint idx : SV_OutputControlPointID)
    			{
    				TessOut o;
    				o.worldPos = i[idx].worldPos;
    				o.uv = i[idx].uv;
    				return o;
    			}
    
    			//指定每个边的细分段数和内部细分段数
    			TessParam ConstantHS(InputPatch<v2t, 3> i, uint id : SV_PrimitiveID)
    			{
    				TessParam o;
    				o.EdgeTess[0] = _EditFactor;
    				o.EdgeTess[1] = _EditFactor;
    				o.EdgeTess[2] = _EditFactor;
    				o.InsideTess = _InsideFactor;
    				return o;
    			}
    
    			//在domainProgram前必须设置domain参数,不然会报错
    			[domain("tri")]
    			//细分之后,把信息传到frag片段程序
    			t2f domainProgram(TessParam tessParam, float3 bary : SV_DomainLocation, const OutputPatch<TessOut, 3> i)
    			{
    				t2f o;				
    				//线性转换
    
    				float2 uv = i[0].uv * bary.x + i[1].uv * bary.y + i[2].uv * bary.z;
    				o.uv = uv;
    				float4 worldPos = i[0].worldPos * bary.x + i[1].worldPos * bary.y + i[2].worldPos * bary.z;
    				o.worldPos = worldPos;
    				o.clipPos = UnityWorldToClipPos(worldPos);
    				return o;
    			}
                fixed4 frag (t2f i) : SV_Target
                {
                    // sample the texture
                    fixed4 col = tex2D(_MainTex, i.uv)*_Color;
    
    				return col;
                }
                ENDCG
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123

    需要注意的地方是:
    1.声明处理程序:

    #pragma hull hullProgram
    #pragma domain domainProgram
    
    • 1
    • 2

    2.在hullProgram之前必须设置这些参数,不然会报错

    [domain("tri")]//图元类型,可选类型有 "tri", "quad", "isoline"
    [partitioning("integer")]//曲面细分的过渡方式是整数还是小数
    [outputtopology("triangle_cw")]//三角面正方向是顺时针还是逆时针
    [outputcontrolpoints(3)]//输出的控制点数
    [patchconstantfunc("ConstantHS")]//对应之前的细分因子配置阶段的方法名
    [maxtessfactor(64.0)]//最大可能的细分段数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.domainProgram前必须设置domain参数,不然会报错

    [domain("tri")]
    
    • 1

    四、根据范围做局部曲面细分

      已经介绍完怎样使用曲面细分了,接下来就是要实现文章一开始说的,根据指定的中心点和范围,做局部的曲面细分。

    1、在顶点片段着色器实现局部细分

      由于使用顶点片段着色器做曲面细分,是可以直接设置每个多边形的边和内部的细分数量,所以要实现局部细分也就非常简单了,思路是:
    1.获得中心点坐标和范围半径
    2.在着色器取得当前顶点的世界坐标,然后判断是否在中心点的半径范围内
    3.用一个smoothStep做一个边缘范围过渡,作为细分强度
    4.根据计算出的细分强度,设置最终的细分值。

    写成代码大概就是这样:

    Shader "azhao/GroundTessVF"
    {
        Properties
        {
    		_MainTex("Texture", 2D) = "white" {}
    		_Color("Color", Color) = (1,1,1,1)
    		_centerPos("CenterPos", Vector) = (0,0,0,0)
    		_minVal("minVal", Float) = 0
    		_maxVal("maxVal", Float) = 10
    		_factor("factor", Float) = 15
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" }
            LOD 100
    
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
    			//在正常的vertex和fragment之间还需要hull和domain,所以在这里加上声明
    			#pragma hull hullProgram
    			#pragma domain domainProgram
                #pragma fragment frag
    
    
                #include "UnityCG.cginc"
    			sampler2D _MainTex;
    			float4 _MainTex_ST;
    			fixed4 _Color;
    			uniform float _minVal;
    			uniform float _maxVal;
    			uniform float3 _centerPos;
    			uniform float _factor;
    			struct a2v
    			{
    				float4 pos	: POSITION;
    				float2 uv  : TEXCOORD0;
    			};
    
    			struct v2t
    			{
    				float4 worldPos	: TEXCOORD0;
    				float2 uv  : TEXCOORD1;
    			};
    			struct t2f
    			{
    				float4 clipPos	       : SV_POSITION;
    				float2 uv     : TEXCOORD0;
    				float4 worldPos            : TEXCOORD1;
    				
    			};
    
    			struct TessOut
    			{
    				float2 uv  : TEXCOORD0;
    				float4 worldPos	: TEXCOORD1;
    				
    			};
    			struct TessParam
    			{
    				float EdgeTess[3]	: SV_TessFactor;//各边细分数
    				float InsideTess : SV_InsideTessFactor;//内部点细分数
    			};
    
    			
    			v2t vert(a2v i)
    			{
    				v2t o;
    				o.worldPos = mul(unity_ObjectToWorld,i.pos);
    				o.uv = i.uv;
    				return o;
    			}
    			//在hullProgram之前必须设置这些参数,不然会报错
    			[domain("tri")]//图元类型,可选类型有 "tri", "quad", "isoline"
    			[partitioning("integer")]//曲面细分的过渡方式是整数还是小数
    			[outputtopology("triangle_cw")]//三角面正方向是顺时针还是逆时针
    			[outputcontrolpoints(3)]//输出的控制点数
    			[patchconstantfunc("ConstantHS")]//对应之前的细分因子配置阶段的方法名
    			[maxtessfactor(64.0)]//最大可能的细分段数
    
    			//vert顶点程序之后调用,计算细分前的三角形顶点信息
    			TessOut hullProgram(InputPatch<v2t, 3> i, uint idx : SV_OutputControlPointID)
    			{
    				TessOut o;
    				o.worldPos = i[idx].worldPos;
    				o.uv = i[idx].uv;
    				return o;
    			}
    
    			//指定每个边的细分段数和内部细分段数
    			TessParam ConstantHS(InputPatch<v2t, 3> i, uint id : SV_PrimitiveID)
    			{
    				TessParam o;
    				float4 worldPos = (i[0].worldPos + i[1].worldPos + i[2].worldPos) / 3;
    				float smoothstepResult = smoothstep(_minVal, _maxVal, distance(worldPos.xz, _centerPos.xz));
    				float fac = max((1.0 - smoothstepResult)*_factor, 1);
    				//由于我这里是根据指定的中心点和半径范围来动态算细分段数,所以才有这个计算,不然可以直接指定变量来设置。
    				o.EdgeTess[0] = fac;
    				o.EdgeTess[1] = fac;
    				o.EdgeTess[2] = fac;
    				o.InsideTess = fac;
    				return o;
    			}
    
    			//在domainProgram前必须设置domain参数,不然会报错
    			[domain("tri")]
    			//细分之后,把信息传到frag片段程序
    			t2f domainProgram(TessParam tessParam, float3 bary : SV_DomainLocation, const OutputPatch<TessOut, 3> i)
    			{
    				t2f o;				
    				//线性转换
    				o.worldPos = i[0].worldPos * bary.x + i[1].worldPos * bary.y + i[2].worldPos * bary.z;
    				o.clipPos = UnityWorldToClipPos(o.worldPos);
    				float2 uv = i[0].uv * bary.x + i[1].uv * bary.y + i[2].uv * bary.z;
    				o.uv = uv;
    				return o;
    			}
                fixed4 frag (t2f i) : SV_Target
                {
                    // sample the texture
                    fixed4 col = tex2D(_MainTex, i.uv)*_Color;
                    return col;
                }
                ENDCG
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128

      使用的时候,在C#端中心点改变的时候,传入centerPos,通过调整_maxVal和_minVal,可以控制半径和边缘强度渐变的效果

    2、在Surface着色器实现局部细分

      在Surface着色器里面实现曲面细分,需要写的代码很少,我们就使用上面介绍的Fixed类型然后同样的通过传入中心点,还有_maxVal和_minVal,来确定需要细分的范围,实现思路和上面的顶点片段着色器是一样的。
    代码会是这样的:

    Shader "azhao/FootStepMeshSurface"
    {
        Properties
        {
    		_MainTex("Texture", 2D) = "white" {}
            _Color ("Color", Color) = (1,1,1,1)
    		_centerPos("centerPos", Vector) = (0,0,0,0)
    		_minVal("minVal", Float) = 0
    		_maxVal("maxVal", Float) = 10
    		_factor("factor", Float) = 15
    		_footstepRect("footstepRect",Vector) = (0,0,0,0)
    		_footstepTex("footstepTex",2D) = "gray"{}
    		_height("height" ,Float) = 0.3
    		_Glossiness("Glossiness",Float) = 0
    		_Metallic("Metallic",Float) = 0
    
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" }
            LOD 200
    
            CGPROGRAM
    		#include "Tessellation.cginc"
            #pragma surface surf Standard fullforwardshadows vertex:vertexDataFunc tessellate:tessFunction 
            #pragma target 4.6
    
    
            struct Input
            {
                float2 uv_texcoord;
            };
    
            half _Glossiness;
            half _Metallic;
            fixed4 _Color;
    		uniform sampler2D _mainTex;
    		SamplerState sampler_mainTex;
    		uniform float4 _mainTex_ST;
    		uniform float _minVal;
    		uniform float _maxVal;
    		uniform float3 _centerPos;
    		uniform float _factor;
    		float4 _footstepRect;
    		sampler2D _footstepTex;
    		float _height;
            UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_INSTANCING_BUFFER_END(Props)
    
    		float RemapUV(float min, float max, float val)
    		{
    			return (val - min) / (max - min);
    		}
    		//这里处理细分相关逻辑
    		float4 tessFunction(appdata_full v0, appdata_full v1, appdata_full v2)
    		{
    			float3 worldPos = mul(unity_ObjectToWorld, (v0.vertex + v1.vertex + v2.vertex) / 3);
    			float smoothstepResult = smoothstep(_minVal, _maxVal, distance(worldPos.xz, _centerPos.xz));
    			float fac = max((1.0 - smoothstepResult)*_factor, 0.1);
    			return fac;
    		}
    
    		void vertexDataFunc(inout appdata_full v)
    		{
    
    		}
    
            void surf (Input IN, inout SurfaceOutputStandard o)
            {
                fixed4 c =  _Color;
    			float2 uv_mainTex = IN.uv_texcoord * _mainTex_ST.xy + _mainTex_ST.zw;
    			float4 mainTex = tex2D(_mainTex, uv_mainTex);
                o.Albedo = mainTex.rgb*c.rgb;
                o.Metallic = _Metallic;
                o.Smoothness = _Glossiness;
                o.Alpha = c.a;
            }
            ENDCG
        }
        FallBack "Diffuse"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    3、选择哪种方式的Shader实现会比较好?

      这个问题是没有直接答案的,需要根据自己的实际情况来选择。
      顶点片段着色器的优点是可控性强,自己可以随意的定义各种光照模型、修改细节的效果,缺点是写法麻烦。
      Surface着色器的优点是写法简单,缺点是可控性比较弱一点。
      我个人是习惯用顶点片段着色器的,因为我比较的喜欢自己控制各个环节的细节。所以在接下来的例子里面,我还是会用顶点片段着色器的写法来继续做这个地面交互效果的demo。不过其实如果顶点片段着色器上知道了怎样实现,在Surface着色器上面实现的过程就更简单了。

  • 相关阅读:
    Springboot 整合 Socket 实战案例 ,实现 单点发送、广播群发,1对1,1对多
    Python时间处理
    OpenHarmony迎来首个互联网技术统一标准,鸿蒙OS生态走向如何?
    C语言文件操作(详解)
    python 打包可执行文件-Nuitka详解
    Linux下C/C++实现进程内存使用分析工具(memstat)
    干掉进程:以一种简易友好的方式干掉进程 - Fkill
    使用脚本定时备份MySql数据库文件
    【Spring】AOP实现原理
    java实现文件加密解密
  • 原文地址:https://blog.csdn.net/liweizhao/article/details/134216537