【Unity Shader URP】序列帧动画(Sprite Sheet)实战教程

张开发
2026/4/10 17:22:47 15 分钟阅读

分享文章

【Unity Shader URP】序列帧动画(Sprite Sheet)实战教程
文章目录0. 效果预览1. 原理简述2. 功能点3. 完整 Shader可直接用4. 使用方法5. 参数说明6. 变体与扩展6.1 带 Billboard 的顶点着色器Shader 内置面向摄像机6.2 外部控制帧索引C# 驱动6.3 Additive 混合火焰/光效7. 常见问题8. 性能建议0. 效果预览序列帧动画是特效制作的基本功把一组连续帧画面排列在一张贴图上Shader 按时间依次采样每一帧实现火焰、爆炸、烟雾、魔法阵等循环动画。不需要骨骼、不需要粒子系统一张图 一个 Shader 就能跑。1. 原理简述序列帧动画的本质把 UV 坐标限制在贴图的某一格内随时间推移切换到下一格。一张 4×4 的序列帧贴图有 16 帧。每一帧占据 1/4 宽度、1/4 高度的 UV 区域。Shader 要做的事// 1. 算出当前是第几帧 int frameIndex floor(_Time.y * _Speed) % totalFrames; // 2. 算出这一帧在第几行第几列 int col frameIndex % _Columns; int row frameIndex / _Columns; // 3. 把 UV 缩放到单帧大小再偏移到对应格子 float2 uv (uv float2(col, row)) / float2(_Columns, _Rows);注意贴图的 UV 原点在左下角但序列帧通常从左上角开始排列所以行号需要翻转。2. 功能点自动播放基于_Time驱动无需 C# 脚本即可循环播放行列可配支持任意 N×M 的序列帧布局4×4、8×8、2×4 等速度可调_Speed控制每秒播放帧数FPS帧间插值可选开启两帧之间的线性混合消除跳帧感透明支持Alpha Blend 渲染适合叠加在场景上的特效Billboard 可选配合顶点着色器实现始终面向摄像机GPU Instancing支持多实例渲染3. 完整 Shader可直接用Shader Custom/SpriteSheet_URP { Properties { // 序列帧贴图一张包含所有帧的图集 _MainTex (Sprite Sheet, 2D) white {} // 主颜色叠乘可用于调色或控制整体透明度 _BaseColor (Base Color, Color) (1,1,1,1) // 列数水平方向有几帧 _Columns (Columns, Int) 4 // 行数垂直方向有几帧 _Rows (Rows, Int) 4 // 播放速度每秒帧数 _Speed (Speed (FPS), Range(1, 60)) 10 // 总帧数如果最后一行没排满填实际帧数 _TotalFrames (Total Frames, Int) 16 // 帧间插值开关1 开启混合0 硬切 [Toggle] _Interpolate (Frame Interpolation, Float) 0 } SubShader { Tags { RenderPipeline UniversalRenderPipeline Queue Transparent RenderType Transparent } Pass { Name SpriteSheetPass Tags { LightMode UniversalForward } Cull Off ZWrite Off Blend SrcAlpha OneMinusSrcAlpha HLSLPROGRAM #pragma vertex vert #pragma fragment frag // GPU Instancing 支持 #pragma multi_compile_instancing #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl // // 贴图声明 // TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); // // 材质属性与 Properties 一一对应 // float4 _MainTex_ST; float4 _BaseColor; int _Columns; int _Rows; float _Speed; int _TotalFrames; float _Interpolate; struct Attributes { float4 positionOS : POSITION; // 模型空间顶点 float2 uv : TEXCOORD0; // UV 坐标 UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionHCS : SV_POSITION; // 裁剪空间位置 float2 uv : TEXCOORD0; // 传递 UV UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; // // 计算某一帧的 UV将原始 UV 缩放并偏移到对应格子 // float2 GetFrameUV(float2 uv, int frameIndex) { // 列号 帧索引 % 列数 int col frameIndex % _Columns; // 行号 帧索引 / 列数从上往下数 int row frameIndex / _Columns; // UV 原点在左下角但序列帧从左上角排列 // 翻转行号实际行 总行数 - 1 - row int flippedRow _Rows - 1 - row; // 每帧占据的 UV 尺寸 float2 frameSize float2(1.0 / _Columns, 1.0 / _Rows); // 缩放 UV 到单帧大小再偏移到目标格子 return uv * frameSize float2(col, flippedRow) * frameSize; } // // 顶点着色器 // Varyings vert(Attributes v) { Varyings o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.positionHCS TransformObjectToHClip(v.positionOS.xyz); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } // // 片元着色器核心序列帧采样 // half4 frag(Varyings i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); // 1) 计算当前时间对应的帧进度浮点数 // _Time.y 自场景加载以来的秒数 float frameProgress _Time.y * _Speed; // 2) 当前帧索引取模实现循环 int currentFrame ((int)floor(frameProgress)) % _TotalFrames; // 3) 采样当前帧 float2 currentUV GetFrameUV(i.uv, currentFrame); half4 currentColor SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, currentUV); // 4) 帧间插值可选 half4 finalColor currentColor; if (_Interpolate 0.5) { // 下一帧索引 int nextFrame (currentFrame 1) % _TotalFrames; float2 nextUV GetFrameUV(i.uv, nextFrame); half4 nextColor SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, nextUV); // 用小数部分作为混合权重 float blend frac(frameProgress); finalColor lerp(currentColor, nextColor, blend); } // 5) 叠乘基础颜色 finalColor * (half4)_BaseColor; return finalColor; } ENDHLSL } } }4. 使用方法在Assets/Shaders/下新建文件SpriteSheet_URP.shader粘贴上方完整代码。新建材质Create → MaterialShader 选择Custom/SpriteSheet_URP。准备序列帧贴图一张包含所有帧的图集帧按从左到右、从上到下排列常见来源特效软件导出Houdini、EmberGen、手绘帧动画、Unity 粒子录制贴图导入设置Texture Type DefaultAlpha Source Input Texture Alpha关闭Generate Mip Maps避免帧边缘渗色将贴图拖到材质的Sprite Sheet槽位。设置参数Columns/Rows与贴图的实际行列数一致如 4×4 就填 4 和 4Total Frames实际帧数如果最后一行没排满填真实帧数而不是行×列Speed每秒播放帧数10~15 适合火焰20~30 适合爆炸创建一个 Quad3D Object → Quad赋上材质在 Game 视图中即可看到动画循环播放。如果需要特效面向摄像机给 Quad 挂一个简单的 Billboard 脚本usingUnityEngine;publicclassBillboard:MonoBehaviour{voidLateUpdate(){// 让物体始终面向摄像机transform.forwardCamera.main.transform.forward;}}5. 参数说明参数类型范围/默认值说明_MainTex2Dwhite序列帧贴图所有帧排列在一张图上_BaseColorColor(1,1,1,1)颜色叠乘可用于调色或控制整体透明度_ColumnsInt4贴图水平方向的帧数_RowsInt4贴图垂直方向的帧数_SpeedRange(1,60)10播放速度每秒帧数FPS_TotalFramesInt16实际总帧数最后一行没排满时填真实值_InterpolateToggle0帧间插值开启后两帧之间线性混合动画更平滑6. 变体与扩展6.1 带 Billboard 的顶点着色器Shader 内置面向摄像机不需要 C# 脚本直接在顶点着色器中实现 BillboardVaryings vert(Attributes v) { Varyings o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); // Billboard用摄像机的右方向和上方向替换模型的 X/Y 轴 float3 centerWS TransformObjectToWorld(float3(0, 0, 0)); float3 camRight UNITY_MATRIX_V[0].xyz; // 摄像机右方向 float3 camUp UNITY_MATRIX_V[1].xyz; // 摄像机上方向 // 用模型空间的 xy 作为偏移量沿摄像机平面展开 float3 positionWS centerWS camRight * v.positionOS.x * unity_ObjectToWorld._m00 // 保留缩放 camUp * v.positionOS.y * unity_ObjectToWorld._m11; o.positionHCS TransformWorldToHClip(positionWS); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; }6.2 外部控制帧索引C# 驱动有时需要精确控制播放进度如技能释放到某个阶段播放特定帧段// 在 Properties 中加一个手动帧索引// _ManualFrame (Manual Frame, Range(0, 64)) -1// 负值 自动播放非负值 锁定到指定帧// C# 端material.SetFloat(_ManualFrame,5);// 锁定到第 5 帧material.SetFloat(_ManualFrame,-1);// 恢复自动播放// frag 中替换帧索引计算 int currentFrame; if (_ManualFrame 0) currentFrame clamp((int)_ManualFrame, 0, _TotalFrames - 1); else currentFrame ((int)floor(_Time.y * _Speed)) % _TotalFrames;6.3 Additive 混合火焰/光效火焰、闪电等发光特效用 Additive 混合比 Alpha Blend 更自然// 把 Pass 中的 Blend 模式改为 Blend SrcAlpha One // Additive源色 × Alpha 加到目标上效果亮的部分叠加发光暗的部分黑色自然消失不需要精确的 Alpha 通道。7. 常见问题Q: 动画播放顺序反了从右到左或从下到上A: 检查两个地方① 贴图的帧排列方向是否是左到右、上到下——这是本 Shader 的默认约定② 如果贴图是左到右、下到上排列把GetFrameUV中的flippedRow改为int flippedRow row;去掉翻转。Q: 帧与帧之间有明显的接缝/渗色A: 两个原因① Mip Maps 导致相邻帧的像素渗透——在贴图导入设置中关闭 Generate Mip Maps② 贴图的 Wrap Mode 设为Clamp而不是Repeat避免边缘采样到对面的帧。Q: 开启帧间插值后画面变模糊A: 正常现象——两帧线性混合本质上就是叠加运动剧烈的帧之间会产生重影。如果不能接受关闭_Interpolate用更高的帧率更多帧来弥补平滑度。Q: 贴图最后一行没排满比如 4×4 但只有 14 帧播放到空白帧A: 把Total Frames设为实际帧数14Shader 会在第 14 帧后循环回第 0 帧不会播放到空白格子。Q: 多个使用同一材质的 Quad 动画完全同步A: 因为它们共享同一个_Time值。解决方案用MaterialPropertyBlock给每个实例设置不同的时间偏移或者在 Shader 中加一个_TimeOffset属性C# 端随机赋值。8. 性能建议贴图尺寸控制4×4 的序列帧贴图每帧 256×256 → 整张图 1024×1024这是移动端的舒适区。8×8 每帧 128×128 也是 1024×1024帧数更多但单帧精度降低按需取舍。关闭 Mip Maps序列帧贴图不需要 Mip MapsUI/特效通常在固定距离观看关掉可以节省 33% 显存。Additive 优于 Alpha Blend如果视觉上允许Additive 混合不需要排序GPU 友好。帧间插值的代价开启插值 每个像素采样两次贴图。如果特效数量多几十个同屏关闭插值可以减半采样开销。合批注意多个使用相同材质的 Quad 可以被 SRP Batcher / GPU Instancing 合批。如果用了MaterialPropertyBlock设置不同参数GPU Instancing 仍然有效但 SRP Batcher 会被打断。

更多文章