DirectX12(D3D12)基础教程(二十)—— 纹理数组(Texture Array)非DDS初始化操作

1、前言

  在本系列教程的 DirectX12(D3D12)基础教程(五)——理解和使用捆绑包,加载并使用DDS Cube Map 中,第一次介绍并引入了基于 DDS 格式的CubeMap的操作和使用方法。在后续示例代码的编写过程中,逐渐发现 DDS CubeMap 的种种局限,主要是 DDS 的 CubeMap 其实很难找,并且使用它来加载 SkyBox ,会因为其相关代码的过渡封装而导致对 Texture 或 Texture Array 本身的各种操作和理解基本上是很困难的。

  另外当时为搜索好看的 Sky Box 找到了 Humus - Textures 这个非常棒的网站,然后下载了很多好看且有氛围感的 Sky Box 资源。然鹅,它们居然是原始的六幅图片形式,如下:

在这里插入图片描述
  当然这难不倒我们,使用古老的 DX DDS 工具做一个 CubeMap 的 DDS 文件不就行了?结果不知道什么原因这个古老的工具做出来的DDS文件根本无法使用,然后我又用 VS 自带的 DDS 功能试着做了一下,结果发现 DX12 示例中带的 DDS 解析工具函数根本没法解析。接着我试图搞清楚这里面究竟发生了什么,最终我放弃了,因为我发现 DDS 版本间的差异以及各版本的复杂度直接超出了我的掌控范围(估计是因为三哥程序员写的代码的缘故),即费时又费力而且还收获颇微,于是果断的放弃了这条路线。

  接着我想到,既然这系列教程叫做基础教程,那么索性更基础一点,从原始的一幅幅图片直接加载成一个 Texture Array 在映射成 CubeMap,然后渲染到一个 Skybox ,这样大家掌握 Cubemap 操作,以及 Texture 相关操作就更牢固一些。只是这个例子太过简单,内容不多,我一直纠结要不要再加点内容,然后再提交。后来,在准备 IBL 的相关示例时,我又发现 Cubemap (主要是 Texture Array) 相关操作在IBL中更是必备的基础技能,所以最终我决定,虽然这个例子很简单,但依然提交,并单独形成一篇教程,让大家牢固掌握 Cubemap Texture 在 D3D12 中的操作技能。

  关于 TextureCube 远平面采样形成 Sky Box 的方法请参看:DirectX12(D3D12)基础教程(五)——理解和使用捆绑包,加载并使用DDS Cube Map ,本章中不再赘述其中渲染 SkyBox 的方法。

  本章示例完整源码已上传至:GRSD3D12Sample/24-CubeMapWithoutDDS ,运行后效果如下:
在这里插入图片描述

