DX12 快速教程(4) —— 画钻石原矿

快速导航

  • 新建项目 "004-DrawTexture"
  • 纹理贴图
    • 纹理采样
    • 纹理过滤
      • 邻近点采样
      • 双线性过滤
      • Mipmap 多级渐远纹理
      • 三线性过滤
      • 各向异性过滤
    • 纹理环绕
    • LOD 细节层次
  • 开始画钻石原矿吧
    • 加载纹理到内存中:LoadTexture
      • 什么是 WIC
      • 如何用 WIC 读取一帧图片
      • 获取图片格式并转换纹理
      • 获取图片信息 (纹理宽高、图像深度)
    • 创建着色器资源描述符堆:CreateSRVHeap
    • 创建上传堆与默认堆资源:CreateUploadAndDefaultResource
      • 计算上传堆资源的所需空间大小
      • 创建上传堆资源
      • 创建默认堆资源
    • 复制资源到默认堆:CopyTextureDataToDefaultResource
      • 分配内存并读取图片数据 (CPU 高速缓存 -> CPU 高速缓存)
      • 复制到上传堆资源 (CPU 高速缓存 -> 共享内存)
      • 复制到默认堆资源 (共享内存 -> 显存)
    • 最终创建 SRV 描述符:CreateSRV
    • 修改着色器代码:shader.hlsl
    • 修改根签名:CreateRootSignature
      • 填写 Range 描述符范围结构体
      • 填写 RootDescriptorTable 根描述表结构体
      • 填写 RootParameter 根参数结构体
      • 填写 StaticSampler 静态采样器
      • 最终填写 RootSignature 根签名信息结构体
    • 修改 PSO:CreatePSO
    • 修改顶点资源:CreateVertexResource
    • 修改渲染代码:Render
  • 第四节全代码
    • main.cpp
    • shader.hlsl


新建项目 “004-DrawTexture”


在这里插入图片描述


将钻石原矿纹理文件 diamond_ore.png 放到项目文件夹 “004-DrawTexture” 里。


在这里插入图片描述

在这里插入图片描述


点我显示钻石原矿纹理 diamond_ore.png:https://s21.ax1x.com/2025/01/19/pEkYn2Q.png



纹理贴图


在这里插入图片描述

上一节我们在讲 Pixel Shader 像素着色器时简单的提到了纹理贴图纹理UV映射,本节教程我们来探讨:纹理是怎么贴到模型上?纹理 UV 坐标又是如何映射到几何体表面的呢?


在这里插入图片描述


纹理采样


前置知识:Sampling 采样与 Aliasing 走样
有没有想过,光栅化是如何将三角形填充到屏幕像素中呢?


在这里插入图片描述


这里就用到了采样 (Sampling)。所谓采样就是一个连续的函数 f(x) 在不同 x 值拿到这个函数的值是多少,相当于把一个函数给离散化的过程。


在这里插入图片描述

采样就是用若干个单独的点去表示一个连续的图像

我们的电脑屏幕像素点是有限的,无法完整表示一个连续图像,但我们可以使用采样技术,用尽可能多的像素点去拟合这个图像。

走样 (Aliasing),表示失去原来的样子。在图形学中表示图形的时候,得到的结果与原本变样了。常见的走样有锯齿 (Jaggies)、摩尔纹 (Moire)、车轮效应 (Wagon wheel effect)。


在这里插入图片描述

锯齿,常常发生在图形边缘

在这里插入图片描述

摩尔纹,常常发生在放大/缩小图形

在这里插入图片描述
在这里插入图片描述

车轮效应,有些高速行驶的汽车,但是我们的眼睛看它的车轮却反而像是在倒着转

造成走样的原因都是由于采样频率低 (采样慢) 跟不上物体的实际变化速度。正所谓:“计划赶不上变化”。


和连续的图像不同,纹理图片其实是由一个个单独的 Texture Pixel 纹理像素 (Texel 纹素) 组成的。


在这里插入图片描述

在这里插入图片描述

Texel 纹素

在屏幕上某一像素绘制时,根据像素所在位置,去图片上寻找对应纹素值的过程,这个过程就是纹理采样!


在这里插入图片描述


纹理过滤



在这里插入图片描述


然而 3D 模型大多数都是不规则的,还有可能是翻转倾斜的,纹理像素好像对不上号,想要给一个模型做纹理贴图似乎很难,拿到纹理 UV 坐标后,该如何进行采样呢?这就是 Texture Filtering 纹理过滤 的工作了。


在这里插入图片描述


邻近点采样


我们先来讲一个最简单的过滤方法:Nearest-Neighbor Sampling 邻近点采样。

这种方法只需要采样一次,选取与纹理坐标最接近的像素点颜色就行了,所以叫邻近点采样。


在这里插入图片描述
在这里插入图片描述


Minecraft 等等这种类似的像素游戏,早期的 3D 游戏,用的就是邻近点采样。


在这里插入图片描述


这样操作简单粗暴,但是像素之间会呈现明显的马赛克现象,尤其是在放大/缩小图像时,会产生摩尔纹。


在这里插入图片描述

在这里插入图片描述

缩小图片时,会产生更刺眼的摩尔纹

双线性过滤


为了解决邻近点采样带来的问题,Bilinear Filtering 双线性过滤 应运而生。

这种方法需要对目标像素周围的四个最近纹理像素进行加权平均,计算出一个插值,近似出这些纹理像素之间的颜色。


在这里插入图片描述
在这里插入图片描述


双线性过滤能使图像变得更加平滑,但很难看出单个纹理像素,会比较模糊。


在这里插入图片描述

左:邻近点采样;右:双线性过滤

Mipmap 多级渐远纹理


邻近点采样会产生尖锐的马赛克,而双线性过滤又会使图像过于模糊。有没有什么方法能同时解决上述两种问题呢?Mipmap 多级渐远纹理 应运而生。

在采样纹理时,纹理大小跟图形大小接近才会有好的显示效果。

透视现象会让同一幅纹理总是出现大小不均的情况,这使得双线性过滤的效果相当差。Mipmap 技术又是如何解决这个问题的呢?Mipmap 的原理是预先生成一系列以 2 为倍数缩小的纹理序列,在采样纹理时根据图形的大小自动选择相近等级的 Mipmap 进行采样。


在这里插入图片描述
在这里插入图片描述


近大远小,离得近的用大纹理采样,离的远的用小纹理采样,这样得到的效果总会是最好的。加上双线性过滤,赢麻了。


在这里插入图片描述

加持 Mipmap 的采样使远方的摩尔纹大幅淡化

三线性过滤


双线性过滤 + Mipmap 还是美中不足,当纹理距离相机刚好处于两个 Mipmap 等级的交界处时,会发生 Mipmap 跳变 (下图红框部分),图像在一条"裂缝"上突然错位模糊。


在这里插入图片描述

可以看到近处纹理空间的锯齿明显消失,但是不同 Mipmap 等级间的分界依然明显

为了解决这一问题,Trilinear Filtering 三线性过滤 应运而生。"三"指的是纹理四格采样 + Mipmap 等级三个维度。通过对两个相邻的 Mipmap 等级的纹理进行双线性过滤采样,并对两个采样结果线性插值得到最终的颜色。这样采样能使 Mipmap 交界处相当平滑,图像质量更好。


在这里插入图片描述

在这里插入图片描述

三线性过滤解决了 Mipmap 过渡中的分界问题,但是对角方向上的纹理依然很奇怪,并且异常模糊

各向异性过滤


使用三线性过滤 + Mipmap 之后,对于在屏幕上呈现 (近似) 正方形的图形,我们已经能够取得很好的效果了。但是游戏中的地形是多变且崎岖的,对于倾斜或者长条状的图形 (上图倾斜的纹理),显示效果依然不够好!

如何完美解决这个问题呢?我们在游戏设置中常见的 Anisotropic Filtering 各向异性过滤 就是来绝毙这个问题的。


在这里插入图片描述

倾斜或是长条形状的物体,在 xy 方向上的纹理坐标变化率可以差距很大。例如下图中,左图在同样的距离上 du 与 dv 基本相等,而右图中 dv 则大约是 du 的两倍。若在这种情况下开启 Mipmap,右边的图形就会被贴上更低一级的 Mipmap,导致模糊。


在这里插入图片描述

“各向异性”是指“ x 方向”和“ y 方向”采用“不一样的缩略比”的意思。各向异性过滤通过采样一个非方形纹理绝毙了这个问题。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

左:三线性过滤;右:各向异性过滤。可以看到远处的地面明显清晰很多。

游戏中常用的各向异性过滤通常是 4X8X,这里的 NX 指的是过滤等级,等级越高需要采样的纹理像素点越多,图像越清晰。8X 与 16X 效果相似,16X 就是人眼能识别的极限了。


纹理过滤类型纹理采样次数
邻近点采样1
双线性过滤4
三线性过滤8
各向异性过滤 2X16
各向异性过滤 4X32
各向异性过滤 8X64
各向异性过滤 16X128

纹理环绕


除了纹理过滤,我们渲染还会经常碰到纹理 UV 坐标超过 [0,1] 范围的情况,这种情况下纹理该如何采样呢?Texture Warp 纹理环绕 就是来解决这个问题的。


在这里插入图片描述

在这里插入图片描述

OpenGL 的四种纹理环绕方式

在 DirectX 中,纹理环绕的方式被称为 Address Mode 寻址模式,有四种:

  1. Wrap Texture Address Mode 重复寻址模式

在这里插入图片描述


  1. Mirror Texture Address Mode 镜像寻址模式

在这里插入图片描述


  1. Clamp Texture Address Mode 钳位寻址模式

在这里插入图片描述


  1. Border Texture Address Mode 边框寻址模式

在这里插入图片描述

注意这里的"边框"实际指的是 Sampler 采样器的预设颜色,不是指物体的描边颜色!

纹理环绕经常被用于墙壁、地板,天空盒这些需要重复或无限延伸的情景。


在这里插入图片描述

无缝衔接的墙面纹理

在这里插入图片描述

地板纹理

在这里插入图片描述

天空盒纹理

LOD 细节层次


最后我们还需要了解一个东西:Level of Detail LOD 细节层次。

物体离摄像机较远时,我们看不清这个物体的细节。远处的物体看起来都差不多,因此我们也没有必要再让 GPU 执行更多的计算/采样工作,科学家们从这个角度进行优化,研究出了 LOD 细节层次技术。


在这里插入图片描述

越往右细节层次越低 (高细节模型 -> 低细节模型),拥有的细节越少

LOD 细节层次根据物体对渲染图像的贡献大小 (物体离摄像机的远近) 决定要使用模型的哪一个细节层次。细节层次包括顶点数量与纹理分辨率。模型离摄像机越近,对渲染图像的贡献越大,要绘制顶点数量越多,纹理分辨率越高;反之,会简化模型,部分看不清的顶点会做相应的剔除,要绘制顶点数量变少,纹理分辨率也变低。


在这里插入图片描述

C4炸药模型 (上) 和猎人模型 (下) 的三个不同的细节层次。 元素在较低的细节级别被简化或删除。 小图显示了简化模型的相对尺寸。

LOD 技术通过使用低细节模型来减少渲染时的多边形数量,减少了 GPU 的带宽,提高了 GPU 渲染的性能,能在保证图像质量的前提下,降低 GPU 的性能开销,让 GPU 可以做更多的事,提升帧率。


开始画钻石原矿吧


加载纹理到内存中:LoadTexture


要想加载一副图片,我们需要用到 Windows 原生组件 WIC 框架,需要包含和链接:

#include<wincodec.h>		// WIC 图像处理框架,用于解码编码转换图片文件#pragma comment(lib,"windowscodecs.lib")	// 链接 WIC DLL

什么是 WIC


Windows Imaging Component (32位 Windows 映像组件,WIC) 是微软在 Windows Vista 开始提供的一套图像处理框架。旨在为应用程序提供统一的接口来处理各种图像格式,WIC不仅支持常见的图像格式如 JPEG、PNG、GIF、BMP、ISO 等,还支持更高分辨率和新格式的图像处理。有了 WIC 框架,我们就能解码、编码、查看、处理图片文件。


在这里插入图片描述

2006 年发布的 Vista 系统,和 2009 年发布的 Win7 长得很像

如何用 WIC 读取一帧图片


在这里插入图片描述


首先,我们需要使用 CoCreateInstance 创建 WIC 工厂:

ComPtr<IWICImagingFactory> m_WICFactory;				// WIC 工厂// 如果还没创建 WIC 工厂,就新建一个 WIC 工厂实例。注意!WIC 工厂不可以重复释放与创建!
if (m_WICFactory == nullptr) CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_WICFactory));

CoCreateInstance 创建一个 COM 组件对象
第一个参数 rclsid 用来唯一标识一个对象的类标识符 (CLSID),需要用它来创建指定对象。我们这里需要指定 WIC 工厂的类标识符 CLSID_WICImagingFactory,表示创建一个 WIC 工厂。


第二个参数 pUnkOuter 用于聚合式对象,这里直接指定 nullptr
第三个参数 dwClsContext 组件类别,这里直接指定 CLSCTX_INPROC_SERVER,表示创建在同一进程中运行的组件。


在这里插入图片描述

CoCreateInstance 的创建原理

创建完工厂后,我们需要使用成员方法 CreateDecoderFromFilename 创建 位图解码器:

std::wstring TextureFilename = L"diamond_ore.png";		// 纹理文件名 (这里用的是相对路径)
ComPtr<IWICBitmapDecoder> m_WICBitmapDecoder;			// 位图解码器// 创建图片解码器,并将图片读入到解码器中
HRESULT hr = m_WICFactory->CreateDecoderFromFilename(TextureFilename.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &m_WICBitmapDecoder);

CreateDecoderFromFilename 根据文件名读取图片创建位图解码器
第一个参数 wzFileName 图片文件名
第二个参数 pguidVendor 首选解码器的 GUID,我们这里直接用 WIC 框架内置的位图解码器,所以填 nullptr。如果想要用第三方解码器,或者是你自己实现的解码器,可以考虑这个参数。
第三个参数 dwDesiredAccess 访问权限,我们这里用 GENERIC_READ,表示图片只读。
第四个参数 metadataOptions 图片数据的缓冲选项,我们这里填 WICDecodeMetadataCacheOnDemand 若需要时就缓冲数据

因为可能会读取失败,我们需要处理 CreateDecoderFromFilename 返回的 HRESULT:

std::wostringstream output_str;		// 用于格式化字符串
switch (hr)
{
case S_OK: break;	// 解码成功,直接 break 进入下一步即可case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):	// 文件找不到output_str << L"找不到文件 " << TextureFilename << L" !请检查文件路径是否有误!";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;case HRESULT_FROM_WIN32(ERROR_FILE_CORRUPT):	// 文件句柄正在被另一个应用进程占用output_str << L"文件 " << TextureFilename << L" 已经被另一个应用进程打开并占用了!请先关闭那个应用进程!";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;case WINCODEC_ERR_COMPONENTNOTFOUND:			// 找不到可解码的组件,说明这不是有效的图像文件output_str << L"文件 " << TextureFilename << L" 不是有效的图像文件,无法解码!请检查文件是否为图像文件!";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;default:			// 发生其他未知错误output_str << L"文件 " << TextureFilename << L" 解码失败!发生了其他错误,错误码:" << hr << L" ,请查阅微软官方文档。";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;
}

