求知若饥,虚心若愚
UnityShader中的内置变量(时间篇)
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。
名称 |
类型 |
描述 |
_Time |
float4 |
t是自该场景加载开始所经过的时间,4个分量的值分别是(t/20,t,2t,3t)。 |
_SinTime |
float4 |
t是时间的正弦值,4个分量的值分别是(t/8,t/4,t/2,t)。 |
_CosTime |
float4 |
t是时间的余弦值,4个分量的值分别是(t/8,t/4,t/2,t)。 |
unity_DeltaTime |
float4 |
dt是时间增量,4个分量的值分别是(dt,1/dt,smoothDt,1/smoothDt)。 |
纹理动画
在游戏中应用广泛。尤其是在资源局限的移动平台上,会使用纹理动画代替复杂的粒子系统模拟动画效果。
序列帧动画
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
| Shader "Unity Shaders Learn/Chapter11/ImageSequenceAnimation" { Properties { _Color ("Color Tint", Color) = (1,1,1,1) _MainTex ("Image Sequence", 2D) = "white" {} _HorizontalAmount ("Horizontal Amount", Float) = 4 _VerticalAmount ("Vertical Amount", Float) = 4 _Speed ("Animation Speed", Range(1, 100)) = 30 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { Tags {"LightMode" = "ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float _HorizontalAmount; float _VerticalAmount; float _Speed;
struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; }
fixed4 frag(v2f i) : SV_Target { float time = floor(_Time.y * _Speed); float row = floor(time / _HorizontalAmount); float col = time - row * _HorizontalAmount;
// 先分成小块,再计算偏移多少个小块 // half2 uv = float2(i.uv.x / _HorizontalAmount, i.uv.y / _VerticalAmount); // uv.x += col/_HorizontalAmount; // uv.y -= row/_VerticalAmount;
half2 uv = i.uv + half2(col, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount;
fixed4 color = tex2D(_MainTex, uv); color.rgb *= _Color; return color; } ENDCG } } FallBack "Transparent/VertexLit" }
|
_Time.y得到场景加载后经过的时间,乘以_Speed来得到模拟的时间。取整后来计算当前纹理的行列,以得到正确的采样坐标。
滚动的背景
很多2D游戏使用了不断滚动的背景来模拟角色在场景中的穿梭,用多个层(layers)来模拟视差效果。
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
| Shader "Unity Shaders Learn/Chapter11/ScrollingBackground" { 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 { Pass { Tags {"LightMode"="ForwardBase"} CGPROGRAM
#include "UnityCG.cginc" #pragma vertex vert; #pragma fragment frag;
sampler2D _MainTex; float4 _MainTex_ST; sampler2D _DetailTex; float4 _DetailTex_ST; float _ScrollX; float _Scroll2X; float _Multiplier;
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); // 这里是不断给uv加偏移,frac是取小数点部分(x - floor(x)), 线性变化 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 secondLayer = tex2D(_DetailTex, i.uv.zw);
fixed4 color = lerp(firstLayer, secondLayer, secondLayer.a); color *= _Multiplier; return color; } ENDCG } } FallBack "VertexLit" }
|
在顶点着色器中,使用一个float4记录两张贴图的纹理坐标,用_Time.y在水平方向上进行偏移,达到滚动的效果。在片元着色器中,分别采样,用近层的透明通道值通过lerp来混合两张纹理。
顶点动画
常使用顶点动画来模拟飘动的旗帜、湍流的小溪等。
流动的河流
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
| Shader "Unity Shaders Learn/Chapter11/Water" { Properties { _Color ("Color Tint", Color) = (1,1,1,1) _MainTex ("Main Tex", 2D) = "white" {} _Magnitude ("Distortion Magnitude", Float) = 1 // 水流波动的幅度 _Frequency ("Distortion Frequency", Float) = 1 // 水流波动的频率 _InWaveLength ("Distortion Inverse Wave Length", Float) = 10 // 波长的倒数 _Speed ("Speed", Float) = 0.5 // 控制uv采样的速度 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True" //不使用批处理,因为要做顶点动画 }
Pass { Tags {"LightMode"="ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off CGPROGRAM #pragma vertex vert; #pragma fragment frag;
#include "UnityCG.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float _Magnitude; float _Frequency; float _InWaveLength; float _Speed;
struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
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 * _InWaveLength + v.vertex.y * _InWaveLength + v.vertex.z * _InWaveLength) * _Magnitude; o.pos = UnityObjectToClipPos(v.vertex + offset);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.uv += float2(0.0, _Time.y * _Speed); return o; }
fixed4 frag(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
|
在SubShader的Tags中,将DisableBatching设置为True来禁用批处理,因为批处理合并了所有相关的模型,各自的模型空间就丢失了,影响我们在物体的模型空间下对顶点位置进行偏移。之后在顶点着色器中,对顶点的X坐标进行位移。
广告牌
广告牌技术(Billboarding),会根据视角方向旋转一个被纹理着色的多边形,使多边形看起来好像总是面对摄像机。应用例如渲染烟雾、云朵、闪光效果等。
本质是构建旋转矩阵。使用的基向量通常是表面法线(normal) 、指向上的方向(up) 、指向右的方向(right) 以及锚点(anchor location) 。
如何构建3个相互正交的基向量:首先会通过初始计算得到目标的表面法线和指向上的方向,两者往往是不垂直的,但两者其中之一是固定的。通过这可以计算出指向右的方向:。例如模拟例子效果时,法线总是指向视角方向, 计算出right后,在叉积计算出修改后向上的方向:。
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 93
| Shader "Unity Shaders Learn/Chapter11/Billboard" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True" //不使用批处理,因为要做顶点动画 } Pass { Tags {"LightMode"="ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off CGPROGRAM #pragma vertex vert; #pragma fragment frag;
#include "UnityCG.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _VerticalBillboarding;
struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
v2f vert(a2v v) { v2f o; float3 center = float3(0, 0, 0); //选定广告牌的锚点 float3 viewer = mul(unity_WorldToObject, fixed4(_WorldSpaceCameraPos, 1)); //获取摄像机模型空间位置
float3 normalDir = viewer - center; //假设法线方向朝向摄像机 //_VerticalBillboarding为0时说明,朝上的方向是固定的(所以这个时候法线的y分量肯定为0,不然怎么和向上垂直) normalDir.y *= _VerticalBillboarding; //_VerticalBillboarding不为0则对法线无影响,做了归一化 normalDir = normalize(normalDir);
// 保证不与法线平行,同时x分量为0时右方向计算是正确的 // 如果法线已经向上,那么向上方向应该朝前 // 除非法线向上,否则先将向上方向近似设置为0,1,0 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)); //更新正确的上方向
// 将坐标变换到新的正交基坐标系 float3 centerOffs = v.vertex.xyz - center; float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
o.pos = UnityObjectToClipPos(localPos);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; }
fixed4 frag(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
|
广告牌效果。
左图显示了摄像机和5个广告牌之间的位置关系,摄像机是从斜上方向下观察它们的。
中间的图显示了当Vertical Restraints属性为1,即固定法线方向为观察视角时所得到的效果,可以看出,所有的广告牌都完全面朝摄像机。
右图显示了当Vertical Restraints属性为0,即固定指向上的方向为(0, 1, 0)时所得到的效果,可以看出,广告牌虽然最大限度地面朝摄像机,但其指向上的方向并未发生改变
注意事项
- 取消批处理会导致一定的性能下降,增加了DrawCall。要尽量避免显式使用模型空间下的一些绝对位置和方向进行运算,在广告牌这个例子中可以利用顶点颜色来存储每个顶点到锚点的距离。
- 顶点动画的物体阴影投射是不带动画的(使用Fallback为Diffuse等),除非重新定义ShadowCasterPass,或者干脆用Transparent/VertexLit不要阴影。
以下对原有河流动画新增ShadowCasterPass
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
| //V2F_SHADOW_CASTER,TRANSFER_SHADOW_CASTER_NORMALOFFSET,SHADOW_CASTER_FRAGMENT三剑客 Pass { Tags {"LightMode"="ShadowCaster"} CGPROGRAM #pragma vertex vert; #pragma fragment frag; #pragma multi_compile_shadowcaster #include "UnityCG.cginc" float _Magnitude; float _Frequency; float _InWaveLength; float _Speed; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { V2F_SHADOW_CASTER; }; 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 * _InWaveLength + v.vertex.y *_InWaveLength + v.vertex.z * _InWaveLength) * _Magnitude; v.vertex = v.vertex + offset; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } fixed4 frag(v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG }
|