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
是经过平滑处理的时间间隔。
准备工作
- 导入序列帧图集资源。
- 新建一个测试场景。
分析利用纹理坐标制作序列帧动画的原理
关键点
- UV 坐标范围
UV 坐标的范围是0 ~ 1
,原点为图片的左下角。 - 图集播放顺序
序列帧动画播放顺序是从左到右,再从上到下。
分析问题
- 如何得到当前应该播放哪一帧动画?
利用时间参数_Time.y
计算当前帧,通过对总帧数取余实现帧循环播放。 - 如何修改采样规则以实现指定范围内采样?
- 确认当前帧对应的小图片采样起始位置。
- 调整 UV 采样范围到小图片范围内。
问题解决思路
- 确定当前帧
使用_Time.y
对总帧数取余,获得当前帧。 - 计算 UV 采样位置与范围
- 起始位置:结合当前帧索引和图集的行列计算。
- 采样范围:将原 UV 坐标范围
0 ~ 1
缩放到小图片范围内。
用 Shader 实现序列帧动画
主要步骤
- 创建新的 Shader 并删除无用代码。
- 声明所需的属性,并进行属性映射:
- 主纹理、图集的行列数、以及序列帧切换的速度。
- 设置透明 Shader:
- 配置渲染标签:
Tags { "RenderType"="Opaque" "IgnoreProjector"="True" "Queue"="Transparent" }
。 - 禁用深度写入并启用混合:
ZWrite Off
和Blend SrcAlpha OneMinusSrcAlpha
。
- 配置渲染标签:
- 定义结构体:
- 只需传递顶点坐标和纹理坐标。
- 编写顶点着色器:
- 进行坐标转换并将纹理坐标传递给片元着色器。
- 编写片元着色器:
- 根据时间计算当前帧数。
- 根据帧数计算当前 UV 采样的起始位置(即小图的 UV 起始点)。
- 计算 UV 缩放比例,将
0~1
范围映射到0~1/n
。 - 根据行列信息进行 UV 偏移,确保在小图格子内进行采样。
- 最终进行纹理采样,获取对应帧的图像数据。
创建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