如果图片读取成功,位图解码器就会成功创建,我们就可以用里面的成员方法 GetFrame 获取一帧图片:

ComPtr<IWICBitmapFrameDecode> m_WICBitmapDecodeFrame;	// 由解码器得到的单个位图帧// 获取图片数据的第一帧,这个 GetFrame 可以用于 gif 这种多帧动图
m_WICBitmapDecoder->GetFrame(0, &m_WICBitmapDecodeFrame);

GetFrame 的第一个参数是帧索引,第一帧的索引是 0,可以用于解码 gif 这种多帧动图。m_WICBitmapDecoder->GetFrameCount 可以获得图片的总帧数。


获取图片格式并转换纹理


在这里插入图片描述


现在我们成功将图片加载到内存了,下一步就是要转换纹理。因为 DX12 只认识 DXGI 格式,而解码得到的位图格式是 WIC 位图格式。

我们需要对位图进行转换,先找到 DX12 相兼容的 标准位图格式 (Standard GUID),再根据这个找到对应的 DXGI 格式,最后再进行一次简单的转换,继承数据接口就能得到 DX12 可读的 WIC 位图资源 了。

首先,我们需要找到相应的 Standard GUID DXGI Format,由于 DX12 为了降低 API 的耦合性与调用开销,简化了架构,开放了大量的底层接口,不再提供 API 内的纹理自动转换,我们需要自己实现纹理格式转换。

鸣谢 GamebabyRockSun 大佬的教程,我这里提供了一个用于纹理格式转换的辅助结构体与函数:

// 命名空间 DX12TextureHelper 包含了帮助我们转换纹理图片格式的结构体与函数
namespace DX12TextureHelper
{// 纹理转换用,不是 DX12 所支持的格式,DX12 没法用// Standard GUID -> DXGI 格式转换结构体struct WICTranslate{GUID wic;DXGI_FORMAT format;};// WIC 格式与 DXGI 像素格式的对应表,该表中的格式为被支持的格式static WICTranslate g_WICFormats[] ={{ GUID_WICPixelFormat128bppRGBAFloat,       DXGI_FORMAT_R32G32B32A32_FLOAT },{ GUID_WICPixelFormat64bppRGBAHalf,         DXGI_FORMAT_R16G16B16A16_FLOAT },{ GUID_WICPixelFormat64bppRGBA,             DXGI_FORMAT_R16G16B16A16_UNORM },{ GUID_WICPixelFormat32bppRGBA,             DXGI_FORMAT_R8G8B8A8_UNORM },{ GUID_WICPixelFormat32bppBGRA,             DXGI_FORMAT_B8G8R8A8_UNORM },{ GUID_WICPixelFormat32bppBGR,              DXGI_FORMAT_B8G8R8X8_UNORM },{ GUID_WICPixelFormat32bppRGBA1010102XR,    DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM },{ GUID_WICPixelFormat32bppRGBA1010102,      DXGI_FORMAT_R10G10B10A2_UNORM },{ GUID_WICPixelFormat16bppBGRA5551,         DXGI_FORMAT_B5G5R5A1_UNORM },{ GUID_WICPixelFormat16bppBGR565,           DXGI_FORMAT_B5G6R5_UNORM },{ GUID_WICPixelFormat32bppGrayFloat,        DXGI_FORMAT_R32_FLOAT },{ GUID_WICPixelFormat16bppGrayHalf,         DXGI_FORMAT_R16_FLOAT },{ GUID_WICPixelFormat16bppGray,             DXGI_FORMAT_R16_UNORM },{ GUID_WICPixelFormat8bppGray,              DXGI_FORMAT_R8_UNORM },{ GUID_WICPixelFormat8bppAlpha,             DXGI_FORMAT_A8_UNORM }};// GUID -> Standard GUID 格式转换结构体struct WICConvert{GUID source;GUID target;};// WIC 像素格式转换表static WICConvert g_WICConvert[] ={// 目标格式一定是最接近的被支持的格式{ GUID_WICPixelFormatBlackWhite,            GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat1bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat2bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat4bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat8bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat2bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat4bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat16bppGrayFixedPoint,   GUID_WICPixelFormat16bppGrayHalf },		// DXGI_FORMAT_R16_FLOAT{ GUID_WICPixelFormat32bppGrayFixedPoint,   GUID_WICPixelFormat32bppGrayFloat },	// DXGI_FORMAT_R32_FLOAT{ GUID_WICPixelFormat16bppBGR555,           GUID_WICPixelFormat16bppBGRA5551 },		// DXGI_FORMAT_B5G5R5A1_UNORM{ GUID_WICPixelFormat32bppBGR101010,        GUID_WICPixelFormat32bppRGBA1010102 },	// DXGI_FORMAT_R10G10B10A2_UNORM{ GUID_WICPixelFormat24bppBGR,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat24bppRGB,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat32bppPBGRA,            GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat32bppPRGBA,            GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat48bppRGB,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat48bppBGR,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppBGRA,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppPRGBA,            GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppPBGRA,            GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat48bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat48bppBGRFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppBGRAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat48bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat128bppPRGBAFloat,      GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat128bppRGBFloat,        GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat128bppRGBAFixedPoint,  GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat128bppRGBFixedPoint,   GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat32bppRGBE,             GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat32bppCMYK,             GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat64bppCMYK,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat40bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat80bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat32bppRGB,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat64bppRGB,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppPRGBAHalf,        GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat128bppRGBAFloat,       GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat64bppRGBAHalf,         GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBA,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat32bppRGBA,             GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat32bppBGRA,             GUID_WICPixelFormat32bppBGRA },			// DXGI_FORMAT_B8G8R8A8_UNORM{ GUID_WICPixelFormat32bppBGR,              GUID_WICPixelFormat32bppBGR },			// DXGI_FORMAT_B8G8R8X8_UNORM{ GUID_WICPixelFormat32bppRGBA1010102XR,    GUID_WICPixelFormat32bppRGBA1010102XR },// DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM{ GUID_WICPixelFormat32bppRGBA1010102,      GUID_WICPixelFormat32bppRGBA1010102 },	// DXGI_FORMAT_R10G10B10A2_UNORM{ GUID_WICPixelFormat16bppBGRA5551,         GUID_WICPixelFormat16bppBGRA5551 },		// DXGI_FORMAT_B5G5R5A1_UNORM{ GUID_WICPixelFormat16bppBGR565,           GUID_WICPixelFormat16bppBGR565 },		// DXGI_FORMAT_B5G6R5_UNORM{ GUID_WICPixelFormat32bppGrayFloat,        GUID_WICPixelFormat32bppGrayFloat },	// DXGI_FORMAT_R32_FLOAT{ GUID_WICPixelFormat16bppGrayHalf,         GUID_WICPixelFormat16bppGrayHalf },		// DXGI_FORMAT_R16_FLOAT{ GUID_WICPixelFormat16bppGray,             GUID_WICPixelFormat16bppGray },			// DXGI_FORMAT_R16_UNORM{ GUID_WICPixelFormat8bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat8bppAlpha,             GUID_WICPixelFormat8bppAlpha }			// DXGI_FORMAT_A8_UNORM};// 查表确定兼容的最接近格式是哪个bool GetTargetPixelFormat(const GUID* pSourceFormat, GUID* pTargetFormat){*pTargetFormat = *pSourceFormat;for (size_t i = 0; i < _countof(g_WICConvert); ++i){if (InlineIsEqualGUID(g_WICConvert[i].source, *pSourceFormat)){*pTargetFormat = g_WICConvert[i].target;return true;}}return false;		// 找不到,就返回 false}// 查表确定最终对应的 DXGI 格式是哪一个DXGI_FORMAT GetDXGIFormatFromPixelFormat(const GUID* pPixelFormat){for (size_t i = 0; i < _countof(g_WICFormats); ++i){if (InlineIsEqualGUID(g_WICFormats[i].wic, *pPixelFormat)){return g_WICFormats[i].format;}}return DXGI_FORMAT_UNKNOWN;		// 找不到,就返回 UNKNOWN}
}

有了辅助函数,我们就可以进行查找转换了。先获取图片的 Source Format 源图格式 (WIC 位图格式),再按 WICPixelGUID -> Standard GUID -> DXGI Format 的顺序依次查找 Target Format 目标格式 (标准位图格式)、DXGI Format 即可:

// 获取图片格式,并将它转化为 DX12 能接受的纹理格式
// 如果碰到格式无法支持的错误,可以用微软提供的 画图3D 来转换,强力推荐!
DXGI_FORMAT TextureFormat = DXGI_FORMAT_UNKNOWN;	// 纹理格式
WICPixelFormatGUID SourceFormat = {};				// 源图格式
GUID TargetFormat = {};								// 目标格式m_WICBitmapDecodeFrame->GetPixelFormat(&SourceFormat);						// 获取源图格式if (DX12TextureHelper::GetTargetPixelFormat(&SourceFormat, &TargetFormat))	// 获取目标格式
{TextureFormat = DX12TextureHelper::GetDXGIFormatFromPixelFormat(&TargetFormat);	// 获取 DX12 支持的格式
}
else	// 如果没有可支持的目标格式
{::MessageBox(NULL, L"此纹理不受支持!", L"提示", MB_OK);return false;
}

最后,我们需要用 IWICFormatConverter 位图转换器 转换位图,继承数据到 WICBitmapSource WIC 位图资源,就大功告成了。

ComPtr<IWICFormatConverter> m_WICFormatConverter;		// 位图转换器
ComPtr<IWICBitmapSource> m_WICBitmapSource;				// WIC 位图资源,用于获取位图数据// 获取目标格式后,将纹理转换为目标格式,使其能被 DX12 使用
m_WICFactory->CreateFormatConverter(&m_WICFormatConverter);		// 创建图片转换器
// 初始化转换器,实际上是把位图进行了转换
m_WICFormatConverter->Initialize(m_WICBitmapDecodeFrame.Get(), TargetFormat, WICBitmapDitherTypeNone,nullptr, 0.0f, WICBitmapPaletteTypeCustom);
// 将位图数据继承到 WIC 位图资源,我们要在这个 WIC 位图资源上获取信息
m_WICFormatConverter.As(&m_WICBitmapSource);

Initialize 初始化位图转换器,转换位图
第一个参数 pISource 要转换的位图帧
第二个参数 dstFormat 要转换的目标格式
第三个参数 dither 颜色抖动类型Dither 颜色抖动 是一种图像处理技术,详情可看:数字图像入门 —— 颜色抖动 ,我们这里不使用颜色抖动,所以填 WICBitmapDitherTypeNone
第四个参数 pIPalette 要使用的调色板,用于需要调色板的 BMP 位图。现代位图都是真彩位图,不需要调色板,所以我们直接填 nullptr 就行 (如果想了解更多可看:Windows 位图 (Bitmap) 和调色板 (Palette))
第五个参数 alphaThresholdPercent alpha 透明度阈值,用于调色板,我们这里选 0.0f 即可
第六个参数 WICBitmapPaletteType 调色板转换类型,我们不用调色板,所以填 WICBitmapPaletteTypeCustom


获取图片信息 (纹理宽高、图像深度)


前置知识:Bit Per Pixel 图像深度
图像深度 是指图像上每个像素占用的比特数。


在这里插入图片描述
在这里插入图片描述


图像深度越大,每个像素占用的比特位越多,能表示的颜色更丰富。
图像深度有 1、8、16、24、32 位。


1 位图像:每个像素占用 1 个比特位,只能表示黑、白两种颜色。
8 位图像:每个像素占用 8 个比特位,能表示 256 种颜色,纯白到纯黑 255-0,中间数值对应不同的灰色。例如下面的 8 位灰度图像。
16 位图像:每个像素占用 16 个比特位,16 位图像俗称 高彩位图,能表示 2 16 {2}^{16} 216 种颜色,蓝色占5位,绿色占6位,红色占5位 (RGB565),能够表示彩色图像,用于早期的电视屏幕或单片机。
24 位图像:每个像素占用 24 个比特位,24 位图像俗称 真彩位图,能表示 2 32 {2}^{32} 232 种颜色,红绿蓝分量分别占 8 个比特 (RGB888),能表示比 16 位更多的颜色,可以达到人眼分辨的极限。
32 位图像:每个像素占用 32 个比特位,相比 24 位图像多了一个 alpha 分量 (同样占 8 个比特),用来表示透明度 (RGBA8888),色彩过渡更自然,色彩表现力更好,更重要的是可以表示透明背景,我们现在用的图片大多是 32 位图像。


在这里插入图片描述
在这里插入图片描述

我们需要通过 WIC 位图资源和 TargetFormat 标准格式 获取纹理的宽度、高度、像素深度,这三个信息对后文资源创建与资源复制相当重要。

UINT TextureWidth = 0;									// 纹理宽度
UINT TextureHeight = 0;									// 纹理高度
UINT BitsPerPixel = 0;									// 图像深度,图片每个像素占用的比特数
UINT BytePerRowSize = 0;								// 纹理每行数据的真实字节大小,用于读取纹理数据、上传纹理资源m_WICBitmapSource->GetSize(&TextureWidth, &TextureHeight);		// 获取纹理宽高ComPtr<IWICComponentInfo> _temp_WICComponentInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度
ComPtr<IWICPixelFormatInfo> _temp_WICPixelInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度
m_WICFactory->CreateComponentInfo(TargetFormat, &_temp_WICComponentInfo);
_temp_WICComponentInfo.As(&_temp_WICPixelInfo);
_temp_WICPixelInfo->GetBitsPerPixel(&BitsPerPixel);				// 获取 BitsPerPixel 图像深度

创建着色器资源描述符堆:CreateSRVHeap


接下来我们要正式接触一个新的描述符:Shader Resource View 着色器资源描述符 (SRV),"着色器资源"指的是着色器可访问的资源,包括位于显存的 Buffer 缓冲与 Texture 纹理。着色器是运行在 GPU 上的程序,访问显存资源会非常快。所以我们需要用到 SRV 描述符。


在这里插入图片描述


老规矩,和 RTVHeap 渲染目标描述符堆一样,我们同样需要创建一个 SRVHeap 着色器资源描述符堆:

ComPtr<ID3D12DescriptorHeap> m_SRVHeap;					// SRV 描述符堆// 创建 SRV 描述符堆 (Shader Resource View,着色器资源描述符)
D3D12_DESCRIPTOR_HEAP_DESC SRVHeapDesc = {};
SRVHeapDesc.NumDescriptors = 1;									// 我们只有一副纹理,只需要用一个 SRV 描述符
SRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;		// 描述符堆类型,CBV、SRV、UAV 这三种描述符可以放在同一种描述符堆上
SRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;	// 描述符堆标志,Shader-Visible 表示对着色器可见// 创建 SRV 描述符堆
m_D3D12Device->CreateDescriptorHeap(&SRVHeapDesc, IID_PPV_ARGS(&m_SRVHeap));