2、纹理(Texture)和纹理数组(Texture Array)

  在本系列教程中,关于纹理(Texture)的话题其实都被分散在了好几处,并且沿用D3D12 中的叫法,都笼统的被称之为了“资源”。其实随着这系列教程的不断深入,尤其是到了 PBR 渲染的领域,就很有必要对纹理及相关操作进行一些单独的专门的讨论了。这不但是课程体系自身的要求,也是无论哪类3D编程中都必须要掌握的核心基础技能。

  那么首先在这里必须要对纹理进行一个定义,或者说解释。

  按照百度百科的说法纹理是“计算机图形学中的纹理既包括通常意义上物体表面的纹理即使物体表面呈现凹凸不平的沟纹,同时也包括在物体的光滑表面上的彩色图案,通常我们更多地称之为花纹。对于花纹而言,就是在物体表面绘出彩色花纹或图案,产生了纹理后的物体表面依然光滑如故。”

  在D3D中纹理表示物体表面细节的一幅或几幅二维图形,也称纹理贴图(texture mapping)当把纹理按照特定的方式映射到物体表面上的时候能使物体看上去更加真实。纹理映射是一种允许我们为三角形赋予图象数据的技术;这让我们能够更细腻更真实地表现我们的场景。

  当然对于 OpenGL、Vulkan 等来说大致意思都差不多。但其实这些定义或说明,只能算是纹理用途的一个解释。这些定义中,都有一个核心的关键词就是“图形”(图案、图片或图像)等,也就是说纹理本质是个图片。另外一个方面就是说,这些无论什么图片,最终都是为了“包裹”在3D物体表面的。

  然鹅这些定义我认为都不是很准确,这里将纹理等同于图片,其实只是说明了纹理的来源之一,而说它包裹在3D物体表面,则也只是说明了它的用途之一。另外假如这种定义正确的话,那么英文中就没必要专门搞个词汇 Texture 来称呼纹理了。并且在之前的一些教程中,我们至少还拿纹理来当做了渲染目标,显然此时这种应用场景不适合于上面的定义。这对我们深刻的理解和掌握纹理来说都会造成障碍,至少会限制我们的思维。比如我们将在这章讲解的纹理数组,以及后面我们会遇到的 MipMap 等等,如果还简单的理解为图片就会出现理解上的困难。

  为了更好的理解和掌握纹理及其操作方法,在本教程中,重定义纹理(Texture)概念如下:

  纹理是存储于显存中的按单一类型定义其元素的一个复合多维数组,常见的有 一维(1D)、二维(2D)、或三维(3D)。

  这里要注意几个要点:

  1、一般一个纹理中最细小每个元素(有时也称之为像素或纹素,本教程中习惯称之为纹素),其格式在整个纹理中是唯一确定的,即不会存在不同格式的纹素;

  2、纹理是纹素的复合数组,是说除了纹素组成简单的一维或二维等数组外,一般它们还能复合,比如本章教程中将要讲解的纹理数组就是多个二维纹素数组复合而成一个纹理例子;

  3、纹理存在于显存中的,这点很重要,因为从最终操作上来说,只有 GPU 是要求严格区分纹理和普通缓冲区的,而且只有 GPU 需要知道关于纹理的丰富的信息,比如纹素格式、维度、如何复合的等,这主要是因为一般只有 GPU 上才有专门的纹理处理单元 TMU(纹理映射单元 Texture mapping unit),对于 CPU 来说一般没有处理纹理的需要,即使有操作纹理的需求,对于 CPU 来说,无论多复杂的纹理都只不过是一段连续的内存中的数组而已;

  4、这个定义中并没有特别强调纹理的图像属性,正如前面所说,其实图像内容只是纹理数据的一个来源而已,并且二者之间除了简单的一对一方式外,还可能有多对一,一对多等复杂的对应关系。比如本章示例教程中要讲解的纹理数组就是需要 6个方向 6幅不同的图片才能生成一个纹理;而另一些情况下,比如法线纹理,对应的法线贴图,其纹素数据就是 3D 物体表面各点处对应的法线数据,更不是图像数据了(要注意的是现代图形学中已经有将图片像素对应的深度值、法线值等也看做图像像素属性之一的趋势,我认为这是因为3D图形学发展反过来丰富了图像的定义所导致的发展趋势);

  至此厘清了纹理的基本概念之后,回顾一下本系列教程中关于纹理的一些用法,除了包裹在3D物体表面之外,还用它来作为渲染目标,或者作为计算着色器(Computer Shader)的计算缓冲等;另外从纹理的创建上来说,除了从图片通过两遍复制(2 Times Copy)初始化一个纹理外,还使用整个场景的渲染结果作为纹理,另外还通过 CS 解算 GIF 图片初始化了纹理等等。

  总之,无论从纹理的初始化方式,还是从其用途方面来讲,将其理解为显存中的多维数组,在理解上就更有启发性和合理性。

  有了纹理的本质概念之后,作为本章的重点纹理数组就比较好理解了,所谓纹理数组就是有多个相同维度的纹素数组简单复合而成的纹理,比如本章教程中就是使用 6 幅画面初始化了一个有 6 个元素的2D纹理数组。

