DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(上)

1、前言

  在本系列教程开头的几章中,开初使用的纹理加载组件是WIC。后来在不断的编码过程中,通过慢慢的摸索学习我才发现之前可能一直有点误解WIC这个组件了。其实这个组件本身的功能是非常强悍的,并且它也伴随着Windows在不断的升级变化中,目前它已经可以支持DDS格式图片的加载与保存,我甚至用它实现了一个简单的从其它格式图片转成DDS纹理格式并保存的小工具,而且保存后的格式默认还是基于BC3压缩的。关于这个小工具,我准备继续深入了解WIC,并在时间允许的情况下完善它的基本功能后再放到GitHub上供大家学习改进。

  本章的目标,就是用WIC来实现GIF图片的加载、转换、并利用DirectComputer完成GIF的动态Alpha计算,最终实现GIF动画纹理的显示。

  本章的重点就是让大家掌握基本的DirectComputer操作,并且了解基本的GIF动画显示的原理,同时也是让大家明白DirectComputer可以作为渲染管线前的一个非常有用的,可以进行纹理、几何体等复杂变换计算的前置组件。当然也可以用于一些后处理,比如也可以用它来进行高斯模糊。总之,DirectComputer是D3D核心渲染管线的有力补充部分。

  因此对于完整的渲染来说DirectComputer组件也是一个非常有用的组件。最终本章示例通过DirectComputer操作GIF图片像素Alpha计算过程的演示,向大家介绍清楚了如何用DirectComputer来操作纹理的基本方法,这是非常重要的一个现代渲染管线或者说现代渲染技术中重要的基本方法。

  需要注意的是,为了突出本章重点的两个主题(WIC解码GIF和DirectComputer处理纹理),也为了让大家能够聚焦相应知识点,所以本章代码继续回到了单线程单显卡的渲染框架方式上来。这样也是为了降低大家的学习难度,尽快掌握核心的方法技巧,以便快速容易的进行封装并用到自己的项目中。基于这样的安排,所以本章内容中将只粘贴和讲解关键的代码段,不再大段的复制粘贴代码来占用过多的篇幅了。完整的代码大家可以去GitHub上下载查看。不便之处敬请大家谅解!

  本章程序运行后的效果截图如下:
13-ShowGIFAndResourceStatus
本章代码已全部上传至GitHub:
13-ShowGIFAndResourceStatus

2、GIF文件简介

  GIF格式的名称是Graphics Interchange Format的缩写,是在1987年由Compu Serve公司为了填补跨平台图像格式的空白而发展起来的。GIF可以被PC和Mactiontosh等多种平台上被支持。

  GIF是一种位图。位图的大致原理是:图片由许多的象素组成,每一个象素都被指定了一种颜色,这些象素综合起来就构成了图片。GIF采用的是Lempel-Zev-Welch(LZW)压缩算法,最高支持256种颜色。由于这种特性,GIF比较适用于色彩较少的图片,比如卡通造型、公司标志等等。如果碰到需要用真彩色的场合,那么GIF的表现力就有限了。GIF通常会自带一个调色板,里面存放需要用到的各种颜色。在Web运用中,图像的文件量的大小将会明显地影响到下载的速度,因此我们可以根据GIF带调色板的特性来优化调色板,减少图像使用的颜色数(有些图像用不到的颜色可以舍去),而不影响到图片的质量。

  GIF格式和其他图像格式的最大区别在于,它完全是作为一种公用标准而设计的,由于Compu Serve网络的流行,许多平台都支持GIF格式。Compu Serve通过免费发行格式说明书推广GIF,但要求使用GIF文件格式的软件要包含其版权信息的说明。

  GIF具有GIF87a和GIF89a两个版本。

  GIF87a版本是1987年推出的,一个文件存储一个图像,严格不支持透明像素;GIF87a采用LZW压缩算法,它能够在保持图像质量的前提下将图像尺寸压缩百分之二十到二十五。

  GIF89a版本是1989年推出的很有特色的版本,该版本允许一个文件存储多个图像,可实现动画功能,允许某些像素透明。在这个版本中,为GIF文档扩充了图形控制区块、备注、说明、应用程序编程接口4个区块,并提供了对透明色和多帧动画的支持。

  其中GIF89a在透明、隔行交错和动画GIF方面做出了重大改进。首先是支持透明,GIF89a允许图片中的某些部分不可见。这项特性非常重要,使得我们在某些场合能够利用这样一种特性来使图像的边缘不再呈现出矩形边框,而变成我们想要的任意形状。这些透明区域,可以很方便地在Photoshop、Fireworks中生成并且导出为GIF89a格式的GIF图片来实现。当然,透明并不意味着边框就不再存在事实上,它是存在的,只不过不显示罢了,这样可以使插入的图片和整体网页更加协调。

  本章教程主要讲解显示GIF89a格式的动画GIF图片的方法。具体过程中需要用WIC对GIF进行帧解析,然后不断更新纹理进行动画显示。本章教程中的按照GIF帧时间节奏不断动态更新纹理的方法,也可用于视频解码后用作纹理等动态效果的基本显示方法。另外因为加入了DirectComputer对解析后的纹理的处理过程,也同样可以用于各种需要在纹理正式用于渲染管线前的一些处理加工过程。