注意结构体的 Flags 成员要设置成 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE 对着色器可见 !因为所有的描述符堆 (RTV、CBV_SRV_UAV、DSV、Sampler) 都是 CPU-Visible 对 CPU 可见 的,但是只有 CBV_SRV_UAV 堆Sampler 纹理采样器堆 才能设置 Shader-Visible 对着色器可见,下文我们设置描述符时,会用到描述符的 GPU 句柄,着色器会通过这个 GPU 句柄寻址,去显存中找资源,这个需要所属描述符堆的 Shader-Visible 权限,所以要在创建时指定 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE 标志,否则后文的 SetDescriptorHeap 操作会失败!

SRV 描述符堆是可以同时存储 CBV (常量缓冲描述符)、SRV (着色器资源描述符)、UAV (无序访问描述符) 三种不同类型的描述符的 (见上文指定描述符堆类型的 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV),DX12 为什么要这样设计呢?为什么不分别设计成 SRVHeap、CBVHeap、UAVHeap 三种不同类型的描述符堆呢?
答案:为了资源复用,降低切换开销。
后文在 CommandList 上的 SetDescriptorHeap,作用是绑定描述符堆供着色器使用,同一类型的 Heap 只能设置一个,并且只能设置 Shader-Visible 的描述符堆 (CBV_SRV_UAV Heap 与 Sampler Heap,其他 Non-Shader-Visible Heap 是放在 CPU 高速缓存上,显存用不上,所以 Non-Shader-Visible 绑定到着色器上没有意义)。
其次,SetDescriptorHeap 会把原来绑定的两个描述符堆给取消绑定,再重新绑定新的描述符,如果我们在绘制过程中频繁调用这个 SetDescriptorHeap 去切换使用不同的 Heap,会产生昂贵的切换开销。所以推荐的解决方法,就是使用一个大的 Heap,然后划分好不同的区域给 Constant Buffer、Shader Resource、Unordered Access Resource 使用。这就是为什么 DX12 要这样设计的原因。

拓展阅读:DX12渲染器开发(3):浅谈DX12程序框架
拓展阅读:游戏引擎编程实践(2)- DirectX 12描述符管理


创建上传堆与默认堆资源:CreateUploadAndDefaultResource


在这里插入图片描述


上一章我们初步认识了三种资源堆,并学会了使用上传堆资源向 GPU 传递顶点资源。上传堆资源高度只能是 1,所以很适合存储像顶点数据那样的一维线性资源。

但我们的纹理图片可是二维资源,有纹理宽高。上传堆不符合我们的需求,那怎么办?这时候就要用到位于显存的 DefaultHeap 默认堆了。

默认堆位于显存,而我们的纹理数据在 CPU 高速缓存,我们需要弄一个上传堆资源当工具人,帮我们中转数据。


计算上传堆资源的所需空间大小


前置知识:Memory Alignment 内存对齐


在硬件实现中,CPU 通常是整块整块内存读取的,内存存储会遵循一定的对齐规则,所以我们会经常看到结构体大小经常是 4 字节或 8 字节的整数倍,这个就叫内存对齐。内存对齐可以减少 CPU 内存操作次数,提高内存操作效率,并提升 CPU 缓存命中率,从而提升整体性能。


CPU 要内存对齐,GPU 这种注重并行计算的更要内存对齐,因为这样可以结构化数据,方便 GPU 同时将数据送到多个寄存器上,加快计算效率。在 DX12 中的内存对齐规则有 256 B 对齐、4 KB 对齐、64 KB 对齐、4 MB 对齐。

首先,我们需要计算上传堆资源的所需空间大小。注意上传堆资源分配只多不少!算少了会创建失败。用于中转纹理数据的上传堆资源需要遵循 256 字节对齐规则 (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT = 256),所以我们需要对纹理每行大小进行 Ceil 向上取整对齐,这样才能算出需要分配的上传堆资源大小:


在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


// 上取整算法,对 A 向上取整,判断至少要多少个长度为 B 的空间才能容纳 A,用于内存对齐
inline UINT Ceil(UINT A, UINT B)
{return (A + B - 1) / B;
}// 计算纹理每行数据的真实数据大小 (单位:Byte 字节),因为纹理图片在内存中是线性存储的
// 想获取纹理的真实大小、正确读取纹理数据、上传到 GPU,必须先获取纹理的 BitsPerPixel 图像深度,因为不同位图深度可能不同
// 然后再计算每行像素占用的字节,除以 8 是因为 1 Byte = 8 bits
UINT BytePerRowSize = TextureWidth * BitsPerPixel / 8;// 纹理的真实大小 (单位:字节)
UINT TextureSize = BytePerRowSize * TextureHeight;// 上传堆资源每行的大小 (单位:字节),注意这里要进行 256 字节对齐!
// 因为 GPU 与 CPU 架构不同,GPU 注重并行计算,注重结构化数据的快速读取,读取数据都是以 256 字节为一组来读的
// 因此要先要对 BytePerRowSize 进行对齐,判断需要有多少组才能容纳纹理每行像素,不对齐的话数据会读错的。
UINT UploadResourceRowSize = Ceil(BytePerRowSize, 256) * 256;// 上传堆资源的总大小 (单位:字节),分配空间必须只多不少,否则会报 D3D12 MinimumAlloc Error 资源内存创建错误
// 注意最后一行不用内存对齐 (因为后面没其他行了,不用内存对齐也能正确读取),所以要 (TextureHeight - 1) 再加 BytePerRowSize
UINT UploadResourceSize = UploadResourceRowSize * (TextureHeight - 1) + BytePerRowSize;

创建上传堆资源


注意上传堆资源高度只能为 1 ,宽度是 UploadResourceSize:

// 用于中转纹理的上传堆资源结构体
D3D12_RESOURCE_DESC UploadResourceDesc = {};
UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 资源类型,上传堆的资源类型都是 buffer 缓冲
UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// 资源布局,指定资源的存储方式,上传堆的资源都是 row major 按行线性存储
UploadResourceDesc.Width = UploadResourceSize;						// 资源宽度,上传堆的资源宽度是资源的总大小,注意资源大小必须只多不少
UploadResourceDesc.Height = 1;										// 资源高度,上传堆仅仅是传递线性资源的,所以高度必须为 1
UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 资源格式,上传堆资源的格式必须为 UNKNOWN
UploadResourceDesc.DepthOrArraySize = 1;							// 资源深度,这个是用于纹理数组和 3D 纹理的,上传堆资源必须为 1
UploadResourceDesc.MipLevels = 1;									// Mipmap 等级,这个是用于纹理的,上传堆资源必须为 1
UploadResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,上传堆资源都是填 1// 上传堆属性的结构体,上传堆位于 CPU 和 GPU 的共享内存
D3D12_HEAP_PROPERTIES UploadHeapDesc = { D3D12_HEAP_TYPE_UPLOAD };ComPtr<ID3D12Resource> m_UploadTextureResource;			// 上传堆资源,位于共享内存,用于中转纹理资源// 创建上传堆资源
m_D3D12Device->CreateCommittedResource(&UploadHeapDesc, D3D12_HEAP_FLAG_NONE, &UploadResourceDesc,D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_UploadTextureResource));

顶点数据也是上传堆资源啊,为什么顶点资源不用对齐?
答案:硬件要求不同。
顶点数据是一维线性资源,可以直接放入上传堆供 GPU 供上传堆使用,使用方式由 VBV 描述符指定 (地址,步长,总大小),无需再耗费额外的内存资源去对齐;纹理是二维资源,硬件对纹理有对齐大小的需求,这样才能结构化二维数据,加速纹理数据传输。


创建默认堆资源


默认堆资源用于放纹理,所以宽度高度均为纹理宽高 (单位:像素),图像深度由 TextureFormat 指定,无需对齐。这样 GPU 就知道如何正确使用纹理了:

// 用于放纹理的默认堆资源结构体
D3D12_RESOURCE_DESC DefaultResourceDesc = {};
DefaultResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;	// 资源类型,这里指定为 Texture2D 2D纹理
DefaultResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;			// 纹理资源的布局都是 UNKNOWN
DefaultResourceDesc.Width = TextureWidth;							// 资源宽度,这里填纹理宽度
DefaultResourceDesc.Height = TextureHeight;							// 资源高度,这里填纹理高度
DefaultResourceDesc.Format = TextureFormat;							// 资源格式,这里填纹理格式,要和纹理一样
DefaultResourceDesc.DepthOrArraySize = 1;							// 资源深度,我们只有一副纹理,所以填 1
DefaultResourceDesc.MipLevels = 1;									// Mipmap 等级,我们暂时不使用 Mipmap,所以填 1
DefaultResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,这里我们填 1 就行// 默认堆属性的结构体,默认堆位于显存
D3D12_HEAP_PROPERTIES DefaultHeapDesc = { D3D12_HEAP_TYPE_DEFAULT };ComPtr<ID3D12Resource> m_DefaultTextureResource;		// 默认堆资源,位于显存,用于放纹理// 创建默认堆资源
m_D3D12Device->CreateCommittedResource(&DefaultHeapDesc, D3D12_HEAP_FLAG_NONE, &DefaultResourceDesc,D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&m_DefaultTextureResource));

复制资源到默认堆:CopyTextureDataToDefaultResource


分配内存并读取图片数据 (CPU 高速缓存 -> CPU 高速缓存)



我们可以使用 WIC 位图资源的成员方法 CopyPixels 获取纹理数据:

// 用于暂时存储纹理数据的指针,这里要用 malloc 分配空间
BYTE* TextureData = (BYTE*)malloc(TextureSize);// 将整块纹理数据读到 TextureData 中,方便后文的 memcpy 复制操作
m_WICBitmapSource->CopyPixels(nullptr, BytePerRowSize, TextureSize, TextureData);

CopyPixels 复制纹理数据
第一个参数 prc 要复制的纹理区域nullptr 表示复制整个纹理区域
第二个参数 cbStride 纹理每行的相隔大小 (步长),单位:字节,这里我们填纹理每行大小 BytePerRowSize
第三个参数 cbBufferSize 要复制到的缓冲区大小 (第四个参数的大小),这里我们填纹理的真实大小 TextureSize
第四个参数 pbBuffer 要复制到的缓冲区,纹理数据将会复制到这个缓冲区上

为什么我们分配内存用 malloc,而不是用 new 呢?
因为 new 还有调用构造函数的操作,所以速度比 malloc 略慢。


复制到上传堆资源 (CPU 高速缓存 -> 共享内存)


在这里插入图片描述


上文我们提到了上传堆资源需要对齐,所以这里要逐行复制,否则后面 GPU 会读错纹理数据:

// 用于传递资源的指针
BYTE* TransferPointer = nullptr;// Map 开始映射,Map 方法会得到上传堆资源的地址 (在共享内存上),传递给指针,这样我们就能通过 memcpy 操作复制数据了
m_UploadTextureResource->Map(0, nullptr, reinterpret_cast<void**>(&TransferPointer));// 这里我们要逐行复制数据!注意两个指针偏移的长度不同!
for (UINT i = 0; i < TextureHeight; i++)
{// 向上传堆资源逐行复制纹理数据 (CPU 高速缓存 -> 共享内存)memcpy(TransferPointer, TextureData, BytePerRowSize);// 纹理指针偏移到下一行TextureData += BytePerRowSize;// 上传堆资源指针偏移到下一行,注意偏移长度不同!TransferPointer += UploadResourceRowSize;
}// Unmap 结束映射,因为我们无法直接读写默认堆资源,需要上传堆复制到那里,在复制之前,我们需要先结束映射,让上传堆处于只读状态
m_UploadTextureResource->Unmap(0, nullptr);TextureData -= TextureSize;		// 纹理资源指针偏移回初始位置
free(TextureData);				// 释放上文 malloc 分配的空间,后面我们用不到它,不要让它占内存

复制到默认堆资源 (共享内存 -> 显存)


最后一步就是将纹理从上传堆复制到默认堆了,默认堆位于显存,我们怎么复制呢?

答案:利用 GPU 的 Copy Engine 复制引擎。

显存只有 GPU 才能高速读写,GPU 有三种引擎:负责 3D 图像绘制的 3D Engine 3D 引擎、负责复制的 Copy Engine 复制引擎、负责辅助计算的 Compute Engine 计算引擎。


在这里插入图片描述

我们可以利用 GPU 的 Copy Engine 实现纹理复制,但是我们怎么操作 CPU 和 GPU 呢?

答案:命令三件套 (CommandList、CommandQueue、CommandAllocator)。

为了准备复制,我们需要填写相关结构体信息:

D3D12_PLACED_SUBRESOURCE_FOOTPRINT PlacedFootprint = {};						// 资源脚本,用来描述要复制的资源
D3D12_RESOURCE_DESC DefaultResourceDesc = m_DefaultTextureResource->GetDesc();	// 默认堆资源结构体// 获取纹理复制脚本,用于下文的纹理复制
m_D3D12Device->GetCopyableFootprints(&DefaultResourceDesc, 0, 1, 0, &PlacedFootprint, nullptr, nullptr, nullptr);D3D12_TEXTURE_COPY_LOCATION DstLocation = {};						// 复制目标位置 (默认堆资源) 结构体
DstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;		// 纹理复制类型,这里必须指向纹理
DstLocation.SubresourceIndex = 0;									// 指定要复制的子资源索引
DstLocation.pResource = m_DefaultTextureResource.Get();				// 要复制到的资源D3D12_TEXTURE_COPY_LOCATION SrcLocation = {};						// 复制源位置 (上传堆资源) 结构体
SrcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;		// 纹理复制类型,这里必须指向缓冲区
SrcLocation.PlacedFootprint = PlacedFootprint;						// 指定要复制的资源脚本信息
SrcLocation.pResource = m_UploadTextureResource.Get();				// 被复制数据的缓冲

填写完相关信息后,我们向 CommandQueue 发出复制命令 CopyTextureRegion 即可。注意后面仍然要进行围栏等待,否则会发生资源冲突!



// 复制资源需要使用 GPU 的 CopyEngine 复制引擎,所以需要向命令队列发出复制命令
m_CommandAllocator->Reset();								// 先重置命令分配器
m_CommandList->Reset(m_CommandAllocator.Get(), nullptr);	// 再重置命令列表,复制命令不需要 PSO 状态,所以第二个参数填 nullptr// 记录复制资源到默认堆的命令 (共享内存 -> 显存) 
m_CommandList->CopyTextureRegion(&DstLocation, 0, 0, 0, &SrcLocation, nullptr);
// 关闭命令列表
m_CommandList->Close();// 用于传递命令用的临时 ID3D12CommandList 数组
ID3D12CommandList* _temp_cmdlists[] = { m_CommandList.Get() };// 提交复制命令!GPU 开始复制!
m_CommandQueue->ExecuteCommandLists(1, _temp_cmdlists);// 将围栏预定值设定为下一帧,注意复制资源也需要围栏等待,否则会发生资源冲突
FenceValue++;
// 在命令队列 (命令队列在 GPU 端) 设置围栏预定值,此命令会加入到命令队列中
// 命令队列执行到这里会修改围栏值,表示复制已完成,"击中"围栏
m_CommandQueue->Signal(m_Fence.Get(), FenceValue);
// 设置围栏的预定事件,当复制完成时,围栏被"击中",激发预定事件,将事件由无信号状态转换成有信号状态
m_Fence->SetEventOnCompletion(FenceValue, RenderEvent);