3、纹理数组的创建

  根据刚才关于纹理的概念定义,因为明确提出纹理是存储在显存中的,那么创建纹理的动作其实就是在显存中申请和分配一块内存。根据本章示例的需要,需要准备一个具有6个 2D 纹理分量的复合纹理,具体代码如下:

D3D12_RESOURCE_DESC stResourceDesc = {};
// 根据图片信息,填充2D纹理资源的信息结构体
stResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
stResourceDesc.MipLevels = 1;
stResourceDesc.Format = staSkyBoxInfo[0].m_emTextureFormat;
stResourceDesc.Width = staSkyBoxInfo[0].m_nTextureW;
stResourceDesc.Height = staSkyBoxInfo[0].m_nTextureH;
stResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
stResourceDesc.DepthOrArraySize = GRS_SKYBOX_IMAGE_CNT;
stResourceDesc.SampleDesc.Count = 1;
stResourceDesc.SampleDesc.Quality = 0;

// 创建纹理
GRS_THROW_IF_FAILED(g_stD3DDevice.m_pID3D12Device4->CreateCommittedResource(
    &g_stDefaultHeapProps
    , D3D12_HEAP_FLAG_NONE
    , &stResourceDesc			
    , D3D12_RESOURCE_STATE_COPY_DEST
    , nullptr
    , IID_PPV_ARGS(&g_stSkyBoxData.m_pITexture)));

  上面的代码根据我们对纹理新下的定义就比较好理解了,代码中明确了复合纹理的每个纹理分量都是 2D Texture(D3D12_RESOURCE_DIMENSION_TEXTURE2D),并且整个纹理中都指定了唯一的纹素格式 staSkyBoxInfo[0].m_emTextureFormat , 这个值来自于加载图片后的图片像素的格式,并且使用了图片信息数组的第一个元素的格式,这其实默认假定被加载的 6 幅图片都具有相同的像素格式,这对 SkyBox 的分离图片来说是一个明显的要求,这不是一个限制,注意要求与限制的区别,并且美工可以很容易做到这一点。

  接着使用图片信息数组的第一个元素的宽和高指定了复合纹理(纹理数组)中每个 2D Texture 都使用相同的尺寸,这也是一个明显的要求。

  然后使用宏定义值 GRS_SKYBOX_IMAGE_CNT(6)来指定复合纹理(纹理数组)中包含的 2D Texture 的个数 DepthOrArraySize,因为是SkyBox,所以肯定是 6 个面。

  综上,结合刚才的概念延伸理解的话,这其实就等于定义了一个 纹素2位数组的数组,相当于就是一个纹素的 3 维数组,纹素个数就是 Width * Height * DepthOrArraySize。其它的参数暂时不用,按代码中的值赋予默认值即可,就不多介绍了,这里只介绍重点的几个参数,以加深我们对纹理概念的理解。

  接着调用D3D12 的 CreateCommittedResource 函数(回忆下提交方式的资源),在默认堆(回忆下之前教程中说过默认堆就是显存)上创建了这个纹理。并且使用初始化状态参数 D3D12_RESOURCE_STATE_COPY_DEST 指定了被新建的纹理处于复制目标的状态,也就是说它还没有被初始化。

  最后非常有意思的是,虽然调用的是 D3D12 的接口函数 CreateCommittedResource 来创建了这个存在于显存中的纹理,但是最终这个分配显存的动作其实是由 CPU (操作系统)完成的,也就是说 GPU 并没有显存管理能力,主要就是分配和销毁一段显存的能力,这些功能都被交给了 CPU 或者说操作系统来完成(当然深究的话系统内部低层可能完全依靠的是中断方式来与显卡交互完成这些功能,但GPU 自身往往不用参与这个功能)。最终 GPU 只是根据自己的要求来读或写访问这些显存中数据而已。

