Unity地面交互效果——1、局部UV采样和混合轨迹

回到目录

  大家好,我是阿赵。
  这期开始,打算介绍一下地面交互的一些做法。
比如:

Unity引擎制作沙地实时凹陷网格的脚印效果

或者:

Unity引擎制作雪地效果

  这些效果的实现,需要基于一些基础的知识。所以这一篇先介绍一下简单的局部UV采样,然后映射纹理到地面的做法。
  大概需要实现的效果是这个视频的前半部分:

Unity曲面细分制作雪地效果

一、轨迹的绘制

  看这段视频的前半部分。可以看到,球在移动的过程中,在地面产生了移动的轨迹
在这里插入图片描述

  这个效果可能很多朋友都会做,一般的做法是计算球的坐标相对于整个地面的位置,然后拾像素绘制在地面的遮罩贴图上面。
  不过这种做法会有一个问题,假如地面很大的时候,通过一张和整个地面匹配UV的遮罩贴图来绘制轨迹,那么这张遮罩贴图的分辨率需要多大,才能显示足够的精度呢?比如一个4096米4096米的地面,就算我们用一张40964096的贴图做遮罩,那么每平方米的面积,才占一个像素,明显是绘制不出这么清晰的轨迹图形的。
  其实我们没有必要去绘制整张贴图,只需要局部绘制就好了
在这里插入图片描述
在这里插入图片描述

  绘制这一个小局部,然后通过局部UV采样的方式,把这个贴图叠加到大贴图上面去。
  这时候,就需要给Shader传入一个范围,让Shader知道,这个局部UV,最终占整个地面UV的多少。
地面的Shader代码是这样的:

Shader "azhao/GroundFootStep"
{
    
    
    Properties
    {
    
    
		_MainTex("Texture", 2D) = "white" {
    
    }
		_Color("Color", Color) = (1,1,1,1)
		_centerPos("CenterPos", Vector) = (0,0,0,0)
		_footstepRect("footstepRect",Vector) = (0,0,0,0)
		_footstepTex("footstepTex",2D) = "gray"{
    
    }
		_footstepColor("footstepColor",Color) = (1,1,1,1)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            #include "UnityCG.cginc"
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Color;

			uniform float3 _centerPos;

			float4 _footstepRect;
			sampler2D _footstepTex;
			float4 _footstepColor;
			struct appdata
			{
    
    
				float4 pos	: POSITION;
				float2 uv  : TEXCOORD0;
			};

			struct v2f
			{
    
    
				float4 pos : SV_POSITION;
				float3 worldPos	: TEXCOORD0;
				float2 uv  : TEXCOORD1;
				float2 footstepUV : TEXCOORD2;
			};


			float RemapUV(float min, float max, float val)
			{
    
    
				return (val - min) / (max - min);
			}
			
			v2f vert(appdata i)
			{
    
    
				v2f o;
				o.pos = UnityObjectToClipPos(i.pos);
				o.worldPos = mul(unity_ObjectToWorld,i.pos.xyz);
				o.uv = i.uv*_MainTex_ST.xy+ _MainTex_ST.zw;
				o.footstepUV = float2(RemapUV(_footstepRect.x, _footstepRect.z, o.worldPos.x), RemapUV(_footstepRect.y, _footstepRect.w, o.worldPos.z));
				return o;
			}
			
            fixed4 frag (v2f i) : SV_Target
            {
    
    
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv)*_Color;
				fixed4 footstepCol = tex2D(_footstepTex, i.footstepUV);
				fixed3 footstepRGB = _footstepColor.rgb;
				fixed3 finalRGB = col.rgb*(1 - footstepCol.a) + footstepRGB * footstepCol.a;
				fixed4 finalCol = fixed4(saturate(finalRGB), 1);
                return finalCol;
				return col;
            }
            ENDCG
        }
    }
}

  从代码可以看出footstepRect是一个很关键的东西,它告诉了Shader,需要绘制轨迹的范围在哪里。然后通过RemapUV方法,拿这个范围和当前的顶点世界坐标去计算出,当前的点该占整体UV的实际位置。
  这个footstepRect其实是C#动态算出来的,根据角色所在的坐标和半径,算出来一个范围。
C#的代码大概是这样:

Vector3 pos = role.transform.position;
mat.SetVector("_centerPos", pos);
mat.SetFloat("_maxVal", radius);
mat.SetVector("_footstepRect", new Vector4(pos.x - radius, pos.z - radius, pos.x + radius, pos.z + radius));

  其实就是中心点加减半径而已。
  这个做法的优点是,只需要局部绘制一张贴图,就能达到比较清晰的轨迹图形
  缺点是,只能在一定范围内显示,超出了footstepRect范围,轨迹就会消失了。

二、绘制轨迹的手段

  绘制轨迹,其实就是连贯的把某个笔刷的像素复制到一张图片上。这个应该不是很难理解的概念。
  上面的例子,球是一个笔刷,它移动的时候,它所在的位置会产生一个圆形的笔刷,通过连续每帧的覆盖,就形成了一个轨迹。
在这里插入图片描述

  如果绘制的间隔拉大一点,看到的情况大概是这样的。
  那么问题来了,球移动的时候,上面说到,相对于地表贴图的footstepRect,是会变化的,所以说,我们不能直接把球的笔刷印到之前的那张图去。