拓展阅读:dx12: 多线程渲染


最终创建 SRV 描述符:CreateSRV


D3D12_CPU_DESCRIPTOR_HANDLE SRV_CPUHandle;				// SRV 描述符 CPU 句柄
D3D12_GPU_DESCRIPTOR_HANDLE SRV_GPUHandle;				// SRV 描述符 GPU 句柄// SRV 描述符信息结构体
D3D12_SHADER_RESOURCE_VIEW_DESC SRVDescriptorDesc = {};
// SRV 描述符类型,这里我们指定 Texture2D 2D纹理
SRVDescriptorDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
// SRV 描述符的格式也要填纹理格式
SRVDescriptorDesc.Format = TextureFormat;
// 纹理采样后每个纹理像素 RGBA 分量的顺序,D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING 表示纹理采样后分量顺序不改变
SRVDescriptorDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
// 这里我们不使用 Mipmap,所以填 1
SRVDescriptorDesc.Texture2D.MipLevels = 1;// 获取 SRV 描述符的 CPU 映射句柄,用于创建资源
SRV_CPUHandle = m_SRVHeap->GetCPUDescriptorHandleForHeapStart();// 创建 SRV 描述符
m_D3D12Device->CreateShaderResourceView(m_DefaultTextureResource.Get(), &SRVDescriptorDesc, SRV_CPUHandle);// 获取 SRV 描述符的 GPU 映射句柄,用于命令列表设置 SRVHeap 描述符堆,着色器引用 SRV 描述符找纹理资源
SRV_GPUHandle = m_SRVHeap->GetGPUDescriptorHandleForHeapStart();

修改着色器代码:shader.hlsl


struct VSInput      // VS 阶段输入顶点数据
{float4 position : POSITION;         // 输入顶点的位置,POSITION 语义对应 C++ 端输入布局中的 POSITIONfloat2 texcoordUV : TEXCOORD;       // 输入顶点的纹理坐标,TEXCOORD 语义对应 C++ 端输入布局中的 TEXCOORD
};struct VSOutput     // VS 阶段输出顶点数据
{float4 position : SV_Position;      // 输出顶点的位置,SV_POSITION 是系统语义,指定顶点坐标已经位于齐次裁剪空间,通知光栅化阶段对顶点进行透视除法和屏幕映射float2 texcoordUV : TEXCOORD;       // 输出顶点纹理坐标时,仍然需要 TEXCOORD 语义
};// Vertex Shader 顶点着色器入口函数 (逐顶点输入),接收来自 IA 阶段输入的顶点数据,处理并返回齐次裁剪空间下的顶点坐标
// 上一阶段:Input Assembler 输入装配阶段
// 下一阶段:Rasterization 光栅化阶段
VSOutput VSMain(VSInput input)
{VSOutput output; // 我们直接向 IA 阶段输入顶点在 NDC 空间下的坐标,所以无需变换,直接赋值返回就行output.position = input.position;output.texcoordUV = input.texcoordUV;return output;
}

// register(*#,spaceN) *表示资源类型,#表示所用的寄存器编号,spaceN 表示使用的 N 号寄存器空间Texture2D m_texure : register(t0, space0);          // 纹理,t 表示 SRV 着色器资源,t0 表示 0 号 SRV 寄存器,space0 表示使用 t0 的 0 号空间
SamplerState m_sampler : register(s0, space0);      // 纹理采样器,s 表示采样器,s0 表示 0 号 sampler 寄存器,space0 表示使用 s0 的 0 号空间// Pixel Shader 像素着色器入口函数 (逐像素输入),接收来自光栅化阶段经过插值后的每个片元,返回像素颜色
// 上一阶段:Rasterization 光栅化阶段
// 下一阶段:Output Merger 输出合并阶段
float4 PSMain(VSOutput input) : SV_Target   // SV_Target 也是系统语义,通知输出合并阶段将 PS 阶段返回的颜色写入到渲染目标(颜色缓冲)上
{return m_texure.Sample(m_sampler, input.texcoordUV);    // 在像素着色器根据光栅化插值得到的 UV 坐标对纹理进行采样
}

修改根签名:CreateRootSignature


填写 Range 描述符范围结构体

D3D12_DESCRIPTOR_RANGE SRVDescriptorRangeDesc = {};						// Range 描述符范围结构体,一块 Range 表示一堆连续的同类型描述符
SRVDescriptorRangeDesc.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;		// Range 类型,这里指定 SRV 类型,CBV_SRV_UAV 在这里分流
SRVDescriptorRangeDesc.NumDescriptors = 1;								// Range 里面的描述符数量 N,一次可以绑定多个描述符到多个寄存器槽上
SRVDescriptorRangeDesc.BaseShaderRegister = 0;							// Range 要绑定的起始寄存器槽编号 i,绑定范围是 [s(i),s(i+N)],我们绑定 s0
SRVDescriptorRangeDesc.RegisterSpace = 0;								// Range 要绑定的寄存器空间,整个 Range 都会绑定到同一寄存器空间上,我们绑定 space0
SRVDescriptorRangeDesc.OffsetInDescriptorsFromTableStart = 0;			// Range 到根描述表开头的偏移量 (单位:描述符),根签名需要用它来寻找 Range 的地址,我们这填 0 就行

填写 RootDescriptorTable 根描述表结构体

D3D12_ROOT_DESCRIPTOR_TABLE RootDescriptorTableDesc = {};				// RootDescriptorTable 根描述表信息结构体,一个 Table 可以有多个 Range
RootDescriptorTableDesc.pDescriptorRanges = &SRVDescriptorRangeDesc;	// Range 描述符范围指针
RootDescriptorTableDesc.NumDescriptorRanges = 1;						// 根描述表中 Range 的数量

填写 RootParameter 根参数结构体

D3D12_ROOT_PARAMETER RootParameter = {};
RootParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;				// 根参数在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理)
RootParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;	// 根参数类型,这里我们选 Table 根描述表,一个根描述表占用 1 DWORD
RootParameter.DescriptorTable = RootDescriptorTableDesc;					// 根参数指针

填写 StaticSampler 静态采样器

D3D12_STATIC_SAMPLER_DESC StaticSamplerDesc = {};						// 静态采样器结构体,静态采样器不会占用根签名
StaticSamplerDesc.ShaderRegister = 0;									// 要绑定的寄存器槽,对应 s0
StaticSamplerDesc.RegisterSpace = 0;									// 要绑定的寄存器空间,对应 space0
StaticSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;		// 静态采样器在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理采样)
StaticSamplerDesc.Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT;	// 纹理过滤类型,这里我们直接选 邻近点采样 就行
StaticSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 U 方向上的纹理寻址方式
StaticSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 V 方向上的纹理寻址方式
StaticSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 W 方向上的纹理寻址方式 (3D 纹理会用到)
StaticSamplerDesc.MinLOD = 0;											// 最小 LOD 细节层次,这里我们默认填 0 就行
StaticSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;							// 最大 LOD 细节层次,这里我们默认填 D3D12_FLOAT32_MAX (没有 LOD 上限)
StaticSamplerDesc.MipLODBias = 0;										// 基础 Mipmap 采样偏移量,我们这里我们直接填 0 就行
StaticSamplerDesc.MaxAnisotropy = 1;									// 各向异性过滤等级,我们不使用各向异性过滤,需要默认填 1
StaticSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;			// 这个是用于阴影贴图的,我们不需要用它,所以填 D3D12_COMPARISON_FUNC_NEVER

最终填写 RootSignature 根签名信息结构体

D3D12_ROOT_SIGNATURE_DESC rootsignatureDesc = {};			// 根签名信息结构体,上限 64 DWORD,静态采样器不占用根签名
rootsignatureDesc.NumParameters = 1;						// 根参数数量
rootsignatureDesc.pParameters = &RootParameter;				// 根参数指针
rootsignatureDesc.NumStaticSamplers = 1;					// 静态采样器数量
rootsignatureDesc.pStaticSamplers = &StaticSamplerDesc;		// 静态采样器指针
// 根签名标志,可以设置渲染管线不同阶段下的输入参数状态。注意这里!我们要从 IA 阶段输入顶点数据,所以要通过根签名,设置渲染管线允许从 IA 阶段读入数据
rootsignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

修改 PSO:CreatePSO


InputElementDesc[1].SemanticName = "TEXCOORD";										// 要锚定的语义
InputElementDesc[1].SemanticIndex = 0;												// 语义索引
InputElementDesc[1].Format = DXGI_FORMAT_R32G32_FLOAT;								// 输入格式
InputElementDesc[1].InputSlot = 0;													// 输入槽编号
// 在输入槽中的偏移,因为 position 与 texcoord 在同一输入槽(0号输入槽)
// position 是 float4,有 4 个 float ,每个 float 占 4 个字节,所以要偏移 4*4=16 个字节,这样才能确定 texcoord 参数的位置,不然装配的时候会覆盖原先 position 的数据
InputElementDesc[1].AlignedByteOffset = 16;											// 在输入槽中的偏移
InputElementDesc[1].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;	// 输入流类型
InputElementDesc[1].InstanceDataStepRate = 0;										// 实例数据步进率

修改顶点资源:CreateVertexResource


struct VERTEX											// 顶点数据结构体
{XMFLOAT4 position;									// 顶点位置XMFLOAT2 texcoordUV;								// 顶点纹理坐标
};

// CPU 高速缓存上的顶点信息数组,注意这里的顶点坐标都是 NDC 空间坐标
VERTEX vertexs[6] =
{{{-0.75f, 0.75f, 0.0f, 1.0f}, {0.0f, 0.0f}},{{0.75f, 0.75f, 0.0f, 1.0f}, {1.0f, 0.0f}},{{0.75f, -0.75f, 0.0f, 1.0f}, {1.0f, 1.0f}},{{-0.75f, 0.75f, 0.0f, 1.0f}, {0.0f, 0.0f}},{{0.75f, -0.75f, 0.0f, 1.0f}, {1.0f, 1.0f}},{{-0.75f, -0.75f, 0.0f, 1.0f}, {0.0f, 1.0f}}
};

修改渲染代码:Render


// 用于设置描述符堆用的临时 ID3D12DescriptorHeap 数组
ID3D12DescriptorHeap* _temp_DescriptorHeaps[] = { m_SRVHeap.Get() };
// 设置描述符堆
m_CommandList->SetDescriptorHeaps(1, _temp_DescriptorHeaps);
// 设置 SRV 句柄
m_CommandList->SetGraphicsRootDescriptorTable(0, SRV_GPUHandle);

第四节全代码


main.cpp