4、纹理数组的初始化(两次复制法)

  完成了纹理的创建,那么接下去就是要对纹理进行初始化,或绘制(当做渲染目标,或写入缓冲等)等操作,否则纹理中没有实质的数据内容就是没有任何意义的。

  在本章示例中,需要加载从前言介绍的网站 Humus - Textures 下载的 SkyBox 图片作为初始化纹理的数据。首先一如既往的使用 WIC 组件来加载这些图片,并且直接使用在之前教程中对其封装的工具函数。注意这里虽然说使用了封装后的函数,其目的不是为了教大家怎么封装,更不是暗示什么思路,只是简单的为了简化已经学习过的代码而已,这也是本系列教程一贯秉持的风格。具体的加载图片数据的代码如下:

struct ST_GRS_IMAGE_INFO
{
    WCHAR           m_pszTextureFile[MAX_PATH];
    DXGI_FORMAT     m_emTextureFormat;
    UINT            m_nTextureW;
    UINT            m_nTextureH;
    UINT            m_nPicRowPitch;
    BYTE*           m_pbImageData;
    size_t          m_szBufferSize;
};

// ......

WCHAR pszAssetPath[MAX_PATH] = {};
::StringCchPrintfW(pszAssetPath, MAX_PATH, L"%s\\SkyBox\\Fjaderholmarna", T2W(g_pszAssetsPath));

ST_GRS_IMAGE_INFO staSkyBoxInfo[GRS_SKYBOX_IMAGE_CNT] = {};

::StringCchPrintfW(staSkyBoxInfo[GRS_SKYBOX_RIGHT].m_pszTextureFile, MAX_PATH, L"%s\\posx.jpg", pszAssetPath);
::StringCchPrintfW(staSkyBoxInfo[GRS_SKYBOX_LEFT].m_pszTextureFile, MAX_PATH, L"%s\\negx.jpg", pszAssetPath);
::StringCchPrintfW(staSkyBoxInfo[GRS_SKYBOX_TOP].m_pszTextureFile, MAX_PATH, L"%s\\posy.jpg", pszAssetPath);
::StringCchPrintfW(staSkyBoxInfo[GRS_SKYBOX_BOTTOM].m_pszTextureFile, MAX_PATH, L"%s\\negy.jpg", pszAssetPath);
::StringCchPrintfW(staSkyBoxInfo[GRS_SKYBOX_FRONT].m_pszTextureFile, MAX_PATH, L"%s\\posz.jpg", pszAssetPath);
::StringCchPrintfW(staSkyBoxInfo[GRS_SKYBOX_BACK].m_pszTextureFile, MAX_PATH, L"%s\\negz.jpg", pszAssetPath);

for (UINT i = 0; i < GRS_SKYBOX_IMAGE_CNT; i++)
{
    if (!::WICLoadImageFromFile(staSkyBoxInfo[i].m_pszTextureFile
        , staSkyBoxInfo[i].m_emTextureFormat
        , staSkyBoxInfo[i].m_nTextureW
        , staSkyBoxInfo[i].m_nTextureH
        , staSkyBoxInfo[i].m_nPicRowPitch
        , staSkyBoxInfo[i].m_pbImageData
        , staSkyBoxInfo[i].m_szBufferSize))
    {
        AtlThrowLastWin32();
    }
}

  上面的代码很简单,就是循环加载每个图片信息。得益于来自 Humus - Textures 网站图片命名格式的规范性,上述代码中只需要更改目录名即可切换不同的SkyBox背景,大家可以自行去网站下载不同的图片加载,以生成不同的 SkyBox 和场景氛围。

  接着在进行“两遍复制”之前,需要搞清楚的就是一个 Texture Array 中的每一个 2D Texture 与 SkyBox 的 CubeMap 是怎样对应的。看下图:

