46.序列帧动画

46.动态效果-纹理动画-序列帧动画


46.1 知识点

知识点回顾

  • float4 _Time
    包含 4 个分量,值分别为:

    • (t/20, t, 2t, 3t),其中 t 表示游戏场景从加载开始经过的时间。
  • float4 _SinTime
    包含 4 个分量,值分别为:

    • (sin(t/8), sin(t/4), sin(t/2), sin(t)),其中 t 表示运行时间的正弦值。
  • float4 _CosTime
    包含 4 个分量,值分别为:

    • (cos(t/8), cos(t/4), cos(t/2), cos(t)),其中 t 表示运行时间的余弦值。
  • float4 unity_DeltaTime
    包含 4 个分量,值分别为:

    • (dt, 1/dt, smoothDt, 1/smoothDt)
    • dt 表示帧间隔时间(上一帧到当前帧的时间差)。
    • smoothDt 是经过平滑处理的时间间隔。

准备工作

  • 导入序列帧图集资源。
  • 新建一个测试场景。

分析利用纹理坐标制作序列帧动画的原理

关键点

  1. UV 坐标范围
    UV 坐标的范围是 0 ~ 1,原点为图片的左下角。
  2. 图集播放顺序
    序列帧动画播放顺序是从左到右,再从上到下。

分析问题

  1. 如何得到当前应该播放哪一帧动画?
    利用时间参数 _Time.y 计算当前帧,通过对总帧数取余实现帧循环播放。
  2. 如何修改采样规则以实现指定范围内采样?
    • 确认当前帧对应的小图片采样起始位置。
    • 调整 UV 采样范围到小图片范围内。

问题解决思路

  1. 确定当前帧
    使用 _Time.y 对总帧数取余,获得当前帧。
  2. 计算 UV 采样位置与范围
    • 起始位置:结合当前帧索引和图集的行列计算。
    • 采样范围:将原 UV 坐标范围 0 ~ 1 缩放到小图片范围内。

用 Shader 实现序列帧动画

主要步骤

  1. 创建新的 Shader 并删除无用代码。
  2. 声明所需的属性,并进行属性映射:
    • 主纹理、图集的行列数、以及序列帧切换的速度。
  3. 设置透明 Shader:
    • 配置渲染标签:Tags { "RenderType"="Opaque" "IgnoreProjector"="True" "Queue"="Transparent" }
    • 禁用深度写入并启用混合:ZWrite OffBlend SrcAlpha OneMinusSrcAlpha
  4. 定义结构体:
    • 只需传递顶点坐标和纹理坐标。
  5. 编写顶点着色器:
    • 进行坐标转换并将纹理坐标传递给片元着色器。
  6. 编写片元着色器:
    1. 根据时间计算当前帧数。
    2. 根据帧数计算当前 UV 采样的起始位置(即小图的 UV 起始点)。
    3. 计算 UV 缩放比例,将 0~1 范围映射到 0~1/n
    4. 根据行列信息进行 UV 偏移,确保在小图格子内进行采样。
    5. 最终进行纹理采样,获取对应帧的图像数据。

创建shader,保留骨架

Shader "Unlit/Lesson46_SequentialFrameAnimation"
{
    Properties
    {
        
    }
    SubShader
    {
        Tags
        {
            
        }

        Pass
        {
            

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            ENDCG
        }
    }
}

声明属性,进行属性映射。包括 主纹理、图集行列、序列帧切换速度

Properties
{
    //主纹理
    _MainTex ("Texture", 2D) = "white" {}
    //图集行列
    _Rows("Rows", int) = 8
    _Columns("Columns", int) = 8
    //切换动画速度变量
    _Speed("Speed", float) = 1
}
// 主纹理
sampler2D _MainTex;
// 纹理的行数
float _Rows;
// 纹理的列数
float _Columns;
// 动画速度
float _Speed;

因为采用背景是透明Shader,需要设置渲染队列和类型为透明并且忽略阴影,还需要并且要关闭深度写入,开启混合

Tags
{
    // 设置渲染类型为透明,这意味着该材质将在透明渲染队列中进行渲染
    "RenderType"="Transparent"
    // 将渲染队列设置为透明队列,通常用于需要在不透明物体之后渲染的透明物体
    "Queue"="Transparent"
    // 设置忽略投影器,即该材质不会受到投影器的影响
    "IgnoreProjector"="True"
}

Pass
{
    //关闭深度写入
    ZWrite Off
    //开启混合
    Blend SrcAlpha OneMinusSrcAlpha|
    //...
}

创建v2f结构体,包括纹理坐标和裁剪坐标

struct v2f
{
    // 纹理坐标,存储在 TEXCOORD0 语义中,用于在片段着色器中对主纹理进行采样
    float2 uv : TEXCOORD0;
    // 裁剪空间下的顶点位置,存储在 SV_POSITION 语义中,是顶点着色器必须输出的变量
    float4 vertex : SV_POSITION;
};

顶点函数直接把裁剪坐标和纹理坐标传如结构体