// (4) DrawTexture:用 DirectX 12 画一个钻石原矿#include<Windows.h>			// Windows 窗口编程核心头文件
#include<d3d12.h>			// DX12 核心头文件
#include<dxgi1_6.h>			// DXGI 头文件,用于管理与 DX12 相关联的其他必要设备,如 DXGI 工厂和 交换链
#include<DirectXColors.h>	// DirectX 颜色库
#include<DirectXMath.h>		// DirectX 数学库
#include<d3dcompiler.h>		// DirectX Shader 着色器编译库
#include<wincodec.h>		// WIC 图像处理框架,用于解码编码转换图片文件#include<wrl.h>				// COM 组件模板库,方便写 DX12 和 DXGI 相关的接口
#include<string>			// C++ 标准 string 库
#include<sstream>			// C++ 字符串流处理库#pragma comment(lib,"d3d12.lib")			// 链接 DX12 核心 DLL
#pragma comment(lib,"dxgi.lib")				// 链接 DXGI DLL
#pragma comment(lib,"dxguid.lib")			// 链接 DXGI 必要的设备 GUID
#pragma comment(lib,"d3dcompiler.lib")		// 链接 DX12 需要的着色器编译 DLL
#pragma comment(lib,"windowscodecs.lib")	// 链接 WIC DLLusing namespace Microsoft;
using namespace Microsoft::WRL;		// 使用 wrl.h 里面的命名空间,我们需要用到里面的 Microsoft::WRL::ComPtr COM智能指针
using namespace DirectX;			// DirectX 命名空间// 命名空间 DX12TextureHelper 包含了帮助我们转换纹理图片格式的结构体与函数
namespace DX12TextureHelper
{// 纹理转换用,不是 DX12 所支持的格式,DX12 没法用// Standard GUID -> DXGI 格式转换结构体struct WICTranslate{GUID wic;DXGI_FORMAT format;};// WIC 格式与 DXGI 像素格式的对应表,该表中的格式为被支持的格式static WICTranslate g_WICFormats[] ={{ GUID_WICPixelFormat128bppRGBAFloat,       DXGI_FORMAT_R32G32B32A32_FLOAT },{ GUID_WICPixelFormat64bppRGBAHalf,         DXGI_FORMAT_R16G16B16A16_FLOAT },{ GUID_WICPixelFormat64bppRGBA,             DXGI_FORMAT_R16G16B16A16_UNORM },{ GUID_WICPixelFormat32bppRGBA,             DXGI_FORMAT_R8G8B8A8_UNORM },{ GUID_WICPixelFormat32bppBGRA,             DXGI_FORMAT_B8G8R8A8_UNORM },{ GUID_WICPixelFormat32bppBGR,              DXGI_FORMAT_B8G8R8X8_UNORM },{ GUID_WICPixelFormat32bppRGBA1010102XR,    DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM },{ GUID_WICPixelFormat32bppRGBA1010102,      DXGI_FORMAT_R10G10B10A2_UNORM },{ GUID_WICPixelFormat16bppBGRA5551,         DXGI_FORMAT_B5G5R5A1_UNORM },{ GUID_WICPixelFormat16bppBGR565,           DXGI_FORMAT_B5G6R5_UNORM },{ GUID_WICPixelFormat32bppGrayFloat,        DXGI_FORMAT_R32_FLOAT },{ GUID_WICPixelFormat16bppGrayHalf,         DXGI_FORMAT_R16_FLOAT },{ GUID_WICPixelFormat16bppGray,             DXGI_FORMAT_R16_UNORM },{ GUID_WICPixelFormat8bppGray,              DXGI_FORMAT_R8_UNORM },{ GUID_WICPixelFormat8bppAlpha,             DXGI_FORMAT_A8_UNORM }};// GUID -> Standard GUID 格式转换结构体struct WICConvert{GUID source;GUID target;};// WIC 像素格式转换表static WICConvert g_WICConvert[] ={// 目标格式一定是最接近的被支持的格式{ GUID_WICPixelFormatBlackWhite,            GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat1bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat2bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat4bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat8bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat2bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat4bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat16bppGrayFixedPoint,   GUID_WICPixelFormat16bppGrayHalf },		// DXGI_FORMAT_R16_FLOAT{ GUID_WICPixelFormat32bppGrayFixedPoint,   GUID_WICPixelFormat32bppGrayFloat },	// DXGI_FORMAT_R32_FLOAT{ GUID_WICPixelFormat16bppBGR555,           GUID_WICPixelFormat16bppBGRA5551 },		// DXGI_FORMAT_B5G5R5A1_UNORM{ GUID_WICPixelFormat32bppBGR101010,        GUID_WICPixelFormat32bppRGBA1010102 },	// DXGI_FORMAT_R10G10B10A2_UNORM{ GUID_WICPixelFormat24bppBGR,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat24bppRGB,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat32bppPBGRA,            GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat32bppPRGBA,            GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat48bppRGB,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat48bppBGR,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppBGRA,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppPRGBA,            GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppPBGRA,            GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat48bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat48bppBGRFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppBGRAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat48bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat128bppPRGBAFloat,      GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat128bppRGBFloat,        GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat128bppRGBAFixedPoint,  GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat128bppRGBFixedPoint,   GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat32bppRGBE,             GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat32bppCMYK,             GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat64bppCMYK,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat40bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat80bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat32bppRGB,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat64bppRGB,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat64bppPRGBAHalf,        GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat128bppRGBAFloat,       GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT{ GUID_WICPixelFormat64bppRGBAHalf,         GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT{ GUID_WICPixelFormat64bppRGBA,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM{ GUID_WICPixelFormat32bppRGBA,             GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM{ GUID_WICPixelFormat32bppBGRA,             GUID_WICPixelFormat32bppBGRA },			// DXGI_FORMAT_B8G8R8A8_UNORM{ GUID_WICPixelFormat32bppBGR,              GUID_WICPixelFormat32bppBGR },			// DXGI_FORMAT_B8G8R8X8_UNORM{ GUID_WICPixelFormat32bppRGBA1010102XR,    GUID_WICPixelFormat32bppRGBA1010102XR },// DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM{ GUID_WICPixelFormat32bppRGBA1010102,      GUID_WICPixelFormat32bppRGBA1010102 },	// DXGI_FORMAT_R10G10B10A2_UNORM{ GUID_WICPixelFormat16bppBGRA5551,         GUID_WICPixelFormat16bppBGRA5551 },		// DXGI_FORMAT_B5G5R5A1_UNORM{ GUID_WICPixelFormat16bppBGR565,           GUID_WICPixelFormat16bppBGR565 },		// DXGI_FORMAT_B5G6R5_UNORM{ GUID_WICPixelFormat32bppGrayFloat,        GUID_WICPixelFormat32bppGrayFloat },	// DXGI_FORMAT_R32_FLOAT{ GUID_WICPixelFormat16bppGrayHalf,         GUID_WICPixelFormat16bppGrayHalf },		// DXGI_FORMAT_R16_FLOAT{ GUID_WICPixelFormat16bppGray,             GUID_WICPixelFormat16bppGray },			// DXGI_FORMAT_R16_UNORM{ GUID_WICPixelFormat8bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM{ GUID_WICPixelFormat8bppAlpha,             GUID_WICPixelFormat8bppAlpha }			// DXGI_FORMAT_A8_UNORM};// 查表确定兼容的最接近格式是哪个bool GetTargetPixelFormat(const GUID* pSourceFormat, GUID* pTargetFormat){*pTargetFormat = *pSourceFormat;for (size_t i = 0; i < _countof(g_WICConvert); ++i){if (InlineIsEqualGUID(g_WICConvert[i].source, *pSourceFormat)){*pTargetFormat = g_WICConvert[i].target;return true;}}return false;		// 找不到,就返回 false}// 查表确定最终对应的 DXGI 格式是哪一个DXGI_FORMAT GetDXGIFormatFromPixelFormat(const GUID* pPixelFormat){for (size_t i = 0; i < _countof(g_WICFormats); ++i){if (InlineIsEqualGUID(g_WICFormats[i].wic, *pPixelFormat)){return g_WICFormats[i].format;}}return DXGI_FORMAT_UNKNOWN;		// 找不到,就返回 UNKNOWN}
}// DX12 引擎
class DX12Engine
{
private:int WindowWidth = 640;		// 窗口宽度int WindowHeight = 640;		// 窗口高度HWND m_hwnd;				// 窗口句柄ComPtr<ID3D12Debug> m_D3D12DebugDevice;					// D3D12 调试层设备UINT m_DXGICreateFactoryFlag = NULL;					// 创建 DXGI 工厂时需要用到的标志ComPtr<IDXGIFactory5> m_DXGIFactory;					// DXGI 工厂ComPtr<IDXGIAdapter1> m_DXGIAdapter;					// 显示适配器 (显卡)ComPtr<ID3D12Device4> m_D3D12Device;					// D3D12 核心设备ComPtr<ID3D12CommandQueue> m_CommandQueue;				// 命令队列ComPtr<ID3D12CommandAllocator> m_CommandAllocator;		// 命令分配器ComPtr<ID3D12GraphicsCommandList> m_CommandList;		// 命令列表ComPtr<IDXGISwapChain3> m_DXGISwapChain;				// DXGI 交换链ComPtr<ID3D12DescriptorHeap> m_RTVHeap;					// RTV 描述符堆ComPtr<ID3D12Resource> m_RenderTarget[3];				// 渲染目标数组,每一副渲染目标对应一个窗口缓冲区D3D12_CPU_DESCRIPTOR_HANDLE RTVHandle;					// RTV 描述符句柄UINT RTVDescriptorSize = 0;								// RTV 描述符的大小UINT FrameIndex = 0;									// 帧索引,表示当前渲染的第 i 帧 (第 i 个渲染目标)ComPtr<ID3D12Fence> m_Fence;							// 围栏UINT64 FenceValue = 0;									// 用于围栏等待的围栏值HANDLE RenderEvent = NULL;								// GPU 渲染事件D3D12_RESOURCE_BARRIER beg_barrier = {};				// 渲染开始的资源屏障,呈现 -> 渲染目标D3D12_RESOURCE_BARRIER end_barrier = {};				// 渲染结束的资源屏障,渲染目标 -> 呈现std::wstring TextureFilename = L"diamond_ore.png";		// 纹理文件名 (这里用的是相对路径)ComPtr<IWICImagingFactory> m_WICFactory;				// WIC 工厂ComPtr<IWICBitmapDecoder> m_WICBitmapDecoder;			// 位图解码器ComPtr<IWICBitmapFrameDecode> m_WICBitmapDecodeFrame;	// 由解码器得到的单个位图帧ComPtr<IWICFormatConverter> m_WICFormatConverter;		// 位图转换器ComPtr<IWICBitmapSource> m_WICBitmapSource;				// WIC 位图资源,用于获取位图数据UINT TextureWidth = 0;									// 纹理宽度UINT TextureHeight = 0;									// 纹理高度UINT BitsPerPixel = 0;									// 图像深度,图片每个像素占用的比特数UINT BytePerRowSize = 0;								// 纹理每行数据的真实字节大小,用于读取纹理数据、上传纹理资源DXGI_FORMAT TextureFormat = DXGI_FORMAT_UNKNOWN;		// 纹理格式ComPtr<ID3D12DescriptorHeap> m_SRVHeap;					// SRV 描述符堆D3D12_CPU_DESCRIPTOR_HANDLE SRV_CPUHandle;				// SRV 描述符 CPU 句柄D3D12_GPU_DESCRIPTOR_HANDLE SRV_GPUHandle;				// SRV 描述符 GPU 句柄ComPtr<ID3D12Resource> m_UploadTextureResource;			// 上传堆资源,位于共享内存,用于中转纹理资源ComPtr<ID3D12Resource> m_DefaultTextureResource;		// 默认堆资源,位于显存,用于放纹理UINT TextureSize = 0;									// 纹理的真实大小 (单位:字节)UINT UploadResourceRowSize = 0;							// 上传堆资源每行的大小 (单位:字节)UINT UploadResourceSize = 0;							// 上传堆资源的总大小 (单位:字节)ComPtr<ID3D12RootSignature> m_RootSignature;			// 根签名ComPtr<ID3D12PipelineState> m_PipelineStateObject;		// 渲染管线状态ComPtr<ID3D12Resource> m_VertexResource;				// 顶点资源struct VERTEX											// 顶点数据结构体{XMFLOAT4 position;									// 顶点位置XMFLOAT2 texcoordUV;								// 顶点纹理坐标};D3D12_VERTEX_BUFFER_VIEW VertexBufferView;				// 顶点缓冲描述符// 视口D3D12_VIEWPORT viewPort = D3D12_VIEWPORT{ 0, 0, float(WindowWidth), float(WindowHeight), D3D12_MIN_DEPTH, D3D12_MAX_DEPTH };// 裁剪矩形D3D12_RECT ScissorRect = D3D12_RECT{ 0, 0, WindowWidth, WindowHeight };public:// 初始化窗口void InitWindow(HINSTANCE hins){WNDCLASS wc = {};					// 用于记录窗口类信息的结构体wc.hInstance = hins;				// 窗口类需要一个应用程序的实例句柄 hinstancewc.lpfnWndProc = CallBackFunc;		// 窗口类需要一个回调函数,用于处理窗口产生的消息wc.lpszClassName = L"DX12 Game";	// 窗口类的名称RegisterClass(&wc);					// 注册窗口类,将窗口类录入到操作系统中// 使用上文的窗口类创建窗口m_hwnd = CreateWindow(wc.lpszClassName, L"DX12画钻石原矿", WS_SYSMENU | WS_OVERLAPPED,10, 10, WindowWidth, WindowHeight,NULL, NULL, hins, NULL);// 因为指定了窗口大小不可变的 WS_SYSMENU 和 WS_OVERLAPPED,应用不会自动显示窗口,需要使用 ShowWindow 强制显示窗口ShowWindow(m_hwnd, SW_SHOW);}// 创建调试层void CreateDebugDevice(){::CoInitialize(nullptr);	// 注意这里!DX12 的所有设备接口都是基于 COM 接口的,我们需要先全部初始化为 nullptr#if defined(_DEBUG)		// 如果是 Debug 模式下编译,就执行下面的代码// 获取调试层设备接口D3D12GetDebugInterface(IID_PPV_ARGS(&m_D3D12DebugDevice));// 开启调试层m_D3D12DebugDevice->EnableDebugLayer();// 开启调试层后,创建 DXGI 工厂也需要 Debug Flagm_DXGICreateFactoryFlag = DXGI_CREATE_FACTORY_DEBUG;#endif}// 创建设备bool CreateDevice(){// 创建 DXGI 工厂CreateDXGIFactory2(m_DXGICreateFactoryFlag, IID_PPV_ARGS(&m_DXGIFactory));// DX12 支持的所有功能版本,你的显卡最低需要支持 11.0const D3D_FEATURE_LEVEL dx12SupportLevel[] ={D3D_FEATURE_LEVEL_12_2,		// 12.2D3D_FEATURE_LEVEL_12_1,		// 12.1D3D_FEATURE_LEVEL_12_0,		// 12.0D3D_FEATURE_LEVEL_11_1,		// 11.1D3D_FEATURE_LEVEL_11_0		// 11.0};// 用 EnumAdapters1 先遍历电脑上的每一块显卡// 每次调用 EnumAdapters1 找到显卡会自动创建 DXGIAdapter 接口,并返回 S_OK// 找不到显卡会返回 ERROR_NOT_FOUNDfor (UINT i = 0; m_DXGIFactory->EnumAdapters1(i, &m_DXGIAdapter) != ERROR_NOT_FOUND; i++){// 找到显卡,就创建 D3D12 设备,从高到低遍历所有功能版本,创建成功就跳出for (const auto& level : dx12SupportLevel){// 创建 D3D12 核心层设备,创建成功就返回 trueif (SUCCEEDED(D3D12CreateDevice(m_DXGIAdapter.Get(), level, IID_PPV_ARGS(&m_D3D12Device)))){DXGI_ADAPTER_DESC1 adap = {};m_DXGIAdapter->GetDesc1(&adap);OutputDebugStringW(adap.Description);return true;}}}// 如果找不到任何能支持 DX12 的显卡,就退出程序if (m_D3D12Device == nullptr){MessageBox(NULL, L"找不到任何能支持 DX12 的显卡,请升级电脑上的硬件!", L"错误", MB_OK | MB_ICONERROR);return false;}}// 创建命令三件套void CreateCommandComponents(){// 队列信息结构体,这里只需要填队列的类型 type 就行了D3D12_COMMAND_QUEUE_DESC queueDesc = {};// D3D12_COMMAND_LIST_TYPE_DIRECT 表示将命令都直接放进队列里,不做其他处理queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;// 创建命令队列m_D3D12Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_CommandQueue));// 创建命令分配器,它的作用是开辟内存,存储命令列表上的命令,注意命令类型要一致m_D3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_CommandAllocator));// 创建图形命令列表,注意命令类型要一致m_D3D12Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_CommandAllocator.Get(),nullptr, IID_PPV_ARGS(&m_CommandList));// 命令列表创建时处于 Record 录制状态,我们需要关闭它,这样下文的 Reset 才能成功m_CommandList->Close();}// 创建渲染目标,将渲染目标设置为窗口void CreateRenderTarget(){// 创建 RTV 描述符堆 (Render Target View,渲染目标描述符)D3D12_DESCRIPTOR_HEAP_DESC RTVHeapDesc = {};			RTVHeapDesc.NumDescriptors = 3;							// 渲染目标的数量RTVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;		// 描述符堆的类型:RTV// 创建一个 RTV 描述符堆,创建成功后,会自动开辟三个描述符的内存m_D3D12Device->CreateDescriptorHeap(&RTVHeapDesc, IID_PPV_ARGS(&m_RTVHeap));// 创建 DXGI 交换链,用于将窗口缓冲区和渲染目标绑定DXGI_SWAP_CHAIN_DESC1 swapchainDesc = {};swapchainDesc.BufferCount = 3;								// 缓冲区数量swapchainDesc.Width = WindowWidth;							// 缓冲区 (窗口) 宽度swapchainDesc.Height = WindowHeight;						// 缓冲区 (窗口) 高度swapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;			// 缓冲区格式,指定缓冲区每个像素的大小swapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;	// 交换链类型,有 FILP 和 BITBLT 两种类型swapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;// 缓冲区的用途,这里表示把缓冲区用作渲染目标的输出swapchainDesc.SampleDesc.Count = 1;							// 缓冲区像素采样次数// 临时低版本交换链接口,用于创建高版本交换链,因为下文的 CreateSwapChainForHwnd 不能直接用于创建高版本接口ComPtr<IDXGISwapChain1> _temp_swapchain;// 创建交换链,将窗口与渲染目标绑定// 注意:交换链需要绑定到命令队列来刷新,所以第一个参数要填命令队列,不填会创建失败!m_DXGIFactory->CreateSwapChainForHwnd(m_CommandQueue.Get(), m_hwnd,&swapchainDesc, nullptr, nullptr, &_temp_swapchain);// 通过 As 方法,将低版本接口的信息传递给高版本接口_temp_swapchain.As(&m_DXGISwapChain);// 创建完交换链后,我们还需要令 RTV 描述符 指向 渲染目标// 因为 ID3D12Resource 本质上只是一块数据,它本身没有对数据用法的说明// 我们要让程序知道这块数据是一个渲染目标,就得创建并使用 RTV 描述符// 获取 RTV 堆指向首描述符的句柄RTVHandle = m_RTVHeap->GetCPUDescriptorHandleForHeapStart();// 获取 RTV 描述符的大小RTVDescriptorSize = m_D3D12Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);for (UINT i = 0; i < 3; i++){// 从交换链中获取第 i 个窗口缓冲,创建第 i 个 RenderTarget 渲染目标m_DXGISwapChain->GetBuffer(i, IID_PPV_ARGS(&m_RenderTarget[i]));// 创建 RTV 描述符,将渲染目标绑定到描述符上m_D3D12Device->CreateRenderTargetView(m_RenderTarget[i].Get(), nullptr, RTVHandle);// 偏移到下一个 RTV 句柄RTVHandle.ptr += RTVDescriptorSize;}}// 创建围栏和资源屏障,用于 CPU-GPU 的同步void CreateFenceAndBarrier(){// 创建 CPU 上的等待事件RenderEvent = CreateEvent(nullptr, false, true, nullptr);// 创建围栏,设定初始值为 0m_D3D12Device->CreateFence(FenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_Fence));// 设置资源屏障// beg_barrier 起始屏障:Present 呈现状态 -> Render Target 渲染目标状态beg_barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;					// 指定类型为转换屏障		beg_barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;beg_barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;// end_barrier 终止屏障:Render Target 渲染目标状态 -> Present 呈现状态end_barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;end_barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;end_barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;}// 加载纹理到内存中bool LoadTextureFromFile(){// 如果还没创建 WIC 工厂,就新建一个 WIC 工厂实例。注意!WIC 工厂不可以重复释放与创建!if (m_WICFactory == nullptr) CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_WICFactory));// 创建图片解码器,并将图片读入到解码器中HRESULT hr = m_WICFactory->CreateDecoderFromFilename(TextureFilename.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &m_WICBitmapDecoder);std::wostringstream output_str;		// 用于格式化字符串switch (hr){case S_OK: break;	// 解码成功,直接 break 进入下一步即可case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):	// 文件找不到output_str << L"找不到文件 " << TextureFilename << L" !请检查文件路径是否有误!";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;case HRESULT_FROM_WIN32(ERROR_FILE_CORRUPT):	// 文件句柄正在被另一个应用进程占用output_str << L"文件 " << TextureFilename << L" 已经被另一个应用进程打开并占用了!请先关闭那个应用进程!";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;case WINCODEC_ERR_COMPONENTNOTFOUND:			// 找不到可解码的组件,说明这不是有效的图像文件output_str << L"文件 " << TextureFilename << L" 不是有效的图像文件,无法解码!请检查文件是否为图像文件!";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;default:			// 发生其他未知错误output_str << L"文件 " << TextureFilename << L" 解码失败!发生了其他错误,错误码:" << hr << L" ,请查阅微软官方文档。";MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);return false;}// 获取图片数据的第一帧,这个 GetFrame 可以用于 gif 这种多帧动图m_WICBitmapDecoder->GetFrame(0, &m_WICBitmapDecodeFrame);// 获取图片格式,并将它转化为 DX12 能接受的纹理格式// 如果碰到格式无法支持的错误,可以用微软提供的 画图3D 来转换,强力推荐!WICPixelFormatGUID SourceFormat = {};				// 源图格式GUID TargetFormat = {};								// 目标格式m_WICBitmapDecodeFrame->GetPixelFormat(&SourceFormat);						// 获取源图格式if (DX12TextureHelper::GetTargetPixelFormat(&SourceFormat, &TargetFormat))	// 获取目标格式{TextureFormat = DX12TextureHelper::GetDXGIFormatFromPixelFormat(&TargetFormat);	// 获取 DX12 支持的格式}else	// 如果没有可支持的目标格式{::MessageBox(NULL, L"此纹理不受支持!", L"提示", MB_OK);return false;}// 获取目标格式后,将纹理转换为目标格式,使其能被 DX12 使用m_WICFactory->CreateFormatConverter(&m_WICFormatConverter);		// 创建图片转换器// 初始化转换器,实际上是把位图进行了转换m_WICFormatConverter->Initialize(m_WICBitmapDecodeFrame.Get(), TargetFormat, WICBitmapDitherTypeNone,nullptr, 0.0f, WICBitmapPaletteTypeCustom);// 将位图数据继承到 WIC 位图资源,我们要在这个 WIC 位图资源上获取信息m_WICFormatConverter.As(&m_WICBitmapSource);m_WICBitmapSource->GetSize(&TextureWidth, &TextureHeight);		// 获取纹理宽高ComPtr<IWICComponentInfo> _temp_WICComponentInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度ComPtr<IWICPixelFormatInfo> _temp_WICPixelInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度m_WICFactory->CreateComponentInfo(TargetFormat, &_temp_WICComponentInfo);_temp_WICComponentInfo.As(&_temp_WICPixelInfo);_temp_WICPixelInfo->GetBitsPerPixel(&BitsPerPixel);				// 获取 BitsPerPixel 图像深度return true;}// 创建 SRV Descriptor Heap 着色器资源描述符堆void CreateSRVHeap(){// 创建 SRV 描述符堆 (Shader Resource View,着色器资源描述符)D3D12_DESCRIPTOR_HEAP_DESC SRVHeapDesc = {};SRVHeapDesc.NumDescriptors = 1;									// 我们只有一副纹理,只需要用一个 SRV 描述符SRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;		// 描述符堆类型,CBV、SRV、UAV 这三种描述符可以放在同一种描述符堆上SRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;	// 描述符堆标志,Shader-Visible 表示对着色器可见// 创建 SRV 描述符堆m_D3D12Device->CreateDescriptorHeap(&SRVHeapDesc, IID_PPV_ARGS(&m_SRVHeap));}// 上取整算法,对 A 向上取整,判断至少要多少个长度为 B 的空间才能容纳 A,用于内存对齐inline UINT Ceil(UINT A, UINT B){return (A + B - 1) / B;}// 创建用于上传的 UploadResource 与用于放纹理的 DefaultResourcevoid CreateUploadAndDefaultResource(){// 计算纹理每行数据的真实数据大小 (单位:Byte 字节),因为纹理图片在内存中是线性存储的// 想获取纹理的真实大小、正确读取纹理数据、上传到 GPU,必须先获取纹理的 BitsPerPixel 图像深度,因为不同位图深度可能不同// 然后再计算每行像素占用的字节,除以 8 是因为 1 Byte = 8 bitsBytePerRowSize = TextureWidth * BitsPerPixel / 8;// 纹理的真实大小 (单位:字节)TextureSize = BytePerRowSize * TextureHeight;// 上传堆资源每行的大小 (单位:字节),注意这里要进行 256 字节对齐!// 因为 GPU 与 CPU 架构不同,GPU 注重并行计算,注重结构化数据的快速读取,读取数据都是以 256 字节为一组来读的// 因此要先要对 BytePerRowSize 进行对齐,判断需要有多少组才能容纳纹理每行像素,不对齐的话数据会读错的。UploadResourceRowSize = Ceil(BytePerRowSize, 256) * 256;// 上传堆资源的总大小 (单位:字节),分配空间必须只多不少,否则会报 D3D12 MinimumAlloc Error 资源内存创建错误// 注意最后一行不用内存对齐 (因为后面没其他行了,不用内存对齐也能正确读取),所以要 (TextureHeight - 1) 再加 BytePerRowSizeUploadResourceSize = UploadResourceRowSize * (TextureHeight - 1) + BytePerRowSize;// 用于中转纹理的上传堆资源结构体D3D12_RESOURCE_DESC UploadResourceDesc = {};UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 资源类型,上传堆的资源类型都是 buffer 缓冲UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// 资源布局,指定资源的存储方式,上传堆的资源都是 row major 按行线性存储UploadResourceDesc.Width = UploadResourceSize;						// 资源宽度,上传堆的资源宽度是资源的总大小,注意资源大小必须只多不少UploadResourceDesc.Height = 1;										// 资源高度,上传堆仅仅是传递线性资源的,所以高度必须为 1UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 资源格式,上传堆资源的格式必须为 UNKNOWNUploadResourceDesc.DepthOrArraySize = 1;							// 资源深度,这个是用于纹理数组和 3D 纹理的,上传堆资源必须为 1UploadResourceDesc.MipLevels = 1;									// Mipmap 等级,这个是用于纹理的,上传堆资源必须为 1UploadResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,上传堆资源都是填 1// 上传堆属性的结构体,上传堆位于 CPU 和 GPU 的共享内存D3D12_HEAP_PROPERTIES UploadHeapDesc = { D3D12_HEAP_TYPE_UPLOAD };// 创建上传堆资源m_D3D12Device->CreateCommittedResource(&UploadHeapDesc, D3D12_HEAP_FLAG_NONE, &UploadResourceDesc,D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_UploadTextureResource));// 用于放纹理的默认堆资源结构体D3D12_RESOURCE_DESC DefaultResourceDesc = {};DefaultResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;	// 资源类型,这里指定为 Texture2D 2D纹理DefaultResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;			// 纹理资源的布局都是 UNKNOWNDefaultResourceDesc.Width = TextureWidth;							// 资源宽度,这里填纹理宽度DefaultResourceDesc.Height = TextureHeight;							// 资源高度,这里填纹理高度DefaultResourceDesc.Format = TextureFormat;							// 资源格式,这里填纹理格式,要和纹理一样DefaultResourceDesc.DepthOrArraySize = 1;							// 资源深度,我们只有一副纹理,所以填 1DefaultResourceDesc.MipLevels = 1;									// Mipmap 等级,我们暂时不使用 Mipmap,所以填 1DefaultResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,这里我们填 1 就行// 默认堆属性的结构体,默认堆位于显存D3D12_HEAP_PROPERTIES DefaultHeapDesc = { D3D12_HEAP_TYPE_DEFAULT };// 创建默认堆资源m_D3D12Device->CreateCommittedResource(&DefaultHeapDesc, D3D12_HEAP_FLAG_NONE, &DefaultResourceDesc,D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&m_DefaultTextureResource));}// 向命令队列发出命令,将纹理数据复制到 DefaultResourcevoid CopyTextureDataToDefaultResource(){// 用于暂时存储纹理数据的指针,这里要用 malloc 分配空间BYTE* TextureData = (BYTE*)malloc(TextureSize);// 将整块纹理数据读到 TextureData 中,方便后文的 memcpy 复制操作m_WICBitmapSource->CopyPixels(nullptr, BytePerRowSize, TextureSize, TextureData);// 用于传递资源的指针BYTE* TransferPointer = nullptr;// Map 开始映射,Map 方法会得到上传堆资源的地址 (在共享内存上),传递给指针,这样我们就能通过 memcpy 操作复制数据了m_UploadTextureResource->Map(0, nullptr, reinterpret_cast<void**>(&TransferPointer));// 这里我们要逐行复制数据!注意两个指针偏移的长度不同!for (UINT i = 0; i < TextureHeight; i++){// 向上传堆资源逐行复制纹理数据 (CPU 高速缓存 -> 共享内存)memcpy(TransferPointer, TextureData, BytePerRowSize);// 纹理指针偏移到下一行TextureData += BytePerRowSize;// 上传堆资源指针偏移到下一行,注意偏移长度不同!TransferPointer += UploadResourceRowSize;}// Unmap 结束映射,因为我们无法直接读写默认堆资源,需要上传堆复制到那里,在复制之前,我们需要先结束映射,让上传堆处于只读状态m_UploadTextureResource->Unmap(0, nullptr);TextureData -= TextureSize;		// 纹理资源指针偏移回初始位置free(TextureData);				// 释放上文 malloc 分配的空间,后面我们用不到它,不要让它占内存D3D12_PLACED_SUBRESOURCE_FOOTPRINT PlacedFootprint = {};						// 资源脚本,用来描述要复制的资源D3D12_RESOURCE_DESC DefaultResourceDesc = m_DefaultTextureResource->GetDesc();	// 默认堆资源结构体// 获取纹理复制脚本,用于下文的纹理复制m_D3D12Device->GetCopyableFootprints(&DefaultResourceDesc, 0, 1, 0, &PlacedFootprint, nullptr, nullptr, nullptr);D3D12_TEXTURE_COPY_LOCATION DstLocation = {};						// 复制目标位置 (默认堆资源) 结构体DstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;		// 纹理复制类型,这里必须指向纹理DstLocation.SubresourceIndex = 0;									// 指定要复制的子资源索引DstLocation.pResource = m_DefaultTextureResource.Get();				// 要复制到的资源D3D12_TEXTURE_COPY_LOCATION SrcLocation = {};						// 复制源位置 (上传堆资源) 结构体SrcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;		// 纹理复制类型,这里必须指向缓冲区SrcLocation.PlacedFootprint = PlacedFootprint;						// 指定要复制的资源脚本信息SrcLocation.pResource = m_UploadTextureResource.Get();				// 被复制数据的缓冲// 复制资源需要使用 GPU 的 CopyEngine 复制引擎,所以需要向命令队列发出复制命令m_CommandAllocator->Reset();								// 先重置命令分配器m_CommandList->Reset(m_CommandAllocator.Get(), nullptr);	// 再重置命令列表,复制命令不需要 PSO 状态,所以第二个参数填 nullptr// 记录复制资源到默认堆的命令 (共享内存 -> 显存) m_CommandList->CopyTextureRegion(&DstLocation, 0, 0, 0, &SrcLocation, nullptr);// 关闭命令列表m_CommandList->Close();// 用于传递命令用的临时 ID3D12CommandList 数组ID3D12CommandList* _temp_cmdlists[] = { m_CommandList.Get() };// 提交复制命令!GPU 开始复制!m_CommandQueue->ExecuteCommandLists(1, _temp_cmdlists);// 将围栏预定值设定为下一帧,注意复制资源也需要围栏等待,否则会发生资源冲突FenceValue++;// 在命令队列 (命令队列在 GPU 端) 设置围栏预定值,此命令会加入到命令队列中// 命令队列执行到这里会修改围栏值,表示复制已完成,"击中"围栏m_CommandQueue->Signal(m_Fence.Get(), FenceValue);// 设置围栏的预定事件,当复制完成时,围栏被"击中",激发预定事件,将事件由无信号状态转换成有信号状态m_Fence->SetEventOnCompletion(FenceValue, RenderEvent);}// 最终创建 SRV 着色器资源描述符,用于描述 DefaultResource 为一块纹理void CreateSRV(){// SRV 描述符信息结构体D3D12_SHADER_RESOURCE_VIEW_DESC SRVDescriptorDesc = {};// SRV 描述符类型,这里我们指定 Texture2D 2D纹理SRVDescriptorDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;// SRV 描述符的格式也要填纹理格式SRVDescriptorDesc.Format = TextureFormat;// 纹理采样后每个纹理像素 RGBA 分量的顺序,D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING 表示纹理采样后分量顺序不改变SRVDescriptorDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;// 这里我们不使用 Mipmap,所以填 1SRVDescriptorDesc.Texture2D.MipLevels = 1;// 获取 SRV 描述符的 CPU 映射句柄,用于创建资源SRV_CPUHandle = m_SRVHeap->GetCPUDescriptorHandleForHeapStart();// 创建 SRV 描述符m_D3D12Device->CreateShaderResourceView(m_DefaultTextureResource.Get(), &SRVDescriptorDesc, SRV_CPUHandle);// 获取 SRV 描述符的 GPU 映射句柄,用于命令列表设置 SRVHeap 描述符堆,着色器引用 SRV 描述符找纹理资源SRV_GPUHandle = m_SRVHeap->GetGPUDescriptorHandleForHeapStart();}// 创建根签名void CreateRootSignature(){ComPtr<ID3DBlob> SignatureBlob;			// 根签名字节码ComPtr<ID3DBlob> ErrorBlob;				// 错误字节码,根签名创建失败时用 OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer()); 可以获取报错信息D3D12_DESCRIPTOR_RANGE SRVDescriptorRangeDesc = {};						// Range 描述符范围结构体,一块 Range 表示一堆连续的同类型描述符SRVDescriptorRangeDesc.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;		// Range 类型,这里指定 SRV 类型,CBV_SRV_UAV 在这里分流SRVDescriptorRangeDesc.NumDescriptors = 1;								// Range 里面的描述符数量 N,一次可以绑定多个描述符到多个寄存器槽上SRVDescriptorRangeDesc.BaseShaderRegister = 0;							// Range 要绑定的起始寄存器槽编号 i,绑定范围是 [s(i),s(i+N)],我们绑定 s0SRVDescriptorRangeDesc.RegisterSpace = 0;								// Range 要绑定的寄存器空间,整个 Range 都会绑定到同一寄存器空间上,我们绑定 space0SRVDescriptorRangeDesc.OffsetInDescriptorsFromTableStart = 0;			// Range 到根描述表开头的偏移量 (单位:描述符),根签名需要用它来寻找 Range 的地址,我们这填 0 就行D3D12_ROOT_DESCRIPTOR_TABLE RootDescriptorTableDesc = {};				// RootDescriptorTable 根描述表信息结构体,一个 Table 可以有多个 RangeRootDescriptorTableDesc.pDescriptorRanges = &SRVDescriptorRangeDesc;	// Range 描述符范围指针RootDescriptorTableDesc.NumDescriptorRanges = 1;						// 根描述表中 Range 的数量D3D12_ROOT_PARAMETER RootParameter = {};RootParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;				// 根参数在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理)RootParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;	// 根参数类型,这里我们选 Table 根描述表,一个根描述表占用 1 DWORDRootParameter.DescriptorTable = RootDescriptorTableDesc;					// 根参数指针D3D12_STATIC_SAMPLER_DESC StaticSamplerDesc = {};						// 静态采样器结构体,静态采样器不会占用根签名StaticSamplerDesc.ShaderRegister = 0;									// 要绑定的寄存器槽,对应 s0StaticSamplerDesc.RegisterSpace = 0;									// 要绑定的寄存器空间,对应 space0StaticSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;		// 静态采样器在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理采样)StaticSamplerDesc.Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT;	// 纹理过滤类型,这里我们直接选 邻近点采样 就行StaticSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 U 方向上的纹理寻址方式StaticSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 V 方向上的纹理寻址方式StaticSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 W 方向上的纹理寻址方式 (3D 纹理会用到)StaticSamplerDesc.MinLOD = 0;											// 最小 LOD 细节层次,这里我们默认填 0 就行StaticSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;							// 最大 LOD 细节层次,这里我们默认填 D3D12_FLOAT32_MAX (没有 LOD 上限)StaticSamplerDesc.MipLODBias = 0;										// 基础 Mipmap 采样偏移量,我们这里我们直接填 0 就行StaticSamplerDesc.MaxAnisotropy = 1;									// 各向异性过滤等级,我们不使用各向异性过滤,需要默认填 1StaticSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;			// 这个是用于阴影贴图的,我们不需要用它,所以填 D3D12_COMPARISON_FUNC_NEVERD3D12_ROOT_SIGNATURE_DESC rootsignatureDesc = {};			// 根签名信息结构体,上限 64 DWORD,静态采样器不占用根签名rootsignatureDesc.NumParameters = 1;						// 根参数数量rootsignatureDesc.pParameters = &RootParameter;				// 根参数指针rootsignatureDesc.NumStaticSamplers = 1;					// 静态采样器数量rootsignatureDesc.pStaticSamplers = &StaticSamplerDesc;		// 静态采样器指针// 根签名标志,可以设置渲染管线不同阶段下的输入参数状态。注意这里!我们要从 IA 阶段输入顶点数据,所以要通过根签名,设置渲染管线允许从 IA 阶段读入数据rootsignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;// 编译根签名,让根签名先编译成 GPU 可读的二进制字节码D3D12SerializeRootSignature(&rootsignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &SignatureBlob, &ErrorBlob);if (ErrorBlob)		// 如果根签名编译出错,ErrorBlob 可以提供报错信息{OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());OutputDebugStringA("\n");}// 用这个二进制字节码创建根签名对象m_D3D12Device->CreateRootSignature(0, SignatureBlob->GetBufferPointer(), SignatureBlob->GetBufferSize(), IID_PPV_ARGS(&m_RootSignature));}// 创建渲染管线状态对象 (Pipeline State Object, PSO)void CreatePSO(){// PSO 信息结构体D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc = {};// Input Assembler 输入装配阶段D3D12_INPUT_LAYOUT_DESC InputLayoutDesc = {};			// 输入样式信息结构体D3D12_INPUT_ELEMENT_DESC InputElementDesc[2] = {};		// 输入元素信息结构体数组InputElementDesc[0].SemanticName = "POSITION";					// 要锚定的语义InputElementDesc[0].SemanticIndex = 0;							// 语义索引,目前我们填 0 就行InputElementDesc[0].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;	// 输入格式InputElementDesc[0].InputSlot = 0;								// 输入槽编号,目前我们填 0 就行InputElementDesc[0].AlignedByteOffset = 0;						// 在输入槽中的偏移// 输入流类型,一种是我们现在用的 D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA 逐顶点输入流,还有一种叫逐实例输入流,后面再学InputElementDesc[0].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;InputElementDesc[0].InstanceDataStepRate = 0;					// 实例数据步进率,目前我们没有用到实例化,填 0InputElementDesc[1].SemanticName = "TEXCOORD";										// 要锚定的语义InputElementDesc[1].SemanticIndex = 0;												// 语义索引InputElementDesc[1].Format = DXGI_FORMAT_R32G32_FLOAT;								// 输入格式InputElementDesc[1].InputSlot = 0;													// 输入槽编号// 在输入槽中的偏移,因为 position 与 texcoord 在同一输入槽(0号输入槽)// position 是 float4,有 4 个 float ,每个 float 占 4 个字节,所以要偏移 4*4=16 个字节,这样才能确定 texcoord 参数的位置,不然装配的时候会覆盖原先 position 的数据InputElementDesc[1].AlignedByteOffset = 16;											// 在输入槽中的偏移InputElementDesc[1].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;	// 输入流类型InputElementDesc[1].InstanceDataStepRate = 0;										// 实例数据步进率InputLayoutDesc.NumElements = 2;						// 输入元素个数InputLayoutDesc.pInputElementDescs = InputElementDesc;	// 输入元素结构体数组指针PSODesc.InputLayout = InputLayoutDesc;					// 设置渲染管线 IA 阶段的输入样式ComPtr<ID3DBlob> VertexShaderBlob;		// 顶点着色器二进制字节码ComPtr<ID3DBlob> PixelShaderBlob;		// 像素着色器二进制字节码ComPtr<ID3DBlob> ErrorBlob;				// 错误字节码,根签名创建失败时用 OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer()); 可以获取报错信息// 编译顶点着色器 Vertex ShaderD3DCompileFromFile(L"shader.hlsl", nullptr, nullptr, "VSMain", "vs_5_1", NULL, NULL, &VertexShaderBlob, &ErrorBlob);if (ErrorBlob)		// 如果着色器编译出错,ErrorBlob 可以提供报错信息{OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());OutputDebugStringA("\n");}// 编译像素着色器 Pixel ShaderD3DCompileFromFile(L"shader.hlsl", nullptr, nullptr, "PSMain", "ps_5_1", NULL, NULL, &PixelShaderBlob, &ErrorBlob);if (ErrorBlob)		// 如果着色器编译出错,ErrorBlob 可以提供报错信息{OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());OutputDebugStringA("\n");}PSODesc.VS.pShaderBytecode = VertexShaderBlob->GetBufferPointer();		// VS 字节码数据指针PSODesc.VS.BytecodeLength = VertexShaderBlob->GetBufferSize();			// VS 字节码数据长度PSODesc.PS.pShaderBytecode = PixelShaderBlob->GetBufferPointer();		// PS 字节码数据指针PSODesc.PS.BytecodeLength = PixelShaderBlob->GetBufferSize();			// PS 字节码数据长度// Rasterizer 光栅化PSODesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;		// 剔除模式,指定是否开启背面/正面/不剔除,这里选背面剔除PSODesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;		// 填充模式,指定是否开启纯色/线框填充,这里选纯色填充// 第一次设置根签名!本次设置是将根签名与 PSO 绑定,设置渲染管线的输入参数状态PSODesc.pRootSignature = m_RootSignature.Get();// 设置基本图元,这里我们设置三角形面PSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;// 设置渲染目标数量,我们只有一副渲染目标 (颜色缓冲) 需要进行渲染,所以填 1PSODesc.NumRenderTargets = 1;// 设置渲染目标的格式,这里要和交换链指定窗口缓冲的格式一致,这里的 0 指的是渲染目标的索引PSODesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;// 设置混合阶段 (输出合并阶段) 下 RGBA 颜色通道的开启和关闭,D3D12_COLOR_WRITE_ENABLE_ALL 表示 RGBA 四色通道全部开启PSODesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;// 设置采样次数,我们这里填 1 就行PSODesc.SampleDesc.Count = 1;// 设置采样掩码,这个是用于多重采样的,我们直接填全采样 (UINT_MAX,就是将 UINT 所有的比特位全部填充为 1) 就行PSODesc.SampleMask = UINT_MAX;// 最终创建 PSO 对象m_D3D12Device->CreateGraphicsPipelineState(&PSODesc, IID_PPV_ARGS(&m_PipelineStateObject));}// 创建顶点资源void CreateVertexResource(){// CPU 高速缓存上的顶点信息数组,注意这里的顶点坐标都是 NDC 空间坐标VERTEX vertexs[6] ={{{-0.75f, 0.75f, 0.0f, 1.0f}, {0.0f, 0.0f}},{{0.75f, 0.75f, 0.0f, 1.0f}, {1.0f, 0.0f}},{{0.75f, -0.75f, 0.0f, 1.0f}, {1.0f, 1.0f}},{{-0.75f, 0.75f, 0.0f, 1.0f}, {0.0f, 0.0f}},{{0.75f, -0.75f, 0.0f, 1.0f}, {1.0f, 1.0f}},{{-0.75f, -0.75f, 0.0f, 1.0f}, {0.0f, 1.0f}}};D3D12_RESOURCE_DESC VertexDesc = {};						// D3D12Resource 信息结构体VertexDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 资源类型,上传堆的资源类型都是 buffer 缓冲VertexDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// 资源布局,指定资源的存储方式,上传堆的资源都是 row major 按行线性存储VertexDesc.Width = sizeof(vertexs);							// 资源宽度,上传堆的资源宽度是资源的总大小VertexDesc.Height = 1;										// 资源高度,上传堆仅仅是传递线性资源的,所以高度必须为 1VertexDesc.Format = DXGI_FORMAT_UNKNOWN;					// 资源格式,上传堆资源的格式必须为 UNKNOWNVertexDesc.DepthOrArraySize = 1;							// 资源深度,这个是用于纹理数组和 3D 纹理的,上传堆资源必须为 1VertexDesc.MipLevels = 1;									// Mipmap 等级,这个是用于纹理的,上传堆资源必须为 1VertexDesc.SampleDesc.Count = 1;							// 资源采样次数,上传堆资源都是填 1// 上传堆属性的结构体,上传堆位于 CPU 和 GPU 的共享内存D3D12_HEAP_PROPERTIES UploadHeapDesc = { D3D12_HEAP_TYPE_UPLOAD };// 创建资源,CreateCommittedResource 会为资源自动创建一个等大小的隐式堆,这个隐式堆的所有权由操作系统管理,开发者不可控制m_D3D12Device->CreateCommittedResource(&UploadHeapDesc, D3D12_HEAP_FLAG_NONE,&VertexDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_VertexResource));// 用于传递资源的指针BYTE* TransferPointer = nullptr;// Map 开始映射,Map 方法会得到这个 D3D12Resource 的地址 (在共享内存上),传递给指针,这样我们就能通过 memcpy 操作复制数据了m_VertexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransferPointer));// 将 CPU 高速缓存上的顶点数据 复制到 共享内存上的 D3D12Resource ,CPU 高速缓存 -> 共享内存memcpy(TransferPointer, vertexs, sizeof(vertexs));// Unmap 结束映射,D3D12Resource 变成只读状态,这样做能加速 GPU 的访问m_VertexResource->Unmap(0, nullptr);// 填写 VertexBufferView VBV 顶点缓冲描述符,描述上面的 D3D12Resource,让 GPU 知道这是一个顶点缓冲VertexBufferView.BufferLocation = m_VertexResource->GetGPUVirtualAddress();		// 顶点缓冲资源的地址VertexBufferView.SizeInBytes = sizeof(vertexs);									// 整个顶点缓冲的总大小VertexBufferView.StrideInBytes = sizeof(VERTEX);								// 每个顶点元素的大小 (步长)}// 渲染void Render(){// 获取 RTV 堆首句柄RTVHandle = m_RTVHeap->GetCPUDescriptorHandleForHeapStart();// 获取当前渲染的后台缓冲序号FrameIndex = m_DXGISwapChain->GetCurrentBackBufferIndex();// 偏移 RTV 句柄,找到对应的 RTV 描述符RTVHandle.ptr += FrameIndex * RTVDescriptorSize;// 先重置命令分配器m_CommandAllocator->Reset();// 再重置命令列表,Close 关闭状态 -> Record 录制状态m_CommandList->Reset(m_CommandAllocator.Get(), nullptr);// 将起始转换屏障的资源指定为当前渲染目标beg_barrier.Transition.pResource = m_RenderTarget[FrameIndex].Get();// 调用资源屏障,将渲染目标由 Present 呈现(只读) 转换到 RenderTarget 渲染目标(只写)m_CommandList->ResourceBarrier(1, &beg_barrier);// 第二次设置根签名!本次设置将会检查 渲染管线绑定的根签名 与 这里的根签名 是否匹配// 以及根签名指定的资源是否被正确绑定,检查完毕后会进行简单的映射m_CommandList->SetGraphicsRootSignature(m_RootSignature.Get());// 设置渲染管线状态,可以在上面 m_CommandList->Reset() 的时候直接在第二个参数设置 PSOm_CommandList->SetPipelineState(m_PipelineStateObject.Get());// 设置视口 (光栅化阶段),用于光栅化里的屏幕映射m_CommandList->RSSetViewports(1, &viewPort);// 设置裁剪矩形 (光栅化阶段)m_CommandList->RSSetScissorRects(1, &ScissorRect);// 用 RTV 句柄设置渲染目标m_CommandList->OMSetRenderTargets(1, &RTVHandle, false, nullptr);// 清空当前渲染目标的背景为天蓝色m_CommandList->ClearRenderTargetView(RTVHandle, DirectX::Colors::SkyBlue, 0, nullptr);// 用于设置描述符堆用的临时 ID3D12DescriptorHeap 数组ID3D12DescriptorHeap* _temp_DescriptorHeaps[] = { m_SRVHeap.Get() };// 设置描述符堆m_CommandList->SetDescriptorHeaps(1, _temp_DescriptorHeaps);// 设置 SRV 句柄m_CommandList->SetGraphicsRootDescriptorTable(0, SRV_GPUHandle);// 设置图元拓扑 (输入装配阶段),我们这里设置三角形列表m_CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);// 设置 VBV 顶点缓冲描述符 (输入装配阶段) m_CommandList->IASetVertexBuffers(0, 1, &VertexBufferView);// Draw Call! 绘制矩形m_CommandList->DrawInstanced(6, 1, 0, 0);// 将终止转换屏障的资源指定为当前渲染目标end_barrier.Transition.pResource = m_RenderTarget[FrameIndex].Get();// 再通过一次资源屏障,将渲染目标由 RenderTarget 渲染目标(只写) 转换到 Present 呈现(只读)m_CommandList->ResourceBarrier(1, &end_barrier);// 关闭命令列表,Record 录制状态 -> Close 关闭状态,命令列表只有关闭才可以提交m_CommandList->Close();// 用于传递命令用的临时 ID3D12CommandList 数组ID3D12CommandList* _temp_cmdlists[] = { m_CommandList.Get() };// 执行上文的渲染命令!m_CommandQueue->ExecuteCommandLists(1, _temp_cmdlists);// 向命令队列发出交换缓冲的命令,此命令会加入到命令队列中,命令队列执行到该命令时,会通知交换链交换缓冲m_DXGISwapChain->Present(1, NULL);// 将围栏预定值设定为下一帧FenceValue++;// 在命令队列 (命令队列在 GPU 端) 设置围栏预定值,此命令会加入到命令队列中// 命令队列执行到这里会修改围栏值,表示渲染已完成,"击中"围栏m_CommandQueue->Signal(m_Fence.Get(), FenceValue);// 设置围栏的预定事件,当渲染完成时,围栏被"击中",激发预定事件,将事件由无信号状态转换成有信号状态m_Fence->SetEventOnCompletion(FenceValue, RenderEvent);}// 渲染循环void RenderLoop(){bool isExit = false;	// 是否退出MSG msg = {};			// 消息结构体while (isExit != true){// MsgWaitForMultipleObjects 用于多个线程的无阻塞等待,返回值是激发事件 (线程) 的 ID// 经过该函数后 RenderEvent 也会自动重置为无信号状态,因为我们创建事件的时候指定了第二个参数为 falseDWORD ActiveEvent = ::MsgWaitForMultipleObjects(1, &RenderEvent, false, INFINITE, QS_ALLINPUT);switch (ActiveEvent - WAIT_OBJECT_0){case 0:				// ActiveEvent 是 0,说明渲染事件已经完成了,进行下一次渲染Sleep(10);Render();break;case 1:				// ActiveEvent 是 1,说明渲染事件未完成,CPU 主线程同时处理窗口消息,防止界面假死// 查看消息队列是否有消息,如果有就获取。 PM_REMOVE 表示获取完消息,就立刻将该消息从消息队列中移除while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){// 如果程序没有收到退出消息,就向操作系统发出派发消息的命令if (msg.message != WM_QUIT){TranslateMessage(&msg);					// 翻译消息,将虚拟按键值转换为对应的 ASCII 码 (后文会讲)DispatchMessage(&msg);					// 派发消息,通知操作系统调用回调函数处理消息}else{isExit = true;							// 收到退出消息,就退出消息循环}}break;case WAIT_TIMEOUT:	// 渲染超时break;}}}// 回调函数static LRESULT CALLBACK CallBackFunc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){// 用 switch 将第二个参数分流,每个 case 分别对应一个窗口消息switch (msg){case WM_DESTROY:			// 窗口被销毁 (当按下右上角 X 关闭窗口时)PostQuitMessage(0);		// 向操作系统发出退出请求 (WM_QUIT),结束消息循环break;// 如果接收到其他消息,直接默认返回整个窗口default: return DefWindowProc(hwnd, msg, wParam, lParam);}return 0;	// 注意这里!}// 运行窗口static void Run(HINSTANCE hins){DX12Engine engine;engine.InitWindow(hins);engine.CreateDebugDevice();engine.CreateDevice();engine.CreateCommandComponents();engine.CreateRenderTarget();engine.CreateFenceAndBarrier();engine.LoadTextureFromFile();engine.CreateSRVHeap();engine.CreateUploadAndDefaultResource();engine.CopyTextureDataToDefaultResource();engine.CreateSRV();engine.CreateRootSignature();engine.CreatePSO();engine.CreateVertexResource();engine.RenderLoop();}
};// 主函数
int WINAPI WinMain(HINSTANCE hins, HINSTANCE hPrev, LPSTR cmdLine, int cmdShow)
{DX12Engine::Run(hins);
}

