Unity Shader 学习笔记(18)纹理动画、顶点动画、广告牌技术
参考书籍:《Unity Shader 入门精要》
【《Real-Time Rendering 3rd》 提炼总结】(九) 第十章 · 游戏开发中基于图像的渲染技术总结
Unity Shader的内置变量
纹理动画
可用于代替粒子系统模拟动画效果。
帧序列动画
8x8帧序列纹理动画
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 ("Speed", Range(1, 100)) = 30 // 播放速度
}
因为序列帧图像通常是透明纹理,所以就按半透明的方式设置。
// 序列帧图像通常包含透明通道
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass {
Tags { "LightMode"="ForwardBase" }
// 关闭深度写入,开启混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
...
fixed4 frag (v2f i) : SV_Target {
float time = floor(_Time.y * _Speed); // floor()取整。CG的函数
float row = floor(time / _HorizontalAmount); // 行索引
float column = time % _HorizontalAmount; // 列索引
// 纹理坐标从上到下,所以是-row。下面两种计算方法
//half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount);
//uv.x += column / _HorizontalAmount;
//uv.y -= row / _VerticalAmount;
half2 uv = i.uv + half2(column, -row);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
fixed4 c = tex2D(_MainTex, uv);
c.rgb *= _Color;
return c;
}
}
滚动动画
可以模拟2D跑酷游戏背景的视察效果。
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 // 纹理亮度
}
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// TRANSFORM_TEX 得到初始纹理坐标,加上偏移坐标。frac为获取小数部分的值。
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 c = lerp(firstLayer, secondLayer, secondLayer.a);
c.rgb *= _Multiplier;
return c;
}
顶点动画
即修改顶点信息的动画。
一张长方形纹理,通过修改顶点信息,实现扭曲效果。
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_Magnitude ("Distortion Magnitude", Float) = 1 // 幅度
_Frequency ("Distortion Frequency", Float) = 1 // 频率
_InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 // 波长的倒数
_Speed ("Speed", Float) = 0.5
}
// DisableBatching:取消批处理。需要对模型空间下的顶点位置进行偏移,所以就不合并模型。
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off // 关闭剔除模式(正反面都显示)
...
v2f vert(a2v v) {
v2f o;
float4 offset;
offset.yzw = float3(0.0, 0.0, 0.0);
// 只改变X变量
offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
o.pos = UnityObjectToClipPos(v.vertex + offset);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv += float2(0.0, _Time.y * _Speed);
return o;
}
}
注意到前面使用内置阴影的Pass是没有处理顶点动画的,所以最后投影的阴影是错误的,需要自定义ShadowCasterPass,在这个Pass做相同顶点变换即可。
下面用到阴影的宏都在UnityCG.cginc中定义。
Pass {
Tags { "LightMode" = "ShadowCaster" }
...
#pragma multi_compile_shadowcaster
...
struct v2f {
// 内置宏,阴影投射需要的变量
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base 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;
v.vertex = v.vertex + offset;
// 计算阴影
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
fixed4 frag(v2f i) : SV_Target {
// 阴影投射
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
广告牌技术(Billboarding)
即让纹理始终面对着镜头。本质就是构建旋转矩阵,三个基向量:表面法线、向上的方向、向右的方向。还需要指定一个锚点,即旋转中心。
计算方法(如下图):
- 先计算向右方向 right = up × normal。起始up是竖直向上的。
- 叉乘即可得到向上方向 up’ = normal × right。
Vertical Restraints值为1,所有星星不管原来什么角度都会转向到镜头。
Vertical Restraints值为0,所有星星固定向上方向,并最大限度面向镜头。
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 // 垂直程度,0:固定向上,1:固定法线
}
// 需要模型空间下的位置来作为锚点计算,所以要关掉批处理。
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
...
v2f vert (a2v v) {
v2f o;
float3 center = float3(0, 0, 0); // 模型空间的原点作为广告牌锚点
float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)); // 获取模型空间下视角
float3 normalDir = viewer - center;
// _VerticalBillboarding 为1为法线固定(始终向上),为0为向上方向固定。
normalDir.y =normalDir.y * _VerticalBillboarding;
normalDir = normalize(normalDir);
// 通过normalDir.y 先判断法线和向上是否平行(叉积会错),来改变向上方向
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(float4(localPos, 1)); // 模型转裁剪空间
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
关于 DisableBatching
如果要进行一些顶点动画,就要关闭批处理。性能会下降,所以尽量避免使用模型空间下的绝对位置和方向进行计算。如广告拍技术中,可以用顶点颜色来存储顶点到锚点的距离。