1. 程式人生 > >DirectX11--深入理解與使用2D紋理資源

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

前言

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

  1. 2D紋理的一般建立方法
  2. DDSTextureLoader和WICTextureLoader
  3. 2D紋理陣列的一般建立方法
  4. 2D紋理立方體的一般建立方法(未完工)
  5. 紋理子資源
  6. 紋理資源的完整複製(未完工)
  7. 紋理子資源指定區域的複製(未完工)
  8. 紋理從GPU映射回CPU進行修改
  9. 使用記憶體初始化紋理

你必須要先了解紋理對映,然後再來看這部分內容。

由於這篇的工作量實在是太大了,目前下面這些程式碼我還沒有提交到專案,而且這篇部落格還在施工中,等我全部寫完再提交一遍。

章節回顧
09 紋理對映與取樣器狀態

DirectX11 With Windows SDK完整目錄

Github專案原始碼

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

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 a;
    uint8_t b;
    uint8_t g;
    uint8_t r;
};

瞭解這個對我們後期通過記憶體填充紋理十分重要。

然後是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()));

DDSTextureLoader和WICTextureLoader庫

作為一個紋理相關的綜合教程,我可能會重複之前我講過的內容。

DDS點陣圖和WIC點陣圖

DDS是一種圖片格式,是DirectDraw Surface的縮寫,它是DirectX紋理壓縮(DirectX Texture Compression,簡稱DXTC)的產物。由NVIDIA公司開發。大部分3D遊戲引擎都可以使用DDS格式的圖片用作貼圖,也可以製作法線貼圖。

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]獲取建立好的紋理資源檢視(可選)