shader.hlsl


// (4) DrawTexture:用 DirectX 12 画一个钻石原矿struct VSInput      // VS 阶段输入顶点数据
{float4 position : POSITION;         // 输入顶点的位置,POSITION 语义对应 C++ 端输入布局中的 POSITIONfloat2 texcoordUV : TEXCOORD;       // 输入顶点的纹理坐标,TEXCOORD 语义对应 C++ 端输入布局中的 TEXCOORD
};struct VSOutput     // VS 阶段输出顶点数据
{float4 position : SV_Position;      // 输出顶点的位置,SV_POSITION 是系统语义,指定顶点坐标已经位于齐次裁剪空间,通知光栅化阶段对顶点进行透视除法和屏幕映射float2 texcoordUV : TEXCOORD;       // 输出顶点纹理坐标时,仍然需要 TEXCOORD 语义
};// Vertex Shader 顶点着色器入口函数 (逐顶点输入),接收来自 IA 阶段输入的顶点数据,处理并返回齐次裁剪空间下的顶点坐标
// 上一阶段:Input Assembler 输入装配阶段
// 下一阶段:Rasterization 光栅化阶段
VSOutput VSMain(VSInput input)
{VSOutput output; // 我们直接向 IA 阶段输入顶点在 NDC 空间下的坐标,所以无需变换,直接赋值返回就行output.position = input.position;output.texcoordUV = input.texcoordUV;return output;
}// register(*#,spaceN) *表示资源类型,#表示所用的寄存器编号,spaceN 表示使用的 N 号寄存器空间Texture2D m_texure : register(t0, space0);          // 纹理,t 表示 SRV 着色器资源,t0 表示 0 号 SRV 寄存器,space0 表示使用 t0 的 0 号空间
SamplerState m_sampler : register(s0, space0);      // 纹理采样器,s 表示采样器,s0 表示 0 号 sampler 寄存器,space0 表示使用 s0 的 0 号空间// Pixel Shader 像素着色器入口函数 (逐像素输入),接收来自光栅化阶段经过插值后的每个片元,返回像素颜色
// 上一阶段:Rasterization 光栅化阶段
// 下一阶段:Output Merger 输出合并阶段
float4 PSMain(VSOutput input) : SV_Target   // SV_Target 也是系统语义,通知输出合并阶段将 PS 阶段返回的颜色写入到渲染目标(颜色缓冲)上
{return m_texure.Sample(m_sampler, input.texcoordUV);    // 在像素着色器根据光栅化插值得到的 UV 坐标对纹理进行采样
}

