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技術,不論什麼說法,都必須保證每個維度中的時間是同步的,否則也被稱爲失真。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章