Unity引擎动态法线贴图制作球滚动轨迹
大家好,我是阿赵。
之前说了一个使用局部UV采样来实现轨迹的方法。这一篇在之前的基础上,使用法线贴图进行凹凸轨迹的绘制。
一、实现的目标
先来回顾一下,上一篇最终我们已经绘制了一个轨迹的贴图
可以思考一下,假如现在我绘制的不是黑白的遮罩,而是一张法线贴图,会怎样呢?比如这样:
如果是这样,剩下的问题就非常简单了,使用局部的UV采样,然后正常的通过法线贴图读取法线方向,最后通过光照模型来表现出法线的凹凸感。
二、动态法线贴图的绘制和融合
说起来好像很简单,但绘制灰度图简单,绘制法线贴图应该怎样做呢?
在球的位置上,实际上我是用了这么一张法线贴图的,还是用顶部摄像机拍摄需要局部采样UV的范围。
在球没有移动的时候,顶部摄像机拍摄到的应该是这样一个情况:
除了球以外,这一片平整的纯色,是算出来的。
这里涉及到一些法线贴图的计算方式。
需要知道的是,法线贴图是一张RGB颜色的贴图,所以它的每一个通道的取值范围是0到1,但法线方向的取值范围是-1到1的,所以如果得到一张法线贴图,需要转换成法线方向时,需要对它乘以2再减1。然后想将一个法线方向转成RGB值,方法是把法线方向乘以0.5再加0.5。
所以当一个正常朝向上方的法线方向是(0,0,1),它转换成RGB值之后,就是(0.5,0.5,1),也就是上图看到占了大部分面积的颜色了。
由于我使用的是打一个正交摄像机在头顶的做法来渲染球所在的区域,这个摄像机通过layer过滤只看得到球所在的法线面片,所以除了球以外,其他区域应该是黑色的:
想要把黑色的部分都填充成(0,0,1)法线方向的颜色,有2种方法:
1、把摄像机的颜色设置成这种颜色
2、在混合的shader里面判断,传入的图片是黑色的部分就填充成这张颜色。
我这里是简单的使用第一种方式,修改摄像机的颜色。
用上一篇说的方法,在移动的过程中,通过求出上一帧球的位置和当前球的位置作为偏移值,然后传入通过Graphics.Blit融合两帧的画面的材质球里面,就可以对两张法线贴图进行混合了。
接下来需要混合两帧之间产生的法线贴图。
混合法线的方法非常多,我常用的消耗比较低的法线混合方式有这么几种:
假设n1和n2是需要混合的2个法线方向,n3是混合后的结果
1、线性混合
公式很简单:
float3 n3 = normalize(n1+n2);
这种方式混合法线,优点是实现简单,而且同一个法线贴图不停的叠加,效果都不会发生变化。缺点也很明显,会减淡本身法线贴图的特征,如果每帧都不停的和(0,0,1)混合,最终所有法线的特征都会被抹平了。
2、偏导混合
公式是:
float3 n3 = normalize( float3(n1.xy/n1.z+n2.xy/n2.z,1));
从效果上看,法线的特征保留会比线性混合要好很多。
如果把公式改一下,
float2 n0 = lerp(n1.xy/n1.z+n2.xy/n2.z,_blendVal);
float3 n3 = normalize( float3(n0,1));
还可以做出混合插值的效果。
3、Whiteout混合
和偏导的结果比较类似
公式:
n3 = normalize( float3( n1.xy+n2.xy,n1.z*n2.z));
三、绘制频率问题和解决
虽然偏导和Whiteout的效果会比线性的好,但如果物体停留在同一个位置,每帧都不停的叠加同一个法线贴图,偏导和Whiteout都会对法线效果越来越加深,到最后就会出现和原来贴图比较偏差的效果,比如刚才那个球的法线如果每一帧不停的用Whiteout算法去叠加,最后就会变成这样
为了解决这个问题,实际上是需要控制绘制的次数。基本思想很简单,就是物体移动的时候,并且移动的距离大于一定长度时,才会去绘制。
这样,就出现了多种解决的方案:
1、还是用顶部摄像机,但只有主角移动的时候,才会调用Graphics.Blit方法去混合。这个方案的问题在于,如果只判断主角移动,那么产生这个法线贴图的主体也只能是主角,方便的其他角色是不能产生法线混合效果的。
2、不用顶部摄像机,而是每个可以产生轨迹的物体身上都挂上脚本,当物体移动的时候,主动的把自己的法线贴图通过Graphics.Blit混合到局部UV的法线贴图里面。这样做相当于每个角色有一个笔刷。不过这样做的问题是,需要逐个角色分别传入混合,还要计算每个角色相对于主角位置的偏移。如果场景里面的角色很多的情况下,这样做性能可能不是特别好。
3、还是用顶部摄像机,每个角色身上有一张法线贴图,只有当角色开始移动时,这张法线贴图才会变成激活可显示状态。那么,我们还是用顶部摄像机来渲染一张RenderTexture就够了,而这张RenderTexture看到的,只会是在绘制局部UV的范围内的正在移动的物体的法线贴图。
4、不管法线叠加变形的问题。实际上如果不是对混合后的法线要求特别精确,比如角色踩出的脚印要清晰到连鞋底都看得清,而只是需要一个大致的范围的话,我觉得直接叠加也无所谓,毕竟法线的大致方向是对的。
四、源码:
1、法线混合的Shader代码
和上一篇差不多的,只是修改一下片段着色器就行
half4 frag(v2f i) : SV_Target
{
//当前帧传入的法线笔刷
half4 col = tex2D(_MainTex, i.uv);
half3 curNormal = col.rgb * 2 - 1;
//上一帧绘制的法线贴图
half4 lastCol = tex2D(_lastTex, i.uv - _offset);
half3 lastNormal = lastCol.rgb * 2 - 1;
//默认的法线方向
float3 defaultNormal = float3(0,0,1);
//用于保持边缘是默认法线方向的遮罩
half4 maskCol = tex2D(_maskTex, i.uv);
//使用whiteout法线混合
float3 finalNormal = normalize(half3(curNormal.xy + lastNormal.xy, curNormal.z*lastNormal.z));
//计算边缘遮罩
finalNormal = normalize(finalNormal * maskCol.r + _defaultDir.xyz*(1 - maskCol.r));
//转RGB颜色
finalNormal = finalNormal * 0.5 + 0.5;
return half4(finalNormal, 1);
}
2、地面上的局部UV采样包含法线贴图的Shader
Shader "azhao/GroundFootStepNormalTex"
{
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)
_NormalTex("Normal Tex", 2D) = "black"{}
_normalScale("normalScale", Range(-5 , 5)) = 0
_normalFootStepScale("normalFootStepScale", Range(-5 , 5)) = 0
_specColor("SpecColor",Color) = (1,1,1,1)
_shininess("shininess", Range(1 , 100)) = 1
_specIntensity("specIntensity",Range(0,1)) = 1
_ambientIntensity("ambientIntensity",Range(0,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;
sampler2D _NormalTex;
float4 _NormalTex_ST;
float _normalScale;
float _normalFootStepScale;
float4 _specColor;
float _shininess;
float _specIntensity;
float _ambientIntensity;
struct appdata
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 normal:NORMAL;
float3 tangent:TANGENT;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float2 uv : TEXCOORD1;
float2 footstepUV : TEXCOORD2;
float3 worldNormal : TEXCOORD3;
float3 worldTangent :TEXCOORD4;
float3 worldBitangent : TEXCOORD5;
};
half3 UnpackScaleNormal(half4 packednormal, half bumpScale)
{
half3 normal;
//由于法线贴图代表的颜色是0到1,而法线向量的范围是-1到1
//所以通过*2-1,把色值范围转换到-1到1
normal = packednormal * 2 - 1;
//对法线进行缩放
normal.xy *= bumpScale;
//向量标准化
normal = normalize(normal);
return normal;
}
//获取HalfLambert漫反射值
float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
NDotL = NDotL * 0.5 + 0.5;
return NDotL;
}
//获取BlinnPhong高光
float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal)
{
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
float specDir = max(dot(normalize(worldNormal), halfDir), 0);
float specVal = pow(specDir, _shininess);
return specVal;
}
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));
o.worldNormal = UnityObjectToWorldNormal(i.normal);
o.worldTangent = UnityObjectToWorldDir(i.tangent);
o.worldBitangent = cross(o.worldNormal, o.worldTangent);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//采样漫反射贴图的颜色
half4 col = tex2D(_MainTex, i.uv*_MainTex_ST.xy + _MainTex_ST.zw);
//计算法线贴图的UV
half2 normalUV = i.uv * _NormalTex_ST.xy + _NormalTex_ST.zw;
//采样法线贴图的颜色
half4 normalCol = tex2D(_NormalTex, normalUV);
fixed4 footstepCol = tex2D(_footstepTex, i.footstepUV);
fixed3 footstepRGB = UnpackScaleNormal(footstepCol, _normalFootStepScale).rgb;
half3 normalVal = UnpackScaleNormal(normalCol, _normalScale).rgb;
//normalVal = footstepRGB;
normalVal = normalize(normalVal + footstepRGB);
//构建TBN矩阵
float3 tanToWorld0 = float3(i.worldTangent.x, i.worldBitangent.x, i.worldNormal.x);
float3 tanToWorld1 = float3(i.worldTangent.y, i.worldBitangent.y, i.worldNormal.y);
float3 tanToWorld2 = float3(i.worldTangent.z, i.worldBitangent.z, i.worldNormal.z);
//通过切线空间的法线方向和TBN矩阵,得出法线贴图代表的物体世界空间的法线方向
float3 worldNormal = float3(dot(tanToWorld0, normalVal), dot(tanToWorld1, normalVal), dot(tanToWorld2, normalVal));
//用法线贴图的世界空间法线,算漫反射
half diffuseVal = GetHalfLambertDiffuse(i.worldPos, worldNormal);
//diffuseVal = clamp(diffuseVal, 0.5, 1);
//用法线贴图的世界空间法线,算高光角度
half3 specCol = _specColor * GetBlinnPhongSpec(i.worldPos, worldNormal)*_specIntensity;
//最终颜色 = 环境色+漫反射颜色+高光颜色
half3 finalCol = UNITY_LIGHTMODEL_AMBIENT * _ambientIntensity + saturate(col.rgb*diffuseVal) + specCol;
return half4(finalCol, 1);
}
ENDCG
}
}
}