在这里插入图片描述



下一节,我们要开始接触线性代数,还要学习一种新的描述符:Constant Buffer View 常量缓冲描述符,正式踏入 3D 世界的大门。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/6919.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FPGA实现任意角度视频旋转(二)视频90度/270度无裁剪旋转

本文主要介绍如何基于FPGA实现视频的90度/270度无裁剪旋转&#xff0c;旋转效果示意图如下&#xff1a; 为了实时对比旋转效果&#xff0c;采用分屏显示进行处理&#xff0c;左边代表旋转前的视频在屏幕中的位置&#xff0c;右边代表旋转后的视频在屏幕中的位置。 分屏显示的…

Blazor-选择循环语句

今天我们来说说Blazor选择语句和循环语句。 下面我们以一个简单的例子来讲解相关的语法&#xff0c;我已经创建好了一个Student类&#xff0c;以此类来进行语法的运用 因为我们需要交互性所以我们将类创建在*.client目录下 if 我们做一个学生信息的显示&#xff0c;Gender为…

数据结构——实验八·学生管理系统

嗨~~欢迎来到Tubishu的博客&#x1f338;如果你也是一名在校大学生&#xff0c;正在寻找各种编程资源&#xff0c;那么你就来对地方啦&#x1f31f; Tubishu是一名计算机本科生&#xff0c;会不定期整理和分享学习中的优质资源&#xff0c;希望能为你的编程之路添砖加瓦⭐&…

