• 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
            }
        }
    }
    
    • 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

      从代码可以看出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));
    
    • 1
    • 2
    • 3
    • 4

      其实就是中心点加减半径而已。
      这个做法的优点是,只需要局部绘制一张贴图,就能达到比较清晰的轨迹图形
      缺点是,只能在一定范围内显示,超出了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
            }
        }
    }
    
    • 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

    三、细节问题

      第一步绘制轨迹通过局部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
            }
        }
    }
    
    • 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
  • 相关阅读:
    数据结构【链表】
    李沐_动手学深度学习第5章卷积神经网络第二部分_笔记
    wordpress搭建自己的博客详细过程以及踩坑
    Flux、Mono、Reactor 实战(史上最全)
    SpringBoot多环境开发分组管理group
    Jnekins Active动态参数 集成Gitlab实践
    springboot启动中动态修改数据源配置
    国外投资哪些域名比较受欢迎?
    Python中except和except Exception的区别
    UE5的TimeLine的理解
  • 原文地址:https://blog.csdn.net/liweizhao/article/details/134046159