Unity提供了一些内置变量用于做动画效果,比如_Time变量等,其中_Time.y代表从游戏开始到现在的正常时间。
对一张纹理进行偏移采样,首先计算出纹理动画是怎么分的,即这个时间段的对应行列,然后对原始的采样坐标先做一个比例缩放,然后加上偏移得到行列对应的采样坐标。
原版写法:
Shader "Custom/Test0"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_AnimationTex("动画纹理",2D)="white"{}
_AnimationSpeed("动画速度",float)=1.0
_HorAmount("水平数量",float)=8
_VerAmount("垂直数量",float)=8
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _AnimationTex;
float4 _AnimationTex_ST;
fixed _AnimationSpeed;
fixed _HorAmount;
fixed _VerAmount;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_AnimationTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
float time=_Time.y*_AnimationSpeed;
float row=(floor(time/_HorAmount)+1)%_HorAmount;
float col=floor(time%_HorAmount)+1;
//x方向偏移
i.uv.x/=_HorAmount;
i.uv.x+=(col-1)/_HorAmount;
//y方向偏移
i.uv.y/=_VerAmount;
i.uv.y+=(_VerAmount-row)/_VerAmount;
//偏移后的结果采样
fixed3 texResult=tex2D(_AnimationTex,i.uv);
return fixed4(texResult,1);
}
ENDCG
}
}
}
结果:动态效果就不展示了,和用animation一下一下k帧等的效果一样,只是这种更方便高效。
不足:如果在inspector界面调节纹理的offset值,会出现纹理闪烁的效果,参考书中的写法也没解决闪烁问题。
出现原因:当物体的采样坐标偏移时,在纹理本身行列不变情况下,缩放后再加上行列的偏移,会使此时的uv坐标中的u坐标会超出1,即采样到了图片外,但使用了repeat模式,会将大于一的数值强制截取小数,一截取,u坐标直接回到了这一行的第一个图片,找到原因后解决就很简单了,要做的就是大于1的时候回到最后一张图片,v坐标同理。
改良版:
Shader "Custom/Test0"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_AnimationTex("动画纹理",2D)="white"{}
_AnimationSpeed("动画速度",float)=1.0
_HorAmount("水平数量",float)=8
_VerAmount("垂直数量",float)=8
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _AnimationTex;
float4 _AnimationTex_ST;
fixed _AnimationSpeed;
fixed _HorAmount;
fixed _VerAmount;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_AnimationTex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
float time=_Time.y*_AnimationSpeed;
float row=(floor(time/_HorAmount)+1)%_HorAmount;
float col=floor(time%_HorAmount)+1;
//x方向偏移
i.uv.x=(i.uv.x%1)/_HorAmount;
i.uv.x+=(col-1)/_HorAmount;
//y方向偏移
i.uv.y=(i.uv.y%1)/_VerAmount;
i.uv.y+=(_VerAmount-row)/_VerAmount;
//偏移后的结果采样
fixed3 texResult=tex2D(_AnimationTex,i.uv);
return fixed4(texResult,1);
}
ENDCG
}
}
}
在计算采样时考虑偏移就行,对一取余。对于偏移正常处理即可。
纹理采样加上x轴的变化即可。
Shader "Custom/Test0"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_FarBackGround("较远背景纹理",2D)="white"{}
_NearBackGround("较近背景纹理",2D)="white"{}
_FarSpeed("较远背景纹理",float)=1.0
_NearSpeed("较近背景纹理",float)=1.0
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _FarBackGround;
sampler2D _NearBackGround;
float4 _FarBackGround_ST;
float4 _NearBackGround_ST;
fixed _FarSpeed;
fixed _NearSpeed;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv.xy=TRANSFORM_TEX(v.texcoord,_FarBackGround)+float2(_Time.y*_FarSpeed,0);
o.uv.zw=TRANSFORM_TEX(v.texcoord,_NearBackGround)+float2(_Time.y*_NearSpeed,0);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed4 FarResult=tex2D(_FarBackGround,i.uv.xy);
fixed4 NearResult=tex2D(_NearBackGround,i.uv.zw);
fixed3 color=lerp(FarResult,NearResult,NearResult.a);
return fixed4(color,1);
}
ENDCG
}
}
}
做顶点动画时一定要注意加上DisableBatching标签,防止优化时对模型进行批处理,批处理会丢失模型本身的顶点信息,当然我们也可以将顶点相对于原点的偏离存储在顶点颜色中,就不用担心批处理的问题。
原理也是很简单,用正弦函数模拟坐标偏移即可。
注意水流效果都由哪些偏移组成,首先是uv偏移,模拟水流移动,其次是顶点偏移,模拟水流起伏。
这里也有一个小坑,那就是不要用Unity自带的Quad!!!,我一开始得到的效果巨怪,还以为自己写错了,谁知道是因为Quad只有4个顶点,根本做不出来sin的效果,说多了都是泪,参考书的Github网址有专门的多顶点Quad。
Shader "Custom/Test0"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("水流贴图",2D)="white"{}
_HorizontalSpeed("水流速度",float)=1.0
_VerticalSpeed("起伏速度",float)=1.0
_Amount("起伏数量",float)=1.0
_Offset("起伏偏移",float)=1.0
//确保多个材质的初始的画面不同
_InitialTime("初始时间",float)=1.0
}
SubShader
{
Tags{"DisableBatching"="True"}
Pass
{
//开启透明度混合的原因是书中提供的素材图片周围有一圈白环
//而白环的透明度是0,开启透明度混合可以过滤透明度为0的地方
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _HorizontalSpeed;
fixed _VerticalSpeed;
fixed _Offset;
fixed _Amount;
fixed _InitialTime;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
float3 offset=float3(0,0,0);
//模型使用的方向是z轴滚动,所以用z轴充当sin函数的变量值
offset.x=sin((v.vertex.z+_VerticalSpeed*(_Time.y+_InitialTime))*_Amount)*_Offset/100;
o.pos=UnityObjectToClipPos(v.vertex+offset);
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv+=float2(0,_Time.y*_HorizontalSpeed);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 color=tex2D(_MainTex,i.uv.xy)*_Color;
return fixed4(color,0.5);
}
ENDCG
}
}
}
这种写法虽然实现了顶点偏移,但实际效果却很一般。
可以看到,上面的偏移和下面的偏移过于相似,导致实际效果看起来有些刻意。下面将展示效果更好的一种写法,有时候不得不佩服某些人的脑洞。
Shader "Custom/Test0"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("水流贴图",2D)="white"{}
_HorizontalSpeed("水流速度",float)=1.0
_VerticalSpeed("起伏速度",float)=1.0
_Amount("起伏数量",float)=1.0
_Offset("起伏偏移",float)=1.0
//确保多个材质的初始的画面不同
_InitialTime("初始时间",float)=1.0
}
SubShader
{
Tags{"DisableBatching"="True"}
Pass
{
//开启透明度混合的原因是书中提供的素材图片周围有一圈白环
//而白环的透明度是0,开启透明度混合可以过滤透明度为0的地方
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _HorizontalSpeed;
fixed _VerticalSpeed;
fixed _Offset;
fixed _Amount;
fixed _InitialTime;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
float3 offset=float3(0,0,0);
//模型使用的方向是z轴滚动,所以用z轴充当sin函数的变量值
offset.x=sin((v.vertex.z+v.vertex.x+_VerticalSpeed*(_Time.y+_InitialTime))*_Amount)*_Offset/100;
o.pos=UnityObjectToClipPos(v.vertex+offset);
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv+=float2(0,_Time.y*_HorizontalSpeed);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed4 color=tex2D(_MainTex,i.uv.xy)*_Color;
return fixed4(color);
}
ENDCG
}
}
}
sin变量的取值加上顶点的x值,实现某些地方看起来更大,某些地方看起来更窄。
这个效果就看起来自然了很多。
无论观察者处于什么位置观察任何图像,广告版都能提供一个朝向观众的面,这个面随着摄像机的改变而改变。
BillBoard技术是计算机图形学领域中进行快速绘制的一种方法。在类似游戏这种对实时性要求较高的场景下采取BillBoard技术可以大大加快绘制的速度从而提高画面的流畅性。把3D的物体用2D来表示,然后让该物体始终朝向镜头。比如场景中的一棵树,对于整个场景来说不是主要物体,因此无需花费大量的时间去计算树的每一部分的细节。通常的做法是首先准备好一张树的照片,然后镜头运动的时候使得树始终正对着镜头,我们看到的始终是树的正面。我们在渲染树木,灌木丛,云,烟等效果时都会用到。
广告牌技术的难点在于构建旋转矩阵,保证广告牌面的正方向永远面向玩家。
广告牌的写法有两种,一个是面法线永远指向视角方向,另一个是向上的方向永远是(0,1,0),而法线永远是尽力指向视角方向,我们在shader中用一个数值来控制法线指向视角的程度。
旋转矩阵的构建我们知道用坐标轴填充,同时基于模型空间的原点。当然,你可以不基于模型的原点,只需将旋转矩阵扩充到齐次坐标空间,最后一列填充偏移值。关于旋转矩阵的填充方式,这里有一个巨坑,我们的广告牌一般都是一张平面图,意味着有正面和反面,为了保证旋转后的面我们仍然可以看到,旋转矩阵内列向量的摆放至关重要。
如果你看过参考书,你会发现书上的写法只针对quad,而plane则会看不到任何结果,可能会有些像素点,大概率由于浮点数精度造成,plane看不到结果是因为,plane只有在y方向才是面的正方向,而quad是在z轴,书中这个旋转矩阵的构建,会使plane模型下的z轴朝向我们,在z轴的方向我们肯定看不到任何结果。
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("纹理贴图",2D)="white"{}
_Level("法线朝向控制",Range(0,1))=1
}
SubShader
{
Tags{"DisableBatching"="True"}
Pass
{
Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Level;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
//假设向上的方向永远是(0,1,0),先根据法线计算出另一个坐标轴
//先计算出模型空间下的视角方向,注意我们要的是面朝向我们,
float3 viewDir=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
float3 normalDir=viewDir;
normalDir.y=normalDir.y*_Level;
normalDir=normalize(normalDir);
float3 upDir=abs(normalDir.y)>0.999?float3(0,0,1):float3(0,1,0);
float3 rightDir=normalize(cross(upDir,normalDir));
upDir=normalize(cross(normalDir,rightDir));
//旋转矩阵构建时,把视角放最后一行,确保旋转后面的z轴朝向我们
float3 localPos=float3(0,0,0)+rightDir*v.vertex.x+upDir*v.vertex.y+normalDir*v.vertex.z;
o.pos=UnityObjectToClipPos(float4(localPos,1));
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 color=tex2D(_MainTex,i.uv.xy);
return fixed4(color,1);
}
ENDCG
}
}
}
Shader "Custom/Test0"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("纹理贴图",2D)="white"{}
_Level("法线朝向控制",Range(0,1))=1
}
SubShader
{
Tags{"DisableBatching"="True"}
Pass
{
Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Level;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
//假设向上的方向永远是(0,1,0),先根据法线计算出另一个坐标轴
//先计算出模型空间下的视角方向,注意我们要的是面朝向我们,
float3 normalDir=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
//通过控制y值,实现up方向的控制
normalDir.y=normalDir.y*_Level;
normalDir=normalize(normalDir);
//防止视角方向和向上方向重合从而得到错误错误结果
float3 upDir=abs(normalDir.y)>0.999?float3(0,0,1):float3(0,1,0);
float3 rightDir=normalize(cross(upDir,normalDir));
upDir=normalize(cross(normalDir,rightDir));
//顶点*旋转矩阵,这里拆开写了,把视角放中间一行,确保旋转后面的y轴朝向我们
float3 localPos=float3(0,0,0)+rightDir*v.vertex.x+normalDir*v.vertex.y+upDir*v.vertex.z;
//旋转只针对顶点,并不针对模型坐标空间,所以还要作矩阵变换
o.pos=UnityObjectToClipPos(float4(localPos,1));
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 color=tex2D(_MainTex,i.uv.xy);
return fixed4(color,1);
}
ENDCG
}
}
}
关于针对偏移的问题,只需要在开始normal计算上加上中心点偏移,同时,旋转矩阵的计算也加上偏移即可。
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("纹理贴图",2D)="white"{}
_Level("法线朝向控制",Range(0,1))=1
Center("中心点",Vector)=(0,0,0)
}
SubShader
{
Tags{"DisableBatching"="True"}
Pass
{
Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Level;
fixed3 Center;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
//假设向上的方向永远是(0,1,0),先根据法线计算出另一个坐标轴
//先计算出模型空间下的视角方向,注意我们要的是面朝向我们
//计算出在center下的视角方向
float3 viewDir_Old=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
float3 tempDir=float3(0,0,0)-Center;
float3 viewDir=tempDir+viewDir_Old;
float3 normalDir=viewDir;
normalDir.y=normalDir.y*_Level;
normalDir=normalize(normalDir);
float3 upDir=abs(normalDir.y)>0.999?float3(0,0,1):float3(0,1,0);
float3 rightDir=normalize(cross(upDir,normalDir));
upDir=normalize(cross(normalDir,rightDir));
//旋转矩阵构建时,把视角放最后一行,确保旋转后面的z轴朝向我们
float3 localPos=Center+rightDir*v.vertex.x+upDir*v.vertex.y+normalDir*v.vertex.z;
o.pos=UnityObjectToClipPos(float4(localPos,1));
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 color=tex2D(_MainTex,i.uv.xy);
return fixed4(color,1);
}
ENDCG
}
}
}
问题:上面三种写法无法屏蔽模型本身旋转的影响,这里的旋转只是对顶点的变换,对于整个模型空间(即决定模型空间三个坐标轴)并未有任何变化。同时,问题二如果视角方向和向上,向下的差距不大时,图片会有一个明显的翻转。
问题一的优化:说白了就是模型旋转导致顶点的旋转矩阵计算时,那个向上的向量也同步变换,我们在模型变换时,对向上的向量也做一次变换,使得向上的向量仍保持向上。
Shader "Custom/Test1"
{
Properties
{
//用于控制纹理的整体颜色表现
_Color("Color",Color)=(1,1,1,1)
_MainTex("纹理贴图",2D)="white"{}
_Level("法线朝向控制",Range(0,1))=1
Center("中心点",Vector)=(0,0,0)
}
SubShader
{
Tags{"DisableBatching"="True"}
Pass
{
Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Level;
fixed3 Center;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
//假设向上的方向永远是(0,1,0),先根据法线计算出另一个坐标轴
//先计算出模型空间下的视角方向,注意我们要的是面朝向我们
//计算出在center下的视角方向
float3 viewDir_Old=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
float3 tempDir=float3(0,0,0)-Center;
float3 viewDir=tempDir+viewDir_Old;
float3 normalDir=viewDir;
normalDir.y=normalDir.y*_Level;
normalDir=normalize(normalDir);
float3 upDir=abs(normalDir.y)>0.999?mul(unity_WorldToObject,float3(0,0,1)):mul(unity_WorldToObject,float3(0,1,0));
float3 rightDir=normalize(cross(upDir,normalDir));
upDir=normalize(cross(normalDir,rightDir));
//旋转矩阵构建时,把视角放最后一行,确保旋转后面的z轴朝向我们
float3 localPos=Center+rightDir*v.vertex.x+upDir*v.vertex.y+normalDir*v.vertex.z;
o.pos=UnityObjectToClipPos(float4(localPos,1));
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 color=tex2D(_MainTex,i.uv.xy);
return fixed4(color,1);
}
ENDCG
}
}
}
这样,无论模型本身怎么旋转都可以屏蔽掉这个影响。
关于问题二的优化:能力有限,暂时无法解决。
补充:你其实可以将我们上面提到的河流和广告牌结合起来,代码我就不展示了,可以看看自己对广告牌的理解是否透彻,毕竟顶点不仅要旋转,还要位移。
顶点动画的阴影效果,我们必须要在Pass中实现自定义的效果,而不是仅仅使用Unity内置的阴影Shader,大致就是做阴影是考虑顶点变换,而不是在顶点变换之前就生成阴影纹理。