DirectX11--深入理解與使用2D紋理資源


前言

寫教程到現在,我發現有關紋理資源的一些解說和應用都寫的太過分散,導致連我自己找起來都不方便。現在決定把這部分的內容整合起來,盡可能做到一篇搞定所有2D紋理相關的內容,其中包括:

  1. 紋理映射的基礎回顧
  2. DirectXTex庫中的DDSTextureLoader、WICTextureLoader和ScreenGrab
  3. 2D紋理的一般創建方法
  4. 2D紋理數組的一般創建方法
  5. 2D紋理立方體的一般創建方法
  6. 紋理子資源
  7. 紋理資源的完整復制
  8. 紋理子資源指定區域的復制
  9. 紋理從GPU映射回CPU進行讀寫
  10. 使用內存初始化紋理
  11. (有待填坑)使用多重采樣紋理

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

紋理映射的基礎回顧

由於內容重復,這里只給出跳轉鏈接:

紋理坐標系

過濾器

對紋理進行采樣

DDSTextureLoader和WICTextureLoader庫

DDS位圖和WIC位圖

DDS是一種圖片格式,是DirectDraw Surface的縮寫,它是DirectX紋理壓縮(DirectX Texture Compression,簡稱DXTC)的產物。由NVIDIA公司開發。大部分3D游戲引擎都可以使用DDS格式的圖片用作貼圖,也可以制作法線貼圖。其中dds格式支持1D紋理、2D紋理、2D紋理數組、2D紋理立方體、3D紋理,支持mipmaps,而且你還可以進行紋理壓縮。

WIC(Windows Imaging Component)是一個可以擴展的平台,為數字圖像提供底層API,它可以支持bmp、dng、ico、jpeg、png、tiff等格式的位圖的編碼與解碼。

如何添加進你的項目

DirectXTex中打開DDSTextureLoader文件夾和WICTextureLoader文件夾,分別找到對應的頭文件和源文件(不帶12的),並加入到你的項目中

DDSTextureLoader

CreateDDSTextureFromFile函數--從文件讀取DDS紋理

HRESULT CreateDDSTextureFromFile(
    ID3D11Device* d3dDevice,                // [In]D3D設備
    const wchar_t* szFileName,              // [In]dds圖片文件名
    ID3D11Resource** texture,               // [Out]輸出一個指向資源接口類的指針,也可以填nullptr
    ID3D11ShaderResourceView** textureView, // [Out]輸出一個指向着色器資源視圖的指針,也可以填nullptr
    size_t maxsize = 0,                     // [In]限制紋理最大寬高,若超過則內部會縮放,默認0不限制
    DDS_ALPHA_MODE* alphaMode = nullptr);  	// [In]忽略
	
HRESULT CreateDDSTextureFromFile(
	ID3D11Device* d3dDevice,				// [In]D3D設備
	ID3D11DeviceContext* d3dContext,		// [In]D3D設備上下文
	const wchar_t* szFileName,				// [In]dds圖片文件名
	ID3D11Resource** texture,				// [Out]輸出一個指向資源接口類的指針,也可以填nullptr
	ID3D11ShaderResourceView** textureView,	// [Out]輸出一個指向着色器資源視圖的指針,也可以填nullptr
	size_t maxsize = 0,						// [In]限制紋理最大寬高,若超過則內部會縮放,默認0不限制
	DDS_ALPHA_MODE* alphaMode = nullptr);	// [In]忽略

第二個重載版本用於為DDS位圖生成mipmaps,但大多數情況下你能載入的DDS位圖本身都自帶mipmaps了,與其運行時生成,不如提前為它制作mipmaps。

CreateDDSTextureFromFileEx函數--從文件讀取DDS紋理的增強版

上面兩個函數都使用了這個函數,而且如果你想要更強的擴展性,就可以了解一下:

HRESULT CreateDDSTextureFromFileEx(
    ID3D11Device* d3dDevice,                // [In]D3D設備
    const wchar_t* szFileName,              // [In].dds文件名
    size_t maxsize,                         // [In]限制紋理最大寬高,若超過則內部會縮放,默認0不限制
    D3D11_USAGE usage,                      // [In]使用D3D11_USAGE枚舉值指定數據的CPU/GPU訪問權限
    unsigned int bindFlags,                 // [In]使用D3D11_BIND_FLAG枚舉來決定該數據的使用類型
    unsigned int cpuAccessFlags,            // [In]D3D11_CPU_ACCESS_FLAG枚舉值
    unsigned int miscFlags,                 // [In]D3D11_RESOURCE_MISC_FLAG枚舉值
    bool forceSRGB,                         // [In]強制使用SRGB,默認false
    ID3D11Resource** texture,               // [Out]獲取創建好的紋理(可選)
    ID3D11ShaderResourceView** textureView, // [Out]獲取創建好的紋理資源視圖(可選)
    DDS_ALPHA_MODE* alphaMode = nullptr);   // [Out]忽略(可選)
	
HRESULT CreateDDSTextureFromFileEx(
    ID3D11Device* d3dDevice,                // [In]D3D設備
    ID3D11DeviceContext* d3dContext,        // [In]D3D設備上下文
    const wchar_t* szFileName,              // [In].dds文件名
    size_t maxsize,                         // [In]限制紋理最大寬高,若超過則內部會縮放,默認0不限制
    D3D11_USAGE usage,                      // [In]使用D3D11_USAGE枚舉值指定數據的CPU/GPU訪問權限
    unsigned int bindFlags,                 // [In]使用D3D11_BIND_FLAG枚舉來決定該數據的使用類型
    unsigned int cpuAccessFlags,            // [In]D3D11_CPU_ACCESS_FLAG枚舉值
    unsigned int miscFlags,                 // [In]D3D11_RESOURCE_MISC_FLAG枚舉值
    bool forceSRGB,                         // [In]強制使用SRGB,默認false
    ID3D11Resource** texture,               // [Out]獲取創建好的紋理(可選)
    ID3D11ShaderResourceView** textureView, // [Out]獲取創建好的紋理資源視圖(可選)
    DDS_ALPHA_MODE* alphaMode = nullptr);   // [Out]忽略(可選)

CreateDDSTextureFromMemory函數--從內存創建DDS紋理

這里我只介紹簡易版本的,因為跟上面提到的函數差別只是讀取來源不一樣,其余參數我就不再贅述:

HRESULT CreateDDSTextureFromMemory(
	ID3D11Device* d3dDevice,				// [In]D3D設備
	const uint8_t* ddsData,					// [In]原dds文件讀取到的完整二進制流
	size_t ddsDataSize,						// [In]原dds文件的大小
	ID3D11Resource** texture,				// [Out]獲取創建好的紋理(可選)
	ID3D11ShaderResourceView** textureView,	// [Out]獲取創建好的紋理資源視圖(可選)
	size_t maxsize = 0,						// [In]限制紋理最大寬高,若超過則內部會縮放,默認0不限制
	DDS_ALPHA_MODE* alphaMode = nullptr);	// [Out]忽略(可選)

如果你需要生成mipmaps,就使用帶D3D設備上下文的重載版本。

WICTextureLoader

CreateWICTextureFromFileEx

由於用法上和DDSTextureLoader大同小異,我這里也只提CreateWICTextureFromFileEx函數:

HRESULT CreateWICTextureFromFileEx(
	ID3D11Device* d3dDevice,				// [In]D3D設備
	const wchar_t* szFileName,				// [In]位圖文件名
	size_t maxsize,							// [In]限制紋理最大寬高,若超過則內部會縮放,默認0不限制
	D3D11_USAGE usage,						// [In]使用D3D11_USAGE枚舉值指定數據的CPU/GPU訪問權限
	unsigned int bindFlags,					// [In]使用D3D11_BIND_FLAG枚舉來決定該數據的使用類型
	unsigned int cpuAccessFlags,			// [In]D3D11_CPU_ACCESS_FLAG枚舉值
	unsigned int miscFlags,					// [In]D3D11_RESOURCE_MISC_FLAG枚舉值
	unsigned int loadFlags,					// [In]默認WIC_LOADER_DEAULT
	ID3D11Resource** texture,				// [Out]獲取創建好的紋理(可選)
	ID3D11ShaderResourceView** textureView);// [Out]獲取創建好的紋理資源視圖(可選)