在这里插入图片描述

  图中已经清楚的标出了每个图片所对应的 SkyBox 的位置,以及它们在 Texture Array 中的索引顺序,这个需要大家有一点空间想想力,能够将上面的图示,对应折叠回一个六面体即可。至于原理原因什么的就不再细赘述了,这基本上就是幼儿园小朋友做的手工作品所需要具备的知识了,剩下的就是请牢牢记住这个图中的顺序以及对应的索引顺序,后面还会大量应用这个索引顺序来写好多段程序的。

  接着就是需要准备对应显存中的 Texture Array 的在共享内存中的缓冲区,需要注意的是在共享内存中只能创建缓冲区,并且两遍复制大法中的第一遍复制就是将数据从内存中复制到共享缓冲中,如果你非要较真,那么其实严格来说还应该将数据从硬盘中复制到内存中称之为真正的第一遍复制,而总的来说一副图片从加载到最终传输到内存中就等于是经历了“3遍复制”(苛求性能的你,不要惊慌,有机会了解下 DirectStorage)!

  另外无论 CPU 还是 GPU 都只能将共享内存中的数据看做是简单的缓冲区类型,或者更直白的说就是简单一个结构体数据或者一维数组,所以在共享缓冲中即使加载了图片数据,也不能被 GPU 当做 Texture 来访问,所以 Texture 目前来说一定只能够存在于显存中,这也是之前对纹理下的定义中特别强调显存的根本原因,当然它带来好处就是 GPU 访问的速度峰值几乎就是全部的带宽大小(可以查看下目前主流显卡中的内存带宽的差异,并试着计算一下一副4k * 4k,4 * float色格式的纹理中的像素被完全访问一遍需要大概多少时间?接着假设一帧场景画面大概最多只能渲染25ms,那么理论上该场景中能使用多少个这样大小的纹理?)。但是作为其它的数据比如常用的顶点数据、索引数据、常量缓冲等都是可以放在共享内存中的,也就是上传堆中,最终可以供 GPU 来访问,当然性能会有所牺牲,这些细节在之前的教程中已经见到很多了,就不啰嗦了,至于这些数据究竟放在哪里是需要在正式的项目中思考的设计问题,更是一个性能权衡的问题,相信通过教程这么长时间的锻炼,你应该已经有了合适的答案。对于缓冲数据,本教程代码中只是为了图方便才混合使用显存(默认堆)或共享内存(上传堆)中的,并没有什么特殊暗示。具体创建 Texture Array 对应的共享内存(上传堆)中的缓冲区的代码如下:

D3D12_RESOURCE_DESC                 pstResDesc = g_stSkyBoxData.m_pITexture->GetDesc();
UINT                                nNumSubresources = GRS_SKYBOX_IMAGE_CNT;

D3D12_PLACED_SUBRESOURCE_FOOTPRINT  pstTexSkyboxLayouts[GRS_SKYBOX_IMAGE_CNT] = {};
UINT                                nNumRows[GRS_SKYBOX_IMAGE_CNT] = {};
UINT64                              n64RowSizeInBytes[GRS_SKYBOX_IMAGE_CNT] = {};

UINT64                              n64TotalBytes = 0;

// 获取资源内存布局信息
g_stD3DDevice.m_pID3D12Device4->GetCopyableFootprints(&pstResDesc
    , 0
    , nNumSubresources
    , 0
    , pstTexSkyboxLayouts
    , nNumRows
    , n64RowSizeInBytes
    , &n64TotalBytes);

g_stBufferResSesc.Width = n64TotalBytes;// 注意上传缓冲就不区分是几个纹理了,直接连续放在一起

// 创建纹理上传缓冲区
GRS_THROW_IF_FAILED(g_stD3DDevice.m_pID3D12Device4->CreateCommittedResource(
    &g_stUploadHeapProps,
    D3D12_HEAP_FLAG_NONE,
    &g_stBufferResSesc,
    D3D12_RESOURCE_STATE_GENERIC_READ,
    nullptr,
    IID_PPV_ARGS(&g_stSkyBoxData.m_pITextureUpload)));

