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

大家好,我是阿赵。

这期开始,打算介绍一下地面交互的一些做法。

比如:

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

或者:

Unity引擎制作雪地效果

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

大概需要实现的效果是这个视频的前半部分:

Unity曲面细分制作雪地效果

一、轨迹的绘制

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

这个效果可能很多朋友都会做,一般的做法是计算球的坐标相对于整个地面的位置,然后拾像素绘制在地面的遮罩贴图上面。

不过这种做法会有一个问题,假如地面很大的时候,通过一张和整个地面匹配UV的遮罩贴图来绘制轨迹,那么这张遮罩贴图的分辨率需要多大,才能显示足够的精度呢?比如一个4096米4096米的地面,就算我们用一张4096 4096的贴图做遮罩,那么每平方米的面积,才占一个像素,明显是绘制不出这么清晰的轨迹图形的。

其实我们没有必要去绘制整张贴图,只需要局部绘制就好了

绘制这一个小局部,然后通过局部UV采样的方式,把这个贴图叠加到大贴图上面去。

这时候,就需要给Shader传入一个范围,让Shader知道,这个局部UV,最终占整个地面UV的多少。

地面的Shader代码是这样的:

bash 复制代码
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#的代码大概是这样:

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

bash 复制代码
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会变成这样:

bash 复制代码
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
        }
    }
}
相关推荐
余生大大1 小时前
Dify依赖管理poetry切换为uv
uv
林枫依依5 小时前
Unity Webgl在编辑器中报错:Cannot connect to destination host
unity·编辑器·webgl
KhalilRuan5 小时前
Unity-Shader详解-其一
unity·游戏引擎
Clank的游戏栈10 小时前
Unity多线程渲染指令队列设计与集成技术详解
windows·unity·游戏引擎
Thomas_YXQ17 小时前
Unity3D IK解算器技术分析
开发语言·搜索引擎·unity·全文检索·unity3d·lucene
逢生博客21 小时前
使用 Python 项目管理工具 uv 快速创建 MCP 服务(Cherry Studio、Trae 添加 MCP 服务)
python·sqlite·uv·deepseek·trae·cherry studio·mcp服务
万山y1 天前
uv run 都做了什么?
uv
星火撩猿1 天前
常见游戏引擎介绍与对比
unity·ue5·游戏引擎·godot
sky_smile_Allen1 天前
[Unity]-[UI]-[Prefab] 关于Unity UGUI 的布局及组件讲解
ui·unity·游戏引擎
太妃糖耶1 天前
URP-利用矩阵在Shader中实现物体的平移和缩放
unity·矩阵