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

文章目录

    • [0. 效果预览](#0. 效果预览)
    • [1. 原理简述](#1. 原理简述)
    • [2. 功能点](#2. 功能点)
    • [3. 完整 Shader(可直接用)](#3. 完整 Shader(可直接用))
    • [4. 使用方法](#4. 使用方法)
    • [5. 参数说明](#5. 参数说明)
    • [6. 变体与扩展](#6. 变体与扩展)
      • [6.1 带 Billboard 的顶点着色器(Shader 内置面向摄像机)](#6.1 带 Billboard 的顶点着色器(Shader 内置面向摄像机))
      • [6.2 外部控制帧索引(C# 驱动)](# 驱动))
      • [6.3 Additive 混合(火焰/光效)](#6.3 Additive 混合(火焰/光效))
    • [7. 常见问题](#7. 常见问题)
    • [8. 性能建议](#8. 性能建议)

0. 效果预览

序列帧动画是特效制作的基本功:把一组连续帧画面排列在一张贴图上,Shader 按时间依次采样每一帧,实现火焰、爆炸、烟雾、魔法阵等循环动画。不需要骨骼、不需要粒子系统,一张图 + 一个 Shader 就能跑。


1. 原理简述

序列帧动画的本质:把 UV 坐标限制在贴图的某一格内,随时间推移切换到下一格。

一张 4×4 的序列帧贴图有 16 帧。每一帧占据 1/4 宽度、1/4 高度的 UV 区域。Shader 要做的事:

hlsl 复制代码
// 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(可直接用)

hlsl 复制代码
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. 使用方法

  1. Assets/Shaders/ 下新建文件 SpriteSheet_URP.shader,粘贴上方完整代码。

  2. 新建材质(Create → Material),Shader 选择 Custom/SpriteSheet_URP

  3. 准备序列帧贴图:

    • 一张包含所有帧的图集,帧按从左到右、从上到下排列
    • 常见来源:特效软件导出(Houdini、EmberGen)、手绘帧动画、Unity 粒子录制
    • 贴图导入设置:Texture Type = DefaultAlpha Source = Input Texture Alpha,关闭 Generate Mip Maps(避免帧边缘渗色)
  1. 将贴图拖到材质的 Sprite Sheet 槽位。

  2. 设置参数:

    • Columns / Rows:与贴图的实际行列数一致(如 4×4 就填 4 和 4)
    • Total Frames:实际帧数(如果最后一行没排满,填真实帧数而不是行×列)
    • Speed:每秒播放帧数,10~15 适合火焰,20~30 适合爆炸
  3. 创建一个 Quad(3D Object → Quad),赋上材质,在 Game 视图中即可看到动画循环播放。

  4. 如果需要特效面向摄像机,给 Quad 挂一个简单的 Billboard 脚本:

csharp 复制代码
using UnityEngine;

public class Billboard : MonoBehaviour
{
    void LateUpdate()
    {
        // 让物体始终面向摄像机
        transform.forward = Camera.main.transform.forward;
    }
}

5. 参数说明

参数 类型 范围/默认值 说明
_MainTex 2D white 序列帧贴图(所有帧排列在一张图上)
_BaseColor Color (1,1,1,1) 颜色叠乘,可用于调色或控制整体透明度
_Columns Int 4 贴图水平方向的帧数
_Rows Int 4 贴图垂直方向的帧数
_Speed Range(1,60) 10 播放速度,每秒帧数(FPS)
_TotalFrames Int 16 实际总帧数(最后一行没排满时填真实值)
_Interpolate Toggle 0 帧间插值:开启后两帧之间线性混合,动画更平滑

6. 变体与扩展

6.1 带 Billboard 的顶点着色器(Shader 内置面向摄像机)

不需要 C# 脚本,直接在顶点着色器中实现 Billboard:

hlsl 复制代码
Varyings 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# 驱动)

有时需要精确控制播放进度(如技能释放到某个阶段播放特定帧段):

csharp 复制代码
// 在 Properties 中加一个手动帧索引
// _ManualFrame ("Manual Frame", Range(0, 64)) = -1
// 负值 = 自动播放,非负值 = 锁定到指定帧

// C# 端
material.SetFloat("_ManualFrame", 5); // 锁定到第 5 帧
material.SetFloat("_ManualFrame", -1); // 恢复自动播放
hlsl 复制代码
// 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 更自然:

hlsl 复制代码
// 把 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 设为实际帧数(14),Shader 会在第 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 Maps(UI/特效通常在固定距离观看),关掉可以节省 33% 显存。
  • Additive 优于 Alpha Blend:如果视觉上允许,Additive 混合不需要排序,GPU 友好。
  • 帧间插值的代价:开启插值 = 每个像素采样两次贴图。如果特效数量多(几十个同屏),关闭插值可以减半采样开销。
  • 合批注意 :多个使用相同材质的 Quad 可以被 SRP Batcher / GPU Instancing 合批。如果用了 MaterialPropertyBlock 设置不同参数,GPU Instancing 仍然有效,但 SRP Batcher 会被打断。
相关推荐
mxwin2 小时前
Unity URP 多线程渲染:理解 Shader 变体对加载时间的影响
unity·游戏引擎·shader
呆呆敲代码的小Y3 小时前
【Unity工具篇】| 游戏完整资源热更新流程,YooAsset官方示例项目
人工智能·游戏·unity·游戏引擎·热更新·yooasset·免费游戏
nainaire4 小时前
自学虚幻引擎记录1
游戏引擎·虚幻
想你依然心痛7 小时前
HarmonyOS 5.0游戏开发实战:构建高性能2D休闲游戏引擎与 monetization 系统
华为·游戏引擎·harmonyos
黄思搏1 天前
基于标注平台数据的 Unity UI 自动化构建工作流设计与工程实践
ui·unity·蓝湖·vectoui
羊羊20352 天前
开发手札:Unity6000与Android交互
android·unity·android-studio
Zarek枫煜2 天前
C3 编程语言 - 现代 C 的进化之选
c语言·开发语言·青少年编程·rust·游戏引擎
Sator12 天前
Unity AStarPath的踩坑点
unity
榮華2 天前
DOTA全图透视辅助下载DOTA全图科技辅助下载DOTA外挂下载魔兽争霸WAR3全图下载
数据库·科技·游戏·游戏引擎·游戏程序·ai编程·腾讯云ai代码助手