BYTE* pData = nullptr;
GRS_THROW_IF_FAILED(g_stSkyBoxData.m_pITextureUpload->Map(0, nullptr, reinterpret_cast<void**>(&pData)));

  上面的代码中关于在上传堆(共享内存)中创建缓冲区的代码就不赘述了。需要引起注意的就是 GetCopyableFootprints 函数,可以这样说,最终要在 D3D12 中彻底玩转 Texture,那么就一定要掌握这个函数,这个函数之前教程中已经提到过了,那么这里再次单独强调一下它的重要性。

  在本章示例中,使用这个函数的根本目的就是获得对应 Texture Array 在共享内存(上传堆)中需要的缓冲区的大小,以及Texture Array中每个 Texture 对应的每行大小(n64RowSizeInBytes)以及行数(nNumRows),而这些数据就是后面准备缓冲区以及复制图片数据到共享内存中的主要依据。这有两层意思,首先就是说对应 Texture 准备的共享内存(上传堆)中的缓冲在布局上要与其在显存中的相一致,方便GPU 上的 Copy Engine 进行“无脑”的第二次复制操作,其次就是说,在 CPU 做第一次复制从内存中复制到共享内存(上传堆)的缓冲中时就要按照这个布局来准备和复制数据。搞不明白第二层意思的可以往后仔细看下第一遍Copy操作的代码,再消化一下。

  这个函数名称中的Footprint这个单词非常有意思,本意是脚印的意思,在这里需要理解为专门针对 Texture 的在显存中的布局的意思。那么何时使用这个函数呢?其实这里以及很明确了,当需要从一个图片或者说别的图形数据初始化显存中的 Texture 时,就要调用这个函数来获取 Texture 在显存中的布局信息,比如总大小、行大小、行数等,因为最终是 GPU 在使用 Texture 的,所以怎么样布局 Texture 只有 GPU 最清楚(实质应该说驱动中已经编写了对应的代码) 。那么最终越复杂的 Texture 就越需要使用 GetCopyableFootprints 函数来获得其存储布局的信息。当然不能因此就认为这个函数的功能好强大,其实这是GPU 必须提供的基本功能在 D3D12 中的对应接口封装而已,是一个基本功能。谁让 GPU 对 Texture 的操作那么特殊和亲密呢?另外,在后续教程中将要介绍的类似 UE4 中的虚拟纹理的相关操作中,这个函数也是比较核心基础的一个函数。所以就必须要牢牢的掌握它。

  接下来,在本章示例代码中,已经按照上面的索引顺序定义和加载了独立的6个面的图片,那么接着就是使用经典的两遍复制法将图片数据加载进 Texture Array 中,完成初始化即可。首先第一遍复制的具体代码如下:

BYTE* pData = nullptr;
GRS_THROW_IF_FAILED(g_stSkyBoxData.m_pITextureUpload->Map(0, nullptr, reinterpret_cast<void**>(&pData)));

// 请注意:为了能深刻理解Texture Array 即 Cube Map 资源布局的原理,所以这里裸写两遍 Copy 的代码

