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官方文档里面的例子是这样

bash 复制代码
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"
    }

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

bash 复制代码
float4 tessFixed()
{
    return _Tess;
}

2.根据距离细分

这里的距离,指的是和摄像机的距离。根据离摄像机不同的距离,设置一个范围来细分

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

bash 复制代码
   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"
    }

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

bash 复制代码
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);
}

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

3.根据边的长度细分

这个根据边的长度,指的是多边形的边,在屏幕里面渲染的大小。

所以从左图可以看出,越近屏幕的边,渲染的长度越大,所以细分得越多,而离屏幕越远的边,渲染的长度越小,细分得也越少。

从右图可以看出,同一个模型,如果通过缩放把边拉长,它的细分程度也会随着模型拉长而变大,最后保持着一个比较固定的细分密度。

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

bash 复制代码
    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"
}

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

bash 复制代码
float4 tessEdge (appdata v0, appdata v1, appdata v2)
{
	return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}

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

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

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

bash 复制代码
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.声明处理程序:

bash 复制代码
#pragma hull hullProgram
#pragma domain domainProgram

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

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

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

bash 复制代码
[domain("tri")]

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

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

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

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

写成代码大概就是这样:

bash 复制代码
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
        }
    }
}

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

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

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

代码会是这样的:

bash 复制代码
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"
}

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

这个问题是没有直接答案的,需要根据自己的实际情况来选择。

顶点片段着色器的优点是可控性强,自己可以随意的定义各种光照模型、修改细节的效果,缺点是写法麻烦。

Surface着色器的优点是写法简单,缺点是可控性比较弱一点。

我个人是习惯用顶点片段着色器的,因为我比较的喜欢自己控制各个环节的细节。所以在接下来的例子里面,我还是会用顶点片段着色器的写法来继续做这个地面交互效果的demo。不过其实如果顶点片段着色器上知道了怎样实现,在Surface着色器上面实现的过程就更简单了。

相关推荐
小贺儿开发1 天前
Unity3D 智慧城市管理平台
数据库·人工智能·unity·智慧城市·数据可视化
June bug2 天前
【领域知识】休闲游戏一次发版全流程:Google Play + Apple App Store
unity
星夜泊客2 天前
C# 基础:为什么类可以在静态方法中创建自己的实例?
开发语言·经验分享·笔记·unity·c#·游戏引擎
dzj20212 天前
PointerEnter、PointerExit、PointerDown、PointerUp——鼠标点击物体,则开始旋转,鼠标离开或者松开物体,则停止旋转
unity·pointerdown·pointerup
心前阳光2 天前
Unity 模拟父子关系
android·unity·游戏引擎
在路上看风景2 天前
26. Mipmap
unity
咸鱼永不翻身2 天前
Unity视频资源压缩详解
unity·游戏引擎·音视频
在路上看风景2 天前
4.2 OverDraw
unity
在路上看风景2 天前
1.10 CDN缓存
unity
ellis19703 天前
Unity插件SafeArea Helper适配异形屏详解
unity