v2f vert(appdata_base appdata_base)
{
    v2f v2f;
    // 将模型空间下的顶点转换到裁剪空间,并将结果存储在 v2f.vertex 中
    v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
    // 将输入的顶点纹理坐标直接赋值给输出结构体中的 uv 成员,用于传递到片段着色器
    v2f.uv = appdata_base.texcoord;
    return v2f;
}

片元函数利用时间计算帧数,确定当前小图索引及在大图中的位置并计算小图采样起始位置。基于格子行列确定缩放比例并最终采样

fixed4 frag(v2f v2f) : SV_Target
{
    // 得到当前帧,利用时间变量计算
    // 用当前帧乘以速度,得到实际对应帧
    float currentFrame = _Time.y * _Speed;

    // 对总行数和总列数取余得到当前小图索引
    float frameIndex = floor(currentFrame) % (_Rows * _Columns);

    // 计算当前小图在整个大图中的行索引
    int rowIndex = floor(frameIndex / _Columns);

    // 计算当前小图在整个大图中的列索引
    int columnIndex = frameIndex % _Columns;

    // 小格子(小图片)采样时XY的起始位置计算
    // 除以对应的行和列,目的是将行列值转换到 0~1 的坐标范围内

    //计算X起始坐标
    float startX = columnIndex / _Columns;

    // 计算Y起始坐标
    //  +1 是因为把格子左上角转换为格子左下角(锚点) 
    //  1- 因为 UV 坐标采样时从左下角进行采样的
    // 例如8行8列 左上角坐标系的第0行 但是实际采样是左下角坐标系
    // (rowIndex + 1) / _Rows 算出来 是1/8
    // 1- 1/8 = 7/8 从7/8的位置进行采样
    // 可以看出+1的作用 因为不加1 得到的结果是1 直接就是左上角最顶上了 无法向上采样
    float startY = 1 - (rowIndex + 1) / _Rows;

    //采样uv 将行列值转换到 0~1 的坐标范围内
    float2 frameUV = float2(startX, startY);

    // 得到 uv 缩放比例,相当于从 0~1 大图映射到一个 0~1/n 的一个小图中
    float2 size = float2(1 / _Columns, 1 / _Rows);

    // 计算最终的 uv 采样坐标信息
    // *size 相当于把 0~1 范围缩放到了 0~1/8 范围
    // +frameUV 相当于把起始的采样位置移动到了对应帧小格子的起始位置

    // 比如uv原来为右上角的点 1 1 等一下缩小后会变成1/8 1/8
    // 即所有uv都会小于等于1/8 等于是在小格子上采样了
    // 在加上起始偏移 得到对应小格子的像素点
    float2 uv = v2f.uv * size + frameUV;

    // 最终采样颜色
    return tex2D(_MainTex, uv);
}

创建材质可以看到帧动画播放的效果,可以把速度调快一点



总结

UV 坐标原点为左下角,而序列帧图集“原点”为左上角。
在实现时需注意 UV 采样起始位置的转换。


46.2 知识点代码

Lesson46_动态效果_纹理动画_序列帧动画.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson46_动态效果_纹理动画_序列帧动画 : MonoBehaviour
{
    void Start()
    {
        #region 知识点回顾

        //1.float4 _Time
        //  4个分量的值分别是(t/20, t, 2t, 3t)
        //  其中t代表该游戏场景从加载开始缩经过的时间

        //2.float4 _SinTime
        //  4个分量的值分别是(t/8, t/4, t/2, t)
        //  其中t代表 游戏运行的时间的正弦值

        //3.float4 _CosTime
        //  4个分量的值分别是(t/8, t/4, t/2, t)
        //  其中t代表 游戏运行的时间的余弦值

        //4.float4 unity_DeltaTime
        //  4个分量的值分别是(dt, 1/dt, smoothDt, 1/smoothDt)
        //  dt代表帧间隔时间(上一帧到当前帧间隔时间)
        //  smoothDt是平滑处理过的时间间隔,对帧间隔时间进行某种平滑算法处理后的结果

        #endregion

        #region 准备工作

        //1.在导入序列帧图集资源
        //2.新建一个测试场景

        #endregion

        #region 知识点一 分析利用纹理坐标制作序列帧动画的原理

        //关键点
        //1.UV坐标范围0~1,原点为图片左下角
        //2.图集序列帧动画播放顺序为从左到右,从上到下

        //分析问题
        //1.如何得到当前应该播放哪一帧动画?
        //2.如何将采样规则从0~1修改为在指定范围内采样?

        //问题解决思路
        //1.用内置时间参数 _Time.y 参与计算得到具体哪一帧
        //  时间是不停增长的数值,用它对总帧数取余,便可以循环获取到当前帧数
        //2.利用时间得到当前应该绘制哪一帧后
        //  我们只需要确认从当前小图片中,采样开始位置,采样范围即可
        //  采样开始位置,可以利用当前帧和行列一起计算
        //  采样范围可以将0~1范围 缩放转换到 小图范围内

        #endregion

        #region 知识点二 用Shader实现序列帧动画

        //1.新建Shader 删除无用代码
        //2.声明属性,进行属性映射
        //  主纹理、图集行列、序列帧切换速度
        //3.透明Shader
        //  设置渲染标签
        //  Tags { "RenderType"="Opaque" "IgnoreProjector"="True" "Queue"="Transparent" }
        //  关闭深度写入,开启混合
        //  ZWrite Off
        //  Blend SrcAlpha OneMinusSrcAlpha
        //4.结构体
        //  只需要顶点坐标和纹理坐标
        //5.顶点着色器
        //  只需要进行坐标转换和纹理坐标赋值
        //6.片元着色器
        //  6-1:利用时间计算帧数
        //  6-2:利用帧数计算当前 uv采样起始位置(得到小图片uv起始位置)
        //  6-3:计算uv缩放比例(将0~1 转换到 0~1/n)
        //  6-4:进行uv偏移计算(在小图片格子中采样)
        //  6-5:采样

        #endregion

        #region 总结

        //Shader实现序列帧动画的关键点是
        //UV坐标原点为左下角,而序列帧图集“原点”为左上角
        //我们需要注意采样开始位置的转换

        #endregion
    }
}