// 第一遍Copy!
for (UINT i = 0; i < nNumSubresources; ++i)
{// SubResources
    if (n64RowSizeInBytes[i] > (SIZE_T)-1)
    {
        AtlThrowImpl(E_FAIL);
    }

    D3D12_MEMCPY_DEST stCopyDestData = { pData + pstTexSkyboxLayouts[i].Offset
        , pstTexSkyboxLayouts[i].Footprint.RowPitch
        , pstTexSkyboxLayouts[i].Footprint.RowPitch * nNumRows[i]
    };

    for (UINT z = 0; z < pstTexSkyboxLayouts[i].Footprint.Depth; ++z)
    {// Mipmap
        BYTE* pDestSlice = reinterpret_cast<BYTE*>(stCopyDestData.pData) + stCopyDestData.SlicePitch * z;
        const BYTE* pSrcSlice = reinterpret_cast<const BYTE*>(staSkyBoxInfo[i].m_pbImageData);

        for (UINT y = 0; y < nNumRows[i]; ++y)
        {// Rows
            memcpy(pDestSlice + stCopyDestData.RowPitch * y,
                pSrcSlice + staSkyBoxInfo[i].m_nPicRowPitch * y,
                (SIZE_T)n64RowSizeInBytes[i]);
        }
    }
}
g_stSkyBoxData.m_pITextureUpload->Unmap(0, nullptr);

  在通用的前提下,需要注意的是上面的 3 层循环是几乎没什么方法来简单优化的。当然对于本章示例代码来说,嵌套在中间的循环Footprint.Depth始终是 1,因为没有涉及 MipMap 等,所以可以最终简化为 2 层循环,又考虑到 Texture Array 中每个子 Texture 都是大小一致的,又可以最终简化为 1 重循环,当然总的循环次数是不变的,因为像素数是不变的。但是这些优化只能是局部的,对于一般的 Texture 来说,从原始图片的加载基本都需要这样的 3 重循环来让 CPU 干体力活。为此在 DX 中很早就提出了压缩纹理的概念及其方法来进行优化,甚至现在压缩和解压缩纹理成为了 GPU 功能的一部分。但是压缩纹理对于理解 Texture 操作来说意义不大,只是对于高阶的引擎封装及优化,尤其是引擎相关的纹理工具来说才是有意义的,因此作为教程来说,需要像这里这样彻底的理解和搞明白 Texture 的相关操作才是核心要关注的问题。另外对于程序生成 Texture 来说,压缩纹理基本上帮不上什么忙。当然未来本教程可能会简单介绍下压缩纹理以及解压的相关方法。这里还需要提示大家的一点就是,假如最终都无法优化这个第一遍复制操作的时候,还有两个办法可以去尝试一下,第一个就是考虑从硬盘加载纹理进内存时就将其整理为 Texture 需要的形式(搞得好还可以考虑使用内存映射文件直接将加载和第一遍复制合并成一个复制操作),这样这里的复制操作就可以一个循环干到底;第二个方法就是使用多线程,在不同的渲染子线程中通过并行的方式来复制不同的几个纹理来加速,这个在 DirectX12(D3D12)基础教程(六)——多线程渲染 中已经演示过了,大家可以去复习一下。

  那么接着在繁重的 CPU 工作结束后,就是发出 GPU 命令让 GPU 上的 Copy Engine 来进行第二个复制操作了,本章示例中代码如下:

for (UINT i = 0; i < nNumSubresources; ++i)
{// 注意每个子资源复制一次
    D3D12_TEXTURE_COPY_LOCATION stDstCopyLocation = {};
    stDstCopyLocation.pResource = g_stSkyBoxData.m_pITexture.Get();
    stDstCopyLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
    stDstCopyLocation.SubresourceIndex = i;

    D3D12_TEXTURE_COPY_LOCATION stSrcCopyLocation = {};
    stSrcCopyLocation.pResource = g_stSkyBoxData.m_pITextureUpload.Get();
    stSrcCopyLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
    stSrcCopyLocation.PlacedFootprint = pstTexSkyboxLayouts[i];

    g_stD3DDevice.m_pIMainCMDList->CopyTextureRegion(&stDstCopyLocation, 0, 0, 0, &stSrcCopyLocation, nullptr);
}

  上面的代码中需要注意的就是,为了演示操作的需要,特意将 Texture Array 中的 6 个 Texture 子资源分成了 6 个复制命令,在描述复制目标时指定了子资源的序号,也就是复合 Texture Array 中的顶层数组索引 stDstCopyLocation.SubresourceIndex = i,而复制来源则直接使用了 GetCopyableFootprints 函数返回的显存布局信息数组的对应子资源布局描述信息元素 stSrcCopyLocation.PlacedFootprint = pstTexSkyboxLayouts[i]。这是在操作复杂的复合 Texture 常用到的方法,也是区别于之前示例代码中一个复制命令就复制整个纹理的方法。当然这里也可以使用一个复制命令来完成整个复合 Texture 数据的复制和初始化,这里只是为了让大家掌握和理解多个命令复制纹理数据的方法而特意这样做的。

  最后,本章示例的其它代码就与 DirectX12(D3D12)基础教程(五)——理解和使用捆绑包,加载并使用DDS Cube Map 示例代码相一致了,也就不在赘述了,大家可以先去复习一下。当然这一章教程以及示例原本是不必要的,但是在继续 IBL 之前,我觉得非常有必要把在 PBR IBL 教程中用到的一些基础技术进行一下剥离,因为单独来讲 IBL 的话,其中牵扯的技术点和方法实在是太多,如果不拆分一股脑都集中在一起的话,估计大家掌握起来会非常的困难,所以,在开始正式的 IBL 教程之前,先把这些技术点一个个呈现出来,尽量使大家的学习曲线平缓一些。对于本章来说,大家一定要记住的重点就是我们关于 Texture 的新定义,以及 Texture Array 对应 CubeMap 的关系,也就是那张六面体展开图,其中的顺序和位置一定要记忆在脑海中。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论