ScreenGrab庫

ScreenGrab既可以用於屏幕截屏輸出,也可以將你在程序中制作好的紋理輸出到文件。

DirectXTex中找到ScreenGrab文件夾,將ScreenGrab.hScreenGrab.cpp加入到你的項目中即可使用。

但為了能保存WIC類別的位圖,還需要包含頭文件wincodec.h以使用里面一些關於WIC控件的GUID。ScreenGrab的函數位於名稱空間DirectX內。

SaveDDSTextureToFile函數--以.dds格式保存紋理

HRESULT SaveDDSTextureToFile(
	ID3D11DeviceContext* pContext,	// [In]設備上下文
	ID3D11Resource* pSource,		// [In]必須為包含ID3D11Texture2D接口類的指針
	const wchar_t* fileName );		// [In]輸出文件名

理論上它可以保存紋理、紋理數組、紋理立方體。

SaveWICTextureToFile函數--以指定WIC型別的格式保存紋理

HRESULT SaveWICTextureToFile(
	ID3D11DeviceContext* pContext,	// [In]設備上下文
    ID3D11Resource* pSource,		// [In]必須為包含ID3D11Texture2D接口類的指針
    REFGUID guidContainerFormat, 	// [In]需要轉換的圖片格式對應的GUID引用
    const wchar_t* fileName,		// [In]輸出文件名
    const GUID* targetFormat = nullptr,		// [In]忽略
    std::function<void(IPropertyBag2*)> setCustomProps = nullptr );	// [In]忽略

下表給出了常用的GUID:

GUID 文件格式
GUID_ContainerFormatPng png
GUID_ContainerFormatJpeg jpg
GUID_ContainerFormatBmp bmp
GUID_ContainerFormatTiff tif

這里演示了如何保存后備緩沖區紋理到文件:

ComPtr<ID3D11Texture2D> backBuffer;
// 輸出截屏
mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf()));
HR(SaveDDSTextureToFile(md3dImmediateContext.Get(), backBuffer.Get(), L"Screenshot\\output.dds"));
HR(SaveWICTextureToFile(md3dImmediateContext.Get(), backBuffer.Get(), GUID_ContainerFormatPng, L"Screenshot\\output.png"));

如果輸出的dds文件打開后發現圖像質量貌似有問題,你可以檢驗輸出紋理的alpha值(關閉Alpha通道查看位圖通常可以恢復正常),也可以嘗試用DDSView程序來打開文件觀看(圖像本身可能沒有問題但程序不能完美預覽高版本產生的dds文件)。

2D紋理

Direct3D 11允許我們創建1D紋理、2D紋理、3D紋理,分別對應的接口為ID3D11Texture1D, ID3D11Texture2DID3D11Texture3D。創建出來的對象理論上不僅在內存中占用了它的實現類所需空間,還在顯存中占用了一定空間以存放紋理的實際數據。

由於實際上我們最常用到的就是2D紋理,因此這里不會討論1D紋理和3D紋理的內容。

首先讓我們看看D3D11對一個2D紋理的描述:

typedef struct D3D11_TEXTURE2D_DESC
{
    UINT Width;         			// 紋理寬度
    UINT Height;        			// 紋理高度
    UINT MipLevels;     			// 允許的Mip等級數
    UINT ArraySize;     			// 可以用於創建紋理數組,這里指定紋理的數目,單個紋理使用1
    DXGI_FORMAT Format; 			// DXGI支持的數據格式,默認DXGI_FORMAT_R8G8B8A8_UNORM
    DXGI_SAMPLE_DESC SampleDesc;    // MSAA描述
    D3D11_USAGE Usage;  			// 使用D3D11_USAGE枚舉值指定數據的CPU/GPU訪問權限
    UINT BindFlags;     			// 使用D3D11_BIND_FLAG枚舉來決定該數據的使用類型
    UINT CPUAccessFlags;    		// 使用D3D11_CPU_ACCESS_FLAG枚舉來決定CPU訪問權限
    UINT MiscFlags;     			// 使用D3D11_RESOURCE_MISC_FLAG枚舉
}   D3D11_TEXTURE2D_DESC;

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;                     // MSAA采樣數
    UINT Quality;                   // MSAA質量等級
} DXGI_SAMPLE_DESC;

這里特別要講一下MipLevels

  1. 如果你希望它不產生mipmap,則應當指定為1(只包含最大的位圖本身)
  2. 如果你希望它能夠產生完整的mipmap,可以指定為0,這樣你就不需要手工去算這個紋理最大支持的mipmap等級數了,在創建好紋理后,可以再調用ID3D11Texture2D::GetDesc來查看實際的MipLevels值是多少
  3. 如果你指定的是其它的值,這里舉個例子,該紋理的寬高為400x400,mip等級為3時,該紋理會產生400x400200x200100x100的mipmap

對於經常作為着色器資源的紋理,通常是不能對其開啟MSAA的,應當把Count設為1,Quality設為0

緊接着是DXGI_FORMAT

它用於指定紋理存儲的數據格式,最常用的就是DXGI_FORMAT_R8G8B8A8_UNORM了。這種格式在內存的排布可以用下面的結構體表示:

struct {
	uint8_t r;
	uint8_t g;
	uint8_t b;
	uint8_t a;
};

了解這個對我們后期通過內存填充紋理十分重要。

然后是Usage

D3D11_USAGE CPU讀 CPU寫 GPU讀 GPU寫
D3D11_USAGE_DEFAULT
D3D11_USAGE_IMMUTABLE
D3D11_USAGE_DYNAMIC
D3D11_USAGE_STAGING

如果一個紋理以D3D11_USAGE_DEFAULT的方式創建,那么它可以使用下面的這些方法來更新紋理:

  1. ID3D11DeviceContext::UpdateSubresource
  2. ID3D11DeviceContext::CopyResource
  3. ID3D11DeviceContext::CopySubresourceRegion

通過DDSTextureLoaderWICTextureLoader創建出來的紋理默認都是這種類型

而如果一個紋理以D3D11_USAGE_IMMUTABLE的方式創建,則必須在創建階段就完成紋理資源的初始化。此后GPU只能讀取,也無法對紋理再進行修改

