原文: DirectX11--深入理解与使用2D纹理资源

前言

写教程到现在,我发现有关纹理资源的一些解说和应用都写的太过分散,导致连我自己找起来都不方便。现在决定把这部分的内容整合起来,尽可能做到一篇搞定所有2D纹理相关的内容,其中包括:

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
  1. DDSTextureLoader和WICTextureLoader
  2. ScreenGrab
  3. 2D纹理的一般创建方法
  4. 2D纹理数组的一般创建方法
  5. 2D纹理立方体的一般创建方法
  6. 纹理子资源
  7. 纹理资源的完整复制
  8. 纹理子资源指定区域的复制
  9. 纹理从GPU映射回CPU进行读写
  10. 使用内存初始化纹理

你必须要先了解纹理映射,然后再来看这部分内容,原本分散在各个章节的部分现在都已经删去。

章节回顾
09 纹理映射与采样器状态

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 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.

DirectX11--深入理解与使用2D纹理资源 随笔 第1张

创建着色器资源视图的演示如下:

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]着色器资源视图数组
);

获取一份不允许CPU读写的纹理到内存中

通常这种资源的类型有可能是D3D11_USAGE_IMMUTABLE或者D3D11_USAGE_DEFAULT。我们可以创建一个D3D11_USAGE_STAGING的纹理,指定CPU读取权限,然后通过下面的方法拷贝一份到我们新创建的纹理,最后进行映射读取。

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. 允许深度/模板缓冲区作为源或目标资源

纹理子资源(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;

DirectX11--深入理解与使用2D纹理资源 随笔 第2张

而如果你使用的是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),它的SysMemPitch为512x4=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(m_pd3dDevice->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(m_pd3dDevice->CreateShaderResourceView(tex.Get(), &srvDesc, m_pTexSRV.GetAddressOf()));
    
    // ...
}

其它部分的代码修改就不讲了,最终效果如下:
DirectX11--深入理解与使用2D纹理资源 随笔 第3张

但是如果你想要以初始化的方式来创建带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的子资源。
DirectX11--深入理解与使用2D纹理资源 随笔 第4张

然后给定当前纹理数组每个纹理的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设备
// [In]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设备
// [In]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*> srcTexVec(arraySize);
    std::vector<D3D11_TEXTURE2D_DESC> texDescVec(arraySize);
    for (UINT i = 0; i < arraySize; ++i)
    {
        // 由于这些纹理并不会被GPU使用,我们使用D3D11_USAGE_STAGING枚举值
        // 使得CPU可以读取资源
        hResult = CreateDDSTextureFromFileEx(d3dDevice,
            fileNames[i].c_str(), 0, D3D11_USAGE_STAGING, 0,
            D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ,
            0, false, (ID3D11Resource**)&srcTexVec[i], nullptr);

        // 读取失败则释放之前读取的纹理并返回
        if (FAILED(hResult))
        {
            for (UINT j = 0; j < i; ++j)
                SAFE_RELEASE(srcTexVec[j]);
            return hResult;
        }

        // 读取创建好的纹理信息
        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]);
            return E_FAIL;
        }
    }

    hResult = CreateTexture2DArray(d3dDevice, d3dDeviceContext, srcTexVec,
        D3D11_USAGE_DEFAULT,
        D3D11_BIND_SHADER_RESOURCE | (generateMips ? D3D11_BIND_RENDER_TARGET : 0),
        0,
        (generateMips ? D3D11_RESOURCE_MISC_GENERATE_MIPS : 0),
        textureArray,
        textureArrayView);

    for (UINT i = 0; i < arraySize; ++i)
        SAFE_RELEASE(srcTexVec[i]);
    return hResult;
}

而WIC版的区别仅在于把CreateDDSTextureFromFileEx替换为CreateWICTextureFromFileEx

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

由于我们给纹理设置的是D3D11_USAGE_STAGING,它无法绑定到渲染管线上生成mipmaps,如果读进来的是dds纹理,它可能本身就自带mipmaps,也可能没有。所以创建mipmap的操作得在后续创建的纹理数组来完成。

在了解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则指定使用的纹理数目。

DirectX11--深入理解与使用2D纹理资源 随笔 第5张

例如我想指定像上面那样的范围,可以指定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。

但是在调用该方法之前,需要确保在创建纹理时,bindFlags需要额外设置D3D11_BIND_RENDER_TARGET枚举值,然后在miscFlags中设置为D3D11_RESOURCE_MISC_GENERATE_MIPS枚举值,否则调用上面的方法将无效。

无论是否需要生成mipmap链,D3D11_BIND_SHADER_RESOURCE标签是必须的,因为它通常会用在着色器资源的绑定。

最终CreateTexture2DArray的实现如下:

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

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

    HRESULT hResult;
    UINT arraySize = (UINT)srcTexVec.size();
    bool generateMips = (bindFlags & D3D11_BIND_RENDER_TARGET) &&
        (miscFlags & D3D11_RESOURCE_MISC_GENERATE_MIPS);
    // ******************
    // 创建纹理数组
    //

    D3D11_TEXTURE2D_DESC texDesc;
    srcTexVec[0]->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 = usage;
    texArrayDesc.BindFlags = bindFlags;
    texArrayDesc.CPUAccessFlags = cpuAccessFlags;
    texArrayDesc.MiscFlags = miscFlags;

    ID3D11Texture2D* texArray = nullptr;
    hResult = d3dDevice->CreateTexture2D(&texArrayDesc, nullptr, &texArray);
    if (FAILED(hResult))
    {
        return hResult;
    }

    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(srcTexVec[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(srcTexVec[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
    {
        SAFE_RELEASE(texArray);
    }

    return hResult;
}

2D纹理立方体

2D纹理立方体的实际上是在以2D纹理数组资源的基础上创建出来的着色器纹理资源视图,通过视图指定哪6个连续的纹理作为纹理立方体。这也意味着你可以在一个2D纹理数组上创建多个纹理立方体。

DirectX11--深入理解与使用2D纹理资源 随笔 第6张

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使用的是下面的坐标系,和纹理坐标系很像:
DirectX11--深入理解与使用2D纹理资源 随笔 第7张

由于选取像素采用的是半开半闭区间,如[left, right),在指定left, top, front的值时会选到该像素,而不对想到right, bottom, back对应的像素。

对于1D纹理来说,是没有Y轴和Z轴的,因此需要令back=0, front=1, top=0, bottom=1才能表示当前的1D纹理,如果出现像back和front相等的情况,则不会选到任何的纹理像素区间。

而2D纹理没有Z轴,在选取像素区域前需要置back=0, front=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,以及有什么问题也可以在这里汇报。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