以下是使用纹理数组的示例代码: 首先,需要创建一个 D3D12_TEXTURE2D_DESC 结构体来描述纹理数组的属性,例如纹理的宽度、高度、格式等等: ``` D3D12_TEXTURE2D_DESC texArrayDesc = {}; texArrayDesc.Width = 1024; texArrayDesc.Height = 1024; texArrayDesc.MipLevels = 1; texArrayDesc.ArraySize = 3; // 纹理数组的大小 texArrayDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; texArrayDesc.SampleDesc.Count = 1; texArrayDesc.SampleDesc.Quality = 0; texArrayDesc.Usage = D3D12_USAGE_DEFAULT; texArrayDesc.BindFlags = D3D12_BIND_SHADER_RESOURCE; texArrayDesc.CPUAccessFlags = 0; texArrayDesc.MiscFlags = D3D12_RESOURCE_MISC_TEXTURECUBE; ``` 然后,需要创建一个纹理数组资源: ``` ComPtr<ID3D12Resource> texArray = nullptr; ThrowIfFailed(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &texArrayDesc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&texArray))); ``` 接下来,需要为纹理数组资源创建一个 SRV 描述符堆: ``` D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {}; srvHeapDesc.NumDescriptors = 1; // 只有一个纹理数组 srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; ThrowIfFailed(device->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&srvHeap))); CD3DX12_CPU_DESCRIPTOR_HANDLE srvHandle(srvHeap->GetCPUDescriptorHandleForHeapStart()); CD3DX12_GPU_DESCRIPTOR_HANDLE srvGpuHandle(srvHeap->GetGPUDescriptorHandleForHeapStart()); // 创建 SRV 描述符 D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Format = texArrayDesc.Format; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; srvDesc.Texture2DArray.MipLevels = 1; srvDesc.Texture2DArray.ArraySize = texArrayDesc.ArraySize; // 数组大小 srvDesc.Texture2DArray.FirstArraySlice = 0; srvDesc.Texture2DArray.PlaneSlice = 0; srvDesc.Texture2DArray.ResourceMinLODClamp = 0.0f; device->CreateShaderResourceView(texArray.Get(), &srvDesc, srvHandle); ``` 最后,需要将纹理数据复制到纹理数组中: ``` // 将纹理数据复制到纹理数组的第 0 个元素中 D3D12_SUBRESOURCE_DATA texArrayData[3] = {}; for (int i = 0; i < 3; ++i) { texArrayData[i].pData = ...; // 纹理数据 texArrayData[i].RowPitch = ...; texArrayData[i].SlicePitch = ...; } UpdateSubresources(commandList.Get(), texArray.Get(), texArrayUpload.Get(), 0, 0, 3, texArrayData); // 将资源状态从 COMMON 转换为 SHADER_RESOURCE CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition( texArray.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); commandList->ResourceBarrier(1, &barrier); ``` 现在,纹理数组就可以在着色器中使用了。例如,在 HLSL 中可以这样定义一个纹理数组: ``` Texture2DArray<float4> g_TexArray : register(t0); ``` 在着色器中使用时,可以通过数组下标来访问不同的纹理: ``` float4 texel = g_TexArray.Sample(texSampler, float3(texU, texV, texIndex)); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GamebabyRockSun_QQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值