D3D11_USAGE_DYNAMIC創建的紋理通常需要頻繁從CPU寫入,使用ID3D11DeviceContext::Map方法將顯存映射回內存,經過修改后再調用ID3D11DeviceContext::Unmap方法應用更改。而且它對紋理有諸多的要求,直接從下面的ERROR可以看到:
D3D11 ERROR: ID3D11Device::CreateTexture2D: A D3D11_USAGE_DYNAMIC Resource must have ArraySize equal to 1. [ STATE_CREATION ERROR #101: CREATETEXTURE2D_INVALIDDIMENSIONS]
D3D11 ERROR: ID3D11Device::CreateTexture2D: A D3D11_USAGE_DYNAMIC Resource must have MipLevels equal to 1. [ STATE_CREATION ERROR #102: CREATETEXTURE2D_INVALIDMIPLEVELS]

上面說到,紋理只能是單個,不能是數組,且mip等級只能是1,即不能有mipmaps

D3D11_USAGE_STAGING則完全允許在CPU和GPU之間的數據傳輸,但它只能作為一個類似中轉站的資源,而不能綁定到渲染管線上,即你也不能用該紋理生成mipmaps。比如說有一個D3D11_USAGE_DEFAULT你想要從顯存拿到內存,只能通過它以ID3D11DeviceContext::CopyResource或者ID3D11DeviceContext::CopySubresourceRegion方法來復制一份到本紋理,然后再通過ID3D11DeviceContext::Map方法取出到內存。

現在來到BindFlags

以下是和紋理有關的D3D11_BIND_FLAG枚舉成員:

D3D11_BIND_FLAG 描述
D3D11_BIND_SHADER_RESOURCE 紋理可以作為着色器資源綁定到渲染管線
D3D11_BIND_STREAM_OUTPUT 紋理可以作為流輸出階段的輸出點
D3D11_BIND_RENDER_TARGET 紋理可以作為渲染目標的輸出點,並且指定它可以用於生成mipmaps
D3D11_BIND_DEPTH_STENCIL 紋理可以作為深度/模板緩沖區
D3D11_BIND_UNORDERED_ACCESS 紋理可以綁定到無序訪問視圖作為輸出

再看看CPUAccessFlags

D3D11_CPU_ACCESS_FLAG 描述
D3D11_CPU_ACCESS_WRITE 允許通過映射方式從CPU寫入,它不能作為管線的輸出,且只能用於D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING綁定的資源
D3D11_CPU_ACCESS_READ 允許通過映射方式給CPU讀取,它不能作為管線的輸入或輸出,且只能用於D3D11_USAGE_STAGING綁定的資源

可以用按位或的方式同時指定上述枚舉值,如果該flag設為0可以獲得更好的資源優化操作。

最后是和紋理相關的MiscFlags

D3D11_RESOURCE_MISC_FLAG 描述
D3D11_RESOURCE_MISC_GENERATE_MIPS 允許通過ID3D11DeviceContext::GenerateMips方法生成mipmaps
D3D11_RESOURCE_MISC_TEXTURECUBE 允許該紋理作為紋理立方體使用,要求必須是至少包含6個紋理的Texture2DArray

ID3D11Device::CreateTexture2D--創建一個2D紋理

填充好D3D11_TEXTURE2D_DESC后,你才可以用它創建一個2D紋理:

HRESULT ID3D11Device::CreateTexture2D( 
    const D3D11_TEXTURE2D_DESC *pDesc,          // [In] 2D紋理描述信息
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In] 用於初始化的資源
    ID3D11Texture2D **ppTexture2D);             // [Out] 獲取到的2D紋理

過程我就不演示了。

2D紋理的資源視圖(以着色器資源視圖為例)

創建好紋理后,我們還需要讓它綁定到資源視圖,然后再讓該資源視圖綁定到渲染管線的指定階段。

D3D11_SHADER_RESOURCE_VIEW_DESC的定義如下:

typedef struct D3D11_SHADER_RESOURCE_VIEW_DESC
    {
    DXGI_FORMAT Format;
    D3D11_SRV_DIMENSION ViewDimension;
    union 
        {
        D3D11_BUFFER_SRV Buffer;
        D3D11_TEX1D_SRV Texture1D;
        D3D11_TEX1D_ARRAY_SRV Texture1DArray;
        D3D11_TEX2D_SRV Texture2D;
        D3D11_TEX2D_ARRAY_SRV Texture2DArray;
        D3D11_TEX2DMS_SRV Texture2DMS;
        D3D11_TEX2DMS_ARRAY_SRV Texture2DMSArray;
        D3D11_TEX3D_SRV Texture3D;
        D3D11_TEXCUBE_SRV TextureCube;
        D3D11_TEXCUBE_ARRAY_SRV TextureCubeArray;
        D3D11_BUFFEREX_SRV BufferEx;
        } 	;
    } 	D3D11_SHADER_RESOURCE_VIEW_DESC;
};

其中Format要和紋理創建時的Format一致,對於2D紋理來說,應當指定D3D11_SRV_DIMENSIOND3D11_SRV_DIMENSION_TEXTURE2D

然后D3D11_TEX2D_SRV結構體定義如下:

typedef struct D3D11_TEX2D_SRV
{
    UINT MostDetailedMip;
    UINT MipLevels;
} 	D3D11_TEX2D_SRV;

通過MostDetailedMap我們可以指定開始使用的紋理子資源,MipLevels則指定使用的子資源數目。如果要使用完整mipmaps,則需要指定MostDetailedMap為0, MipLevels為-1.

例如我想像下圖那樣使用mip等級為1到2的紋理子資源,可以指定MostDetailedMip為1,MipLevels為2.

創建着色器資源視圖的演示如下:

D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Texture2D.MostDetailedMip = 0;
HR(md3dDevice->CreateShaderResourceView(tex.Get(), &srvDesc, texSRV.GetAddressOf()));

ID3D11DeviceContext::*SSetShaderResources方法--設置着色器資源

我們創建着色器資源的目的就是以它作為媒介,傳遞給着色器使用。上面打*意味着渲染管線的所有可編程着色器階段都有該方法。

此外,着色器資源視圖不僅可以綁定紋理資源,還可以綁定緩沖區資源。

目前在DDSTextureLoaderWICTextureLoader中,我們只需要用到紋理的着色器資源。這里以ID3D11DeviceContext::PSSetShaderResources為例:

void ID3D11DeviceContext::PSSetShaderResources(
	UINT StartSlot,	// [In]起始槽索引,對應HLSL的register(t*)
	UINT NumViews,	// [In]着色器資源視圖數目
	ID3D11ShaderResourceView * const *ppShaderResourceViews	// [In]着色器資源視圖數組
);

紋理子資源(Texture Subresources)

通常我們將可能包含mipmaps的紋理稱作紋理,那么紋理子資源實際上指的就是其中的一個mip等級對應的2維數組(針對2維紋理來說)。比如512x512的紋理加載進來包含的mipmap等級數(Mipmap Levels)為10,包含了從512x512, 256x256, 128x128...到1x1的10個二維數組顏色數據,這十個紋理子資源在紋理中的內存是相對緊湊的。

例如:上述紋理(R8G8B8A8格式) mip等級為1的紋理子資源首元素地址 為 從mip等級為0的紋理子資源首元素地址再偏移512x512x4字節的地址。

Direct3D API使用Mip切片(Mip slice)來指定某一mip等級的紋理子資源,也有點像索引。比如mip slice值為0時,對應的是512x512的紋理,而mip slice值1對應的是256x256,以此類推。

描述一個紋理子資源的兩種結構體:D3D11_SUBRESOURCE_DATA 和 D3D11_MAPPED_SUBRESOURCE

如果你想要為2D紋理進行初始化,那么你要接觸到的結構體類型為D3D11_SUBRESOURCE_DATA。定義如下:

typedef struct D3D11_SUBRESOURCE_DATA
{
    const void *pSysMem;	// 用於初始化的數據
    UINT SysMemPitch;		// 當前子資源一行所占的字節數(2D/3D紋理使用)
    UINT SysMemSlicePitch;	// 當前子資源一個完整切片所占的字節數(僅3D紋理使用)
} 	D3D11_SUBRESOURCE_DATA;

而如果你使用的是ID3D11DeviceContext::Map方法來獲取一個紋理子資源,那么獲取到的是D3D11_MAPPED_SUBRESOURCE,其定義如下:

typedef struct D3D11_MAPPED_SUBRESOURCE {
    void *pData;		// 映射到內存的數據or需要提交的地址范圍
    UINT RowPitch;		// 當前子資源一行所占的字節數(2D/3D紋理有意義)
    UINT DepthPitch;	// 當前子資源一個完整切片所占的字節數(僅3D紋理有意義)
} D3D11_MAPPED_SUBRESOURCE;

若一張512x512的紋理(R8G8B8A8),那么它的RowPitch為512x4=2048字節,同理在初始化一個512x512的紋理(R8G8B8A8),它的RowPitch有可能為512x4=2048字節。

注意:在運行的時候,RowPitchDepthPitch有可能會比你所期望的值更大一些,因為在每一行的數據之間有可能會填充數據進去以對齊。

通常情況下我們希望讀出來的RGBA是連續的,然而下述映射回內存的做法是錯誤的,因為每一行的數據都有填充,讀出來的話你可能會發現圖像會有錯位:

std::vector<unsigned char> imageData;
m_pd3dImmediateContext->Map(texOutputCopy.Get(), 0, D3D11_MAP_READ, 0, &mappedData);
memcpy_s(imageData.data(), texWidth * texHeight * 4, mappedData.pData, texWidth * texHeight * 4);
m_pd3dImmediateContext->Unmap(texOutputCopy.Get(), 0);

下面的讀取方式才是正確的:

std::vector<unsigned char> imageData;
m_pd3dImmediateContext->Map(texOutputCopy.Get(), 0, D3D11_MAP_READ, 0, &mappedData);
unsigned char* pData = reinterpret_cast<unsigned char*>(mappedData.pData);
for (UINT i = 0; i < texHeight; ++i)
{
    memcpy_s(&imageData[i * texWidth], texWidth * 4, pData, texWidth * 4);
    pData += mappedData.RowPitch;
}
pImpl->d3dContext->Unmap(texOutputCopy.Get(), 0);

獲取一份不允許CPU讀寫的紋理到內存中

通常這種資源的類型有可能是D3D11_USAGE_IMMUTABLE或者D3D11_USAGE_DEFAULT。我們需要按下面的步驟進行:

  1. 創建一個D3D11_USAGE_STAGING的紋理,指定CPU讀取權限,紋理寬高一致,Mip等級和數組大小都為1;
  2. 進行內存映射,然后使用ID3D11DeviceContext::CopyResource方法拷貝一份到我們新創建的紋理,注意需要嚴格按照上面提到的讀取方式進行讀取,最后解除映射。

ID3D11DeviceContext::CopyResource方法--復制一份資源

該方法通過GPU將一份完整的源資源復制到目標資源:

void ID3D11DeviceContext::CopyResource(
	ID3D11Resource *pDstResource,	// [InOut]目標資源
	ID3D11Resource *pSrcResource	// [In]源資源
);

但是需要注意:

  1. 不支持以D3D11_USAGE_IMMUTABLE創建的目標資源
  2. 兩者資源類型要一致
  3. 兩者不能是同一個指針
  4. 要有一樣的維度(包括寬度,高度,深度,大小)
  5. 要有兼容的DXGI格式,兩者格式最好是能相同,或者至少是相同的組別,比如DXGI_FORMAT_R32G32B32_FLOAT,DXGI_FORMAT_R32G32B32_UINTDXGI_FORMAT_R32G32B32_TYPELESS相互間就可以復制。
  6. 兩者任何一個在調用該方法的時候不能被映射(先前調用過ID3D11DeviceContext::Map方法又沒有Unmap)
  7. 允許深度/模板緩沖區作為源或目標資源

通過內存初始化紋理

現在我們嘗試通過代碼的形式來創建一個紋理(以項目09作為修改),代碼如下:

uint32_t ColorRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
	return (r | (g << 8) | (b << 16) | (a << 24));
}


bool GameApp::InitResource()
{
	uint32_t black = ColorRGBA(0, 0, 0, 255), orange = ColorRGBA(255, 108, 0, 255);

	// 紋理內存映射,用黑色初始化
	std::vector<uint32_t> textureMap(128 * 128, black);
	uint32_t(*textureMap)[128] = reinterpret_cast<uint32_t(*)[128]>(textureArrayMap.data());

	for (int y = 7; y <= 17; ++y)
		for (int x = 25 - y; x <= 102 + y; ++x)
			textureMap[y][x] = textureMap[127 - y][x] = orange;

	for (int y = 18; y <= 109; ++y)
		for (int x = 7; x <= 120; ++x)
			textureMap[y][x] = orange;

	// 創建紋理
	D3D11_TEXTURE2D_DESC texDesc;
	texDesc.Width = 128;
	texDesc.Height = 128;
	texDesc.MipLevels = 1;
	texDesc.ArraySize = 1;
	texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	texDesc.SampleDesc.Count = 1;		// 不使用多重采樣
	texDesc.SampleDesc.Quality = 0;
	texDesc.Usage = D3D11_USAGE_DEFAULT;
	texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	texDesc.CPUAccessFlags = 0;
	texDesc.MiscFlags = 0;	// 指定需要生成mipmap

	D3D11_SUBRESOURCE_DATA sd;
	uint32_t * pData = textureMap.data();
	sd.pSysMem = pData;
	sd.SysMemPitch = 128 * sizeof(uint32_t);
	sd.SysMemSlicePitch = 128 * 128 * sizeof(uint32_t);


	ComPtr<ID3D11Texture2D> tex;
	HR(m_pd3dDevice->CreateTexture2D(&texDesc, &sd, tex.GetAddressOf()));

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
	srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
	srvDesc.Texture2D.MipLevels = 1;
	srvDesc.Texture2D.MostDetailedMip = 0;
	HR(m_pd3dDevice->CreateShaderResourceView(tex.Get(), &srvDesc, m_pTexSRV.GetAddressOf()));
	
	// ...
}

其它部分的代碼修改就不講了,最終效果如下:

但是如果你想要以初始化的方式來創建帶mipmap的Texture2D紋理,則在初始化的時候需要提供D3D11_SUBRESOURCE_DATA數組,元素數目為MipLevels.

再或者如果你是要以初始化的方式來創建帶mipmap的Texture2D紋理數組,則提供的元素數目為MipLevels * ArraySize.

2D紋理數組

之前提到,D3D11_TEXTURE2D_DESC中可以通過指定ArraySize的值來將其創建為紋理數組。

HLSL中的2D紋理數組

首先來到HLSL代碼,我們之所以不使用下面的這種形式創建紋理數組:

Texture2D gTexArray[7] : register(t0);

// 像素着色器
float4 PS(VertexPosHTex pIn) : SV_Target
{
    float4 texColor = gTexArray[gTexIndex].Sample(gSam, float2(pIn.Tex));
    return texColor;
}

是因為這樣做的話HLSL編譯器會報錯:sampler array index must be a literal experssion,即pin.PrimID的值也必須是個字面值,而不是變量。但我們還是想要能夠根據變量取對應紋理的能力。

正確的做法應當是聲明一個Texture2DArray的數組:

Texture2DArray gTexArray : register(t0);

Texture2DArray同樣也具有Sample方法,用法示例如下:

// 像素着色器
float4 PS(VertexPosHTex pIn) : SV_Target
{
    float4 texColor = gTexArray.Sample(gSam, float3(pIn.Tex, gTexIndex));
    return texColor;
}

Sample方法的第一個參數依然是采樣器

而第二個參數則是一個3D向量,其中x與y的值對應的還是紋理坐標,而z分量即便是個float,主要是用於作為索引值選取紋理數組中的某一個具體紋理。同理索引值0對應紋理數組的第一張紋理,1對應的是第二張紋理等等...

使用紋理數組的優勢是,我們可以一次性預先創建好所有需要用到的紋理,並綁定到HLSL的紋理數組中,而不需要每次都重新綁定一個紋理。然后我們再使用索引值來訪問紋理數組中的某一紋理。

D3D11CalcSubresource函數--計算子資源的索引值

對於紋理數組,每個元素都會包含同樣的mip等級數。Direct3D API使用數組切片(array slice)來訪問不同紋理,也是相當於索引。這樣我們就可以把所有的紋理資源用下面的圖來表示,假定下圖有4個紋理,每個紋理包含3個子資源,則當前指定的是Array Slice為2,Mip Slice為1的子資源。

然后給定當前紋理數組每個紋理的mipmap等級數(Mipmap Levels),數組切片(Array Slice)和Mip切片(Mip Slice),我們就可以用下面的函數來求得指定子資源的索引值:

inline UINT D3D11CalcSubresource(UINT MipSlice, UINT ArraySlice, UINT MipLevels )
{ return MipSlice + ArraySlice * MipLevels; }

創建一個紋理數組

現在我們手頭上僅有的就是DDSTextureLoader.hWICTextureLoader.h中的函數,但這里面的函數每次都只能加載一張紋理。我們還需要修改龍書樣例中讀取紋理的函數,具體的操作順序如下:

  1. 讀取第一個存有紋理的文件,得到ID3D11Texture2D對象,並通過GetDesc獲取信息;
  2. 創建一個ID3D11Texture2D對象,它同時也是一個紋理數組;
  3. 讀取下一個紋理文件,然后檢查寬高、數據格式等屬性是否相同,再將其復制到該紋理數組中。重復直到所有紋理讀取完畢;
  4. 為該紋理數組對象創建創建一個紋理資源視圖(Shader Resource View)。

