大家好,我是阿赵。
这期开始,打算介绍一下地面交互的一些做法。
比如:
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的局部里面,它的相对位置是变化了的。
下面来说一下具体的做法。
这里为了渲染一张顶视图,我是打了一个摄像机在运动的球的上方,然后摄像机跟随这球移动。
需要注意的是,摄像机一定要是正交的,然后通过控制orthographicSize参数,可以准确的绘制符合footstepRect的范围。最后,给这个摄像机的targetTexture赋予一张RenderTexture,作为输出。
刚才那个RenderTexture是每帧都会渲染一次的。我们需要2张RenderTexture,一张是上一次留下的,一张是这一帧渲染出来的。
接下来就是把两张RenderTexture,通过Graphics.Blit方法合并在一起。由于Graphics.Blit方法是可以传入一个材质球的,所以可以通过写一个Shader来混合2张贴图。具体的方式是,计算上一帧和当前帧角色所在位置的偏移,然后用偏移来控制上一帧的贴图的UV采样,再把两张贴图合并在一起就可以了。
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
}
}
}