
“Flipbook UV Animation”(序列帧UV动画)的核心作用是将一张包含动画序列的静态纹理图集,通过随时间动态修改UV坐标,转换成动态的、逐帧播放的动画效果。它就像是电子版的“翻书动画”,快速切换书页(纹理中的格子)来形成连续的视觉动作。
简单来说,它的工作原理可以分为以下几个关键步骤:
🎬 核心原理
-
划分网格:它把一张包含了连续动作的纹理,在逻辑上按指定的行数和列数分割成一个个大小相等的格子(即动画的“帧”)。
-
计算帧索引:在着色器中,它会利用Unity提供的时间变量(如
_Time.y)和一个速度参数,计算出一个随时间增长的“当前播放到第几帧”的索引值。 -
动态偏移UV:根据计算出的帧索引,它动态地计算出该帧在整张纹理中的具体位置(即偏移量),并对原始的UV坐标进行修改,让纹理采样指针精确地对准当前需要显示的那一帧。
✨ 主要应用场景
这项技术在游戏开发中非常实用,主要用于以下几个方面:
-
粒子特效:这是最常见的应用之一。通过序列帧动画,可以用一张纹理创造出爆炸、火焰、烟雾、魔法光效等复杂且富有表现力的粒子效果,既节省了性能,又保证了视觉效果。
-
2D 精灵动画:在早期的2D游戏中,角色的跑、跳、攻击等动作,都是通过快速切换序列帧来实现的。即使现在,它也是制作2D动画的一种高效方式。
-
复杂材质效果:在一些高级效果中,比如制作动态的水面涟漪,开发者会对多张法线贴图(用于表现表面凹凸的纹理)进行序列帧动画,从而模拟出复杂且不重复的水波扩散效果。
1. 核心数学原理:从时间到 UV 偏移
Flipbook UV Animation 的本质是将一维的时间变量映射到二维的格子索引,再映射到 UV 偏移量。这个过程可以分为三个步骤:
1.1 计算当前帧索引
假设我们有一个包含 _Rows 行、_Columns 列的序列帧图集,总帧数 _TotalFrames = _Rows * _Columns。我们需要根据游戏时间 _Time.y 和播放速度 _Speed 计算出当前应该显示第几帧。
-
帧索引(线性增长):
text
float frameIndex = floor(_Time.y * _Speed);
这里
floor取整,保证帧索引是整数,从 0 开始逐步增加。 -
循环播放:
通常我们希望动画循环播放,所以需要对总帧数取模:text
float frameIndex = floor(_Time.y * _Speed) % _TotalFrames;
-
单次播放(不循环):
如果需要动画只播放一次,最后停留在最后一帧,可以增加一个clamp:text
float rawIndex = floor(_Time.y * _Speed); float frameIndex = min(rawIndex, _TotalFrames - 1);
1.2 将线性帧索引转换为二维行列索引
假设我们的图集是逐行排列的:第一行从左到右是第 0,1,2... 帧,第二行是紧接着的下一行。那么:
text
float row = floor(frameIndex / _Columns); // 行号,从 0 开始 float col = frameIndex - row * _Columns; // 列号,也可以用 frameIndex % _Columns
1.3 将行列索引转换为 UV 偏移量
原始 UV 坐标 (u, v) 范围是 [0,1],覆盖整张图集。为了只采样其中一个格子,我们需要:
-
缩放 UV:将 UV 范围从 [0,1] 缩小到
[0, 1/_Columns]在 U 方向,[0, 1/_Rows]在 V 方向。text
half2 scaledUV = half2(i.uv.x / _Columns, i.uv.y / _Rows);
-
平移 UV:将缩放后的 UV 移动到目标格子所在的区域。平移量取决于当前的行列。
-
U 方向平移量 =
col / _Columns -
V 方向平移量 =
row / _Rows
但需要注意:纹理坐标的原点通常在左下角(OpenGL 风格,Unity 默认如此),而序列帧的播放顺序通常是从左上角开始(第一行是动画的第一帧)。因此,V 方向的平移方向是向下,即需要减去
row / _Rows。text
half2 finalUV = scaledUV + half2(col / _Columns, -row / _Rows);
如果图集的排列是从左下角开始的(即第一行在底部),则 V 方向应该是加法,但这种情况较少见。你可以通过材质参数控制偏移方向。
-
2. 在 Shader 中的实现方式
2.1 在片元着色器中计算(逐像素)
最常见的方法是在片元着色器中计算 UV,因为每一帧的索引只需要计算一次,而 UV 偏移对每个像素都一样。代码示例如下:
// 材质属性
float _Speed;
float _Rows;
float _Columns;
// 输入结构体
struct v2f {
float2 uv : TEXCOORD0;
// ... 其他
};
fixed4 frag (v2f i) : SV_Target {
// 1. 计算当前帧索引(循环)
float totalFrames = _Rows * _Columns;
float frameIndex = floor(_Time.y * _Speed) % totalFrames;
// 2. 计算行和列
float row = floor(frameIndex / _Columns);
float col = frameIndex - row * _Columns; // 或 frameIndex % _Columns
// 3. 构建新的 UV
float2 uv = i.uv;
uv.x = (uv.x + col) / _Columns;
uv.y = (uv.y + (totalFrames/_Columns - 1 - row)) / _Rows; // 兼容从上到下
// 4. 采样纹理
fixed4 col = tex2D(_MainTex, uv);
return col;
}
2.2 在顶点着色器中计算(逐顶点)
如果希望节省片元着色器的计算量,可以将 UV 偏移计算提前到顶点着色器。但需要注意:UV 偏移是基于时间的,而顶点着色器是逐顶点执行的,如果网格顶点数不多,这样做是有效的。但 UV 偏移是全局统一的,所以可以计算一次然后传递给片元。
一种方法是在顶点着色器中计算偏移量,然后传递到片元。但由于顶点着色器无法直接访问片元着色器需要的每个像素的缩放 UV(除非将缩放后的 UV 插值),我们仍然需要在片元中做缩放,但可以将平移量预先算好:
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float2 offset : TEXCOORD1; // 传递偏移量
};
v2f vert (appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy;
// 计算帧索引(全局统一,但在顶点中计算会每个顶点都算一次,可以优化为使用静态变量或 uniform)
float totalFrames = _Rows * _Columns;
float frameIndex = floor(_Time.y * _Speed) % totalFrames;
float row = floor(frameIndex / _Columns);
float col = frameIndex - row * _Columns;
o.offset = float2(col / _Columns, -row / _Rows);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// 应用缩放和偏移
float2 uv = i.uv / float2(_Columns, _Rows) + i.offset;
return tex2D(_MainTex, uv);
}
但注意,_Time.y 在顶点着色器中也是每帧变化的,所以帧索引在每个顶点计算时值是相同的,不会有问题。但每个顶点都重复计算一次,对于低面数模型是可以接受的,对于高面数模型可能不如在片元中统一计算。
2.3 使用 Unity 内置宏
Unity 提供了 TRANSFORM_TEX 宏来简化 UV 变换,但它不支持动态偏移量计算。因此我们通常需要手动实现。
3. 高级技巧与常见问题
3.1 帧率平滑
当 _Speed 很大时,帧索引可能会跳帧,导致动画不连续。如果你希望动画平滑过渡,可以使用线性插值在两个相邻帧之间混合,实现“运动模糊”或“平滑帧”效果。这需要采样两帧并混合:
float f = _Time.y * _Speed;
float frameIndex1 = floor(f) % totalFrames;
float frameIndex2 = floor(f + 1) % totalFrames;
float blend = frac(f); // 小数部分
// 分别计算两个 UV
float2 uv1 = ...; // 使用 frameIndex1
float2 uv2 = ...; // 使用 frameIndex2
fixed4 col1 = tex2D(_MainTex, uv1);
fixed4 col2 = tex2D(_MainTex, uv2);
fixed4 finalCol = lerp(col1, col2, blend);
3.2 处理纹理边缘(防止出血)
由于我们采样的格子边界可能正好是纹理像素边界,如果纹理过滤模式为双线性,可能会混合相邻格子的像素,导致边缘出现其他帧的颜色。解决方法:
-
在纹理导入设置中,将 Wrap Mode 设置为 Clamp,防止采样超出边界。
-
在 UV 缩放时,可以略微缩小 UV 范围(比如向内缩半个像素的偏移量),这被称为“padding”或“uv inset”。但最简单的方法是在制作图集时,在每个格子周围留出 1 像素的透明边。
3.3 非正方形图集和子区域
有时图集并不是严格的等分格子,或者你只想使用图集中的一部分区域作为序列帧。你可以通过额外的参数控制起始区域和格子大小。例如,提供 _StartUV(左下角坐标)和 _CellSize(格子大小),然后动态计算:
float2 cellUV = i.uv * _CellSize; // 格子内的相对位置
float2 finalUV = _StartUV + cellUV + float2(col * _CellSize.x, -row * _CellSize.y);
3.4 性能优化
-
减少计算:如果动画速度是固定的,可以在 CPU 端计算好帧索引,通过 MaterialPropertyBlock 传递给着色器,避免 GPU 重复计算。
-
使用 Texture2DArray:如果硬件支持,可以将每一帧作为单独的纹理存入 Texture2D 数组,直接通过帧索引采样,可以避免 UV 变换的麻烦,并且支持各向异性过滤等高级特性,但需要编写相应的采样代码。
-
顶点着色器优化:对于粒子系统(每个粒子是独立的 quad),在顶点中计算可以大幅减少片元计算压力。
4. 在 Unity Shader Graph 中的实现
Unity 的 Shader Graph 提供了内置的 Flipbook 节点,极大地简化了流程。你只需要:
-
创建一个新的 Unlit/Shader Graph。
-
添加一个 Texture 2D 属性作为序列图集。
-
添加 Vector1 属性控制行数、列数、速度。
-
在图中使用 Flipbook 节点,输入 UV、时间(可用
Time节点)、行数、列数。 -
连接 Flipbook 的输出到 Sample Texture 2D 的 UV 输入。
Flipbook 节点内部封装了所有计算,并提供了可选参数(如是否循环、是否随机起始帧)。这是目前推荐的可视化方式,尤其适合非程序员使用。
5. 与 Vertex TexCoord 的关系
回到最初的 Vertex TexCoord,它是 Flipbook 动画的基础。每个顶点携带的原始 UV 坐标定义了模型表面如何覆盖整个图集。Flipbook UV Animation 是在原始 UV 坐标的基础上进行数学变换,将其重新映射到当前帧对应的子区域。因此,原始 UV 坐标扮演了“画布”的角色,而 Flipbook 动画就是在这个画布上选择一块区域进行显示。
例如,对于一个平面网格,它的原始 UV 通常是 (0,0) 到 (1,1) 覆盖整个面。经过 Flipbook 变换后,UV 坐标会被压缩并平移,使得网格的每个点仍然对应一个格子内的相对位置,从而实现动画效果。
6. 总结
Flipbook UV Animation 是游戏开发中一种极为高效且灵活的动画技术。它通过简单的数学运算,将静态的纹理图集转化为动态的视觉效果,广泛应用于粒子特效、2D 角色动画、UI 动画等场景。理解其背后的 UV 变换原理,不仅能让你编写出更高效的着色器代码,还能在遇到问题时快速定位和解决。
73



被折叠的 条评论
为什么被折叠?