3、使用WIC加载并解析GIF文件

  在WIC组件中,本身已经带有解析GIF文件的解码器,所以加载和解析GIF文件并不复杂,代码如下调用即可:

//使用纯COM方式创建WIC类厂对象,也是调用WIC第一步要做的事情
GRS_THROW_IF_FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&g_pIWICFactory)));
//使用WIC类厂对象接口加载纹理图片,并得到一个WIC解码器对象接口,图片信息就在这个接口代表的对象中了
StringCchPrintfW(g_pszTexcuteFileName, MAX_PATH, _T("%sAssets\\\\MM.gif"), g_pszAppPath);

OPENFILENAME ofn;
RtlZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = nullptr;
ofn.lpstrFilter = L"*Gif 文件\\0*.gif\\0";
ofn.lpstrFile = g_pszTexcuteFileName;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrTitle = L"请选择一个要显示的GIF文件...";
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;

if (!GetOpenFileName(&ofn))
{
	StringCchPrintfW(g_pszTexcuteFileName, MAX_PATH, _T("%sAssets\\\\MM.gif"), g_pszAppPath);
}

//只载入文件,但不生成纹理,在渲染循环中再生成纹理
if (!LoadGIF(g_pszTexcuteFileName, g_pIWICFactory.Get(), g_stGIF))
{
	throw CGRSCOMException(E_FAIL);
}
BOOL LoadGIF(LPCWSTR pszGIFFileName, IWICImagingFactory* pIWICFactory, ST_GRS_GIF& g_stGIF, const WICColor& nDefaultBkColor)
{
	BOOL bRet = TRUE;
	try
	{
		PROPVARIANT					stCOMPropValue = {};
		DWORD						dwBGColor = 0;
		BYTE						bBkColorIndex = 0;
		WICColor					pPaletteColors[256] = {};
		UINT						nColorsCopied = 0;
		IWICPalette* pIWICPalette = NULL;
		IWICMetadataQueryReader* pIGIFMetadate = NULL;

		PropVariantInit(&stCOMPropValue);

		GRS_THROW_IF_FAILED(pIWICFactory->CreateDecoderFromFilename(
			pszGIFFileName,						// 文件名
			NULL,								// 不指定解码器,使用默认
			GENERIC_READ,						// 访问权限
			WICDecodeMetadataCacheOnDemand,		// 若需要就缓冲数据 
			&g_stGIF.m_pIWICDecoder				// 解码器对象
		));

		GRS_THROW_IF_FAILED(g_stGIF.m_pIWICDecoder->GetFrameCount(&g_stGIF.m_nFrames));
		GRS_THROW_IF_FAILED(g_stGIF.m_pIWICDecoder->GetMetadataQueryReader(&pIGIFMetadate));
		if (
			SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/logscrdesc/GlobalColorTableFlag", &stCOMPropValue))
			&& VT_BOOL == stCOMPropValue.vt
			&& stCOMPropValue.boolVal
			)
		{// 如果有背景色,则读取背景色
			PropVariantClear(&stCOMPropValue);
			if (SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/logscrdesc/BackgroundColorIndex", &stCOMPropValue)))
			{
				if (VT_UI1 == stCOMPropValue.vt)
				{
					bBkColorIndex = stCOMPropValue.bVal;
				}
				GRS_THROW_IF_FAILED(pIWICFactory->CreatePalette(&pIWICPalette));
				GRS_THROW_IF_FAILED(g_stGIF.m_pIWICDecoder->CopyPalette(pIWICPalette));
				GRS_THROW_IF_FAILED(pIWICPalette->GetColors(ARRAYSIZE(pPaletteColors), pPaletteColors, &nColorsCopied));

				// Check whether background color is outside range 
				if (bBkColorIndex <= nColorsCopied)
				{
					g_stGIF.m_nBkColor = pPaletteColors[bBkColorIndex];
				}
			}
		}
		else
		{
			g_stGIF.m_nBkColor = nDefaultBkColor;
		}

		PropVariantClear(&stCOMPropValue);

		if (
			SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/logscrdesc/Width", &stCOMPropValue))
			&& VT_UI2 == stCOMPropValue.vt
			)
		{
			g_stGIF.m_nWidth = stCOMPropValue.uiVal;
		}
		PropVariantClear(&stCOMPropValue);

		if (
			SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/logscrdesc/Height", &stCOMPropValue))
			&& VT_UI2 == stCOMPropValue.vt
			)
		{
			g_stGIF.m_nHeight = stCOMPropValue.uiVal;
		}
		PropVariantClear(&stCOMPropValue);

		if (
			SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/logscrdesc/PixelAspectRatio", &stCOMPropValue))
			&& VT_UI1 == stCOMPropValue.vt
			)
		{
			UINT uPixelAspRatio = stCOMPropValue.bVal;
			if (uPixelAspRatio != 0)
			{
				FLOAT pixelAspRatio = (uPixelAspRatio + 15.f) / 64.f;
				if (pixelAspRatio > 1.f)
				{
					g_stGIF.m_nPixelWidth = g_stGIF.m_nWidth;
					g_stGIF.m_nPixelHeight = static_cast<UINT>(g_stGIF.m_nHeight / pixelAspRatio);
				}
				else
				{
					g_stGIF.m_nPixelWidth = static_cast<UINT>(g_stGIF.m_nWidth * pixelAspRatio);
					g_stGIF.m_nPixelHeight = g_stGIF.m_nHeight;
				}
			}
			else
			{
				g_stGIF.m_nPixelWidth = g_stGIF.m_nWidth;
				g_stGIF.m_nPixelHeight = g_stGIF.m_nHeight;
			}
		}
		PropVariantClear(&stCOMPropValue);

		if (SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/appext/application", &stCOMPropValue))
			&& stCOMPropValue.vt == (VT_UI1 | VT_VECTOR)
			&& stCOMPropValue.caub.cElems == 11
			&& (!memcmp(stCOMPropValue.caub.pElems, "NETSCAPE2.0", stCOMPropValue.caub.cElems)
				||
				!memcmp(stCOMPropValue.caub.pElems, "ANIMEXTS1.0", stCOMPropValue.caub.cElems)))
		{
			PropVariantClear(&stCOMPropValue);

			if (
				SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/appext/data", &stCOMPropValue))
				&& stCOMPropValue.vt == (VT_UI1 | VT_VECTOR)
				&& stCOMPropValue.caub.cElems >= 4
				&& stCOMPropValue.caub.pElems[0] > 0
				&& stCOMPropValue.caub.pElems[1] == 1
				)
			{
				g_stGIF.m_nTotalLoopCount = MAKEWORD(stCOMPropValue.caub.pElems[2], stCOMPropValue.caub.pElems[3]);

				if (g_stGIF.m_nTotalLoopCount != 0)
				{
					g_stGIF.m_bHasLoop = TRUE;
				}
				else
				{
					g_stGIF.m_bHasLoop = FALSE;
				}
			}
		}
	}
	catch (CGRSCOMException & e)
	{
		e;
		bRet = FALSE;
	}
	return bRet;
}

  上面两段代码中,为了能够切换加载不同的GIF,所以特意使用了Windows的打开文件对话框,并且将具体GIF文件加载和解析的过程封装成了函数LoadGIF()。在程序运行过程中可以随时通过按下Tab键来选择切换不同的GIF。需要注意的是切换GIF的过程代码在CPU和GPU同步控制上有问题,因为牵扯到复杂的消息循环旁路、以及GPU资源的释放重加载等,实际需要的控制比这里要复杂的多,但为了尽量简单的演示核心原理的缘故,这个同步过程我就没有再进一步细究和修正了,因为这可能还牵扯到一定的封装问题,也背离了本教程系列的一贯宗旨,所以最终这个问题就没有再去纠结了。大家可以在示例的基础上,通过封装(主要是封装函数即可做到)来自行修正它。

  OK,让我们回到代码本身的讨论上来,第一段代码没什么稀奇的,就是把WIC的基本工厂接口创建好,以备后用。接着就弹出文件选择对话框,选择一个需要显示的GIF文件。

  真正的重点是在LoadGIF函数中。首先调用了WIC工厂接口的CreateDecoderFromFilename方法来加载和创建指定GIF文件的解码器,其实这个函数在成功返回后,内部其实已经完成了全部GIF加载和解码的工作。一般的其实任何的图片文件,包括DDS文件都可以使用这个WIC函数来进行加载解码,如果指定的文件格式正确并能够被WIC解析,那么一般都会成功返回,如果返回失败就可以通过返回的错误码获知出错的原因,如果是不被支持的格式,就不能简单的这样处理了,这种情况下如果非要用WIC,就需要自己为WIC设计并开发对应的文件格式解码器了,具体的方式大家可以去看下MSDN中的例子,这里就不要纠结了。目前,只要知道至少标准的GIF能够被加载和解析就行了。

  接着通过调用解码器对象的GetFrameCount()方法得到被解析的GIF动画的帧数。紧接着最重要的调用就是解码器对象的GetMetadataQueryReader()方法,这个方法其实也就是整个WIC中比较有特色或者比较核心的一个方法了。它会返回一个统一的关于被解析的图片文件的相应属性集合读取的接口。通过这个接口以及类似XML Path路径式的文法属性索引,就可以进一步读取关于文件的全部属性信息,包括扩展属性或隐藏属性。而这些属性在WIC中都被叫做Metadate(元数据)。

  因此获取到Metadate接口之后,就是逐项获取我们关心的关于GIF的一些详细的属性信息了。

3.1、解析GIF全局背景色

  通过GetMetadataByName(L"/logscrdesc/GlobalColorTableFlag", &stCOMPropValue),即可以判定GIF中是否带有调色板,

  关于调色板在GIF简介中已经说了。因为GIF终归是个非常古老的格式,那个年代为了能显示尽可能丰富的色彩并缩小体积以及兼容各种硬件,大多数图片格式都是被设计的带有一个最大256色的调色板的,所谓调色板其实就是一组可以有24Bit真彩色颜色值的数组,这样256个颜色组成一个调色板,图片中的像素就只需要8Bit来索引不同的调色板颜色值,从而达到了缩小整个图片大小的目的,当然代价就是色彩空间上的牺牲,一般都是将原来图片中相近颜色值用统一的一个颜色值来替代,这样颜色层次本身比较丰富的图片,最终经过这样的“色彩空间压缩“之后,就会失真,当然如果颜色层次不丰富的图片,影响不是很大。所以最终代码中就先来判定这个GIF中是否有调色板。但是需要注意的是其实我们例子中显示GIF的时候是不用调色板的,这里的调用只是为了在调色板中找到GIF的背景色,以便用背景色先填充整个图片背景。

  另外就需要注意函数的第一个参数L"/logscrdesc/GlobalColorTableFlag",这其实是类似XPath(XML中的术语)一种属性检索方式,也是当下比较流行的一种属性定位表达语法,很多其他的语言或软件包中都大量使用了这种方式。在WIC中使用这种方式,显然不是为了赶时髦而已,真正的目的是为了用统一的接口来访问不同格式图片文件中的千差万别的属性值。这样在接口不变的情况下,不同文件中不同的属性值只需要用不同的Path语法字符串来索引即可。这样与传统的图形软件包相比,WIC在编码的方便性、一致性、扩展性等方面就有了比较明显的优势。在传统的如CxImage这类图片处理库中,甚至Windows自身之前的读取BMP的相关方法中,都大量的使用了结构体,这为编程带来了很大的麻烦,因为现在很多流行的图片格式也是经常处在不断变换的情况中,而且主要变化的就是文件格式中的各种属性及相关结构,这样一来基于结构体的编程方式,自然就面临着随时可能失效的挑战。

  通过GetMetadataByName(L"/logscrdesc/BackgroundColorIndex", &stCOMPropValue)的调用,就得到了GIF全局默认背景色在调色板数组中的索引值。然后通过下面三个调用,就最终得到了全局背景色:

GRS_THROW_IF_FAILED(pIWICFactory->CreatePalette(&pIWICPalette)); 
GRS_THROW_IF_FAILED(g_stGIF.m_pIWICDecoder->CopyPalette(pIWICPalette));
GRS_THROW_IF_FAILED(pIWICPalette->GetColors(ARRAYSIZE(pPaletteColors), pPaletteColors, &nColorsCopied));

3.2、获取GIF的像素尺寸

  不论显示任何图片,都需要获得其像素尺寸,这里就通过下面两个调用即可:

pIGIFMetadate->GetMetadataByName(L"/logscrdesc/Width", &stCOMPropValue)			
pIGIFMetadate->GetMetadataByName(L"/logscrdesc/Height", &stCOMPropValue)

  这里需要注意的是,这样得到的尺寸是图片的逻辑像素尺寸,即假设像素无论真实物理大小,都被假定为是正方形。原因看后面这个调用就明白了。

3.3、获取像素纵横比修正图片像素尺寸

  GIF作为一种古老的图片格式,当初它还考虑了设备无关显示的一些特性,其中像素纵横比就是这样的特性之一,因为彼时,设备的像素还比较粗糙,像素点比较大,而且像素点都不是正方形或圆形等规则图形,往往都是有一定边长比例的矩形,这样为了适应不同边长比例的矩形像素点阵上的正确显示,那么就必须记录最初GIF生成时每个像素点矩形边长的纵横比例。并且还需要换算成正确的图片最终显示的宽高属性。具体的就是下面的代码所做的:

if (
	SUCCEEDED(pIGIFMetadate->GetMetadataByName(L"/logscrdesc/PixelAspectRatio", &stCOMPropValue))
	&& VT_UI1 == stCOMPropValue.vt
	)
{
	UINT uPixelAspRatio = stCOMPropValue.bVal;
	if (uPixelAspRatio != 0)
	{
		FLOAT pixelAspRatio = (uPixelAspRatio + 15.f) / 64.f;
		if (pixelAspRatio > 1.f)
		{
			g_stGIF.m_nPixelWidth = g_stGIF.m_nWidth;
			g_stGIF.m_nPixelHeight = static_cast<UINT>(g_stGIF.m_nHeight / pixelAspRatio);
		}
		else
		{
			g_stGIF.m_nPixelWidth = static_cast<UINT>(g_stGIF.m_nWidth * pixelAspRatio);
			g_stGIF.m_nPixelHeight = g_stGIF.m_nHeight;
		}
	}
	else
	{
		g_stGIF.m_nPixelWidth = g_stGIF.m_nWidth;
		g_stGIF.m_nPixelHeight = g_stGIF.m_nHeight;
	}
}

  其中详细的计算过程就不多啰嗦了,代码中已经计算的很清楚了,无非就是横向比例大了,就按比例缩小高度,反之就缩小宽度,这样最终图片的大小就正确的反映了原始图片的宽高大小。

  最后LoadGIF()函数中,还读取了GIF的具体应用信息,并且读取了其中是否有循环次数的属性,而这对我们的显示来说影响并不大,我们总是从第一帧播放到最后一帧然后自动跳回第一帧循环播放动画。这里就不啰嗦介绍了,大家可以自行阅读代码加以理解。

3.4、读取GIF帧和属性

  从本质上说,任何动态的视频或动画,其实都是一幅幅可连续播放的图片而已,只是各自采用的存储格式、压缩方式等方面千差万别而已。最终针对这些图片的显示,在解码了文件的基础上就需要进一步解析出一幅幅图片,然后按照一定的时间间隔连续绘制,即可形成连续的动态画面。对于GIF来说,也是这样,所以接下来就是继续解析一副副图片的内容和属性。

  加载GIF的帧,因为之前说的我特意做了一个Tab键切换不同GIF文件的功能,所以也被分装成了独立的函数LoadGIFFrame()。这个函数一开始就先通过解码器的GetFrame函数得到GIF中指定索引的图片,这个函数在本教程的第二篇中已经介绍过了,只是这里带上了图片的序号,之前加载的文件实质上都只有一张图片,所以默认都是加载0索引的图片即第一幅图片即可,本例中就需要使用索引号了,当然最大图片数刚才已经得到了,剩下的就是按照顺序一帧一帧的读取即可。

  读取到具体的一帧画面后,接着要做的就是将画面转换为D3D12兼容的纹理格式,而这个转换的过程与第二篇教程中的过程也是一样的,这里就不再赘述了。

  得到了GIF的具体帧及接口之后,就需要详细的读取每一帧的属性了。首先要像下面这样读取帧延迟时间,也就是这一帧要显示多长时间,单位默认是10毫秒的整数倍,具体代码如下:

// 获取帧延迟时间
if (SUCCEEDED(pIWICMetadata->GetMetadataByName(L"/grctlext/Delay", &stCOMPropValue)))
{
	if (VT_UI2 == stCOMPropValue.vt)
	{
		// Convert the delay retrieved in 10 ms units to a delay in 1 ms units
		GRS_THROW_IF_FAILED(UIntMult(stCOMPropValue.uiVal, 10, &stGIFFrame.m_nFrameDelay));
	}
}

  需要注意的就是,这些属性值,包括前面的整个GIF文件的属性值,在WIC读取出来时都是用了标准的COM“万金油”数据类型PROPVARIANT,这种类型本质上是一种自带类型说明的,再用多种数据类型变量形成联合体(union)之后的一个结构体(struct),对它的操作有一组标准的COM函数,整个代码中都在使用这组标准的COM函数来操作这个数据类型,这样做的好处就是不用去碰具体数据里面的那个复杂的联合体,更不用去判断具体的数据类型是否正确等,只需要按照默认的属性类型去调用相应的函数即可。这里调用的就是UIntMult函数,让属性的UINT分量乘以10然后存入自定义的GIF帧信息结构体中,以备在渲染循环中判定帧延迟时间是否到时间了。这个函数内部就会首先判定数据类型是否是UINT,然后再按照类型安全的要求进行指定的操作,因为是UINT值,所以它还会判定乘积的结果是否超出了UINT可以表示的最大值范围,并且它的返回值也是标准的COM HRESULT数据类型,也方便代码中统一按照COM标准返回值的判定方式进行处理。这里提醒大家的就是,针对这样的变量,请都使用标准的COM函数来处理,详细的API列表及帮助可以在MSDN中搜索。

  接下去要读取的就是具体的一帧GIF画面相对于整个GIF的显示位置信息,也就是左上角的相对偏移,以及这一帧画面的具体宽高值,读取的代码如下:

if (SUCCEEDED(pIWICMetadata->GetMetadataByName(L"/imgdesc/Left", &stCOMPropValue)))
{
	if (VT_UI2 == stCOMPropValue.vt)
	{
		stGIFFrame.m_rtGIFFrame.m_fLeft = static_cast<FLOAT>(stCOMPropValue.uiVal);
	}
}
PropVariantClear(&stCOMPropValue);

if (SUCCEEDED(pIWICMetadata->GetMetadataByName(L"/imgdesc/Top", &stCOMPropValue)))
{
	if (VT_UI2 == stCOMPropValue.vt)
	{
		stGIFFrame.m_rtGIFFrame.m_fTop = static_cast<FLOAT>(stCOMPropValue.uiVal);
	}
}
PropVariantClear(&stCOMPropValue);

if (SUCCEEDED(pIWICMetadata->GetMetadataByName(L"/imgdesc/Width", &stCOMPropValue)))
{
	if (VT_UI2 == stCOMPropValue.vt)
	{
		stGIFFrame.m_rtGIFFrame.m_fRight = static_cast<FLOAT>(stCOMPropValue.uiVal)
			+ stGIFFrame.m_rtGIFFrame.m_fLeft;
	}
}
PropVariantClear(&stCOMPropValue);

if (SUCCEEDED(pIWICMetadata->GetMetadataByName(L"/imgdesc/Height", &stCOMPropValue)))
{
	if (VT_UI2 == stCOMPropValue.vt)
	{
		stGIFFrame.m_rtGIFFrame.m_fBottom = static_cast<FLOAT>(stCOMPropValue.uiVal)
			+ stGIFFrame.m_rtGIFFrame.m_fTop;
	}
}

  上面的代码及相应属性都很好理解,也就不再啰嗦解释了。只是这里的宽高指的是这一帧画面的宽高,与读取的整个GIF的宽高含义是不一样的。可以理解为整个GIF宽高是指整个GIF占用多大的屏幕像素矩形,而具体的一帧画面是指在整个GIF像素矩形中的一个偏移相对位置后的小矩形。这样安排的目的主要是为了仅存储每一帧画面相对于上一帧画面中变化的部分,而不变的部分也就是在这个小矩形之外的部分就不去存储了,这样在一些情况下就可以大大减少每一帧具体画面需要占用的存储空间。这样综合前面介绍的调色板技术,以及这里的只存储变化小矩形部分的方法,就可以看出GIF为什么能够在那么小的体量下存储一个小规模的动画内容了。其实这里的只存储变化部分的思想也是很多视频压缩算法的一个主要思路。

  最后需要读取的GIF帧的关键属性就是Disposal(清理方式)属性了,它表示本帧图像信息在显示之前,需要对背景或上一帧画面显示后的结果做怎样的处理。刚才已经介绍了,为了减少存储占用GIF中的一帧,总是想办法只存储与上一帧画面中不同的部分,但其实稍微一想,仅仅把这个范围限制成一个矩形,其实实际能做到的存储优化是很有限的,现实中,其实一帧画面相对于前一帧画面的变化部分往往是不规则的,用矩形只能说框出一个变化范围的最小矩形而已,但这个最小矩形在最糟糕的情况下可能就是整个GIF的大小。而即使是这种最坏的情况下,实质变化的内容又往往可能是一个很不规则的范围,这时GIF在存储策略上,就又想出具体标记每个变化范围内的像素点的透明度来实现这种不规则部分的存储,具体的就是某个像素相对于前一帧的同一像素是否有变化,有变化的就存储调色板中对应颜色的索引值,而无变化的像素就简单的存个0值表示该点透明即可,具体存储时完全可以用1bit来标记这个透明值,这样相对于8bit的调色板索引又进一步节省了7bit。根据这样的存储优化策略,以及在连续画-擦-画的循环显示每帧画面的过程中,就还需要标记清楚这一帧画面要怎样清理上一帧画面的策略了(不变化的部分要按原样显示),这就是Disposal属性中存储的值的含义了。具体的该属性一般取下面这些值:

enum EM_GRS_DISPOSAL_METHODS
{
	DM_UNDEFINED = 0,
	DM_NONE = 1,
	DM_BACKGROUND = 2,
	DM_PREVIOUS = 3
};

  具体的含义就是说,当Disposal值是0(DM_UNDEFINED)时,关于背景清理的操作是未定义的,也就是不清理背景,这时一般就是保留画板上GIF显示区域的背景色,不做任何处理,然后直接显示当前帧即可(实际中一般都按与1值相同的处理方式)。

  当Disposal值是1(DM_NONE)时就表示保留前一帧渲染结果画面做背景,不要擦除,这一帧画面就直接与被保留的前一帧画面进行Alpha混色处理即可,其实这里的Alpha混色也非常简单,即当前帧的像素是0值时就显示被保留的像素颜色,否则就用当前帧像素的颜色值填充对应的像素即可。

  当Disposal值是2(DM_BACKGROUND)时,就表示先将画板对应子帧部分用之前读取到的GIF背景色进行填充,然后再按照Alpha方式来绘制当前帧。

  当Disposal值是3(DM_PREVIOUS)时,就表示保留前一帧,当前帧不显示,并跳过,这个值一般很少见,因为这个效果通过延长上一帧的延时值即可做到,所以一般不设置该值。

3.5、创建GIF纹理(画板)和帧纹理

  最终GIF的信息以及对应的帧画面及属性信息都读取并转换完毕后,就需要创建代表整个GIF的纹理和每一帧的纹理。在示例中,GIF纹理是按照整个GIF的像素大小创建的一个纹理,并且要求它可以无序访问(D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS),因为它实质上就是后续每一帧画面在其上进行绘制的画板。

  而具体的每一帧画面没有一次性全部加载,对于动画显示来说,这即无必要,也极度的浪费资源,同时考虑到示例原理还要具备一定的通用性,所以对于每帧画面都是需要时在读取转换,并最终加载到一个单独的帧像素大小的纹理上的。这里要注意的是,帧的像素大小有时候是与GIF本身的大小不一样的,原因就是刚才已经叙述过的为了节省存储空间,有些GIF帧画面只是存储了相对于上一帧变换的部分,这样其大小大多数时候是小于整个GIF大小的,并且还带有相对的偏移位置信息。

  这样在渲染循环中,就有一个根据帧延时值判定显示时长是否足够了,然后逐帧加载画面并创建和上传纹理的过程。这部分代码就没什么其它新奇的地方了,就不再粘贴占用篇幅了。其中主要就是调用LoadGIFFrame()函数和UploadGIFFrame()函数。而UploadGIFFrame()的核心功能就跟第二篇教程中加载并上传图片纹理至默认堆的两遍Copy过程基本大同小异了。

  最后再强调一点,因为GIF本质上是一种8bit像素宽的图片格式,有时甚至还用了LZW压缩,所以最终都需要用WIC的格式转换功能转换为D3D12兼容的纹理格式,而一般都是被转换为DXGI_FORMAT_R8G8B8A8_UNORM的格式,其中的RGB值其实就是根据8bit像素值查询调色板数组后得到的24位颜色值,而Alpha值根据前述的GIF存储优化原理,其实转换后只有0或1两个值,也即当前像素要么透明(0),要么不透明(1)。后面介绍的DirectComputer处理过程中,就充分的利用了这个特性。这里提示一下,如果您的机器内存足够用,或者预期机器的内存足够用,那么这个过程就不用每次都重新加载一帧读取一帧转换一帧,而可以根据帧数量创建数组保留转换后的结果,或者只是缓冲一定数量的转换后的帧画面,以加快显示的效率。当然显示的速度最终是由每帧的延时值来决定的,这个不要轻易加快,不然画面的播放速度不正常,动画看上去也会很诡异。(提示:有些说法中也将3D动画称为4D技术,即3D画面/3D视觉效果+1D时间轴,甚至再加上声音,又被称为5D技术,以此类推,这里的动画也可以被称为4D技术,不论什么说法,都必须保证每个维度中的时间是同步的,否则也被称为失真。)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片