d3dUtil.h中實現了這樣一個函數:

// ------------------------------
// CreateTexture2DArrayFromFile函數
// ------------------------------
// 該函數要求所有紋理的寬高、數據格式、mip等級一致
// [In]d3dDevice			D3D設備
// [In]d3dDeviceContext		D3D設備上下文
// [In]fileNames			dds文件名數組
// [OutOpt]textureArray		輸出的紋理數組資源
// [OutOpt]textureArrayView 輸出的紋理數組資源視圖
// [In]generateMips			是否生成mipmaps
HRESULT CreateTexture2DArrayFromFile(
	ID3D11Device* d3dDevice,
	ID3D11DeviceContext* d3dDeviceContext,
	const std::vector<std::wstring>& fileNames,
	ID3D11Texture2D** textureArray,
	ID3D11ShaderResourceView** textureArrayView,
	bool generateMips = false);

在了解完整實現前你需要下面這些內容:

ID3D11DeviceContext::Map函數--獲取指向子資源中數據的指針並拒絕GPU對該子資源的訪問

HRESULT ID3D11DeviceContext::Map(
    ID3D11Resource           *pResource,          // [In]包含ID3D11Resource接口的資源對象
    UINT                     Subresource,         // [In]子資源索引
    D3D11_MAP                MapType,             // [In]D3D11_MAP枚舉值,指定讀寫相關操作
    UINT                     MapFlags,            // [In]填0,忽略
    D3D11_MAPPED_SUBRESOURCE *pMappedResource     // [Out]獲取到的已經映射到內存的子資源
);

D3D11_MAP枚舉值類型的成員如下:

D3D11_MAP成員 含義
D3D11_MAP_READ 映射到內存的資源用於讀取。該資源在創建的時候必須綁定了
D3D11_CPU_ACCESS_READ標簽
D3D11_MAP_WRITE 映射到內存的資源用於寫入。該資源在創建的時候必須綁定了
D3D11_CPU_ACCESS_WRITE標簽
D3D11_MAP_READ_WRITE 映射到內存的資源用於讀寫。該資源在創建的時候必須綁定了
D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE標簽
D3D11_MAP_WRITE_DISCARD 映射到內存的資源用於寫入,之前的資源數據將會被拋棄。該
資源在創建的時候必須綁定了D3D11_CPU_ACCESS_WRITE和
D3D11_USAGE_DYNAMIC標簽
D3D11_MAP_WRITE_NO_OVERWRITE 映射到內存的資源用於寫入,但不能復寫已經存在的資源。
該枚舉值只能用於頂點/索引緩沖區。該資源在創建的時候需要
有D3D11_CPU_ACCESS_WRITE標簽,在Direct3D 11不能用於
設置了D3D11_BIND_CONSTANT_BUFFER標簽的資源,但在
11.1后可以。具體可以查閱MSDN文檔

ID3D11DeviceContext::UpdateSubresource函數[2]--將內存數據拷貝到不可進行映射的子資源中,再從GPU拷貝到目標子資源

這個函數在之前我們主要是用來將內存數據拷貝到常量緩沖區中,現在我們也可以用它將內存數據拷貝到紋理的子資源當中:

void ID3D11DeviceContext::UpdateSubresource(
  ID3D11Resource  *pDstResource,    // [In]目標資源對象
  UINT            DstSubresource,   // [In]對於2D紋理來說,該參數為指定Mip等級的子資源
  const D3D11_BOX *pDstBox,			// [In]這里通常填nullptr,或者拷貝的數據寬高比當前子資源小時可以指定范圍	
  const void      *pSrcData,        // [In]用於拷貝的內存數據
  UINT            SrcRowPitch,      // [In]該2D紋理的 寬度*數據格式的位數
  UINT            SrcDepthPitch     // [In]對於2D紋理來說並不需要用到該參數,因此可以任意設置
);

ID3D11DeviceContext::Unmap函數--讓指向資源的指針無效並重新啟用GPU對該資源的訪問權限

void ID3D11DeviceContext::Unmap(
    ID3D11Resource *pResource,      // [In]包含ID3D11Resource接口的資源對象
    UINT           Subresource      // [In]需要取消的子資源索引
);

D3D11_TEX2D_ARRAY_SRV結構體

在創建着色器目標視圖時,你還需要填充共用體中的D3D11_TEX2D_ARRAY_SRV結構體:

typedef struct D3D11_TEX2D_ARRAY_SRV
{
    UINT MostDetailedMip;		
    UINT MipLevels;
    UINT FirstArraySlice;
    UINT ArraySize;
} 	D3D11_TEX2D_ARRAY_SRV;

通過FirstArraySlice我們可以指定開始使用的紋理,ArraySize則指定使用的紋理數目。

例如我想指定像上面那樣的范圍,可以指定FirstArraySlice為1,ArraySize為2,MostDetailedMip為1,MipLevels為2.

ID3D11DeviceContext::GenerateMips--為紋理資源視圖綁定的所有紋理創建完整的mipmap鏈

由於通過該函數讀取進來的紋理mip等級可能只有1,如果還需要創建mipmap鏈的話,還需要用到下面的方法。

void ID3D11DeviceContext::GenerateMips(
  ID3D11ShaderResourceView *pShaderResourceView	// [In]需要創建mipamp鏈的SRV
);

比如一張1024x1024的紋理,經過該方法調用后,就會生成剩余的512x512, 256x256 ... 1x1的子紋理資源,加起來一共是11級mipmap。

在調用該方法之前,需要做好大量的准備:

  1. 在創建2D紋理資源時,UsageD3D11_USAGE_DEFAULT以允許GPU寫入,BindFlags要綁定D3D11_BIND_RENDER_TARGETD3D11_BIND_SHADER_RESOURCEMiscFlags設置D3D11_RESOURCE_MISC_GENERATE_MIPS枚舉值,mipLevels設置為0使得在創建紋理的時候會自動預留出其余mipLevel所需要用到的內存大小。
  2. 如果是2D紋理,將圖片的RGBA數據寫入到子資源0中。此時創建好的紋理,子資源0為圖片內容,其余子資源為黑色。如果是2D紋理數組,你可以利用D3D11CalcSubresource為所有紋理元素的首mipLevel來填充圖片。
  3. 為該2D紋理資源創建着色器資源視圖,指定MostDetailedMip為0,MipLevels為-1以訪問完整mipmaps。

做好這些准備后你才可以調用GenerateMips,否則可能會產生異常。

CreateTexture2DArrayFromFile完整實現如下:

HRESULT CreateTexture2DArrayFromFile(
	ID3D11Device* d3dDevice,
	ID3D11DeviceContext* d3dDeviceContext,
	const std::vector<std::wstring>& fileNames,
	ID3D11Texture2D** textureArray,
	ID3D11ShaderResourceView** textureArrayView,
	bool generateMips)
{
	// 檢查設備、文件名數組是否非空
	if (!d3dDevice || fileNames.empty())
		return E_INVALIDARG;

	HRESULT hr;
	UINT arraySize = (UINT)fileNames.size();

	// ******************
	// 讀取第一個紋理
	//
	ID3D11Texture2D* pTexture;
	D3D11_TEXTURE2D_DESC texDesc;

	hr = CreateDDSTextureFromFileEx(d3dDevice,
		fileNames[0].c_str(), 0, D3D11_USAGE_STAGING, 0,
		D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
		0, false, reinterpret_cast<ID3D11Resource**>(&pTexture), nullptr);
	if (FAILED(hr))
	{
		hr = CreateWICTextureFromFileEx(d3dDevice,
			fileNames[0].c_str(), 0, D3D11_USAGE_STAGING, 0,
			D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
			0, false, reinterpret_cast<ID3D11Resource**>(&pTexture), nullptr);
	}

	if (FAILED(hr))
		return hr;

	// 讀取創建好的紋理信息
	pTexture->GetDesc(&texDesc);

	// ******************
	// 創建紋理數組
	//
	D3D11_TEXTURE2D_DESC texArrayDesc;
	texArrayDesc.Width = texDesc.Width;
	texArrayDesc.Height = texDesc.Height;
	texArrayDesc.MipLevels = generateMips ? 0 : texDesc.MipLevels;
	texArrayDesc.ArraySize = arraySize;
	texArrayDesc.Format = texDesc.Format;
	texArrayDesc.SampleDesc.Count = 1;		// 不能使用多重采樣
	texArrayDesc.SampleDesc.Quality = 0;
	texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
	texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | (generateMips ? D3D11_BIND_RENDER_TARGET : 0);
	texArrayDesc.CPUAccessFlags = 0;
	texArrayDesc.MiscFlags = (generateMips ? D3D11_RESOURCE_MISC_GENERATE_MIPS : 0);

	ID3D11Texture2D* pTexArray = nullptr;
	hr = d3dDevice->CreateTexture2D(&texArrayDesc, nullptr, &pTexArray);
	if (FAILED(hr))
	{
		SAFE_RELEASE(pTexture);
		return hr;
	}

	// 獲取實際創建的紋理數組信息
	pTexArray->GetDesc(&texArrayDesc);
	UINT updateMipLevels = generateMips ? 1 : texArrayDesc.MipLevels;

	// 寫入到紋理數組第一個元素
	D3D11_MAPPED_SUBRESOURCE mappedTex2D;
	for (UINT i = 0; i < updateMipLevels; ++i)
	{
		d3dDeviceContext->Map(pTexture, i, D3D11_MAP_READ, 0, &mappedTex2D);
		d3dDeviceContext->UpdateSubresource(pTexArray, i, nullptr,
			mappedTex2D.pData, mappedTex2D.RowPitch, mappedTex2D.DepthPitch);
		d3dDeviceContext->Unmap(pTexture, i);
	}
	SAFE_RELEASE(pTexture);

	// ******************
	// 讀取剩余的紋理並加載入紋理數組
	//
	D3D11_TEXTURE2D_DESC currTexDesc;
	for (UINT i = 1; i < texArrayDesc.ArraySize; ++i)
	{
		hr = CreateDDSTextureFromFileEx(d3dDevice,
			fileNames[0].c_str(), 0, D3D11_USAGE_STAGING, 0,
			D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
			0, false, reinterpret_cast<ID3D11Resource**>(&pTexture), nullptr);
		if (FAILED(hr))
		{
			hr = CreateWICTextureFromFileEx(d3dDevice,
				fileNames[0].c_str(), 0, D3D11_USAGE_STAGING, 0,
				D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
				0, WIC_LOADER_DEFAULT, reinterpret_cast<ID3D11Resource**>(&pTexture), nullptr);
		}

		if (FAILED(hr))
		{
			SAFE_RELEASE(pTexArray);
			return hr;
		}

		pTexture->GetDesc(&currTexDesc);
		// 需要檢驗所有紋理的mipLevels,寬度和高度,數據格式是否一致,
		// 若存在數據格式不一致的情況,請使用dxtex.exe(DirectX Texture Tool)
		// 將所有的圖片轉成一致的數據格式
		if (currTexDesc.MipLevels != texDesc.MipLevels || currTexDesc.Width != texDesc.Width ||
			currTexDesc.Height != texDesc.Height || currTexDesc.Format != texDesc.Format)
		{
			SAFE_RELEASE(pTexArray);
			SAFE_RELEASE(pTexture);
			return E_FAIL;
		}
		// 寫入到紋理數組的對應元素
		for (UINT j = 0; j < updateMipLevels; ++j)
		{
			// 允許映射索引i紋理中,索引j的mipmap等級的2D紋理
			d3dDeviceContext->Map(pTexture, j, D3D11_MAP_READ, 0, &mappedTex2D);
			d3dDeviceContext->UpdateSubresource(pTexArray,
				D3D11CalcSubresource(j, i, texArrayDesc.MipLevels),	// i * mipLevel + j
				nullptr, mappedTex2D.pData, mappedTex2D.RowPitch, mappedTex2D.DepthPitch);
			// 停止映射
			d3dDeviceContext->Unmap(pTexture, j);
		}
		SAFE_RELEASE(pTexture);
	}

	// ******************
	// 必要時創建紋理數組的SRV
	//
	if (generateMips || textureArrayView)
	{
		D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
		viewDesc.Format = texArrayDesc.Format;
		viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
		viewDesc.Texture2DArray.MostDetailedMip = 0;
		viewDesc.Texture2DArray.MipLevels = -1;
		viewDesc.Texture2DArray.FirstArraySlice = 0;
		viewDesc.Texture2DArray.ArraySize = arraySize;

		ID3D11ShaderResourceView* pTexArraySRV;
		hr = d3dDevice->CreateShaderResourceView(pTexArray, &viewDesc, &pTexArraySRV);
		if (FAILED(hr))
		{
			SAFE_RELEASE(pTexArray);
			return hr;
		}

		// 生成mipmaps
		if (generateMips)
		{
			d3dDeviceContext->GenerateMips(pTexArraySRV);
		}

		if (textureArrayView)
			*textureArrayView = pTexArraySRV;
		else
			SAFE_RELEASE(pTexArraySRV);
	}

	if (textureArray)
		*textureArray = pTexArray;
	else
		SAFE_RELEASE(pTexArray);

	return S_OK;
}

2D紋理立方體

2D紋理立方體的實際上是在以2D紋理數組資源的基礎上創建出來的着色器紋理資源視圖,通過視圖指定哪6個連續的紋理作為紋理立方體。這也意味着你可以在一個2D紋理數組上創建多個紋理立方體。

Direct3D提供了枚舉類型D3D11_TEXTURECUBE_FACE來標識立方體某一表面:

typedef enum D3D11_TEXTURECUBE_FACE {
	D3D11_TEXTURECUBE_FACE_POSITIVE_X = 0,
	D3D11_TEXTURECUBE_FACE_NEGATIVE_X = 1,
	D3D11_TEXTURECUBE_FACE_POSITIVE_Y = 2,
	D3D11_TEXTURECUBE_FACE_NEGATIVE_Y = 3,
	D3D11_TEXTURECUBE_FACE_POSITIVE_Z = 4,
	D3D11_TEXTURECUBE_FACE_NEGATIVE_Z = 5
} D3D11_TEXTURECUBE_FACE;

可以看出:

  1. 索引0指向+X表面;
  2. 索引1指向-X表面;
  3. 索引2指向+Y表面;
  4. 索引3指向-Y表面;
  5. 索引4指向+Z表面;
  6. 索引5指向-Z表面;

使用立方體映射意味着我們需要使用3D紋理坐標進行尋址,通過向量的形式來指定使用立方體某個表面的其中一點。

在HLSL中,立方體紋理用TextureCube來表示。

創建一個紋理立方體

對於創建好的DDS立方體紋理,我們只需要使用DDSTextureLoader就可以很方便地讀取進來:

HR(CreateDDSTextureFromFile(
	device.Get(), 
	cubemapFilename.c_str(), 
	nullptr, 
	textureCubeSRV.GetAddressOf()));

然而從網絡上能夠下到的天空盒資源經常要么是一張天空盒貼圖,要么是六張天空盒的正方形貼圖,用DXTex導入還是比較麻煩的一件事情。我們也可以自己編寫代碼來構造立方體紋理。

將一張天空盒貼圖轉化成立方體紋理需要經歷以下4個步驟:

  1. 讀取天空盒的貼圖
  2. 創建包含6個紋理的數組
  3. 選取原天空盒紋理的6個子正方形區域,拷貝到該數組中
  4. 創建立方體紋理的SRV

而將六張天空盒的正方形貼圖轉換成立方體需要經歷這4個步驟:

  1. 讀取這六張正方形貼圖
  2. 創建包含6個紋理的數組
  3. 將這六張貼圖完整地拷貝到該數組中
  4. 創建立方體紋理的SRV

可以看到這兩種類型的天空盒資源在處理上有很多相似的地方。