Lesson46_SequentialFrameAnimation.shader

Shader "Unlit/Lesson46_SequentialFrameAnimation"
{
    Properties
    {
        //主纹理
        _MainTex ("Texture", 2D) = "white" {}
        //图集行列
        _Rows("Rows", int) = 8
        _Columns("Columns", int) = 8
        //切换动画速度变量
        _Speed("Speed", float) = 1
    }
    SubShader
    {
        Tags
        {
            // 设置渲染类型为透明,这意味着该材质将在透明渲染队列中进行渲染
            "RenderType"="Transparent"
            // 将渲染队列设置为透明队列,通常用于需要在不透明物体之后渲染的透明物体
            "Queue"="Transparent"
            // 设置忽略投影器,即该材质不会受到投影器的影响
            "IgnoreProjector"="True"
        }

        Pass
        {
            //关闭深度写入
            ZWrite Off
            //开启混合
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            // 主纹理
            sampler2D _MainTex;
            // 纹理的行数
            float _Rows;
            // 纹理的列数
            float _Columns;
            // 动画速度
            float _Speed;


            struct v2f
            {
                // 纹理坐标,存储在 TEXCOORD0 语义中,用于在片段着色器中对主纹理进行采样
                float2 uv : TEXCOORD0;
                // 裁剪空间下的顶点位置,存储在 SV_POSITION 语义中,是顶点着色器必须输出的变量
                float4 vertex : SV_POSITION;
            };


            v2f vert(appdata_base appdata_base)
            {
                v2f v2f;
                // 将模型空间下的顶点转换到裁剪空间,并将结果存储在 v2f.vertex 中
                v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
                // 将输入的顶点纹理坐标直接赋值给输出结构体中的 uv 成员,用于传递到片段着色器
                v2f.uv = appdata_base.texcoord;
                return v2f;
            }


            fixed4 frag(v2f v2f) : SV_Target
            {
                // 得到当前帧,利用时间变量计算
                // 用当前帧乘以速度,得到实际对应帧
                float currentFrame = _Time.y * _Speed;

                // 对总行数和总列数取余得到当前小图索引
                float frameIndex = floor(currentFrame) % (_Rows * _Columns);

                // 计算当前小图在整个大图中的行索引
                int rowIndex = floor(frameIndex / _Columns);

                // 计算当前小图在整个大图中的列索引
                int columnIndex = frameIndex % _Columns;

                // 小格子(小图片)采样时XY的起始位置计算
                // 除以对应的行和列,目的是将行列值转换到 0~1 的坐标范围内

                //计算X起始坐标
                float startX = columnIndex / _Columns;

                // 计算Y起始坐标
                //  +1 是因为把格子左上角转换为格子左下角(锚点) 
                //  1- 因为 UV 坐标采样时从左下角进行采样的
                // 例如8行8列 左上角坐标系的第0行 但是实际采样是左下角坐标系
                // (rowIndex + 1) / _Rows 算出来 是1/8
                // 1- 1/8 = 7/8 从7/8的位置进行采样
                // 可以看出+1的作用 因为不加1 得到的结果是1 直接就是左上角最顶上了 无法向上采样
                float startY = 1 - (rowIndex + 1) / _Rows;

                //采样uv 将行列值转换到 0~1 的坐标范围内
                float2 frameUV = float2(startX, startY);

                // 得到 uv 缩放比例,相当于从 0~1 大图映射到一个 0~1/n 的一个小图中
                float2 size = float2(1 / _Columns, 1 / _Rows);

                // 计算最终的 uv 采样坐标信息
                // *size 相当于把 0~1 范围缩放到了 0~1/8 范围
                // +frameUV 相当于把起始的采样位置移动到了对应帧小格子的起始位置

                // 比如uv原来为右上角的点 1 1 等一下缩小后会变成1/8 1/8
                // 即所有uv都会小于等于1/8 等于是在小格子上采样了
                // 在加上起始偏移 得到对应小格子的像素点
                float2 uv = v2f.uv * size + frameUV;

                // 最终采样颜色
                return tex2D(_MainTex, uv);
            }
            ENDCG
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