比如上一张图的位置是在这里
在这里插入图片描述

  下一张图的位置就变成了这里
在这里插入图片描述

  留意看左下角的球,它在世界中的位置是一直没有变化的,但在这个footstepRect的局部里面,它的相对位置是变化了的。
  下面来说一下具体的做法。

1、通过摄像机绘制RenderTexture

在这里插入图片描述

  这里为了渲染一张顶视图,我是打了一个摄像机在运动的球的上方,然后摄像机跟随这球移动。
  需要注意的是,摄像机一定要是正交的,然后通过控制orthographicSize参数,可以准确的绘制符合footstepRect的范围。最后,给这个摄像机的targetTexture赋予一张RenderTexture,作为输出。

2、通过偏移来叠加上一张图

  刚才那个RenderTexture是每帧都会渲染一次的。我们需要2张RenderTexture,一张是上一次留下的,一张是这一帧渲染出来的。
  接下来就是把两张RenderTexture,通过Graphics.Blit方法合并在一起。由于Graphics.Blit方法是可以传入一个材质球的,所以可以通过写一个Shader来混合2张贴图。具体的方式是,计算上一帧和当前帧角色所在位置的偏移,然后用偏移来控制上一帧的贴图的UV采样,再把两张贴图合并在一起就可以了。

3、合并的Shader

Shader "azhao/DrawFootstep"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_lastTex("lastTex",2D) = "black"{
    
    }
		_offset("offset",Vector) = (0,0,0,0)

    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
			sampler2D _lastTex;
			float2 _offset;


            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
    
    
                // sample the texture
				half4 col = saturate(tex2D(_MainTex, i.uv));
				half3 curRGB = col.rgb * 2 - 1;
				half4 lastCol = saturate(tex2D(_lastTex, i.uv - _offset));
				float lastAlpha = lastCol.a;

				half3 lastRGB = lastCol.rgb*2-1;

				half mr = lastRGB.r*lastAlpha;

				if (col.a >0)
				{
    
    
					if (curRGB.r > 0)
					{
    
    
						if (lastAlpha == 0)
						{
    
    
							mr = curRGB.r;
						}
					}
					else if (curRGB.r < 0)
					{
    
    
						mr = min(curRGB.r,mr);
					}
				}
				else
				{
    
    
					mr = lastRGB.r;
				}
				mr = (mr + 1) / 2;
				float alpha = max(col.a, lastAlpha);
				half3 mixRGB = half3(mr, mr, mr);
				half3 finalRGB = mixRGB;
                return half4(finalRGB, alpha);

            }
            ENDCG
        }
    }
}

三、细节问题

  第一步绘制轨迹通过局部UV坐标采样,和地表的贴图纹理混合。这里会存在一个问题。通过第二步绘制出来的轨迹贴图,是Clamp平铺方式的
在这里插入图片描述

  这意味着,超出了UV的0到1范围的坐标,会直接采样了0或者1的UV。具体的表现是这样的:
在这里插入图片描述

  这个黑线,其实就是到边缘了,所以超出的部分,都会是黑的
在这里插入图片描述

  为了解决这个问题,可以加一个渐变的遮罩叠加
在这里插入图片描述

  把UV接近0和1的地方都变成纯黑色,这样就不会出现Clamp平铺的问题,也可以让接近边缘的地方不会有一个很硬的消失,而是稍微柔软的过渡。
在这里插入图片描述
在这里插入图片描述

  所以用于绘制轨迹混合的shader会变成这样:

Shader "azhao/DrawFootstep"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_lastTex("lastTex",2D) = "black"{
    
    }
		_offset("offset",Vector) = (0,0,0,0)
		_maskTex("maskTex",2D) = "white"{
    
    }
		_reduceVal("reduceVal",float) = 0.001
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
			sampler2D _lastTex;
			float2 _offset;
			sampler2D _maskTex;
			float _reduceVal;

            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
    
    
                // sample the texture
				half4 col = saturate(tex2D(_MainTex, i.uv));
				half3 curRGB = col.rgb * 2 - 1;
				half4 lastCol = saturate(tex2D(_lastTex, i.uv - _offset));
				float lastAlpha = saturate(lastCol.a - _reduceVal);
				half4 maskCol = tex2D(_maskTex, i.uv);
				half3 lastRGB = lastCol.rgb*2-1;

				half mr = lastRGB.r*lastAlpha;

				if (col.a >0)
				{
    
    
					if (curRGB.r > 0)
					{
    
    
						if (lastAlpha == 0)
						{
    
    
							mr = curRGB.r;
						}
					}
					else if (curRGB.r < 0)
					{
    
    
						mr = min(curRGB.r,mr);
					}
				}
				else
				{
    
    
					mr = lastRGB.r;
				}
				mr = (mr + 1) / 2;
				float alpha = max(col.a, lastAlpha)*maskCol.r;
				half3 mixRGB = half3(mr, mr, mr);
				half3 finalRGB = mixRGB * maskCol.rgb;
                return half4(finalRGB, alpha);

            }
            ENDCG
        }
    }
}

Guess you like

Origin blog.csdn.net/qq_35995514/article/details/134581665