Chapter11让画面动起来
- 一、Unity Shader中的内置变量(时间篇)
- 二、纹理动画
- 1.序列帧动画
- 2.滚动背景
- 三、顶点动画
- 1.流动的河流
- 2.广告牌
- 3.注意事项
- ①批处理问题
- ②阴影投射问题
一、Unity Shader中的内置变量(时间篇)
Unity Shader 提供了一系列时间变量来允许我们方便地在Shader中访问运行时间
二、纹理动画
1.序列帧动画
- 原理:依次播放关键帧图像,形成连续动画
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)) = 3
}
- _MainTex 就是包含了所有关键帧图像的纹理
- _HorizontalAmount 和 _VerticalAmount 分别代表了该图像在水平方向和竖直方向包含的关键帧图像的个数
- _Speed 用于控制序列帧动画的播放速度
SubShader
{
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
Pass {
Tags {"LightMode" = "ForwardBase"}
Zwrite Off
Blend SrcAlpha OneMinusSrcAlpha
- 序列帧通常是透明纹理,可以被当成一个半透明对象
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
- 把顶点纹理坐标存储到了v2f中
fixed4 frag(v2f i):SV_Target
{
float time = floor(_Time.y * _Speed);
float row = floor(time / _HorizontalAmount);
float column = time - row * _HorizontalAmount;
// 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;
}
- 前三行计算了行列数
- _Time.y * _Speed 相乘得到模拟的时间,floor函数用来对结果取整
- time / _HorizontalAmount 商作为当前对应的行索引,余数是列索引
- 使用行列索引值来构建真正的采样坐标 (注释的)
- 我们可以首先把原纹理坐标 i.uv 按行数和列数进行等分, 得到每个子图像的纹理坐标范围
- 使用当前的行列数对上面的结果进行偏移,得到当前子图像的纹理坐标范围
- 竖直方向坐标偏移需要使用减法,因为Unity纹理坐标竖直方向顺序是从上到下,序列帧的图像是从下到上的,相反的
- 将原纹理坐标 i.uv 加上一个向量 (column, -row),然后分别除以水平方向和竖直方向关键帧数量,得到当前子图像的纹理坐标范围。这种方法更加通用,即使关键帧图像大小不同也可以使用。
2.滚动背景
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
}
- _MainTex 表示第一层(较远),_DetailTex 表示第二层(较近)
- _ScrollX 和 _Scroll2X 对应了各自的水平滚动速度
- _Multiplier 控制整体亮度
v2f vert (a2v 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;
}
- 最基本的顶点变换,把顶点变换到裁剪空间
- 首先利用 TRANSFORM_TEX() 得到初始纹理坐标,再使用内置的_Time.y 变量在水平方向上对纹理坐标进行偏移
- 把两张纹理存在一个uv中
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;
}
-
使用 i.uv.xy 和 i.uv.zw 对两张背景纹理进行采样
-
使用第二层纹理的透明通道来混合两张纹理
-
远处的背景应该移动的比近处的慢
三、顶点动画
1.流动的河流
- 原理就是使用正弦函数来模拟水流波动效果
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
}
- _MainTex 是河流纹理, _Color 控制整体颜色,_Magnitude 控制水波流动的幅度,_Frequency 控制波动频率,_InvWaveLength 控制波长的倒数,_Speed 控制速度
SubShader {
// Need to disable batching because of the vertex animation
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- “DisableBatching”=“True” 通过该标签来直接指明是否对该SubShader使用批处理(需要特殊处理的Shader就包含了模型空间的顶点动画),批处理会合并所有相关模型,而这些模型各自的模型空间就会丢失,在本例中,需要在物体模型空间下对顶点进行位置偏移,因此需要取消批处理操作
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 = UnityObjectToClipPos(v.vertex + offset);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv += float2(0.0, _Time.y * _Speed);
return o;
}
- 先计算顶点位移量,我们只希望顶点x方向进行位移,所以yzw都被设置为0
- 利用_Frequency 属性和内置的 _Time.y 来控制正弦函数的频率
- 为了让不同位置有不同的位移,我们对上述结果加上了模型空间下的位置分量,并×_InvWaveLength 来控制波长
- 最后×_Magnitude 控制波动幅度,得到最终位移
- 然后把位移量添加到顶点上,再进行顶点变换即可
fixed4 frag(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
c.rgb *= _Color.rgb;
return c;
}
- 直接对纹理进行采样再添加颜色即可
2.广告牌
- 广告牌技术:会根据视角方向来旋转一个被纹理着色的多边形,使得好像总是面对着相机
- 本质就是构建旋转矩阵
- 一个变换矩阵需要3个基向量,广告牌技术使用的通常是 表面法线、指向上的方向、指向右的方向,除此之外,还需要指定一个 锚点(在旋转过程中固定不变的,以此来确认多边形在空间中的位置)
- 计算基向量(相互正交的):
- 法线方向: 通常为视角方向或固定方向
- 指向上的方向: 通常为 (0, 1, 0) 或固定方向
- 指向右的方向: 通过法线方向和指向上的方向计算得到 r i g h t = u p × n o r m a l right = up\times normal right=up×normal 叉积
- 对
r
i
g
h
t
right
right 归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向
u
p
′
=
n
o
r
a
m
l
×
r
i
g
h
t
up' = noraml \times right
up′=noraml×right
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
}
- _MainTex 是广告牌的透明纹理,_Color 控制整体颜色,_VerticalBillboarding 调整是固定法线还是固定指向上的方向(0是向上,1是法线)
SubShader {
// Need to disable batching because of the vertex animation
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- 在广告牌技术中,我们需要使用物体模型空间下的位置来作为模电进行计算,所以要取消批处理
v2f vert (a2v v) {
v2f o;
// Suppose the center in object space is fixed
float3 center = float3(0, 0, 0);
float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));
float3 normalDir = viewer - center;
// If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir
// Which means the normal dir is fixed
// Or if _VerticalBillboarding equals 0, the y of normal is 0
// Which means the up dir is fixed
normalDir.y =normalDir.y * _VerticalBillboarding;
normalDir = normalize(normalDir);
// Get the approximate up dir
// If normal dir is already towards up, then the up dir is towards front
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));
// Use the three vectors to rotate the quad
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;
}
- 所有的计算都是在模型空间下计算的 (下面解释顶点着色器内容)
- 首先选择模型空间的原点作为广告牌的锚点,利用内置变量 mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)) 获取模型空间下的视角位置
- 然后开始计算三个正交基
float3 normalDir = viewer - center;
// If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir
// Which means the normal dir is fixed
// Or if _VerticalBillboarding equals 0, the y of normal is 0
// Which means the up dir is fixed
normalDir.y =normalDir.y * _VerticalBillboarding;
normalDir = normalize(normalDir);
- 根据观察位置和锚点计算目标法线方向,并使用_VerticalBillboarding 来控制垂直方向上的约束度
- 1时法线方向固定为视角方向,0时向上方向固定为(0,1,0)
float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
- 接着粗略地得到向上方向,为了放置法线方向和向上方向平行,对法线方向的y分量进行判断
float3 rightDir = normalize(cross(upDir, normalDir));
- 然后得到了向右方向,并进行归一化
upDir = normalize(cross(normalDir, rightDir));
- 在根据准确的法线方向和向右方向得到最后向上方向
// Use the three vectors to rotate the quad
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);
- 最后把模型空间的顶点位置变换到裁剪空间
- 片元着色器只需要对纹理进行采样,再与颜色相乘即可
fixed4 frag (v2f i) : SV_Target {
fixed4 c = tex2D (_MainTex, i.uv);
c.rgb *= _Color.rgb;
return c;
}
3.注意事项
①批处理问题
- 因为批处理会 合并模型,导致模型空间丢失,而顶点动画又需要依赖模型空间的位置进行计算
- 合并模型:将所有使用同一 Shader 的材质的模型合并成一个批次进行渲染,每个模型都被转换到了一个统一的坐标系中
- 解决方法
- 禁用批处理:通过在 SubShader 标签中 使用 “DisableBatching” 标签,可以强制禁用对该 Shader 的批处理,从而避免动画效果被破坏,但这样做会降低性能,因为会增加 Draw Call
- 使用相对位置和方向:尽量避免使用模型空间下的绝对位置和方向进行计算,例如,使用顶点颜色存储每个顶点到锚点的距离,而不是直接使用模型空间的中心作为锚点
- 为模型的每个顶点设置一个顶点颜色值,该颜色值表示该顶点到模型中心点的距离,以将颜色值的 R 分量设置为距离值,G、B 分量设置为 0
- 读取每个顶点的顶点颜色值,并从中提取出距离值
②阴影投射问题
- 内置的 ShadowCaster Pass 无法处理顶点动画: Unity 的内置阴影投射 Pass 没有进行顶点动画的处理,因此会导致阴影与物体的实际位置不匹配
- 解决方法:自定义ShadowCaster Pass (见书)