// ------------------------------
// CreateWICTexture2DCubeFromFile函數
// ------------------------------
// 根據給定的一張包含立方體六個面的位圖,創建紋理立方體
// 要求紋理寬高比為4:3,且按下面形式布局:
// .  +Y .  .
// -X +Z +X -Z 
// .  -Y .  .
// [In]d3dDevice			D3D設備
// [In]d3dDeviceContext		D3D設備上下文
// [In]cubeMapFileName		位圖文件名
// [OutOpt]textureArray		輸出的紋理數組資源
// [OutOpt]textureCubeView	輸出的紋理立方體資源視圖
// [In]generateMips			是否生成mipmaps
HRESULT CreateWICTexture2DCubeFromFile(
	ID3D11Device * d3dDevice,
	ID3D11DeviceContext * d3dDeviceContext,
	const std::wstring& cubeMapFileName,
	ID3D11Texture2D** textureArray,
	ID3D11ShaderResourceView** textureCubeView,
	bool generateMips = false);

// ------------------------------
// CreateWICTexture2DCubeFromFile函數
// ------------------------------
// 根據按D3D11_TEXTURECUBE_FACE索引順序給定的六張紋理,創建紋理立方體
// 要求位圖是同樣寬高、數據格式的正方形
// 你也可以給定超過6張的紋理,然后在獲取到紋理數組的基礎上自行創建更多的資源視圖
// [In]d3dDevice			D3D設備
// [In]d3dDeviceContext		D3D設備上下文
// [In]cubeMapFileNames		位圖文件名數組
// [OutOpt]textureArray		輸出的紋理數組資源
// [OutOpt]textureCubeView	輸出的紋理立方體資源視圖
// [In]generateMips			是否生成mipmaps
HRESULT CreateWICTexture2DCubeFromFile(
	ID3D11Device * d3dDevice,
	ID3D11DeviceContext * d3dDeviceContext,
	const std::vector<std::wstring>& cubeMapFileNames,
	ID3D11Texture2D** textureArray,
	ID3D11ShaderResourceView** textureCubeView,
	bool generateMips = false);

從完整天空盒位圖生成紋理立方體的實現

現在我們要將位圖讀進來,這是讀取一張完整天空盒的實現版本的開頭

HRESULT CreateWICTexture2DCubeFromFile(
	ID3D11Device * d3dDevice,
	ID3D11DeviceContext * d3dDeviceContext,
	const std::wstring & cubeMapFileName,
	ID3D11Texture2D ** textureArray,
	ID3D11ShaderResourceView ** textureCubeView,
	bool generateMips)
{
	// 檢查設備、設備上下文是否非空
	// 紋理數組和紋理立方體視圖只要有其中一個非空即可
	if (!d3dDevice || !d3dDeviceContext || !(textureArray || textureCubeView))
		return E_INVALIDARG;

	// ******************
	// 讀取天空盒紋理
	//

	ID3D11Texture2D* srcTex = nullptr;
	ID3D11ShaderResourceView* srcTexSRV = nullptr;

	// 該資源用於GPU復制
	HRESULT hResult = CreateWICTextureFromFileEx(d3dDevice,
		d3dDeviceContext,
		cubeMapFileName.c_str(),
		0,
		D3D11_USAGE_DEFAULT,
		D3D11_BIND_SHADER_RESOURCE | (generateMips ? D3D11_BIND_RENDER_TARGET : 0),
		0,
		(generateMips ? D3D11_RESOURCE_MISC_GENERATE_MIPS : 0),
		WIC_LOADER_DEFAULT,
		(ID3D11Resource**)&srcTex,
		(generateMips ? &srcTexSRV : nullptr));

	// 文件未打開
	if (FAILED(hResult))
	{
		return hResult;
	}
	
	// ...

現在我們可以利用CreateWICTextureFromFileEx函數內部幫我們預先生成mipmaps,必須要同時提供d3dDeviceContextsrcTexSRV才能生成。

而且關於紋理的拷貝操作可以不需要從GPU讀到CPU再進行,而是直接在GPU之間進行拷貝,因此可以將usage設為D3D11_USAGE_DEFAULTcpuAccessFlags設為0.

接下來需要創建一個新的紋理數組。首先需要填充D3D11_TEXTURE2D_DESC結構體內容,這里的大部分參數可以從天空盒紋理取得。

對於mip等級需要特別處理,比如一個4096x3072的完整天空盒位圖,其生成的mip等級是13,但是對於里面的1024x1024立方體表面,其生成的mip等級是11,可以得到這樣的一個關系:紋理數組的mip等級比讀進來的天空盒位圖mip等級少2.

	UINT squareLength = texDesc.Width / 4;
	texArrayDesc.Width = squareLength;
	texArrayDesc.Height = squareLength;
	texArrayDesc.MipLevels = (generateMips ? texDesc.MipLevels - 2 : 1);	// 立方體的mip等級比整張位圖的少2
	texArrayDesc.ArraySize = 6;
	texArrayDesc.Format = texDesc.Format;
	texArrayDesc.SampleDesc.Count = 1;
	texArrayDesc.SampleDesc.Quality = 0;
	texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
	texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	texArrayDesc.CPUAccessFlags = 0;
	texArrayDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;	// 允許從中創建TextureCube
	
	ID3D11Texture2D* texArray = nullptr;
	hResult = d3dDevice->CreateTexture2D(&texArrayDesc, nullptr, &texArray);
	if (FAILED(hResult))
	{
		SAFE_RELEASE(srcTex);
		SAFE_RELEASE(srcTexSRV);
		return hResult;
	}

D3D11_BIND_SHADER_RESOURCED3D11_RESOURCE_MISC_TEXTURECUBE的標簽記得不要遺漏。

D3D11_BOX結構體

現在我們需要對源位圖進行節選,但節選之前,首先我們需要了解定義3D盒的結構體D3D11_BOX

typedef struct D3D11_BOX {
	UINT left;	
	UINT top;
	UINT front;
	UINT right;
	UINT bottom;
	UINT back;
} D3D11_BOX;

3D box使用的是下面的坐標系,和紋理坐標系很像:

由於選取像素采用的是半開半閉區間,如[left, right),在指定left, top, front的值時會選到該像素,而不對想到right, bottom, back對應的像素。

對於1D紋理來說,是沒有Y軸和Z軸的,因此需要令front=0, back=1, top=0, bottom=1才能表示當前的1D紋理,如果出現像back和front相等的情況,則不會選到任何的紋理像素區間。

而2D紋理沒有Z軸,在選取像素區域前需要置front=0, back=1

3D紋理(體積紋理)可以看做一系列紋理的堆疊,因此frontback可以用來選定哪些紋理需要節選。

ID3D11DeviceContext::CopySubresourceRegion方法--從指定資源選取區域復制到目標資源特定區域

void ID3D11DeviceContext::CopySubresourceRegion(
	ID3D11Resource  *pDstResource,	// [In/Out]目標資源
	UINT            DstSubresource,	// [In]目標子資源索引
	UINT            DstX,			// [In]目標起始X值
	UINT            DstY,			// [In]目標起始Y值
	UINT            DstZ,			// [In]目標起始Z值
	ID3D11Resource  *pSrcResource,	// [In]源資源
	UINT            SrcSubresource,	// [In]源子資源索引
	const D3D11_BOX *pSrcBox		// [In]指定復制區域
);

例如現在我們要將該天空盒的+X面對應的mipmap鏈拷貝到ArraySlice為0(即D3D11_TEXTURECUBE_FACE_POSITIVE_X)的目標資源中,則可以像下面這樣寫:

	D3D11_BOX box;
	// box坐標軸如下: 
	//    front
	//   / 
	//  /_____right
	//  |
	//  |
	//  bottom
	box.front = 0;
	box.back = 1;

	for (UINT i = 0; i < texArrayDesc.MipLevels; ++i)
	{
		// +X面拷貝
		box.left = squareLength * 2;
		box.top = squareLength;
		box.right = squareLength * 3;
		box.bottom = squareLength * 2;
		d3dDeviceContext->CopySubresourceRegion(
			texArray,
			D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_POSITIVE_X, texArrayDesc.MipLevels),
			0, 0, 0,
			srcTex,
			i,
			&box);
			
		// -X面拷貝
		box.left = 0;
		box.top = squareLength;
		box.right = squareLength;
		box.bottom = squareLength * 2;
		d3dDeviceContext->CopySubresourceRegion(
			texArray,
			D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_NEGATIVE_X, texArrayDesc.MipLevels),
			0, 0, 0,
			srcTex,
			i,
			&box);

		// +Y面拷貝
		box.left = squareLength;
		box.top = 0;
		box.right = squareLength * 2;
		box.bottom = squareLength;
		d3dDeviceContext->CopySubresourceRegion(
			texArray,
			D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_POSITIVE_Y, texArrayDesc.MipLevels),
			0, 0, 0,
			srcTex,
			i,
			&box);


		// -Y面拷貝
		box.left = squareLength;
		box.top = squareLength * 2;
		box.right = squareLength * 2;
		box.bottom = squareLength * 3;
		d3dDeviceContext->CopySubresourceRegion(
			texArray,
			D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_NEGATIVE_Y, texArrayDesc.MipLevels),
			0, 0, 0,
			srcTex,
			i,
			&box);

		// +Z面拷貝
		box.left = squareLength;
		box.top = squareLength;
		box.right = squareLength * 2;
		box.bottom = squareLength * 2;
		d3dDeviceContext->CopySubresourceRegion(
			texArray,
			D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_POSITIVE_Z, texArrayDesc.MipLevels),
			0, 0, 0,
			srcTex,
			i,
			&box);

		// -Z面拷貝
		box.left = squareLength * 3;
		box.top = squareLength;
		box.right = squareLength * 4;
		box.bottom = squareLength * 2;
		d3dDeviceContext->CopySubresourceRegion(
			texArray,
			D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_NEGATIVE_Z, texArrayDesc.MipLevels),
			0, 0, 0,
			srcTex,
			i,
			&box);
		
		// 下一個mipLevel的紋理寬高都是原來的1/2
		squareLength /= 2;
	}