在 Ubuntu22.04 上安装 Splunk

ELK感觉太麻烦了&#xff0c;换个日志收集工具 Splunk 是一种 IT 工具&#xff0c;可帮助在任何设备上收集日志、分析、可视化、审计和创建报告。简单来说&#xff0c;它将“机器生成的数据转换为人类可读的数据”。它支持从虚拟机、网络设备、防火墙、基于 Unix 和基于 Windo…

【C++高并发服务器WebServer】-2:exec函数簇、进程控制

本文目录 一、exec函数簇介绍二、exec函数簇 一、exec函数簇介绍 exec 函数族的作用是根据指定的文件名找到可执行文件&#xff0c;并用它来取代调用进程的内容&#xff0c;换句话说&#xff0c;就是在调用进程内部执行一个可执行文件。 exec函数族的函数执行成功后不会返回&…

[ACTF2020 新生赛]Upload1

题目 以为是前端验证&#xff0c;试了一下PHP传不上去 可以创建一个1.phtml文件。对.phtml文件的解释: 是一个嵌入了PHP脚本的html页面。将以下代码写入该文件中 <script languagephp>eval($_POST[md]);</script><script languagephp>system(cat /flag);&l…

第24篇 基于ARM A9处理器用汇编语言实现中断<六>

Q&#xff1a;怎样设计ARM处理器汇编语言程序使用定时器中断实现实时时钟&#xff1f; A&#xff1a;此前我们曾使用轮询定时器I/O的方式实现实时时钟&#xff0c;而在本实验中将采用定时器中断的方式。新增第三个中断源A9 Private Timer&#xff0c;对该定时器进行配置&#…

SpringMVC新版本踩坑[已解决]

问题&#xff1a; 在使用最新版本springMVC做项目部署时&#xff0c;浏览器反复500&#xff0c;如下图&#xff1a; 异常描述&#xff1a; 类型异常报告 消息Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [int] not specifie…

系统思考—复杂问题的根源分析

在企业中&#xff0c;许多问题看似简单&#xff0c;背后却潜藏着复杂的因果关系。传统的思维方式往往只能看到表面&#xff0c;而无法深入挖掘问题的真正根源。我们常常通过“表面解决”来应对眼前的症状&#xff0c;但这往往只是治标不治本。 比如&#xff0c;销量下降时&…

安装VMware17

一、VMware Workstation 简介 VMware Workstation是一款由VMware公司开发的功能强大的桌面虚拟化软件。它允许用户在单一的物理电脑上同时运行多个操作系统作为虚拟机&#xff08;VMs&#xff09;&#xff0c;每个虚拟机都可配置有自己的独立硬件资源&#xff0c;如CPU核心、内…

三、双链表

链表的种类有很多&#xff0c;单链表是不带头不循环单向链表&#xff0c;但双链表是带头循环双向链表&#xff0c;并且双链表还有一个哨兵位&#xff0c;哨兵位不是头节点 typedef int LTDataType;typedef struct ListNode{struct ListNode* next; //指针保存下⼀个结点的地址s…

【知识】可视化理解git中的cherry-pick、merge、rebase

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 这三个确实非常像&#xff0c;以至于对于初学者来说比较难理解。 总结对比 先给出对比&#xff1a; 特性git mergegit rebasegit cherry-pick功能合并…

SpringBoot开发(三)SpringBoot介绍、项目创建、运行

1. SpringBoot 1.1. SpringBoot介绍 Spring Boot给世界程序员带来了春天&#xff0c;越来越多的企业选择使用spring boot来开发他们的软件&#xff0c;因此学习spring boot是科技发展的必然趋势。本门课程将从web最基础的知识点开始讲起&#xff0c;逐步带你攻破spring boot的…

438. 找到字符串中所有字母异位词

【题目】&#xff1a;438. 找到字符串中所有字母异位词 class Solution { public:vector<int> findAnagrams(string s, string p) {vector<int> res;vector<int> curVec(26, 0); // 统计p中字母出现的次数for(char c : p) {curVec[c - a];}for(int l 0, r …

Leetcode-两数之和

1.暴力枚举 class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {int lennums.size();int i,j;for(i0;i<len;i){for(ji1;j<len;j){if(nums[i]nums[j]target){return{i,j};}}}return {i,j};} }; 新知识&#xff1a; return {…

边缘网关具备哪些功能?

边缘网关&#xff0c;又称边缘计算网关&#xff0c;部署在网络边缘&#xff0c;它位于物联网设备与云计算平台之间&#xff0c;充当着数据流动的“守门员”和“处理器”。通过其强大的数据处理能力和多样化的通信协议支持&#xff0c;边缘网关能够实时分析、过滤和存储来自终端…

高等数学学习笔记 ☞ 微分方程

1. 微分方程的基本概念 1. 微分方程的基本概念&#xff1a; &#xff08;1&#xff09;微分方程&#xff1a;含有未知函数及其导数或微分的方程。 举例说明微分方程&#xff1a;&#xff1b;。 &#xff08;2&#xff09;微分方程的阶&#xff1a;指微分方程中未知函数的导数…

【优选算法】9----长度最小的子数组

----------------------------------------begin-------------------------------------- 铁子们&#xff0c;前面的双指针算法篇就算告一段落啦~ 接下来是我们的滑动窗口篇&#xff0c;不过有一说一&#xff0c;算法题就跟数学题一样&#xff0c;只要掌握方法&#xff0c;多做…

计算机网络之链路层

本文章目录结构出自于《王道计算机考研 计算机网络_哔哩哔哩_bilibili》 02 数据链路层 在网上看到其他人做了详细的笔记&#xff0c;就不再多余写了&#xff0c;直接参考着学习吧。 1 详解数据链路层-数据链路层的功能【王道计算机网络笔记】_wx63088f6683f8f的技术博客_51C…

Kubernetes v1.28.0安装dashboard v2.6.1(k8s图形化操作界面)

准备工作 Kubernetes v1.28.0搭建教程请参考&#xff1a;Kubernetes v1.28.0集群快速搭建教程-CSDN博客 查看当前集群nodes都是ready状态 查看当前pods都是running状态 下载并修改配置文件 下载 recommended.yaml &#xff0c;下载好之后&#xff0c;进入文件编辑 下载地址…