在 Unity 中实现 GIF 动态功能
引言
- 工具链接在文章末尾。
- Unity 本身是不支持 GIF 图的直接播放的,不过有多种方法可以间接实现:
- 通过
.net
的Drawing库
来实现 GIF 图片解析,不过只能在 windows 平台使用。 - 使用
AE
等动画软件先将 GIF 转成序列帧,然后再在 Unity 中作为动画调用。 - 在脚本中手动解码 GIF 文件,然后将 GIF 的帧信息缓存在数组中,进行轮播。
- 通过
- 本文主要介绍第三种方法。目的是将 GIF 图变为和普通图片一样, 可以导入到Unity中, 并挂载在脚本中运行。具体分为三个步骤, 导入、解码和播放。
导入 GIF 图
- Unity默认会将
.gif
文件导入成TextureAsset
,这显然不是我们需要的,解码 GIF 图需要文件的所有二进制信息,因此此处需要自定义一种 Asset ,代码如下。
1 | using UnityEngine; |
- 然后在导入 GIF 图的时候,把GIF图的数据存入
gifData
里的二进制数组中。麻烦的是, Unity 虽然不支持 GIF 图,但是会识别.gif
文件。此处我的做法是给 GIF 图增加一个后缀.bytes
。bytes
文件默认会导入成文本资产(TextAsset)
。通过textAsset.bytes
就可以轻松获取到 GIF 图的二进制数据。具体代码如下。
1 | using System.IO; |
- 上述代码的作用是后处理的过程中,识别出所有的
.gif.bytes
文件,创建一个GifData
资产,并且删掉原有的文本资产。此时,第一步导入 GIF 图已经完成。
获取 GIF 数据(以源码为标准,文章中的代码仅供参考,不保证正确性(其实是懒得改))
- GIF 的文件结构
- GIF 格式的文件结构整体上分为三部分:文件头、GIF 数据流、文件结尾。其中,GIF 数据流分为全局配置和图像块。
- GIF 使用大端存储字节
GIF署名(Signature)和版本号(Version)
- GIF 的前 6 个字节内容是 GIF 的署名和版本号。我们可以通过前 3 个字节判断文件是否为 GIF 格式,后 3 个字节判断 GIF 格式的版本。
1 | /// <summary> |
逻辑屏幕标识符(Logical Screen Descriptor)
- 逻辑屏幕标识符配置了 GIF 一些全局属性,我们通过读取解析它,获取 GIF 全局的一些配置。
- 屏幕逻辑宽度:定义了 GIF 图像的像素宽度,大小为 2 字节(第一个字节是
基数
,第二个字节是倍数
,宽度 = 基数 + 256 * 倍数
); - 屏幕逻辑高度:定义了 GIF 图像的像素高度,大小为 2 字节(同上);
- 接下来是一个压缩字节:
- 第一个
Bit
为标志位,表示全局颜色列表是否存在。 - 接下来三个
Bit
表示图像调色板中每个颜色的原色所占用的Bit
数,011
表示占用4
个Bit
,111
占用8
个Bit
(三个Bit
的二进制数值 +1
),以此类推。调色板最多只包含256个颜色(实际有很多优化方案能提高颜色分辨率,如加入局部调色板)。 - 第五个
Bit
为标志位,表示颜色列表排序方式。若为1
,表示颜色列表是按照颜色在图像中出现的频率降序排列。 - 随后三个
Bit
表示全局颜色列表的大小,计算方法是2 ^ (N + 1)
,其中N
为这三个Bit
的二进制数值。
- 第一个
- 背景颜色:背景颜色在全局颜色列表中的索引(PS:是索引而不是
RGB
值,所以如果没有全局颜色列表时,该值没有意义); - 像素宽高比:全局像素的宽度与高度的比值。大多数时候这个值都是
0
,若值为N
, 则图像的宽高比 = (N + 15)/ 64
。
1 | /// <summary> |
- 颜色深度和颜色列表排序方式,这两者在实际中使用较少。此外
pixelAspectRatio
也只是读取,后续的解析中并没有使用到。
全局颜色列表(Global Color Table)
- 全局颜色列表,在逻辑屏幕标识之后,每个颜色索引由三字节组成,按
RGB
顺序排列(例如:全局颜色列表的大小是16
,每个颜色占3
个字节,按照RGB
排列,所以它占有48
个字节)。
- 整个 GIF 在每一帧的画面数组时,是不会出现
RGB
值的,画面中所有像素的RGB
值,都是通过从全局 / 局部颜色列表中取得。可以让颜色列表理解为调色板。我需要什么RGB
,我不直接写,而是写我想要RGB
对应颜色列表的索引。这样做的好处,比如我想对GIF
进行调色,如果我每一帧画面直接使用了RGB
,那我每一帧都需要进行图像处理。有了调色盘,我只需要对调色板进行处理,每帧画面都会改变。 全局彩色表 (Global Color Table)
存在与否由逻辑屏幕描述块 (LogicalScreen Descriptor)
中字节5
的全局彩色表标志 (GlobalColor Table Flag )
的值确定。
1 | /// <summary> |
- 至此,GIF 文件的全局配置就完成了,接下来是每一帧的配置 or 数据。
图像块(Image Block)
- 一个GIF文件中可以有多个图像块,每个图像块都有图像标识符(Image Descriptor),描述了当前帧的一些属性。
- 图像标识符以
','(0x2c)
作为开始标志。接着定义了当前帧的偏移量
和宽高
。 - 最后5个标志的意义分别为:
m
- 局部颜色列表标志(Local Color Table Flag):若为1,表示有一个局部彩色表(Local Color Table)将紧跟在这个图像描述块(ImageDescriptor)之后;若为0,表示图像描述块(Image Descriptor)后面没有局部彩色表(Local Color Table),该图像要使用全局彩色表(GlobalColor Table),忽略pixel
值。i
- 交插显示标志(Interlace Flag):若为0,表示该图像不是交插图像;若为1表示该图像是交插图像。使用该位标志可知道图像数据是如何存放的。s
- 局部颜色排序标志:与全局彩色表(GlobalColor Table)中(Sort Flag)域的含义相同。r
- 保留(未被使用,必须初始化为0)。pixel
- 局部颜色列表大小:用来计算局部彩色表(Global Color Table)中包含的字节数。
- 交插显示标志将在图片的解码时单独解释。
基于颜色列表的图像数据
- 基于颜色列表的图像数据必须紧跟在图像标识符后面。数据的第一个字节表示
LZW
编码初始表大小的位数。图像数据(Image Data)由数据子块(Data Sub-blocks)序列组成。
- 数据块的结构:
- 每个数据块,第一个字节表示当前块的大小,这个大小不包括第一个字节。这是一个可变长度的数据块,字节数在
0 ~ 255
之间。
1 | /// <summary> |
图形控制扩展(Graphic Control Extension)
- 在
89a
版本,GIF 添加了图形控制扩展块。放在一个图像块(图像标识符)的前面,用来控制紧跟在它后面的第一个图像的显示。
- 对于上图中的第四个字节的八位:
- 前三位(7~5):保留(未使用)。
- 中间三位(4~2)处置方法(Disposal Method):指出处置图形的方法,当值为:
0
- 不使用处置方法。1
- 不处置图形,把图形从当前位置移去。2
- 恢复到背景色。3
- 恢复到先前状态。4 ~ 7
- 保留(未使用 / 未定义)
- 倒数第二位(1)- 自定义用户输入标志(User Input Flag):指出是否期待用户有输入之后才继续进行下去,值为真表示期待,值为否表示不期待。用户输入可以是按回车键、鼠标点击等,可以和延迟时间一起使用。在设置的延迟时间内用户有输入则马上继续进行,没有输入则等待延迟时间结束再继续。利用延迟时间,我们可以展示出速度不均匀的 GIF 。
- 倒数第一位(0)透明颜色标志(Transparent Color Flag):值为真表示使用透明颜色。解码器会通过透明色索引在颜色列表中找到改颜色,标记为透明,当渲染图像时,标记为透明色的颜色将不会绘制,显示下面的背景。
1 | /// <summary> |
注释扩展块(Comment Extension)
- 注释扩展块的内容用来说明图形、作者或者其他任何非图形数据和控制信息的文本信息。结构如下图,其中的注释数据是序列数据子块(Data Sub-blocks),每块最多 255 个字节,最少 1 个字节。
1 | /// <summary> |
无格式文本扩展块(PlainText Extension)(图像说明扩充块)
- 无格式文本扩展块(PlainText Extension)包含
文本数据
和描绘文本
所须的参数。文本数据用7
位的 ASCII 字符编码并以图形形式显示。扩展块的结构如下图。
BlockSize
用来指定该图像扩充块的长度,其取值固定为13
。Text Grid Left Position
用来指定文字显示方格相对于逻辑屏幕左上角的X
坐标(以像素为单位)。Text Grid Top Position
用来指定文字显示方格相对于逻辑屏幕左上角的Y
坐标。Text Grid Width
用来指定文字显示方格的宽度。Text Grid Height
用来指定文字显示方格的高度。Character Cell Width
用来指定字符的宽度。Character Cell Height
用来指定字符的高度。Text Foreground Color Index
用来指定字符的前景色。Text Background Color Index
用来指定字符的背景色。
1 | /// <summary> |
应用扩展块(Application Extension)
- 应用扩展块(ApplicationExtension)包含制作该图像文件的应用程序的相关信息,它的结构如下图
Block Size
用来指定该应用程序扩充块的长度,其取值固定为12
。Identifier
用来指定应用程序名称。Authentication
用来指定应用程序的识别码。
1 | /// <summary> |
结束块(GIF Trailer)
- 结束块(GIF Trailer)表示 GIF 文件的结尾,它包含一个固定的数值:
0x3B
。
1 | /// <summary> |
GIF 解码
- 待补充(内容有点多,想偷懒)
播放 GIF
- 根据 GIF 状态、循环次数循环赋值纹理即可
1 | /// <summary> |
后记
- 其余代码几乎均为业务逻辑代码,可自行观看源码,不难理解就不赘述了
- 以下是该工具未来可能会优化的部分:
- 使用对象池对纹理进行复用
- 批处理多个协程 GIF 的播放(渲染合批)
- 以下是该工具未来可能会添加的功能:
- 在线获取 GIF 并播放
- 附该工具的 GitHub 地址:UnityGif
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SleepyLoser's Blog!
评论