最后就是創建紋理立方體着色器資源視圖了:

	if (textureCubeView)
	{
		D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
		viewDesc.Format = texArrayDesc.Format;
		viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
		viewDesc.TextureCube.MostDetailedMip = 0;
		viewDesc.TextureCube.MipLevels = texArrayDesc.MipLevels;

		hResult = d3dDevice->CreateShaderResourceView(texArray, &viewDesc, textureCubeView);
	}
	
	// 檢查是否需要紋理數組
	if (textureArray)
	{
		*textureArray = texArray;
	}
	else
	{
		SAFE_RELEASE(texArray);
	}

	SAFE_RELEASE(srcTex);
	SAFE_RELEASE(srcTexSRV);

	return hResult;
}

從六張天空盒的位圖創建立方體紋理

第一步是讀取六張紋理,並根據需要生成mipmaps:

HRESULT CreateWICTexture2DCubeFromFile(
	ID3D11Device * d3dDevice,
	ID3D11DeviceContext * d3dDeviceContext,
	const std::vector<std::wstring>& cubeMapFileNames,
	ID3D11Texture2D ** textureArray,
	ID3D11ShaderResourceView ** textureCubeView,
	bool generateMips)
{
	// 檢查設備與設備上下文是否非空
	// 文件名數目需要不小於6
	// 紋理數組和資源視圖只要有其中一個非空即可
	UINT arraySize = (UINT)cubeMapFileNames.size();

	if (!d3dDevice || !d3dDeviceContext || arraySize < 6 || !(textureArray || textureCubeView))
		return E_INVALIDARG;

	// ******************
	// 讀取紋理
	//

	HRESULT hResult;
	std::vector<ID3D11Texture2D*> srcTexVec(arraySize, nullptr);
	std::vector<ID3D11ShaderResourceView*> srcTexSRVVec(arraySize, nullptr);
	std::vector<D3D11_TEXTURE2D_DESC> texDescVec(arraySize);

	for (UINT i = 0; i < arraySize; ++i)
	{
		// 該資源用於GPU復制
		hResult = CreateWICTextureFromFile(d3dDevice,
			(generateMips ? d3dDeviceContext : nullptr),
			cubeMapFileNames[i].c_str(),
			(ID3D11Resource**)&srcTexVec[i],
			(generateMips ? &srcTexSRVVec[i] : nullptr));

		// 讀取創建好的紋理信息
		srcTexVec[i]->GetDesc(&texDescVec[i]);

		// 需要檢驗所有紋理的mipLevels,寬度和高度,數據格式是否一致,
		// 若存在數據格式不一致的情況,請使用dxtex.exe(DirectX Texture Tool)
		// 將所有的圖片轉成一致的數據格式
		if (texDescVec[i].MipLevels != texDescVec[0].MipLevels || texDescVec[i].Width != texDescVec[0].Width ||
			texDescVec[i].Height != texDescVec[0].Height || texDescVec[i].Format != texDescVec[0].Format)
		{
			for (UINT j = 0; j < i; ++j)
			{
				SAFE_RELEASE(srcTexVec[j]);
				SAFE_RELEASE(srcTexSRVVec[j]);
			}
			return E_FAIL;
		}
	}

然后是創建數組,即便提供的紋理數目超過6,也是允許的:

	// ******************
	// 創建紋理數組
	//
	D3D11_TEXTURE2D_DESC texArrayDesc;
	texArrayDesc.Width = texDescVec[0].Width;
	texArrayDesc.Height = texDescVec[0].Height;
	texArrayDesc.MipLevels = (generateMips ? texDescVec[0].MipLevels : 1);
	texArrayDesc.ArraySize = arraySize;
	texArrayDesc.Format = texDescVec[0].Format;
	texArrayDesc.SampleDesc.Count = 1;
	texArrayDesc.SampleDesc.Quality = 0;
	texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
	texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	texArrayDesc.CPUAccessFlags = 0;
	texArrayDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;	// 允許從中創建TextureCube

	ID3D11Texture2D* texArray = nullptr;
	hResult = d3dDevice->CreateTexture2D(&texArrayDesc, nullptr, &texArray);

	if (FAILED(hResult))
	{
		for (UINT i = 0; i < arraySize; ++i)
		{
			SAFE_RELEASE(srcTexVec[i]);
			SAFE_RELEASE(srcTexSRVVec[i]);
		}

		return hResult;
	}

由於我們不需要指定源位圖的具體區域,可以將pSrcBox設置為nullptr

	// ******************
	// 將原紋理的所有子資源拷貝到該數組中
	//
	texArray->GetDesc(&texArrayDesc);

	for (UINT i = 0; i < arraySize; ++i)
	{
		for (UINT j = 0; j < texArrayDesc.MipLevels; ++j)
		{
			d3dDeviceContext->CopySubresourceRegion(
				texArray,
				D3D11CalcSubresource(j, i, texArrayDesc.MipLevels),
				0, 0, 0,
				srcTexVec[i],
				j,
				nullptr);
		}
	}

最后就是創建立方體紋理着色器資源視圖,默認只指定0到5索引的紋理,如果這個紋理數組包含索引6-11的紋理,你還想創建一個新的視圖的話,就可以拿取創建好textureArray,然后自己再寫創建視圖相關的調用:

	// ******************
	// 創建立方體紋理的SRV
	//
	if (textureCubeView)
	{
		D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
		viewDesc.Format = texArrayDesc.Format;
		viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
		viewDesc.TextureCube.MostDetailedMip = 0;
		viewDesc.TextureCube.MipLevels = texArrayDesc.MipLevels;

		hResult = d3dDevice->CreateShaderResourceView(texArray, &viewDesc, textureCubeView);
	}

	// 檢查是否需要紋理數組
	if (textureArray)
	{
		*textureArray = texArray;
	}
	else
	{
		SAFE_RELEASE(texArray);
	}

	// 釋放所有資源
	for (UINT i = 0; i < arraySize; ++i)
	{
		SAFE_RELEASE(srcTexVec[i]);
		SAFE_RELEASE(srcTexSRVVec[i]);
	}

	return hResult;
}

至此有關2D紋理相關的講解就基本上結束了。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM