Home 序列帧动画
Post
Cancel

序列帧动画

image-20230205185014167

序列帧动画

  1. 序列帧动画是事先准备好动画中的每一帧画面,然后依次播放。
  2. 序列帧动画适合卡通风格的2D游戏,特别是“像素风”,更是绝配。

使用 AnimationClip

[[Animation Clips]]

使用 ShaderGraph

image-20230205185053912

使用 Shader 顶点着色器

  1. 先定义帧动画的总帧数、图元排列的行数和列数,以及播放速度,在顶点着色时就计算好UV,计算过程是通过当前的时间和速度及总帧数,获得当前所在的帧数,再用帧数计算图片所在的行位置和列位置,最后用行位置和列位置计算UV数据,UV数据传入片元后,片元着色器从图片中提取像素颜色,交给后面的步骤进行渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader UVAni
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}        // 序列帧贴图
        _Total ("total", float) = 1                     // 总帧数
        _Rows ("rows", float) = 1                       // 图元行数
        _Cols ("cols", float) = 1                       // 图元列数
        _Fps ("speed", float) = 1                       // 播放速度
    }

    SubShader
    {
        Pass
        {
            Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }

            Lighting Off ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            sampler2D _MainTex;

            float_Total;
            float_Rows;
            float_Cols;
            float_Fps;        

            struct v2f {
                float4  pos : POSITION;
                float2  uv : TEXCOORD0;
            } ;

            v2f vert(appdata_base v)
            {            
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP,v.vertex); // 顶点3D坐标转换为屏幕2D坐标            
                float floorModTime = floor(fmod( _Time.y  * _Fps,  _Total)); 
                                                                        // 获得帧数
                float uIdx = fmod(floorModTime , _Cols);        // 获得横坐标索引
                float vIdx = _Rows - 1 - floor(floorModTime / _Rows); 
                                                                        // 获得纵坐标索引
                o.uv = float2(v.texcoord.x / _Cols + uIdx / _Cols, v.texcoord.
                    y / _Rows + vIdx / _Rows);          // 计算贴图中的UV位置                
                return o;
            }
            float4 frag(v2f i) : COLOR
            {
                float4 texCol = tex2D(_MainTex,i.uv);            
                return texCol;
            }
            ENDCG    
        }                
    }
}

C#中修改Mesh顶点UV做序列帧动画

image-20230205185112679

1
2
3
4
5
6
7
8
9
10
float row = 4, col = 2;
tileSize = new Vector2(0.5f, 0.25f);
frameCount = 4;
startIndex = 0 or 4;

curIndex = Mathf.FloorToInt(time * speed % frameCount + startIndex);
minUV_x = curIndex % row * tileSize.x;
minUV_y = 1 - (Mathf.FloorToInt(curIndex / col) + 1) * tileSize.y;
maxUV_x = minUV_x + tileSize.x;
maxUV_y = minUV_y + tileSize.y;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
private float playSpeed;   // 播放速度
private float row;     // 有多少行
private float col;     // 有多少列

private List<FlipBookInfo> flipBookInfos;

private bool isDirty;
private float timeElapsed;

private MeshFilter meshFilter;
private Mesh flipBookMesh;
private Vector3[] vertices;
private int verticesCount;
private Vector2[] uvs;
private int uvsCount;
private int[] triangles;
private int trianglesCount;

private Vector2 tileSize;   // 每个tile的尺寸 [0~1]

private void Update()
{
    timeElapsed += (Time.deltaTime * playSpeed);
    if (timeElapsed > 1)
    {
        // 计算当前时刻每个序列帧动画播放到了第几帧
        foreach (var flipBookInfo in flipBookInfos)
        {
            int tileIndex = Mathf.FloorToInt(timeElapsed % flipBookInfo.frameCount + flipBookInfo.startIndex);
            if (tileIndex != flipBookInfo.curIndex)
            {
                flipBookInfo.curIndex = tileIndex;
                isDirty = true;
            }
        }
    }
    if (isDirty)
    {
        RefreshMesh();
        isDirty = false;
    }
}


private void RefreshMesh()
{
    // 初始化
    if (meshFilter == null)
    {
        Init();
    }
    
    // 根据flipBookInfo 构建网格
    foreach (var flipBookInfo in flipBookInfos)
    {
        // 拷贝 triangles 和 vertices
        var mesh = flipBookInfo.meshFilter.mesh;
        Transform trans = flipBookInfo.meshFilter.transform;
        for (int i = 0; i < mesh.triangles.Length; i++)
        {
            triangles[trianglesCount++] = mesh.triangles[i] + verticesCount;
        }
        
        for (int i = 0; i < mesh.vertices.Length; i++)
        {
            vertices[verticesCount++] = trans.TransformPoint(mesh.vertices[i]);
        }
        // 根据 curIndex 计算 UV
        float minUV_x = flipBookInfo.curIndex % row * tileSize.x;
        float minUV_y = 1 - (Mathf.FloorToInt(flipBookInfo.curIndex / col) + 1) * tileSize.y;
        float maxUV_x = minUV_x + tileSize.x;
        float maxUV_y = minUV_y + tileSize.y;
        uvs[uvsCount++] = new Vector2(minUV_x, minUV_y);   // 左下
        uvs[uvsCount++] = new Vector2(minUV_x, maxUV_y);   // 左上
        uvs[uvsCount++] = new Vector2(maxUV_x, maxUV_y);   // 右上
        uvs[uvsCount++] = new Vector2(maxUV_x, minUV_y);   // 右下 
    }
    
    // 更新 Mesh
    flipBookMesh.Clear();
    flipBookMesh.SetVertices(vertices, 0, verticesCount);
    flipBookMesh.SetTriangles(triangles, 0, trianglesCount, 0);
    flipBookMesh.SetUVs(0, uvs, 0, uvsCount);
    verticesCount = 0;
    trianglesCount = 0;
    uvsCount = 0;
}

private void Init()
{
    meshFilter = GetComponent<MeshFilter>();
    flipBookMesh = meshFilter.sharedMesh = new Mesh();
    flipBookMesh.name = "FlipBook";
    vertices = new Vector3[64];
    uvs = new Vector2[64];
    triangles = new int[64];

    tileSize = new Vector2(1 / col, 1 / row);
}

This post is licensed under CC BY 4.0 by the author.
Contents