紋理子資源(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,以此類推。

描述一個紋理子資源的兩種結構體

如果你想要為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_SUBRESOURCE_DATA結構體,其定義如下:

typedef struct D3D11_MAPPED_SUBRESOURCE {
    void *pData;        // 對映到記憶體的資料or需要提交的地址範圍
    UINT RowPitch;      // 當前子資源一行所佔的位元組數(2D/3D紋理有意義)
    UINT DepthPitch;    // 當前子資源一個完整切片所佔的位元組數(僅3D紋理有意義)
} D3D11_MAPPED_SUBRESOURCE;

若一張512x512的紋理(R8G8B8A8),那麼它的RowPitch為5124=2048位元組,同理在初始化一個512x512的紋理(R8G8B8A8),它的SysMemPitch為5124=2048位元組。

通過記憶體初始化紋理

現在我們嘗試通過程式碼的形式來建立一個紋理(以專案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> textureArrayMap(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 texArrayDesc;
    texArrayDesc.Width = 128;
    texArrayDesc.Height = 128;
    texArrayDesc.MipLevels = 1;
    texArrayDesc.ArraySize = 1;
    texArrayDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    texArrayDesc.SampleDesc.Count = 1;      // 不使用多重取樣
    texArrayDesc.SampleDesc.Quality = 0;
    texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
    texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    texArrayDesc.CPUAccessFlags = 0;
    texArrayDesc.MiscFlags = 0; // 指定需要生成mipmap

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


    ComPtr<ID3D11Texture2D> tex;
    HR(md3dDevice->CreateTexture2D(&texArrayDesc, &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(md3dDevice->CreateShaderResourceView(tex.Get(), &srvDesc, mTexSRV.GetAddressOf()));
    
    // ...
}

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

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

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物件,這裡的每個物件單獨包含一張紋理;
  2. 建立一個ID3D11Texture2D物件,它同時也是一個紋理陣列;
  3. 將之前讀取的所有紋理有條理地複製到剛建立的紋理陣列物件中;
  4. 為該紋理陣列物件建立建立一個紋理資源檢視(Shader Resource View)。

為了避免出現一些問題,這裡實現的紋理陣列載入的函式只考慮寬度和高度、資料格式、mip等級都一致的情況。

d3dUtil.h中實現了這樣兩個函式:

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

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

還有就是d3dUtil.cpp用到的函式CreateTexture2DArray

第一步先討論紋理的載入,這裡`CreateDDSTexture2DArrayFromFile函式的實現如下:

HRESULT CreateDDSTexture2DArrayFromFile(
    ID3D11Device * d3dDevice,
    ID3D11DeviceContext * d3dDeviceContext,
    const std::vector<std::wstring>& fileNames,
    ID3D11Texture2D** textureArray,
    ID3D11ShaderResourceView** textureArrayView,
    bool generateMips)
{
    // 檢查裝置、著色器資源檢視、檔名陣列是否非空
    if (!d3dDevice || !textureArrayView || fileNames.empty())
        return E_INVALIDARG;

    HRESULT hResult;
    // ******************
    // 讀取所有紋理
    //

    UINT arraySize = (UINT)fileNames.size();
    std::vector<ID3D11Texture2D*> srcTex(arraySize, nullptr);
    for (size_t i = 0; i < arraySize; ++i)
    {
        // 由於這些紋理並不會被GPU使用,我們使用D3D11_USAGE_STAGING列舉值
        // 使得CPU可以讀取資源
        hResult = CreateDDSTextureFromFileEx(d3dDevice, d3dDeviceContext,
            fileNames[i].c_str(), 0, D3D11_USAGE_STAGING, 0,
            D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ, 
            0, false, (ID3D11Resource**)&srcTex[i], nullptr);

        if (FAILED(hResult))
            break;
    }

    if (hResult == S_OK)
    {
        hResult = CreateTexture2DArray(d3dDevice, d3dDeviceContext, srcTex,
            D3D11_USAGE_DEFAULT,
            D3D11_BIND_SHADER_RESOURCE | (generateMips ? D3D11_BIND_RENDER_TARGET : 0),
            0,
            (generateMips ? D3D11_RESOURCE_MISC_GENERATE_MIPS : 0),
            textureArray,
            textureArrayView);
    }
    

    for (size_t i = 0; i < arraySize; ++i)
        if (srcTex[i])
            srcTex[i]->Release();

    return hResult;

}

而WIC版的區別僅在於把CreateDDSTextureFromFileEx替換為CreateWICTextureFromFileEx

hResult = CreateWICTextureFromFileEx(d3dDevice, d3dDeviceContext,
    fileNames[i].c_str(), 0, D3D11_USAGE_STAGING, 0,
    D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
    0, WIC_LOADER_DEFAULT, (ID3D11Resource**)&srcTex[i], nullptr);

在瞭解CreateTexture2DArray函式的實現前,你需要下面這些內容:

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]--將記憶體資料拷貝到不可進行對映的子資源中

這個函式在之前我們主要是用來將記憶體資料拷貝到常量緩衝區中,現在我們也可以用它將記憶體資料拷貝到紋理的子資源當中:

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.

最終CreateTexture2DArray的實現如下:

static HRESULT CreateTexture2DArray(
    ID3D11Device * d3dDevice,
    ID3D11DeviceContext * d3dDeviceContext,
    std::vector<ID3D11Texture2D*>& srcTex,
    D3D11_USAGE usage,
    UINT bindFlags,
    UINT cpuAccessFlags,
    UINT miscFlags,
    ID3D11Texture2D** textureArray,
    ID3D11ShaderResourceView** textureArrayView)
{

    if (!textureArray && !textureArrayView || !d3dDevice || !d3dDeviceContext || srcTex.empty())
        return E_INVALIDARG;

    // 需要檢驗所有紋理的mipLevels,寬度和高度,資料格式是否一致,
    // 若存在資料格式不一致的情況,請使用dxtex.exe(DirectX Texture Tool)
    // 將所有的圖片轉成一致的資料格式
    UINT arraySize = (UINT)srcTex.size();
    std::vector<D3D11_TEXTURE2D_DESC> texDesc(arraySize);
    for (int i = 0; i < arraySize; ++i)
    {
        if (texDesc[i].MipLevels != texDesc[0].MipLevels || texDesc[i].Width != texDesc[0].Width ||
            texDesc[i].Height != texDesc[0].Height || texDesc[i].Format != texDesc[0].Format)
        {
            return E_FAIL;
        }
    }
    
    HRESULT hResult;
    bool generateMips = (bindFlags & D3D11_BIND_RENDER_TARGET) &&
        (miscFlags & D3D11_RESOURCE_MISC_GENERATE_MIPS);
    // ******************
    // 建立紋理陣列
    //

    D3D11_TEXTURE2D_DESC texArrayDesc;
    texArrayDesc.Width = texDesc[0].Width;
    texArrayDesc.Height = texDesc[0].Height;
    texArrayDesc.MipLevels = generateMips ? 0 : texDesc[0].MipLevels;
    texArrayDesc.ArraySize = arraySize;
    texArrayDesc.Format = texDesc[0].Format;
    texArrayDesc.SampleDesc.Count = 1;      // 不能使用多重取樣
    texArrayDesc.SampleDesc.Quality = 0;
    texArrayDesc.Usage = usage;
    texArrayDesc.BindFlags = bindFlags;
    texArrayDesc.CPUAccessFlags = cpuAccessFlags;
    texArrayDesc.MiscFlags = miscFlags;

    ID3D11Texture2D* texArray;
    hResult = d3dDevice->CreateTexture2D(&texArrayDesc, nullptr, &texArray);
    if (FAILED(hResult))
    {
        for (size_t i = 0; i < arraySize; ++i)
            srcTex[i]->Release();
        return E_FAIL;
    }

    texArray->GetDesc(&texArrayDesc);
    // ******************
    // 將所有的紋理子資源賦值到紋理陣列中
    //

    UINT minMipLevels = (generateMips ? 1 : texArrayDesc.MipLevels);
    // 每個紋理元素
    for (UINT i = 0; i < texArrayDesc.ArraySize; ++i)
    {
        // 紋理中的每個mipmap等級
        for (UINT j = 0; j < minMipLevels; ++j)
        {
            D3D11_MAPPED_SUBRESOURCE mappedTex2D;
            // 允許對映索引i紋理中,索引j的mipmap等級的2D紋理
            d3dDeviceContext->Map(srcTex[i],
                j, D3D11_MAP_READ, 0, &mappedTex2D);

            d3dDeviceContext->UpdateSubresource(
                texArray,
                D3D11CalcSubresource(j, i, texArrayDesc.MipLevels), // i * mipLevel + j
                nullptr,
                mappedTex2D.pData,
                mappedTex2D.RowPitch,
                mappedTex2D.DepthPitch);
            // 停止對映
            d3dDeviceContext->Unmap(srcTex[i], j);
        }
    }

    // ******************
    // 建立紋理陣列的SRV
    //
    if (textureArrayView)
    {
        D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
        viewDesc.Format = texArrayDesc.Format;
        viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
        viewDesc.Texture2DArray.MostDetailedMip = 0;
        viewDesc.Texture2DArray.MipLevels = texArrayDesc.MipLevels;
        viewDesc.Texture2DArray.FirstArraySlice = 0;
        viewDesc.Texture2DArray.ArraySize = arraySize;

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

        // 生成mipmaps
        if (hResult == S_OK && generateMips)
        {
            d3dDeviceContext->GenerateMips(*textureArrayView);
        }
    }

    // 檢查是否需要紋理陣列
    if (textureArray)
    {
        *textureArray = texArray;
    }
    else
    {
        texArray->Release();
    }

    return hResult;
}

(未完待續)

DirectX11 With Windows SDK完整目錄

Github專案原始碼

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