前序文章目录
DirectX12(D3D12)基础教程(一)——基础教程
DirectX12(D3D12)基础教程(二)——理解根签名、初识显存管理和加载纹理、理解资源屏障
DirectX12(D3D12)基础教程(三)——使用独立堆以“定位方式”创建资源、创建动态采样器、初步理解采取器类型
DirectX12(D3D12)基础教程(四)——初识DirectXMath库、使用独立堆创建常量缓冲、理解管线状态对象、理解围栏同步
DirectX12(D3D12)基础教程(五)——理解和使用捆绑包,加载并使用DDS Cube Map
DirectX12(D3D12)基础教程(六)——多线程渲染
DirectX12(D3D12)基础教程(七)——渲染到纹理、正交投影、UI渲染基础
DirectX12(D3D12)基础教程(八)——多显卡渲染基础、共享纹理、多GPU同步
DirectX12(D3D12)基础教程(九)——多线程渲染BUG修正及MsgWaitForMultipleObjects函数详解
DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(上)
DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(下)
DirectX12(D3D12)基础教程(十一)——几个“上古时代”的基于Pixel Shader的滤镜效果
DirectX12(D3D12)基础教程(十二)——多线程+多显卡渲染及水彩画效果和标准简化版高斯模糊
DirectX12(D3D12)基础教程(十三)——D2D、DWrite On D3D12与文字输出
DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(上)
DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(中)
DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(下)
DirectX12(D3D12)基础教程(十六)——实现渲染线程池:3个内核同步对象实现渲染线程池/大规模线程池
DirectX12(D3D12)基础教程(外篇一)——编译Assimp
DirectX12(D3D12)基础教程(外篇二)——编译DirectXShaderCompiler库
DirectX12(D3D12)基础教程(外篇三)——CreateGraphicsPipelineState 错误 #682的修复,深刻理解POSITION和SV_POSITION_
DirectX12(D3D12)基础教程(外篇四)——用Assimp载入模型基础操作(无渲染纯命令行版)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【1】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【2】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【3】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【4】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【5】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【6】)_
DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(上)
DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(中)
DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(下)
DirectX12(D3D12)基础教程(十九)—— 多实例渲染
DirectX12(D3D12)基础教程(二十)—— 纹理数组(Texture Array)非DDS初始化操作
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(1/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(2/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(3/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(4/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(5/5)
目录
- 1、前言
- 2、等距柱面映射(Equidistant cylindrical projection、equirectangular projection)的数学原理
- 3、HDR 环境映射贴图加载
- 4、普通的6遍渲染解算过程
- 4.1、创建需要执行 6 次的 PSO
- 4.2、渲染 6 次方法的Shader
- 5、利用 GS 一次性解算
- 5.1、Geometry Shader
- 5.2、Geometry Shader 中指定渲染目标数组索引
- 5.3、特别设定的 Cube Vertex 数据
- 5.4、创建 1 遍渲染 6 个面带 GS 的 PSO
- 5.5、1 Pass 预渲染(预处理)
- 5.6、其它扩展形式
- 6、直接在最终渲染过程中解算
- 6.1、无穷远平面 Skybox 中的直接使用等距柱面环境映射
- 6.2、在解算过程的 PS 中使用等距柱面环境映射
- 7、后记
- 附录:系列教程前序文章目录
1、前言
在上一讲 DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理 中,着重介绍了整个基于 IBL 光源的 PBR 渲染过程中的数学原理,核心还是围绕反射方程(渲染方程)的简化高效计算过程展开的。当然其中数学过程稍微有些复杂。
并且上一讲中已经提到在实际 IBL 应用中,一般使用的是将原本需要6个面来存储的环境映射CubeMap 贴图作为环境光源,通常被投影到一张被称为 “等距柱面”贴图的映射,通常像下面的样子:
这个图形看上去很扭曲,其实可以简单的将他理解为我们常见的世界地图,本质上就是将本应是一个球面的图形拓扑平铺到了一个平面上。只是等距柱面贴图是按照圆柱面映射球面,最后再把圆柱面展开平铺成平面图形。
当然你一定很好奇怎么会有这样的图,或者说会有这样的需求,其实这一切都来自于现在比较流行和热门的领域——全局光照(global illumination,简称 GI )。在 GI 中带有“主角光环”的角色一般都处在场景“中心”,然后接受着来自四面八方的光芒的照射,这时候如果一个个光源(包括漫反射光源)的计算光照效果,那么整个渲染的过程都会被拖慢到死机的程度,此时 IBL 的应用就显得非常适宜。
IBL的大致过程就是针对被环境包围的主角,用以其为中心的单位球体针对环境中所有的光源,什么直射的漫反射的二次散射的等等全部采样(拍照)到一个球面纹理中(通常就是Cube Map),然后再将这个球面纹理平铺保存成等距柱面贴图,使用时整个过程反过来就行。因为光照被存储成了纹理图片,因此 IBL 就被命名为“基于图像的光照”。现实中,也可以直接用支持 HDR 的 3D 立体摄像机直接拍摄真实环境中的 IBL 图片,然后渲染时直接使用,可以造成相对真实的3D渲染效果,实际中现在的 VR、MR等技术设备中都或多或少使用了类似的技术。
因此也可以认为 IBL 就是为了给 “主角” 加 “光环” 而诞生的,所以学习 IBL 渲染方法是非常有必要的。谁不想成为生活中的主角呢?!
在这一讲中就着重讲一下这个“等距柱面贴图”如何再映射回“球面”的过程,实际就是重新解算到 CubeMap 的 6 个面上。同时介绍一些其它教程中未涉及的简捷计算过程或方法。
2、等距柱面映射(Equidistant cylindrical projection、equirectangular projection)的数学原理
其实等距柱面贴图本身没有太复杂的东西,只需要我们直观的想象将一个球体表面沿某条连接两级的大圆弧的半圆弧作为分割线,将整个表面拉伸成一幅平面矩形图即可。
可以想象将第一小节的图原封不动的贴回球体上的效果。明显的可以看出对矩形来说上下两边是被拉伸开了,因此分辨率是降低了,而赤道附近则保留了原始的分辨率。因此等距柱面贴图的球体赤道附近分辨率是保持了原貌的,而朝向两极则是分辨率急剧下降,并且有些扭曲的,因为球面本身是没法简单的线性拓扑到平面上的。其实这也比较符合我们观看现实场景的习惯,假设以你的眼睛为原点,一般我们会比较注意观察朝向地平线的方向,而很少朝高空或者脚底观看。而地平线的方向,对于等距柱面来说,就是赤道附近的方向。
最终因为一般等距柱面贴图,都会被我们以各种方式反映射回球体表面,而正好反向弥补了其分辨率扭曲的问题。所以最终等距柱面贴图是比较好的球体表面贴图映射。有些版本的世界地图也采取了相似的方法。(现在还有一种可以保持几乎所有分辨率的创建环境映射的方法,被称之为"“双曲柱面 Hyperboloid” 是一种几何形状,可以用来创建照明环境。它通常由两个双曲面组成,其中一个位于顶部,另一个位于底部,两个双曲面通过一个椭圆形截面连接在一起。这种形状可以形成一个类似于带子的结构,可以包裹住整个场景。通过将环境贴图投影到双曲柱面上,可以模拟出不同方向上的光照情况。当场景中的物体与双曲柱面相交时,可以根据相交点的位置和法线来计算物体表面的光照强度和颜色。这样可以实现基于图像的全局光照效果,使得物体在环境中看起来更加真实和自然。总之,IBL 双曲柱面是一种用于模拟环境光照的几何形状,通过投影环境贴图并计算物体表面的光照强度和颜色,实现更加真实的渲染效果。)本章教程中我们还是主要以相对简单一些的等距柱面为主,来介绍 IBL 环境贴图的基本原理和方法。
通过下图,可以清楚的看到等距柱面贴图,与单位球坐标之间的关系,从而也可以快速的了解纹理的(u,v)坐标与球面的极坐标之间的换算关系:
看着图,可以想象一下将你的地球仪表面的经纬度图,平铺到纹理坐标系中的样子。(本人懒得画图,引用自网络图片,侵删!)
根据图中所示的单位球面极坐标情况,在本章示例程序的“GRSD3D12Sample/GRS_PBR_Function.hlsli”文件中的函数 SampleSphericalMap 根据这一映射关系实现如下:
float2 SampleSphericalMap(float3 coord)
{
float theta = acos(coord.y);
float phi = atan2(coord.x, coord.z);
phi += (phi < 0) ? 2 * PI : 0;
float u = phi * INV_TWO_PI;
float v = theta * INV_PI;
return float2(u, v);
}
上述代码中,前三句就是非常经典的xyz坐标反算单位球面极坐标的两个极角值,显然传入的3D向量一定是单位向量,才能这样计算。接着就是根据图中显示的映射关系,将极角值,接着按比例计算为纹理坐标(u,v)值。
这里有必要复习一下关于 笛卡尔坐标转球面坐标的公式:
{
x
=
r
sin
θ
cos
ϕ
y
=
r
sin
θ
sin
ϕ
z
=
r
cos
θ
\begin{cases} x = r \sin \theta \cos \phi \\[2ex] y = r \sin \theta \sin \phi \\[2ex] z = r \cos \theta \end{cases}
⎩
⎨
⎧x=rsinθcosϕy=rsinθsinϕz=rcosθ
反过来有:
{
r
=
(
x
2
+
y
2
+
z
2
)
θ
=
arccos
(
z
(
x
2
+
y
2
+
z
2
)
)
φ
=
arctan
(
y
x
)
\begin{cases} r =\sqrt{(x^2 + y^2 + z^2)} \\[2ex] θ = \arccos \left( \cfrac{z}{\sqrt{(x^2 + y^2 + z^2)}} \right) \\[2ex] φ = \arctan(\cfrac{y}{x}) \end{cases}
⎩
⎨
⎧r=(x2+y2+z2)θ=arccos((x2+y2+z2)z)φ=arctan(xy)
这样结合之前图中的映射关系,以及
r
=
1
⇒
(
x
2
+
y
2
+
z
2
)
=
1
r=1 \ \Rightarrow \sqrt{(x^2 + y^2 + z^2)}=1
r=1 ⇒(x2+y2+z2)=1 这个条件,可以很方便的计算球面坐标下的
(
θ
,
ϕ
)
(\theta,\phi)
(θ,ϕ) 就可以完成单位 3D 向量(Normalize)到等距柱面纹理坐标的转换:
{
r
=
1
θ
=
arccos
(
z
)
φ
=
arctan
(
y
x
)
\begin{cases} r = 1 \\[2ex] θ = \arccos \left( {z} \right) \\[2ex] φ = \arctan(\cfrac{y}{x}) \end{cases}
⎩
⎨
⎧r=1θ=arccos(z)φ=arctan(xy)
一般的程序中,到这里就是进一步利用 Cube Map 中 Cube 的六个面,并假设相机在Cube 的几何中心,然后投影映射,并进行 6 次渲染计算,将等距柱面贴图解算成 Cube Map 的 6 个面纹理数组,接着在后续的 PBR 渲染中使用。因为我们是二班的程序,所以我们还需要继续深入的研究下怎么更加便捷的利用这个公式,以达到简化渲染过程的目的。
3、HDR 环境映射贴图加载
之前教程中也介绍过,在 sIBL Archive (hdrlabs.com) 可以免费下载到 HDR 格式的等距柱面环境映射贴图,但是最近发现好像这个网站不能访问了,大家可以去另外一个非常棒的 HDRIs • Poly Haven 网站上下载,下载的时候注意选择文件格式为 HDR,分辨率不要超过8K,否则可能因为系统限制而无法简单的加载。
加载 HDR 环境映射,需要专门的解析库,示例里就使用了开源的 stblib nothings/stb ,大家可以用 Git 自行下载(为方便各位学习使用,这里没有将 stb 库关联成 Git 子模块方式,而是直接克隆了一份放在代码中,而原链接现在放在教程中,以此向 stb 的所有贡献值和维护者表示崇高的敬意!)。这个库的使用也很简单,只需要包含 #include “…/stb/stb_image.h” 文件即可,调用上也非常方便,在本章示例教程中加载 HDR 代码如下:
g_stHDRData.m_pfHDRData = stbi_loadf(T2A(g_stHDRData.m_pszHDRFile)
, &g_stHDRData.m_iHDRWidth
, &g_stHDRData.m_iHDRHeight
, &g_stHDRData.m_iHDRPXComponents
, 0);
if (nullptr == g_stHDRData.m_pfHDRData)
{
ATLTRACE(_T("无法加载HDR文件:\"%s\"\n"), g_stHDRData.m_pszHDRFile);
AtlThrowLastWin32();
}
if (3 == g_stHDRData.m_iHDRPXComponents)
{
g_stHDRData.m_emHDRFormat = DXGI_FORMAT_R32G32B32_FLOAT;
}
else
{
ATLTRACE(_T("HDR文件:\"%s\",不是3部分Float格式,无法使用!\n"), g_stHDRData.m_pszHDRFile);
AtlThrowLastWin32();
}
加载完 HDR 图片的原始数据后,就需要通过我们熟悉的 2 遍 Copy 大法,全部上传到显存中,供 GPU 使用。2 遍 Copy 的过程就不在赘述了,直接使用的就是本教程中自行封装的工具函数,大家应该已经很熟悉其过程了。
这里要注意的就是,因为环境映射是 HDR 数据,也就是高动态范围光影数据,其范围是远超过一般归一化纹理的 [0.0f-1.0f] 范围的,也不能简单的映射到 [0,255] 范围,所以代码中使用的纹理格式是 DXGI_FORMAT_R32G32B32_FLOAT(一般情况下 DXGI_FORMAT_R16G16B16A16_FLOAT 也够用了,可以节约一定量的显存。同时需要注意没有DXGI_FORMAT_R16G16B16_FLOAT,这样的格式,想想为什么?),这使得每个像素颜色值尺寸变成了 3 x 4 = 12 Bytes,再加上其分辨率都在8k以上,所以这是个很耗内存的纹理,这也是很多现代的基于 PBR 光照的游戏很耗显存的原因之一,这种场景下通常就需要使用类似 tile 或 虚拟显存等方式来加载纹理(需要注意的是,纹理一旦需要被加载,那么纹理压缩等方法一般也就不适用了,纹理压缩适用于纹理的存储与传输,并不适用于纹理在显存中加载后的使用,请注意区别,不要问为什么)。
关于 HDR 以及虚拟显存等话题,看后续教程在恰当的时候进行介绍。各位急友也可以先自行去搜索相关资料自行了解先。
4、普通的6遍渲染解算过程
为了让大家更好的理解整个解算等距柱面环境映射的过程,首先在示例中,为了便于大家理解,专门编写了一段基于 6 遍渲染的程序,每次渲染 CubeMap 的 1 个面。那么我们先来看一下这是怎么做的。
4.1、创建需要执行 6 次的 PSO
在C++代码中,按照我们已经熟悉的方式,编写代码如下:
D3D12_DESCRIPTOR_RANGE1 stDSPRanges[3] = {};
// 1 Const Buffer ( MVP Matrix )
stDSPRanges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
stDSPRanges[0].NumDescriptors = 1;
stDSPRanges[0].BaseShaderRegister = 0;
stDSPRanges[0].RegisterSpace = 0;
stDSPRanges[0].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
stDSPRanges[0].OffsetInDescriptorsFromTableStart = 0;
// 1 Texture ( HDR Texture )
stDSPRanges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
stDSPRanges[1].NumDescriptors = 1;
stDSPRanges[1].BaseShaderRegister = 0;
stDSPRanges[1].RegisterSpace = 0;
stDSPRanges[1].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
stDSPRanges[1].OffsetInDescriptorsFromTableStart = 0;
// 1 Sampler ( Linear Sampler )
stDSPRanges[2].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER;
stDSPRanges[2].NumDescriptors = 1;
stDSPRanges[2].BaseShaderRegister = 0;
stDSPRanges[2].RegisterSpace = 0;
stDSPRanges[2].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
stDSPRanges[2].OffsetInDescriptorsFromTableStart = 0;
D3D12_ROOT_PARAMETER1 stRootParameters[3] = {};
stRootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
stRootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;//CBV是所有Shader可见
stRootParameters[0].DescriptorTable.NumDescriptorRanges = 1;
stRootParameters[0].DescriptorTable.pDescriptorRanges = &stDSPRanges[0];
stRootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
stRootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;//SRV仅PS可见
stRootParameters[1].DescriptorTable.NumDescriptorRanges = 1;
stRootParameters[1].DescriptorTable.pDescriptorRanges = &stDSPRanges[1];
stRootParameters[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
stRootParameters[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;//SAMPLE仅PS可见
stRootParameters[2].DescriptorTable.NumDescriptorRanges = 1;
stRootParameters[2].DescriptorTable.pDescriptorRanges = &stDSPRanges[2];
D3D12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc = {};
stRootSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
stRootSignatureDesc.Desc_1_1.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
stRootSignatureDesc.Desc_1_1.NumParameters = _countof(stRootParameters);
stRootSignatureDesc.Desc_1_1.pParameters = stRootParameters;
stRootSignatureDesc.Desc_1_1.NumStaticSamplers = 0;
stRootSignatureDesc.Desc_1_1.pStaticSamplers = nullptr;
ComPtr<ID3DBlob> pISignatureBlob;
ComPtr<ID3DBlob> pIErrorBlob;
GRS_THROW_IF_FAILED(D3D12SerializeVersionedRootSignature(&stRootSignatureDesc
, &pISignatureBlob
, &pIErrorBlob));
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateRootSignature(0
, pISignatureBlob->GetBufferPointer()
, pISignatureBlob->GetBufferSize()
, IID_PPV_ARGS(&g_stHDR2CubemapPSO.m_pIRS)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_stHDR2CubemapPSO.m_pIRS);
//-----------------------------------------------------------------------------
UINT nCompileFlags = 0;
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
nCompileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
//编译为行矩阵形式
nCompileFlags |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;
TCHAR pszShaderFileName[MAX_PATH] = {};
ComPtr<ID3DBlob> pIVSCode;
ComPtr<ID3DBlob> pIPSCode;
ComPtr<ID3DBlob> pIErrorMsg;
::StringCchPrintf(pszShaderFileName
, MAX_PATH
, _T("%s\\GRS_6Times_HDR_2_Cubemap_VS.hlsl")
, g_pszShaderPath);
HRESULT hr = D3DCompileFromFile(pszShaderFileName, nullptr, &grsD3DCompilerInclude
, "VSMain", "vs_5_0", nCompileFlags, 0, &pIVSCode, &pIErrorMsg);
if (FAILED(hr))
{
ATLTRACE("编译 Vertex Shader:\"%s\" 发生错误:%s\n"
, T2A(pszShaderFileName)
, pIErrorMsg ? pIErrorMsg->GetBufferPointer() : "无法读取文件!");
GRS_THROW_IF_FAILED(hr);
}
pIErrorMsg.Reset();
::StringCchPrintf(pszShaderFileName
, MAX_PATH
, _T("%s\\GRS_HDR_Spherical_Map_2_Cubemap_PS.hlsl")
, g_pszShaderPath);
hr = D3DCompileFromFile(pszShaderFileName, nullptr, &grsD3DCompilerInclude
, "PSMain", "ps_5_0", nCompileFlags, 0, &pIPSCode, &pIErrorMsg);
if (FAILED(hr))
{
ATLTRACE("编译 Pixel Shader:\"%s\" 发生错误:%s\n"
, T2A(pszShaderFileName)
, pIErrorMsg ? pIErrorMsg->GetBufferPointer() : "无法读取文件!");
GRS_THROW_IF_FAILED(hr);
}
pIErrorMsg.Reset();
// 定义输入顶点的数据结构
D3D12_INPUT_ELEMENT_DESC stInputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
// 创建 graphics pipeline state object (PSO)对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC stPSODesc = {};
stPSODesc.InputLayout = { stInputElementDescs, _countof(stInputElementDescs) };
stPSODesc.pRootSignature = g_stHDR2CubemapPSO.m_pIRS.Get();
// VS -> PS
stPSODesc.VS.BytecodeLength = pIVSCode->GetBufferSize();
stPSODesc.VS.pShaderBytecode = pIVSCode->GetBufferPointer();
stPSODesc.PS.BytecodeLength = pIPSCode->GetBufferSize();
stPSODesc.PS.pShaderBytecode = pIPSCode->GetBufferPointer();
stPSODesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
stPSODesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
stPSODesc.BlendState.AlphaToCoverageEnable = FALSE;
stPSODesc.BlendState.IndependentBlendEnable = FALSE;
stPSODesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
stPSODesc.SampleMask = UINT_MAX;
stPSODesc.SampleDesc.Count = 1;
stPSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
stPSODesc.NumRenderTargets = 1;
stPSODesc.RTVFormats[0] = g_st6TimesRTAStatus.m_emRTAFormat;
stPSODesc.DepthStencilState.StencilEnable = FALSE;
stPSODesc.DepthStencilState.DepthEnable = FALSE;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateGraphicsPipelineState(&stPSODesc, IID_PPV_ARGS(&g_stHDR2CubemapPSO.m_pIPSO)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_stHDR2CubemapPSO.m_pIPSO);
// 创建 CBV SRV Heap
D3D12_DESCRIPTOR_HEAP_DESC stSRVHeapDesc = {};
stSRVHeapDesc.NumDescriptors = 2; // 1 CBV + 1 SRV
stSRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stSRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateDescriptorHeap(&stSRVHeapDesc
, IID_PPV_ARGS(&g_stHDR2CubemapHeap.m_pICBVSRVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_stHDR2CubemapHeap.m_pICBVSRVHeap);
// 创建 Sample Heap
D3D12_DESCRIPTOR_HEAP_DESC stSamplerHeapDesc = {};
stSamplerHeapDesc.NumDescriptors = 1; // 1 Sample
stSamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
stSamplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateDescriptorHeap(&stSamplerHeapDesc
, IID_PPV_ARGS(&g_stHDR2CubemapHeap.m_pISAPHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_stHDR2CubemapHeap.m_pISAPHeap);
上面的代码是在本系列教程中一直比较常见的代码了,就不啰嗦赘述了。需要注意的是,代码中,在结构和相互关系上给你了很多暗示,比如 RS 的创建,关乎后面的 Descriptor Heap 的创建,而创建 Descriptor Heap 和 PSO 放在一起是比较容易校验正确性的,而整个 RS、PSO、Descriptor Heap创建和其它部分就没有关系了,完全是可以独立的。希望现在你已经完全能够看明白这些信息和给你的暗示了。
4.2、渲染 6 次方法的Shader
接着就是PSO中需要的两个Shader,一个用于VS、一个用于 PS,详细代码如下:
GRS_6Times_HDR_2_Cubemap_VS.hlsl
#include "GRS_Scene_CB_Def.hlsli"
struct ST_GRS_HLSL_VS_IN
{
float4 m_v4LPos : POSITION; // Model Local position
};
struct ST_GRS_HLSL_VS_OUT
{
float4 m_v4HPos : SV_POSITION; // Projection coord
float4 m_v4WPos : POSITION; // World position
};
ST_GRS_HLSL_VS_OUT VSMain(ST_GRS_HLSL_VS_IN stVSInput)
{
ST_GRS_HLSL_VS_OUT stVSOutput;
stVSOutput.m_v4WPos = stVSInput.m_v4LPos;
// 从模型空间转换到世界坐标系
stVSOutput.m_v4HPos = mul(stVSInput.m_v4LPos, g_mxWVP);
return stVSOutput;
}
这段VS代码实现的很简单,就是将顶点 Position 属性,先直接赋值给世界坐标,本质上这里传入的顶点就是CubeMap 一个正方形面的 4 个顶点中的 1 个。(想想为啥非要是正方形,矩形行不行?)
GRS_HDR_Spherical_Map_2_Cubemap_PS.hlsl:
#include "0-1 HDR_COLOR_CONV.hlsli"
#include "GRS_PBR_Function.hlsli" // for SampleSphericalMap
float2 SampleSphericalMap(float3 coord)
{
float theta = acos(coord.y);
float phi = atan2(coord.x, coord.z);
phi += (phi < 0) ? 2 * PI : 0;
float u = phi * INV_TWO_PI;
float v = theta * INV_PI;
return float2(u, v);
}
Texture2D g_txHDR : register(t0);
SamplerState g_smpLinear : register(s0);
struct ST_GRS_HLSL_PS_IN
{
float4 m_v4HPos : SV_POSITION; // Projection coord
float4 m_v4WPos : POSITION; // World position
};
float4 PSMain(ST_GRS_HLSL_PS_IN stPSInput) : SV_Target
{
float2 v2UV = SampleSphericalMap(normalize(stPSInput.m_v4WPos.xyz));
return float4(g_txHDR.Sample(g_smpLinear, v2UV).rgb, 1.0f);
}
PS 的代码也非常简单,只是将处于世界坐标系中物体表面的位置向量的xyz分量,简单的等效为其表面该点处的法线,并且标准化之后,传递给 SampleSphericalMap 函数换算为等距柱面纹理上一点的坐标,然后按这个纹理坐标采样等距柱面纹理,返回作为 CubeMap 6个面之一上的对应点处的颜色值。最终程序将解算完毕的 CubeMap 保存作为后续 IBL 光照过程中的 CubeMap 环境映射光源即可。
5、利用 GS 一次性解算
接着我们来思考一下上面这个常规的过程,发现上述过程虽然简单易理解,但过程却显得有些啰嗦。首先这个过程是一个预处理过程,其直接结果就是将 1 幅纹理,变成了 6 幅纹理,需要单独的 6 Pass 渲染,也就是一个面需要 1 Pass 渲染,循环 6 次;其次,在后续使用中采样过程就变成了繁琐的 3D Vector Sample ,性能上肯定不如简单的 2D Sample;再次,我们仔细观察上述的 Shader 代码 ,尤其是 SampleSphericalMap 函数会发现,这个函数的本质就是将一个 3D Vector 变成 2D UV 坐标向量的,而且计算过程也不复杂,是完全可以代入到最终需要使用 3D Vector Sample 过程中,直接简化 Sample 过程的!
因此在本章示例教程中,参考龚大之前的文章,首先考虑使用 Geometry Shader (GS),先考虑一次性完成等距柱面贴图的解算,并直接将解算的结果存入一个具有 6 个分量的纹理数组也就是 CubeMap 中。
5.1、Geometry Shader
首先让我们来简单了解下 Geometry Shader,几何着色器( Geometry Shader)是图形渲染管线(包括D3D10以上、OpenGL、Vulkan)中的一个可编程阶段,它位于顶点着色器和像素着色器之间。几何着色器主要用于处理图元(如点、线、三角形等)的几何信息,可以对输入的图元进行修改、增删顶点,以及生成新的图元。
几何着色器的输入是由顶点着色器输出的图元数据,包括顶点坐标、法线、纹理坐标等。几何着色器可以对这些图元进行各种变换和操作,并生成新的图元数据。例如,可以对输入的三角形进行细分,生成更多的三角形;或者根据输入的顶点坐标生成法线和切线信息;还可以根据场景需求生成粒子系统等。
几何着色器的输出是经过修改后的图元数据,传递给片段着色器进行光栅化和像素级处理。通过几何着色器,可以在图形渲染过程中动态地修改和生成几何形状,实现一些高级的效果和技术,如几何细分(与Tesselation方式有区别)、扩展几何体、粒子系统等。几何着色器在D3D12渲染管线中是可选的。
5.2、Geometry Shader 中指定渲染目标数组索引
在 D3D Geometry Shader 中,可以使用一个系统变量 SV_RenderTargetArrayIndex 来指定顶点最终光栅化输出后所对应的 Render Target Array (渲染目标数组,注意不是多渲染目标 MRT 不要搞混,这里是说还是一个渲染目标,不过这个渲染目标是一个纹理数组)中的索引,即可以通过这个变量来指定顶点对应的渲染目标。
借助这个变量的能力,我们就可以将最终渲染目标直接设计成一个纹理数组,分别对应 CubeMap 的 6 个面,然后在 Geometry Shader 中按照法线方向指定顶点最终对应的 CubeMap 的面索引。
在本章示例教程代码 (GRSD3D12Sample/25-IBL-MultiInstance-Sphere/Shader/GRS_1Times_GS_HDR_2_CubeMap_VS_GS.hlsl) 中完整的展示了如何在 Geometry Shader 中利用这个变量的整个过程:
#include "GRS_Scene_CB_Def.hlsli"
struct ST_GRS_HLSL_VS_IN
{
float4 m_v4LPos : POSITION; // Model Local position
};
struct ST_GRS_HLSL_GS_IN
{
float4 m_v4WPos : SV_POSITION; // World position
};
struct ST_GRS_HLSL_GS_OUT
{
float4 m_v4HPos : SV_POSITION; // Projection coord
float4 m_v4WPos : POSITION; // World position
uint RTIndex : SV_RenderTargetArrayIndex;// Render Target Array Index
};
ST_GRS_HLSL_GS_IN VSMain(ST_GRS_HLSL_VS_IN stVSInput)
{
ST_GRS_HLSL_GS_IN stVSOutput;
// 从模型空间转换到世界坐标系
stVSOutput.m_v4WPos = mul(stVSInput.m_v4LPos, g_mxWorld);
return stVSOutput;
}
[maxvertexcount(18)]
void GSMain(triangle ST_GRS_HLSL_GS_IN stGSInput[3], inout TriangleStream<ST_GRS_HLSL_GS_OUT> CubeMapStream)
{
for (int f = 0; f < GRS_CUBE_MAP_FACE_CNT; ++f)
{
ST_GRS_HLSL_GS_OUT stGSOutput;
stGSOutput.RTIndex = f; //设定输出的缓冲索引
for (int v = 0; v < 3; v++)
{
// 下面的乘积是可以优化的,可以提前在 CPP 中 将6个View矩阵分别先与Projection矩阵相乘,再传入Shader
// 当然因为这里是固定的Cube
stGSOutput.m_v4WPos = stGSInput[v].m_v4WPos;
stGSOutput.m_v4HPos = mul(stGSInput[v].m_v4WPos, g_mxGSCubeView[f]);
stGSOutput.m_v4HPos = mul(stGSOutput.m_v4HPos, g_mxProj);
CubeMapStream.Append(stGSOutput);
}
CubeMapStream.RestartStrip();
}
}
代码中,首先在 GS 阶段的输出结构体 ST_GRS_HLSL_GS_OUT 中定义了变量 uint RTIndex : SV_RenderTargetArrayIndex; 然后在 GSMain函数中循环依次给每个顶点这个变量赋值为对应的面索引(Face Index)即可:stGSOutput.RTIndex = f;。
这里需要注意的是,Shader 代码中并没有通过判定顶点指向方向或其法线方向来选取对应的面,而是在程序传入顶点顺序上严格按照教程:DirectX12(D3D12)基础教程(二十)—— 纹理数组(Texture Array)非DDS初始化操作 中标识的CubeMap Texture Array 中面的顺序,依次安排每个面对应顶点的顺序,同时,我们没有使用顶点对应的索引数组 Mesh 方式,而是直接使用了 strip 顶点依次缠绕的数据形式,这是因为如果使用索引数组,那么 Cube 的 8个顶点只会被计算八次,而这样顺序赋值面索引,就会引起错误,因为实际上这里要一次性渲染6个面,那么实际上每个顶点要出现在不同的3个面上,而且是不同的方向,所以不能用带有索引的 Cube Mesh。
5.3、特别设定的 Cube Vertex 数据
在程序中,是这样设计这个 Cube 的 Vertex 数组的:
// 定义正方体的3D数据结构
struct ST_GRS_VERTEX_CUBE_BOX
{
XMFLOAT4 m_v4Position;
};
ST_GRS_VERTEX_CUBE_BOX pstVertices[] = {
{ { -1.0f, 1.0f, -1.0f, 1.0f} },
{ { -1.0f, -1.0f, -1.0f, 1.0f} },
{ { 1.0f, 1.0f, -1.0f, 1.0f} },
{ { 1.0f, -1.0f, -1.0f, 1.0f} },
{ { 1.0f, 1.0f, 1.0f, 1.0f} },
{ { 1.0f, -1.0f, 1.0f, 1.0f} },
{ { -1.0f, 1.0f, 1.0f, 1.0f} },
{ { -1.0f, -1.0f, 1.0f, 1.0f} },
{ { -1.0f, 1.0f, -1.0f, 1.0f} },
{ { -1.0f, -1.0f, -1.0f, 1.0f} },
{ { -1.0f, -1.0f, 1.0f, 1.0f} },
{ { 1.0f, -1.0f, -1.0f, 1.0f} },
{ { 1.0f, -1.0f, 1.0f, 1.0f} },
{ { 1.0f, 1.0f, 1.0f, 1.0f} },
{ { -1.0f, 1.0f, 1.0f, 1.0f} },
{ { 1.0f, 1.0f, -1.0f, 1.0f} },
{ { -1.0f, 1.0f, -1.0f, 1.0f} },
};
注意上面数据中顶点的逆时针然后又顺时针的顺序,每 3 个点代表一个面上的半个三角形,并且每 4 个点中间 2 个点是前后两个三角形共用的。因为这样的缠绕三角形顶点顺序中既有逆时针也有顺时针的三角形,所以渲染时需要将背面剔除选项关闭掉,否则每个面会有半个三角形被剔除,而不会被最终渲染到 Render Target上。这在创建PSO时指定。
5.4、创建 1 遍渲染 6 个面带 GS 的 PSO
下面代码段就是创建 1 Pass 渲染 Cubemap 6个面的PSO的代码,其中关闭了背面剔除 stPSODesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; 并且管线的流程是 VS -> GS -> PS,也就是加入了 GS(Geometry Shader) 阶段。整个代码的框架各位应该已经非常熟悉了,也可以看出在整个管线中加入 GS 阶段也是很简单的,主要基本的 PSO 创建的逻辑搞清楚了,那么整个管线的流程就是由你来组织逻辑了。
D3D12_DESCRIPTOR_RANGE1 stDSPRanges[3] = {};
// 2 Const Buffer ( MVP Matrix + Cube Map 6 View Matrix)
stDSPRanges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
stDSPRanges[0].NumDescriptors = 2;
stDSPRanges[0].BaseShaderRegister = 0;
stDSPRanges[0].RegisterSpace = 0;
stDSPRanges[0].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
stDSPRanges[0].OffsetInDescriptorsFromTableStart = 0;
// 1 Texture ( HDR Texture )
stDSPRanges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
stDSPRanges[1].NumDescriptors = 1;
stDSPRanges[1].BaseShaderRegister = 0;
stDSPRanges[1].RegisterSpace = 0;
stDSPRanges[1].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
stDSPRanges[1].OffsetInDescriptorsFromTableStart = 0;
// 1 Sampler ( Linear Sampler )
stDSPRanges[2].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER;
stDSPRanges[2].NumDescriptors = 1;
stDSPRanges[2].BaseShaderRegister = 0;
stDSPRanges[2].RegisterSpace = 0;
stDSPRanges[2].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
stDSPRanges[2].OffsetInDescriptorsFromTableStart = 0;
D3D12_ROOT_PARAMETER1 stRootParameters[3] = {};
stRootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
stRootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;//CBV是所有Shader可见
stRootParameters[0].DescriptorTable.NumDescriptorRanges = 1;
stRootParameters[0].DescriptorTable.pDescriptorRanges = &stDSPRanges[0];
stRootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
stRootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;//SRV仅PS可见
stRootParameters[1].DescriptorTable.NumDescriptorRanges = 1;
stRootParameters[1].DescriptorTable.pDescriptorRanges = &stDSPRanges[1];
stRootParameters[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
stRootParameters[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;//SAMPLE仅PS可见
stRootParameters[2].DescriptorTable.NumDescriptorRanges = 1;
stRootParameters[2].DescriptorTable.pDescriptorRanges = &stDSPRanges[2];
D3D12_VERSIONED_ROOT_SIGNATURE_DESC stRootSignatureDesc = {};
stRootSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
stRootSignatureDesc.Desc_1_1.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
stRootSignatureDesc.Desc_1_1.NumParameters = _countof(stRootParameters);
stRootSignatureDesc.Desc_1_1.pParameters = stRootParameters;
stRootSignatureDesc.Desc_1_1.NumStaticSamplers = 0;
stRootSignatureDesc.Desc_1_1.pStaticSamplers = nullptr;
ComPtr<ID3DBlob> pISignatureBlob;
ComPtr<ID3DBlob> pIErrorBlob;
GRS_THROW_IF_FAILED(D3D12SerializeVersionedRootSignature(&stRootSignatureDesc
, &pISignatureBlob
, &pIErrorBlob));
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateRootSignature(0
, pISignatureBlob->GetBufferPointer()
, pISignatureBlob->GetBufferSize()
, IID_PPV_ARGS(&g_st1TimesGSPSO.m_pIRS)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_st1TimesGSPSO.m_pIRS);
//--------------------------------------------------------------------------------------------------------------------------------
UINT nCompileFlags = 0;
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
nCompileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
//编译为行矩阵形式
nCompileFlags |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;
TCHAR pszShaderFileName[MAX_PATH] = {};
ComPtr<ID3DBlob> pIVSCode;
ComPtr<ID3DBlob> pIGSCode;
ComPtr<ID3DBlob> pIPSCode;
ComPtr<ID3DBlob> pIErrorMsg;
::StringCchPrintf(pszShaderFileName
, MAX_PATH
, _T("%s\\GRS_1Times_GS_HDR_2_CubeMap_VS_GS.hlsl"), g_pszShaderPath);
HRESULT hr = D3DCompileFromFile(pszShaderFileName, nullptr, &grsD3DCompilerInclude
, "VSMain", "vs_5_0", nCompileFlags, 0, &pIVSCode, &pIErrorMsg);
if (FAILED(hr))
{
ATLTRACE("编译 Vertex Shader:\"%s\" 发生错误:%s\n"
, T2A(pszShaderFileName)
, pIErrorMsg ? pIErrorMsg->GetBufferPointer() : "无法读取文件!");
GRS_THROW_IF_FAILED(hr);
}
pIErrorMsg.Reset();
hr = ::D3DCompileFromFile(pszShaderFileName, nullptr, &grsD3DCompilerInclude
, "GSMain", "gs_5_0", nCompileFlags, 0, &pIGSCode, &pIErrorMsg);
if (FAILED(hr))
{
ATLTRACE("编译 Geometry Shader:\"%s\" 发生错误:%s\n"
, T2A(pszShaderFileName)
, pIErrorMsg ? pIErrorMsg->GetBufferPointer() : "无法读取文件!");
GRS_THROW_IF_FAILED(hr);
}
pIErrorMsg.Reset();
::StringCchPrintf(pszShaderFileName
, MAX_PATH
, _T("%s\\GRS_HDR_Spherical_Map_2_Cubemap_PS.hlsl")
, g_pszShaderPath);
hr = D3DCompileFromFile(pszShaderFileName, nullptr, &grsD3DCompilerInclude
, "PSMain", "ps_5_0", nCompileFlags, 0, &pIPSCode, &pIErrorMsg);
if (FAILED(hr))
{
ATLTRACE("编译 Pixel Shader:\"%s\" 发生错误:%s\n"
, T2A(pszShaderFileName)
, pIErrorMsg ? pIErrorMsg->GetBufferPointer() : "无法读取文件!");
GRS_THROW_IF_FAILED(hr);
}
pIErrorMsg.Reset();
// 定义输入顶点的数据结构
D3D12_INPUT_ELEMENT_DESC stInputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
// 创建 graphics pipeline state object (PSO)对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC stPSODesc = {};
stPSODesc.InputLayout = { stInputElementDescs, _countof(stInputElementDescs) };
stPSODesc.pRootSignature = g_st1TimesGSPSO.m_pIRS.Get();
// VS -> GS -> PS
stPSODesc.VS.BytecodeLength = pIVSCode->GetBufferSize();
stPSODesc.VS.pShaderBytecode = pIVSCode->GetBufferPointer();
stPSODesc.GS.BytecodeLength = pIGSCode->GetBufferSize();
stPSODesc.GS.pShaderBytecode = pIGSCode->GetBufferPointer();
stPSODesc.PS.BytecodeLength = pIPSCode->GetBufferSize();
stPSODesc.PS.pShaderBytecode = pIPSCode->GetBufferPointer();
stPSODesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
// 注意关闭背面剔除,因为定义的Triangle Strip Box的顶点有反向的
stPSODesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
stPSODesc.BlendState.AlphaToCoverageEnable = FALSE;
stPSODesc.BlendState.IndependentBlendEnable = FALSE;
stPSODesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
stPSODesc.SampleMask = UINT_MAX;
stPSODesc.SampleDesc.Count = 1;
stPSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
stPSODesc.NumRenderTargets = 1;
stPSODesc.RTVFormats[0] = g_st1TimesGSRTAStatus.m_emRTAFormat;
stPSODesc.DepthStencilState.StencilEnable = FALSE;
stPSODesc.DepthStencilState.DepthEnable = FALSE;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateGraphicsPipelineState(&stPSODesc, IID_PPV_ARGS(&g_st1TimesGSPSO.m_pIPSO)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_st1TimesGSPSO.m_pIPSO);
// 创建 CBV SRV Heap
D3D12_DESCRIPTOR_HEAP_DESC stSRVHeapDesc = {};
stSRVHeapDesc.NumDescriptors = 6; // 5 CBV + 1 SRV
stSRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stSRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateDescriptorHeap(&stSRVHeapDesc
, IID_PPV_ARGS(&g_st1TimesGSHeap.m_pICBVSRVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_st1TimesGSHeap.m_pICBVSRVHeap);
g_st1TimesGSHeap.m_nSRVOffset = 5; //第6个是SRV
// 创建 Sample Heap
D3D12_DESCRIPTOR_HEAP_DESC stSamplerHeapDesc = {};
stSamplerHeapDesc.NumDescriptors = 1; // 1 Sample
stSamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
stSamplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pID3D12Device4->CreateDescriptorHeap(&stSamplerHeapDesc
, IID_PPV_ARGS(&g_st1TimesGSHeap.m_pISAPHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(g_st1TimesGSHeap.m_pISAPHeap);
5.5、1 Pass 预渲染(预处理)
资源、PSO 都准备好后,就可以直接进行渲染了,因为解算环境映射贴图的过程其实本质上是可以一次性计算完毕,然后保存供后续渲染反复使用的,所以其渲染就不必加入到渲染循环中反复执行,而只需要在渲染循环前合适的步骤一次性解算即可。通常的这样的只需要在渲染循环前执行的一些渲染步骤,或者说需要调用GPU来完成的步骤,通常被称为预处理。
上面的步骤准备完毕后,就可以进行环境映射解算的预处理了,代码如下:
// 等待前面的Copy命令执行结束
if (WAIT_OBJECT_0 != ::WaitForSingleObject(g_stD3DDevices.m_hEventFence, INFINITE))
{
::AtlThrowLastWin32();
}
// 开始执行 GS Cube Map渲染,将HDR等距柱面纹理 一次性渲染至6个RTV
//命令分配器先Reset一下
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pIMainCMDAlloc->Reset());
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pIMainCMDList->Reset(g_stD3DDevices.m_pIMainCMDAlloc.Get(), nullptr));
g_stD3DDevices.m_pIMainCMDList->RSSetViewports(1, &g_st1TimesGSRTAStatus.m_stViewPort);
g_stD3DDevices.m_pIMainCMDList->RSSetScissorRects(1, &g_st1TimesGSRTAStatus.m_stScissorRect);
// 设置渲染目标 注意一次就把所有的离屏渲染目标放进去
D3D12_CPU_DESCRIPTOR_HANDLE stRTVHandle = g_st1TimesGSRTAStatus.m_pIRTAHeap->GetCPUDescriptorHandleForHeapStart();
g_stD3DDevices.m_pIMainCMDList->OMSetRenderTargets(1, &stRTVHandle, FALSE, nullptr);
// 继续记录命令,并真正开始渲染
g_stD3DDevices.m_pIMainCMDList->ClearRenderTargetView(stRTVHandle, g_stD3DDevices.m_faClearColor, 0, nullptr);
//----------------------------------------------------------------------------------
// Draw !
//----------------------------------------------------------------------------------
// 渲染Cube
// 设置渲染管线状态对象
g_stD3DDevices.m_pIMainCMDList->SetGraphicsRootSignature(g_st1TimesGSPSO.m_pIRS.Get());
g_stD3DDevices.m_pIMainCMDList->SetPipelineState(g_st1TimesGSPSO.m_pIPSO.Get());
arHeaps.RemoveAll();
arHeaps.Add(g_st1TimesGSHeap.m_pICBVSRVHeap.Get());
arHeaps.Add(g_st1TimesGSHeap.m_pISAPHeap.Get());
g_stD3DDevices.m_pIMainCMDList->SetDescriptorHeaps((UINT)arHeaps.GetCount(), arHeaps.GetData());
D3D12_GPU_DESCRIPTOR_HANDLE stGPUHandle = g_st1TimesGSHeap.m_pICBVSRVHeap->GetGPUDescriptorHandleForHeapStart();
// 设置CBV
g_stD3DDevices.m_pIMainCMDList->SetGraphicsRootDescriptorTable(0, stGPUHandle);
// 设置SRV
stGPUHandle.ptr += (g_st1TimesGSHeap.m_nSRVOffset * g_stD3DDevices.m_nCBVSRVDescriptorSize);
g_stD3DDevices.m_pIMainCMDList->SetGraphicsRootDescriptorTable(1, stGPUHandle);
// 设置Sample
stGPUHandle = g_st1TimesGSHeap.m_pISAPHeap->GetGPUDescriptorHandleForHeapStart();
g_stD3DDevices.m_pIMainCMDList->SetGraphicsRootDescriptorTable(2, stGPUHandle);
// 渲染手法:三角形带
g_stD3DDevices.m_pIMainCMDList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
// 设置网格信息
g_stD3DDevices.m_pIMainCMDList->IASetVertexBuffers(0, 1, &g_stBoxData_Strip.m_stVertexBufferView);
// Draw Call
g_stD3DDevices.m_pIMainCMDList->DrawInstanced(g_stBoxData_Strip.m_nVertexCount, 1, 0, 0);
//---------------------------------------------------------------------------------
// 使用资源屏障切换下状态,实际是与后面要使用这几个渲染目标的过程进行同步
g_st1TimesGSRTAStatus.m_stRTABarriers.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
g_st1TimesGSRTAStatus.m_stRTABarriers.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
g_st1TimesGSRTAStatus.m_stRTABarriers.Transition.pResource = g_st1TimesGSRTAStatus.m_pITextureRTA.Get();
g_st1TimesGSRTAStatus.m_stRTABarriers.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
//g_st1TimesGSRTAStatus.m_stRTABarriers.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
g_st1TimesGSRTAStatus.m_stRTABarriers.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
g_st1TimesGSRTAStatus.m_stRTABarriers.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
//资源屏障,用于确定渲染已经结束可以提交画面去显示了
g_stD3DDevices.m_pIMainCMDList->ResourceBarrier(1, &g_st1TimesGSRTAStatus.m_stRTABarriers);
//关闭命令列表,可以去执行了
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pIMainCMDList->Close());
//执行命令列表
g_stD3DDevices.m_pIMainCMDQueue->ExecuteCommandLists(static_cast<UINT>(arCmdList.GetCount()), arCmdList.GetData());
//开始同步GPU与CPU的执行,先记录围栏标记值
const UINT64 n64CurrentFenceValue = g_stD3DDevices.m_n64FenceValue;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pIMainCMDQueue->Signal(g_stD3DDevices.m_pIFence.Get(), n64CurrentFenceValue));
g_stD3DDevices.m_n64FenceValue++;
GRS_THROW_IF_FAILED(g_stD3DDevices.m_pIFence->SetEventOnCompletion(n64CurrentFenceValue, g_stD3DDevices.m_hEventFence));
上述代码与我们早已熟悉的渲染循环的代码很类似,只是少了循环语句。其中关键的就是渲染手法,按照之前的描述,我们设置为三角形带形式:g_stD3DDevices.m_pIMainCMDList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
5.6、其它扩展形式
按照龚大的说法,D3D12 中大概可以有六种方式渲染到 CubeMap,除了我们这里列举的一种常规 6 Pass方法和结合 GS 的方法外,比较重要的还有基于 Shader Modul 5.2 以上的 VS 中直接设定 SV_RenderTargetArrayIndex 变量的方法,这个我还没有时间去实验,如果这个可行的话,那么就不需要为这一个小小的变量,而大动干戈的请GS 下场帮忙了。并且在未来的 Mesh Shader 中,VS、GS、DS、HS等等就都被整合到 MS 中了,就可以直接随意使用 SV_RenderTargetArrayIndex 变量,并且因为几个阶段的能力都被合成到了一起,那么实际在控制上将更加灵活和精细。当然前提是你完全搞懂了本章教程中关于 SV_RenderTargetArrayIndex 变量的用法,和最终达成的目标。
6、直接在最终渲染过程中解算
在之前的讨论中,我们已经知道,其实整个等距柱面环境映射解算过程的核心就是那个 3D 向量变为 2D 纹理坐标的函数。其实这个函数是可以直接被代入后续的渲染过程直接使用的,而不需要之前的这些所谓的预处理的过程的。
在本章示例教程中,实质上最终就是直接在渲染过程中调用 SampleSphericalMap 函数来完成这个过程的。
6.1、无穷远平面 Skybox 中的直接使用等距柱面环境映射
在之前的系列教程中,我们一直使用简易的无穷远平面型天空盒,这种 Skybox 不需要创建复杂的内向的正方体盒子或者是半球型穹顶,而只需要一个无穷远的平面,将天空盒正对摄像机的面当成背景渲染即可。在本章示例中,就更进一步结合等距柱面环境映射解算的函数,直接将远平面中的射线变为等距柱面贴图上需要采样的纹理坐标进行采样,整体的 Shader 中是这样写的:
#include "0-1 HDR_COLOR_CONV.hlsli"
#include "GRS_Scene_CB_Def.hlsli"
#include "GRS_PBR_Function.hlsli"
Texture2D g_txSphericalMap : register(t0);
SamplerState g_smpLinear : register(s0);
struct ST_GRS_HLSL_VS_IN
{
float4 m_v4LPos : POSITION;
};
struct ST_GRS_HLSL_PS_IN
{
float4 m_v4LPos : SV_POSITION;
float4 m_v4PPos : TEXCOORD0;
};
ST_GRS_HLSL_PS_IN SkyboxVS(ST_GRS_HLSL_VS_IN stVSInput)
{
ST_GRS_HLSL_PS_IN stVSOutput;
stVSOutput.m_v4LPos = stVSInput.m_v4LPos;
stVSOutput.m_v4PPos = normalize(mul(stVSInput.m_v4LPos, g_mxInvVP));
return stVSOutput;
}
float4 SkyboxPS(ST_GRS_HLSL_PS_IN stPSInput) : SV_TARGET
{
float2 v2UV = SampleSphericalMap(normalize(stPSInput.m_v4PPos.xyz));
return float4(g_txSphericalMap.Sample(g_smpLinear, v2UV).rgb,1.0f);
}
可以看到,在 PS 函数中直接进行向量到纹理坐标的换算,然后直接采样等距柱面贴图即可。
6.2、在解算过程的 PS 中使用等距柱面环境映射
可 可无论是之前介绍的常规 6 遍渲染的等距柱面环境映射解算过程,还是简便一些的运用了 GS 的等距柱面解算过程,最终都使用了相同的 PS 过程,其中也直接使用了 SampleSphericalMap 函数进行采样,代码如下:
#include "0-1 HDR_COLOR_CONV.hlsli"
#include "GRS_PBR_Function.hlsli"
Texture2D g_txHDR : register(t0);
SamplerState g_smpLinear : register(s0);
struct ST_GRS_HLSL_PS_IN
{
float4 m_v4HPos : SV_POSITION; // Projection coord
float4 m_v4WPos : POSITION; // World position
};
float4 PSMain(ST_GRS_HLSL_PS_IN stPSInput) : SV_Target
{
float2 v2UV = SampleSphericalMap(normalize(stPSInput.m_v4WPos.xyz));
return float4(g_txHDR.Sample(g_smpLinear, v2UV).rgb, 1.0f);
}
在其他的 IBL 核心渲染的 Shader 中,最终采样其实也可以直接调用 SampleSphericalMap 函数采样原始的等距柱面贴图来实现,但为了便于大家理解整个过程,我没有这样简化去处理,以防止大家再学习 IBL 渲染过程本身时,不好理解这里的简化步骤,所以只是在天空盒渲染和解算的过程中这样做了一下,并且在这一章教程中解释清楚了整个过程,剩下的就是各位在搞明白 IBL 渲染的基础上,自行修改实验一下即可。
7、后记
这一篇教程编写过程可能是所有教程中历时最长的一篇,主要的原因是我最近还要深度学习单片机开发、物联网开发等内容,这也是我感兴趣的另一个领域,正在抓紧时间深入学习ing,后续也会出一系列的博文,将我所知所学分享给大家。期间实在是忙的不可开交,不但要编程,还要复习电路知识,有时甚至还要再次拿起电烙铁焊上那么两下子,因此本系列教程的写作过程就有些耽搁了。这几天有些时间就抓紧赶紧写完,算是给大家交个差。
成文仓促,文中纰漏还望各位批评斧正。耽搁时间久,还请见谅。
附录:系列教程前序文章目录
DirectX12(D3D12)基础教程(一)——基础教程
DirectX12(D3D12)基础教程(二)——理解根签名、初识显存管理和加载纹理、理解资源屏障
DirectX12(D3D12)基础教程(三)——使用独立堆以“定位方式”创建资源、创建动态采样器、初步理解采取器类型
DirectX12(D3D12)基础教程(四)——初识DirectXMath库、使用独立堆创建常量缓冲、理解管线状态对象、理解围栏同步
DirectX12(D3D12)基础教程(五)——理解和使用捆绑包,加载并使用DDS Cube Map
DirectX12(D3D12)基础教程(六)——多线程渲染
DirectX12(D3D12)基础教程(七)——渲染到纹理、正交投影、UI渲染基础
DirectX12(D3D12)基础教程(八)——多显卡渲染基础、共享纹理、多GPU同步
DirectX12(D3D12)基础教程(九)——多线程渲染BUG修正及MsgWaitForMultipleObjects函数详解
DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(上)
DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(下)
DirectX12(D3D12)基础教程(十一)——几个“上古时代”的基于Pixel Shader的滤镜效果
DirectX12(D3D12)基础教程(十二)——多线程+多显卡渲染及水彩画效果和标准简化版高斯模糊
DirectX12(D3D12)基础教程(十三)——D2D、DWrite On D3D12与文字输出
DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(上)
DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(中)
DirectX12(D3D12)基础教程(十四)——使用WIC、Computer Shader显示GIF动画纹理(下)
DirectX12(D3D12)基础教程(十六)——实现渲染线程池:3个内核同步对象实现渲染线程池/大规模线程池
DirectX12(D3D12)基础教程(外篇一)——编译Assimp
DirectX12(D3D12)基础教程(外篇二)——编译DirectXShaderCompiler库
DirectX12(D3D12)基础教程(外篇三)——CreateGraphicsPipelineState 错误 #682的修复,深刻理解POSITION和SV_POSITION_
DirectX12(D3D12)基础教程(外篇四)——用Assimp载入模型基础操作(无渲染纯命令行版)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【1】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【2】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【3】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【4】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【5】)
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【6】)_
DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(上)
DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(中)
DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(下)
DirectX12(D3D12)基础教程(十九)—— 多实例渲染
DirectX12(D3D12)基础教程(二十)—— 纹理数组(Texture Array)非DDS初始化操作
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(1/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(2/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(3/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(4/5)
DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(5/5)