内置变量
纹理动画
序列帧动画
Shader "ZhangQr/Sequence"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_HorizontalAmount("HorizontalAmount",Range(1,10)) = 8
_VerticalAmount("VerticalAmount",Range(1,10)) = 8
_Speed("Speed",Range(0,100)) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent"}
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _HorizontalAmount;
float _VerticalAmount;
float _Speed;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float time = floor(_Time.y * _Speed);
float row = floor(time/_VerticalAmount); // 第多少行 ,row 应该属于 [0-正无穷],所以需要将 Wrap Mode 设置为 Repeat
float column = time - _VerticalAmount * row; // 第多少列 , column 应该属于 [0-_(VerticalAmount-1)] 循环
float2 uv = float2(i.uv.x / _VerticalAmount,i.uv.y / _HorizontalAmount); 将原来的 uv 压缩到左下角那一小块
uv.x += column / _VerticalAmount; // 再根据行列进行偏移
uv.y -= row / _HorizontalAmount;
fixed4 col = tex2D(_MainTex,uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
上面的代码的运行效果就是
在这个代码中,我们是不关心动画开始时间的,也就是说如果从 3 秒开始运行,那么起始帧可能是左下角,从 4 秒开始运行的话起始帧可能是右下角。对于爆炸之类的需要从左上角到右下角的执行顺序的就很不友好。而且即使是从 0 秒开始运行,也是从左下角开始循环。
Parallax
游戏里面经常会有前景和后景移动速度不一样产生景深效果,叫做 Parallax,可以用代码写,现在又学会了直接使用 Shader 来写,原理很简单,直接看代码就可以了。值得注意的就是使用前景的透明度来混合才能让前面的透明部分显示为后面的。
Shader "ZhangQr/Parallax"
{
Properties
{
_MainTex ("Base Layer (RGB)", 2D) = "white" {}
_DetailTex ("2nd Layer (RGB)", 2D) = "white" {}
_ScrollX ("Base layer Scroll Speed", Float) = 1.0
_Scroll2X ("2nd layer Scroll Speed", Float) = 1.0
_Multiplier ("Layer Multiplier", Float) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 pos : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DetailTex;
float4 _DetailTex_ST;
float _ScrollX;
float _Scroll2X;
float _Multiplier;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 firstLayer = tex2D(_MainTex,i.uv.xy);
fixed4 secondLayder = tex2D(_DetailTex,i.uv.zw);
fixed4 col = lerp(firstLayer,secondLayder,secondLayder.a);
col *= _Multiplier;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
如果是 2D 卷轴游戏,感觉随着人物的移动来调节速度比较好,比如人物不动的时候背景也应该静止。你也可以做多层 Parallax ,但不能在同一张图片上做前景。
顶点动画
sin 升 sin 落
其实重点都在顶点着色器,直接看式子的话会觉得有点突然,但如果抛弃书上的式子直接观察现象来自己写的话,反而更好理解一些。
Shader "Unlit/Water"
{
Properties
{
_MainColor("Color",Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_Amplitude("Amplitude",float) = 1
_Frequency("Frequency",float) = 1
_XSpeed("XSpeed",float) = 1
_Cross("Cross",float) = 1
_Speed("Speed",float) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "DisableBatching" = "True" }
Cull Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _MainColor;
float4 _MainTex_ST;
float _Amplitude;
float _Frequency;
float _XSpeed;
float _Cross;
float _Speed;
v2f vert (appdata v)
{
v2f o;
float3 offset = (0.0,0.0,0.0);
offset.x = sin(_Time.y * _XSpeed + v.vertex.z * _Frequency + v.vertex.x * _Cross) * _Amplitude;
o.vertex = UnityObjectToClipPos(v.vertex + offset);
o.uv = TRANSFORM_TEX(float2(v.uv.x , v.uv.y + _Time.y * _Speed), _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv) * _MainColor;
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
首先,这个模型不能直接使用 Plane,因为它的点数太少了,效果会非常僵硬
首先,我们要让模型上每个点的 local x 坐标的偏移变成一个 sin 曲线,这么写就好了,只受到 z 轴(local 的 z 应该是世界的 x)和时间的影响
offset.x = sin(_Time.y + v.vertex.z);
,效果如下:
只看最左边的某一个点,会发现他是在上下移动,准确来说是按照 sin(t) 的值做上下移动,但这个振幅太大了,所以再加一个
_Amplitude
参数把振幅降下来:offset.x = sin(_Time.y + v.vertex.z)* _Amplitude;
,效果如下:
现在振幅降下来了,屏幕中大概只有只有一个多一点点的 sin 周期,这个参数应该也可以调整,其实就是控制 v.vertex.z,所以再加一个参数 _Frequency
来控制频率,offset.x = sin(_Time.y + v.vertex.z * _Frequency)* _Amplitude;
如下所示:
然后从视觉效果来看,这个波浪移动的太慢了,从数学角度来说,就是还需要加一个控制时间快慢的参数,_XSpeed
,offset.x = sin(_Time.y * _XSpeed+ v.vertex.z * _Frequency)* _Amplitude;
效果如下:
现在还差一点,这个河流是同胖同瘦的,应该最好下面的 sin 曲线跟上面有一点交错的感觉,其实就是根据 local 的 x 再进行一点控制,offset.x = sin(_Time.y * _XSpeed + v.vertex.z * _Frequency + v.vertex.x * _Cross) * _Amplitude;
,效果如下:
现在所有参数都可控,可以调出一开始的效果了。
再回头看书里面的代码
v2f vert(a2v v) {
v2f o;
float4 offset;
offset.yzw = float3(0.0, 0.0, 0.0);
offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex + offset);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv += float2(0.0, _Time.y * _Speed);
return o;
}
会发现是差不多,因为他是把上面的 _Cross
和 _Frequency
用一个参数来控制了,并且还用这个控制了 local y,但因为这个是一个平面,所以 local y 是没有作用的。
广告牌
因为在 Scene 窗口,我们的观察方向就是摄像机的方向,所以直接在 Scene 窗口做测试是一样的,效果就是这个广告牌一直都是面向镜头的,可以看三个本地坐标的方向,始终没有变过。其实原理理解了之后也挺简单的,首先我们需要构建一个新的坐标系,一个轴向是观察点到模型中心点 x,为什么呢,因为我们需要模型面朝我们,那么肯定需要这一个轴向,那其实已经结束了,我们有无数种方法再构造出另外两个轴向,只要三个轴都互相垂直就可以了。下面是一个原始的图,用的是 Plane 直接把贴图拖上去。
可以看到我们是希望 Y (绿)轴成为上面提到的 x 轴,想象以下面的动图是另外两个轴的所有可能情况。
此时可以发现,其实还需要让箭头尽量看起来是朝上的,在观察一下就会发现,本地坐标的 Y 轴是不变的,而我们希望的就是箭头方向和 Y 轴方向尽量保持一个锐角的状态,此时无数种可能的另外两个轴被限制住了,现在 Y 轴和 x 轴不是垂直的,我们先用这个不垂直的平面计算出真正的 右轴
的位置
float4 localCameraPos = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1)); // 摄像机在模型坐标的位置
float3 center = (0,0,0);
float3 normal = normalize(localCameraPos - center);
float3 right = normalize(cross(float3(0,1,0),normal)); // 计算右方向轴
这里需要注意的是 float3(0,1,0) 是不变的,如果模型没有旋转的话,那么就是世界坐标中朝上的方向,现在 右轴
出来了,我们再根据 x 轴和 右轴
计算真正的 up 轴,后面就没什么好说的了,完整代码如下:
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "ZhangQr/Billborad"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "DisableBatching"="True" }
Cull Off
Blend SrcAlpha OneMinusSrcAlpha
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
float4 localCameraPos = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1)); // 摄像机在模型坐标的位置
float3 center = (0,0,0);
float3 normal = normalize(localCameraPos - center);
float3 right = normalize(normal.z>0.999f ? cross(float3(1,0,0),normal):cross(float3(0,1,0),normal)); // 计算右方向轴
float3 up = normalize(cross(normal,right));
float3 originDir = v.vertex - center; // 计算出这个点原来距离模型中心点的距离
v.vertex.xyz = center - originDir.z * up + center + originDir.y * normal + center - originDir.x * right;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
加了一个如果 normal 轴跟 Y 轴一样的话,箭头就朝着 X 轴方向,然后坐标转化的时候要注意,比如箭头方向其实是 -z 轴,然后 -z 轴你是希望让他变成新构造的三个轴中的 up轴,那么就应该写成 center - originDir.z * up
。
最后说一下,批处理,书上说的是在应用顶点动画的 Shader 中如果不关闭批处理的话,会丢失模型的本地坐标之类的,虽然没懂什么意思,但可以不关批处理多复制一个 Plane 试试,就会发现失效了,单独一个的时候没啥问题。