Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

时间:2022-11-10 07:23:25

原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

代码工程地址:

https://github.com/jiabaodan/Direct12BookReadingNotes



学习目标

  1. 学习什么是立方体贴图,并且如何在HLSL中对它们采样;
  2. 如何使用DX的纹理工具创建立方体贴图;
  3. 学习如何用立方体贴图来模仿反射;
  4. 学习如何使用立方体贴图对球体采样来模拟一个天空和远处的山。


1 立方体纹理映射

在Direct3D中,立方体纹理是使用一组具有6个元素的纹理数组:

1、索引0代表指向+X面;

2、索引1代表指向-X面;

3、索引2代表指向+Y面;

4、索引3代表指向-Y面;

5、索引4代表指向+Z面;

6、索引5代表指向-Z面;

相对于2D纹理,我们不能再用2D纹理坐标来采样,需要使用3D纹理坐标(代表看向的方向)来采样。在第九章中介绍的纹理滤波器对于立方体纹理依然适用。

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

对于立方体纹理采样,查找向量的长度是不重要的,只有方向重要;如果两个具有相同方向但是不同长度的向量,采样出来的结果是一致的。

在HLSL中立方体纹理是TextureCube类型,下面的代码段展示了如何采样:

TextureCube gCubeMap;
SamplerState gsamLinearWrap : register(s2);

// in pixel shader
float3 v = float3(x,y,z); // some lookup vector

查找向量应该和立方体纹理关联的坐标系是一致的,否则采样结果会不正确。



2 环境贴图

对于立方体贴图最主要的应用就是环境纹理映射(environment mapping)。它的思路就是将相机固定在一个位置,然后朝6个方向分别拍摄图片,然后组成立方体纹理来模拟环境:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

根据上面的描述,我们需要对场景中的每个物体创建用以环境纹理采样的环境贴图,这样会更准确,但是也需要更多的纹理内存。有一种折中的办法是,在场景中几个重要的点上创建环境纹理,然后物体从最近的环境纹理上进行采样;这个方法在应用中效果不错,因为对于曲面物体,不正确的采样很难让玩家察觉。还有一种简化的方法是省略场景中一些特定的物品:比如只拍摄远处的山和天空,近处的物体直接省略掉。如果要拍摄近处的物体,我们就需要使用Direct3D来渲染6个图片,这个在本章第五节中讲解。

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

如果相机向下拍摄的图片是是在世界坐标的轴,那么这个环境贴图我们就说是与世界坐标系相关。

因为立方体贴图只是保存了纹理数据,所以它可以让艺术家提前制作,而不需要在D3D中实时渲染。对于户外环境,可以使用软件Terragen(http://www.planetside.co.uk/)来生成。

如果你尝试使用Terragen,你需要到摄像机设置中,设置缩放因子为1.0来达到一个90度的视野。同时设置输出图片的维度要相等,这样水平和竖直方向的视野就都是相等的90度。

(https://developer.valvesoftware.com/wiki/Skybox_(2D)_with_Terragen)中有一个很好用的Terragen脚本,会使用当前摄像机位置,渲染6个立方体贴图使用的纹理。

DDS纹理贴图格式可以支持立方体贴图,并且我们可以使用texassemble工具通过6个图像来创建立方体贴图。下面是使用texassemble创建的一个例子:

texassemble -cube -w 256 -h 256 -o cubemap.dds
lobbyxposjpg lobbyxneg.jpg lobbyypos.jpg
lobbyyneg.jpg lobbyzpos.jpg lobbyzneg.jpg

NVIDIA提供了一个PS的保存.DDS格式立方体贴图的插件:https://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop


2.1 在D3D中加载和使用立方体贴图

我们的DDS纹理加载代码(DDSTextureLoader.h/.cpp)已经支持的对立方体贴图的加载。加载代码会检测出包含的立方体贴图,然后创建纹理数组并加载它们:

auto skyTex = std::make_unique<Texture>();
skyTex->Name = "skyTex";
skyTex->Filename = L"Textures/grasscube1024.dds";
ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(md3dDevice.mCommandList.Get(), skyTex->Filename.c_str(),
skyTex->Resource, skyTex->UploadHeap));

在SRV中使用D3D12_SRV_DIMENSION_TEXTURECUBE维度和TextureCube属性:

D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping =
D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MostDetailedMip = 0;
srvDesc.TextureCube.MipLevels = skyTex->GetDesc().MipLevels;
srvDesc.TextureCube.ResourceMinLODClamp = 0.0f;
srvDesc.Format = skyTex->GetDesc().Format;
md3dDevice->CreateShaderResourceView(skyTex.Get(), &srvDesc, hDescriptor);


3 纹理映射一个天空

创建一个大的球体然后映射一个环境贴图:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

我们假设天空球是无限远的,并且把它在世界坐标系下的位置设置为何摄像机一致。

着色器文件代码如下:

//*********************************************************************
// Sky.hlsl by Frank Luna (C) 2015 All Rights Reserved.
//********************************************************************* // Include common HLSL code.
#include "Common.hlsl" struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
}; struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosL : POSITION;
}; VertexOut VS(VertexIn vin)
{
VertexOut vout; // Use local vertex position as cubemap lookup vector.
vout.PosL = vin.PosL; // Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld); // Always center sky about camera.
posW.xyz += gEyePosW; // Set z = w so that z/w = 1 (i.e., skydome always on far plane).
vout.PosH = mul(posW, gViewProj).xyww; return vout;
} float4 PS(VertexOut pin) : SV_Target
{
return gCubeMap.Sample(gsamLinearWrap, pin.PosL);
}

绘制天空的着色器程序明显和我们绘制物体的着色器程序不同,但是他们分享了相同的根签名,所以我们不需要切换根签名。下面的代码在Default.hlsl和Sky.hlsl是一样的,所以直接移到了Common.hlsl里面,来防止代码重复:

//****************************************************************************
// Common.hlsl by Frank Luna (C) 2015 All Rights Reserved.
//**************************************************************************** // Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif // Include structures and functions for lighting.
#include "LightingUtil.hlsl" struct MaterialData
{
float4 DiffuseAlbedo;
float3 FresnelR0;
float Roughness;
float4x4 MatTransform;
uint DiffuseMapIndex;
uint MatPad0;
uint MatPad1;
uint MatPad2;
}; TextureCube gCubeMap : register(t0); // An array of textures, which is only supported in shader model 5.1+. Unlike
// Texture2DArray, the textures in this array can be different sizes and
// formats, making it more flexible than texture arrays.
Texture2D gDiffuseMap[4] : register(t1); // Put in space1, so the texture array does not overlap with these resources.
// The texture array will occupy registers t0, t1, …, t3 in space0.
StructuredBuffer<MaterialData> gMaterialData : register(t0, space1); SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5); // Constant data that varies per frame.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform;
uint gMaterialIndex;
uint gObjPad0;
uint gObjPad1;
uint gObjPad2;
}; // Constant data that varies per material.
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
float4 gAmbientLight; // Indices [0, NUM_DIR_LIGHTS) are directional lights;
// indices [NUM_DIR_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights;
// indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS,
// NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS)
// are spot lights for a maximum of MaxLights per object.
Light gLights[MaxLights];
};

在以前,应用通常优先绘制天空,然后使用它替换掉渲染目标,和深度/模板缓冲。但是“ATI Radeon HD 2000 Programming Guide”反对这个做法,原因有下:第一,深度/模板缓冲会为了内部硬件优化被明确的清空掉,对于渲染目标也是一样的;第二,因为大部分天空都是被其他物体比如建筑和地形遮挡的,所以如果我们先绘制天空,会导致很多像素需要重新绘制,这样很浪费性能。所以现在推荐最后再清空和绘制天空。

绘制天空需要不同的着色器程序和PSO。所以我们把天空放置在不同的层来绘制:

// Draw opaque render-items.
mCommandList->SetPipelineState(mPSOs["opaque"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]); // Draw the sky render-item.
mCommandList->SetPipelineState(mPSOs["sky"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Sky]);

另外,渲染天空需要一些不同的渲染设置:比如相机是在天空球内部,所以要关闭背面剔除(或者反向三角形),然后修改深度对比方程到LESS_EQUAL,这样天空将会通过深度测试:

D3D12_GRAPHICS_PIPELINE_STATE_DESC skyPsoDesc = opaquePsoDesc;

// The camera is inside the sky sphere, so just turn off culling.
skyPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; // Make sure the depth function is LESS_EQUAL and not just LESS.
// Otherwise, the normalized depth values at z = 1 (NDC) will
// fail the depth test if the depth buffer was cleared to 1.
skyPsoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
skyPsoDesc.pRootSignature = mRootSignature.Get(); skyPsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["skyVS"]->GetBufferPointer()),
mShaders["skyVS"]->GetBufferSize()
};
skyPsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["skyPS"]->GetBufferPointer()),
mShaders["skyPS"]->GetBufferSize()
}; ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(
&skyPsoDesc, IID_PPV_ARGS(&mPSOs["sky"])));


4 模拟反射

当我们在O点创建一个环境贴图的时候,我们实际上是记录了在O点所有照射过来的光的数据,所以贴图上的每个像素可以看做当前方向照射光照的强度。我们使用这个数据来模拟高光反射。考虑下图,从环境照射进来的方向光I和反射平面和进入眼睛的方向v = E − p。光是从查找方向r = reflect(−v, n)对环境贴图纹理采样的。这让平面产生一种镜面的感觉:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

我们逐像素计算反射向量,然后用它来对环境贴图进行采样:

const float shininess = 1.0f - roughness;

// Add in specular reflections.
float3 r = reflect(-toEyeW, pin.NormalW);
float4 reflectionColor = gCubeMap.Sample(gsamLinearWrap, r);
float3 fresnelFactor = SchlickFresnel(fresnelR0, pin.NormalW, r);
litColor.rgb += shininess * fresnelFactor * reflectionColor.rgb;

因为我们讨论的是反射,所以我们需要应用菲涅尔效果,我们通过材质的光泽度对反射的光照强度进行缩放–粗糙的材质反射的光线应该很小。

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

因为环境贴图采样时不关系位置,只关心方向,上图中两个反射向量的方向是相同的,所以他们采样出来的结果是一样的。但实际上正确的结果应该从不同位置采样的不同的结果。对于平滑的平面,这个问题更容易被玩家发觉,而对于曲面就不容易发觉,因为反射向量差别就比较大。

其中一种方法是对环境贴图关联一些代理几何体。比如,假设我们有一个四方的房间的环境贴图。如下图,如果包围盒关联的立方体贴图输入到着色器程序中,那么射线/盒子相交检测可以在像素着色器中进行,并且我们可以在像素着色器中进一步计算查找向量来进行立方体纹理采样:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

下面的函数展示了查找向量如何被计算:

float3 BoxCubeMapLookup(float3 rayOrigin, float3 unitRayDir,
float3 boxCenter, float3 boxExtents)
{
// Based on slab method as described in Real- Time Rendering
// 16.7.1 (3rd edition).
// Make relative to the box center.
float3 p = rayOrigin - boxCenter; // The ith slab ray/plane intersection formulas for AABB are:
//
// t1 = (-dot(n_i, p) + h_i)/dot(n_i, d) = (- p_i + h_i)/d_i
// t2 = (-dot(n_i, p) - h_i)/dot(n_i, d) = (- p_i - h_i)/d_i
// Vectorize and do ray/plane formulas for every slab together.
float3 t1 = (-p+boxExtents)/unitRayDir;
float3 t2 = (-p-boxExtents)/unitRayDir; // Find max for each coordinate. Because we assume the ray is inside
// the box, we only want the max intersection parameter.
float3 tmax = max(t1, t2); // Take minimum of all the tmax components:
float t = min(min(tmax.x, tmax.y), tmax.z); // This is relative to the box center so it can be used as a
// cube map lookup vector.
return p + t*unitRayDir;
}


5 动态立方体贴图

之前已经讨论过静态立方体贴图,它是提前制作完成的。动态立方体贴图就是指在运行时,每帧进行创建,这样就可以捕捉到场景中运动的物体:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十八章:立方体贴图

渲染动态立方体贴图非常占用性能,它需要进行6次渲染,生成6张纹理来组成立方体贴图。所以尽可能减少立方体贴图的数量:比如对场景中重要的物体只做动态反射,然后对不重要的物体使用静态立方体贴图,它们的小消失可能不会被发现。另外立方体贴图的尺寸应该尽可能减少,比如使用256x256,这样可以减少像素着色器的执行次数。


5.1 动态立方体贴图辅助类

为了辅助动态立方体贴图的创建,我们创建了CubeRenderTarget类:

class CubeRenderTarget
{
public:
CubeRenderTarget(ID3D12Device* device, UINT width, UINT height, DXGI_FORMAT format);
CubeRenderTarget(const CubeRenderTarget& rhs)=delete;
CubeRenderTarget& operator=(const CubeRenderTarget& rhs)=delete;
˜CubeRenderTarget()=default; ID3D12Resource* Resource(); CD3DX12_GPU_DESCRIPTOR_HANDLE Srv();
CD3DX12_CPU_DESCRIPTOR_HANDLE Rtv(int faceIndex);
D3D12_VIEWPORT Viewport()const;
D3D12_RECT ScissorRect()const; void BuildDescriptors(
CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuSrv,
CD3DX12_GPU_DESCRIPTOR_HANDLE hGpuSrv,
CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuRtv[6]); void OnResize(UINT newWidth, UINT newHeight); private:
void BuildDescriptors();
void BuildResource(); private:
ID3D12Device* md3dDevice = nullptr;
D3D12_VIEWPORT mViewport;
D3D12_RECT mScissorRect;
UINT mWidth = 0;
UINT mHeight = 0;
DXGI_FORMAT mFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
CD3DX12_CPU_DESCRIPTOR_HANDLE mhCpuSrv;
CD3DX12_GPU_DESCRIPTOR_HANDLE mhGpuSrv;
CD3DX12_CPU_DESCRIPTOR_HANDLE mhCpuRtv[6];
Microsoft::WRL::ComPtr<ID3D12Resource> mCubeMap = nullptr;
};

5.2 创建立方体贴图资源

创建立方体贴图纹理是通过创建一个具有6个元素的纹理数组,因为是用以立方体贴图,所以标签要设置为D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET,下面是创建的函数:

void CubeRenderTarget::BuildResource()
{
D3D12_RESOURCE_DESC texDesc;
ZeroMemory(&texDesc, sizeof(D3D12_RESOURCE_DESC));
texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
texDesc.Alignment = 0;
texDesc.Width = mWidth;
texDesc.Height = mHeight;
texDesc.DepthOrArraySize = 6;
texDesc.MipLevels = 1;
texDesc.Format = mFormat;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
texDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&texDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mCubeMap)));
}

5.3 额外的描述堆空间

渲染立方体贴图需要附加的6个针对每个面和1个深度/模板缓冲的RTV,所以我们需要重写D3DApp::CreateRtvAndDsvDescriptorHeaps并且申请额外的描述:

void DynamicCubeMapApp::CreateRtvAndDsvDescriptorHeaps()
{
// Add +6 RTV for cube render target.
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors = SwapChainBufferCount + 6;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0; ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc,
IID_PPV_ARGS(mRtvHeap.GetAddressOf()))); D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 2;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0; ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc,
IID_PPV_ARGS(mDsvHeap.GetAddressOf()))); mCubeDSV = CD3DX12_CPU_DESCRIPTOR_HANDLE(
mDsvHeap->GetCPUDescriptorHandleForHeapStart(),
1,
mDsvDescriptorSize);
}

另外我们需要一个额外的SRV,这样我们绑定立方体贴图到着色器的输入。

描述句柄传递到CubeRenderTarget::BuildDescriptors方法可以节省一个句柄的复制,并实际创建描述:

auto srvCpuStart = mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart();
auto srvGpuStart = mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart();
auto rtvCpuStart = mRtvHeap->GetCPUDescriptorHandleForHeapStart(); // Cubemap RTV goes after the swap chain descriptors.
int rtvOffset = SwapChainBufferCount;
CD3DX12_CPU_DESCRIPTOR_HANDLE cubeRtvHandles[6]; for(int i = 0; i < 6; ++i)
cubeRtvHandles[i] = CD3DX12_CPU_DESCRIPTOR_HANDLE(
rtvCpuStart, rtvOffset + i,
mRtvDescriptorSize); mDynamicCubeMap->BuildDescriptors(
CD3DX12_CPU_DESCRIPTOR_HANDLE(
srvCpuStart, mDynamicTexHeapIndex,
mCbvSrvDescriptorSize),
CD3DX12_GPU_DESCRIPTOR_HANDLE(
srvGpuStart, mDynamicTexHeapIndex,
mCbvSrvDescriptorSize),
cubeRtvHandles); void CubeRenderTarget::BuildDescriptors(CD3DX12_CPU_DESCRIPTOR_hCpuSrv,
CD3DX12_GPU_DESCRIPTOR_HANDLE hGpuSrv,
CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuRtv[6])
{
// Save references to the descriptors.
mhCpuSrv = hCpuSrv;
mhGpuSrv = hGpuSrv; for(int i = 0; i < 6; ++i)
mhCpuRtv[i] = hCpuRtv[i]; // Create the descriptors
BuildDescriptors();
}

5.4 创建描述

前一节,我们在堆上申请了描述的空间,并缓存引用到描述,但是没有真正为资源创建描述。现在我们为立方体贴图资源创建SRV,并且为每一个纹理元素创建SRV,这样我们可以一个接一个渲染面纹理:

void CubeRenderTarget::BuildDescriptors()
{
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = mFormat;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MostDetailedMip = 0;
srvDesc.TextureCube.MipLevels = 1;
srvDesc.TextureCube.ResourceMinLODClamp = 0.0f; // Create SRV to the entire cubemap resource.
md3dDevice->CreateShaderResourceView(mCubeMap.Get(), &srvDesc, mhCpuSrv); // Create RTV to each cube face.
for(int i = 0; i < 6; ++i)
{
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc;
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DARRAY;
rtvDesc.Format = mFormat;
rtvDesc.Texture2DArray.MipSlice = 0;
rtvDesc.Texture2DArray.PlaneSlice = 0; // Render target to ith element.
rtvDesc.Texture2DArray.FirstArraySlice = i; // Only view one element of the array.
rtvDesc.Texture2DArray.ArraySize = 1; // Create RTV to ith cubemap face.
md3dDevice->CreateRenderTargetView(mCubeMap.Get(), &rtvDesc, mhCpuRtv[i]);
}
}

5.5 创建深度缓冲

通常情况下,立方体贴图面的分辨率和主后置缓冲的不同。所以我们需要创建一个和立方体贴图面的分辨率一致的深度缓冲。因为我们一次只渲染一个面,所以只需要一个深度缓冲即可。创建深度缓冲和DSV的代码如下:

void DynamicCubeMapApp::BuildCubeDepthStencil()
{
// Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = CubeMapSize;
depthStencilDesc.Height = CubeMapSize;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mCubeDepthStencilBuffer.GetAddressOf()))); // Create descriptor to mip level 0 of entire resource using
// the format of the resource.
md3dDevice->CreateDepthStencilView(
mCubeDepthStencilBuffer.Get(), nullptr,
mCubeDSV); // Transition the resource from its initial state to be used as a depth buffer.
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(
mCubeDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_DEPTH_WRITE));
}

5.6 立方体贴图的视口和剪切框

因为和主后置缓冲分辨率不同,所以需要创建新的视口和剪切框:

CubeRenderTarget::CubeRenderTarget(ID3D12Device* device,
UINT width, UINT height,
DXGI_FORMAT format)
{
md3dDevice = device;
mWidth = width;
mHeight = height;
mFormat = format;
mViewport = { 0.0f, 0.0f, (float)width, (float)height, 0.0f, 1.0f };
mScissorRect = { 0, 0, width, height }; BuildResource();
} D3D12_VIEWPORT CubeRenderTarget::Viewport()const
{
return mViewport;
}
D3D12_RECT CubeRenderTarget::ScissorRect()const
{
return mScissorRect
}

5.7 设置立方体贴图的摄像机

为了方便,我们创建6个摄像机分别面向每个面,中心点位置在(x, y, z):

Camera mCubeMapCamera[6];

void DynamicCubeMapApp::BuildCubeFaceCamera(float x, float y, float z)
{
// Generate the cube map about the given position.
XMFLOAT3 center(x, y, z);
XMFLOAT3 worldUp(0.0f, 1.0f, 0.0f); // Look along each coordinate axis.
XMFLOAT3 targets[6] =
{
XMFLOAT3(x + 1.0f, y, z), // +X
XMFLOAT3(x - 1.0f, y, z), // -X
XMFLOAT3(x, y + 1.0f, z), // +Y
XMFLOAT3(x, y - 1.0f, z), // -Y
XMFLOAT3(x, y, z + 1.0f), // +Z
XMFLOAT3(x, y, z - 1.0f) // -Z
}; // Use world up vector (0,1,0) for all directions except +Y/-Y. In these cases, we
// are looking down +Y or -Y, so we need a different "up" vector.
XMFLOAT3 ups[6] =
{
XMFLOAT3(0.0f, 1.0f, 0.0f), // +X
XMFLOAT3(0.0f, 1.0f, 0.0f), // -X
XMFLOAT3(0.0f, 0.0f, -1.0f), // +Y
XMFLOAT3(0.0f, 0.0f, +1.0f), // -Y
}; for(int i = 0; i < 6; ++i)
{
mCubeMapCamera[i].LookAt(center, targets[i], ups[i]);
mCubeMapCamera[i].SetLens(0.5f*XM_PI, 1.0f, 0.1f, 1000.0f);
mCubeMapCamera[i].UpdateViewMatrix();
}
}

因为渲染每个面使用不同的摄像机,所以每个面拥有自己的PassConstants。我们可以在创建帧资源的时候增加6个PassConstants数量:

void DynamicCubeMapApp::BuildFrameResources()
{
for(int i = 0; i < gNumFrameResources; ++i)
{
mFrameResources.push_back(std::make_unique<FrameResource>
(md3dDevice.Get(),
7, (UINT)mAllRitems.size(),
(UINT)mMaterials.size()));
}
}

第0个是主渲染pass,后面对于立方体贴图的面。

我们实现下面的函数来为每个面设置常量数据:

void DynamicCubeMapApp::UpdateCubeMapFacePassCBs()
{
for(int i = 0; i < 6; ++i)
{
PassConstants cubeFacePassCB = mMainPassCB;
XMMATRIX view = mCubeMapCamera[i].GetView();
XMMATRIX proj = mCubeMapCamera[i].GetProj();
XMMATRIX viewProj = XMMatrixMultiply(view, proj);
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
XMMATRIX invProj = XMMatrixInverse(&XMMatrixDeterminant(proj), proj);
XMMATRIX invViewProj = XMMatrixInverse(&XMMatrixDeterminant(viewProj), viewProj);
XMStoreFloat4x4(&cubeFacePassCB.View, XMMatrixTranspose(view));
XMStoreFloat4x4(&cubeFacePassCB.InvView, XMMatrixTranspose(invView));
XMStoreFloat4x4(&cubeFacePassCB.Proj, XMMatrixTranspose(proj));
XMStoreFloat4x4(&cubeFacePassCB.InvProj, XMMatrixTranspose(invProj));
XMStoreFloat4x4(&cubeFacePassCB.ViewProj, XMMatrixTranspose(viewProj));
XMStoreFloat4x4(&cubeFacePassCB.InvViewProj, XMMatrixTranspose(invViewProj));
cubeFacePassCB.EyePosW = mCubeMapCamera[i].GetPosition3f();
cubeFacePassCB.RenderTargetSize = XMFLOAT2((float)CubeMapSize, (float)CubeMapSize);
cubeFacePassCB.InvRenderTargetSize = XMFLOAT2(1.0f / CubeMapSize, 1.0f / CubeMapSize);
auto currPassCB = mCurrFrameResource->PassCB.get(); // Cube map pass cbuffers are stored in elements 1-6.
currPassCB->CopyData(1 + i, cubeFacePassCB);
}
}

5.8 绘制到立方体贴图

Demo中层级关系:

{
Opaque = 0,
OpaqueDynamicReflectors,
Sky,
Count
};

OpaqueDynamicReflectors层包括了Demo中中间的那个球体(将使用动态立方体贴图)。我们第一步是绘制立方体贴图的6个面,但是不包括中间的球体;这代表我们只需要渲染Opaque 和Sky层到立方体贴图:

void DynamicCubeMapApp::DrawSceneToCubeMap()
{
mCommandList->RSSetViewports(1, &mDynamicCubeMap->Viewport());
mCommandList->RSSetScissorRects(1, &mDynamicCubeMap->ScissorRect()); // Change to RENDER_TARGET.
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(
mDynamicCubeMap->Resource(),
D3D12_RESOURCE_STATE_GENERIC_READ,
D3D12_RESOURCE_STATE_RENDER_TARGET)); UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants)); // For each cube map face.
for(int i = 0; i < 6; ++i)
{
// Clear the back buffer and depth buffer.
mCommandList->ClearRenderTargetView(
mDynamicCubeMap->Rtv(i),
Colors::LightSteelBlue, 0, nullptr);
mCommandList->ClearDepthStencilView(mCubeDSV,
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.0f, 0, 0, nullptr); // Specify the buffers we are going to render to.
mCommandList->OMSetRenderTargets(1, &mDynamicCubeMap->Rtv(i), true, &mCubeDSV); // Bind the pass constant buffer for this cube map face so we use
// the right view/proj matrix for this cube face.
auto passCB = mCurrFrameResource->PassCB->Resource();
D3D12_GPU_VIRTUAL_ADDRESS passCBAddress = passCB->GetGPUVirtualAddress() +
(1+i)*passCBByteSize;
mCommandList->SetGraphicsRootConstantBufferView(1, passCBAddress); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
mCommandList->SetPipelineState(mPSOs["sky"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Sky]);
mCommandList->SetPipelineState(mPSOs["opaque"].Get());
} // Change back to GENERIC_READ so we can read the texture in a shader.
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(
mDynamicCubeMap->Resource(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_GENERIC_READ));
}

绘制中心的球体:


DrawSceneToCubeMap(); // Set main render target settings.
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect); // Indicate a state transition on the resource usage.
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(
CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET)); // Clear the back buffer and depth buffer.
mCommandList->ClearRenderTargetView(CurrentBackBufferView(),
Colors::LightSteelBlue, 0, nullptr);
mCommandList->ClearDepthStencilView( DepthStencilView(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.0f, 0, 0, nullptr); // Specify the buffers we are going to render to.
mCommandList->OMSetRenderTargets(1,
&CurrentBackBufferView(), true,
&DepthStencilView()); auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(1,
passCB->GetGPUVirtualAddress()); // Use the dynamic cube map for the dynamic reflectors layer.
CD3DX12_GPU_DESCRIPTOR_HANDLE dynamicTexDescriptor(
mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
dynamicTexDescriptor.Offset(mSkyTexHeapIndex + 1, mCbvSrvDescriptorSize);
mCommandList->SetGraphicsRootDescriptorTable(3, dynamicTexDescriptor); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::OpaqueDynamicReflectors]); // Use the static "background" cube map for the other objects (including the sky)
mCommandList->SetGraphicsRootDescriptorTable(3, skyTexDescriptor);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
mCommandList->SetPipelineState(mPSOs["sky"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Sky]); // Indicate a state transition on the resource usage.
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(
CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT));



6 动态立方体贴图和几何着色器

在Direct3D 10里有一个例子“CubeMapGS”,基于几何着色器使用一个绘制调用就可以渲染立方体贴图。本节主要关注这个例子是如何执行的。虽然本小节展示的是DX10的代码,但是实现的策略是一致的,并且可以简单的移植到DX12。

首先创建整个纹理数组(不是每个单个的纹理)的RTV:

// Create the 6-face render target view
D3D10_RENDER_TARGET_VIEW_DESC DescRT;
DescRT.Format = dstex.Format;
DescRT.ViewDimension = D3D10_RTV_DIMENSION_TEXTURE2DARRAY;
DescRT.Texture2DArray.FirstArraySlice = 0;
DescRT.Texture2DArray.ArraySize = 6;
DescRT.Texture2DArray.MipSlice = 0;
V_RETURN( pd3dDevice->CreateRenderTargetView(g_pEnvMap, &DescRT, &g_pEnvMapRTV ) );

并且它需要一个立方体贴图的深度缓冲,整张深度缓冲的DSV创建代码如下:

// Create the depth stencil view for the entire cube
D3D10_DEPTH_STENCIL_VIEW_DESC DescDS;
DescDS.Format = DXGI_FORMAT_D32_FLOAT;
DescDS.ViewDimension = D3D10_DSV_DIMENSION_TEXTURE2DARRAY;
DescDS.Texture2DArray.FirstArraySlice = 0;
DescDS.Texture2DArray.ArraySize = 6;
DescDS.Texture2DArray.MipSlice = 0;
V_RETURN( pd3dDevice->CreateDepthStencilView(g_pEnvMapDepth, &DescDS, &g_pEnvMapDSV ) );

然后绑定RTV和DSV到渲染管线的OM阶段:

ID3D10RenderTargetView* aRTViews[ 1 ] = { g_pEnvMapRTV };
pd3dDevice->OMSetRenderTargets(sizeof(aRTViews)/sizeof(aRTViews[0]), aRTViews, g_pEnvMapDSV );

在常量缓冲中同时包含六个面的变换矩阵。几何着色器复制输入的三角形6次。并且通过设置一个系统值SV_RenderTargetArrayIndex来指定三角形到6个RTV中的一个。改系统值只能作为几何着色器的输出,来指定渲染目标数组的索引;并且只能是当RTV是数组的时候使用:

struct PS_CUBEMAP_IN
{
float4 Pos : SV_POSITION; // Projection coord
float2 Tex : TEXCOORD0; // Texture coord
uint RTIndex : SV_RenderTargetArrayIndex;
}; [maxvertexcount(18)]
void GS_CubeMap( triangle GS_CUBEMAP_IN input[3],
inout TriangleStream<PS_CUBEMAP_IN> CubeMapStream)
{
// For each triangle
for( int f = 0; f < 6; ++f )
{
// Compute screen coordinates
PS_CUBEMAP_IN output; // Assign the ith triangle to the ith render target.
output.RTIndex = f; // For each vertex in the triangle
for( int v = 0; v < 3; v++ )
{
// Transform to the view space of the ith cube face.
output.Pos = mul( input[v].Pos, // Transform to homogeneous clip space.
output.Pos = mul( output.Pos, mProj );
output.Tex = input[v].Tex;
CubeMapStream.Append( output );
} CubeMapStream.RestartStrip();
}
}

这样就可以实现在一个绘制调用总绘制完整的立方体贴图。

想要了解更多细节可以查看D3D10的“CubeMapGS”例子中的代码。

这个策略验证的多渲染目标和SV_RenderTargetArrayIndex系统值,但是它并不一定是最优解,有2个问题导致它并不那么吸引人:

1、它使用几何着色器输出大量数据,回顾第12章,输出大量数据对于几何着色器来说是不高效的,所以以这个目的使用几何着色器是会影响性能;

2、在一些特殊的场景,三角形并不会覆盖多余1个的立方体贴图的面;所以复制一个三角形并渲染到每个立方体面,其中6分之5的面都是浪费掉的(会被剔除掉)。虽然我们自己的Dmeo为了简化也是绘制整个场景到立方体的每个面。但是在实际应用中(非Demo),我们会使用截头锥体剔除,然后只渲染可见的立方体贴图面;物体级别的截头锥体剔除无法在几何着色器中实现。

但是在另一种情况下,需要渲染整个包围场景的网格的时候,这种策略会运行得非常好。比如说你有一个动态的天空系统,它会根据时间的变化,移动云彩和改变天空的颜色。因为天空是变化的,所以我们无法预先烘焙立方体贴图纹理,所以我们需要动态纹理贴图。因为它包围了整个场景,所以6个面都是可见的。几何着色器策略就不许要考虑上述第二点问题,并且减少绘制调用后可以提高性能。

Recent optimizations available in NVIDIA’s Maxwell architecture enables geometry targets with without the penalties of using a geometry shader (see

http://docs.nvidia.com/gameworks/content/gameworkslibrary/graphicssamples/opengl_which uses the Viewport Multicast and Fast Geometry Shader features). At the time exposed by Direct3D 12, but will probably be in a future update.



7 总结

  1. 一个立方体贴图包含6张纹理,看起来像个立方体。在D3D12我们使用ID3D12Resource接口来表示立方体贴图。在HLSL使用TextureCube类型表示。我们通过3D纹理坐标来确认立方体贴图中的像素;
  2. 环境贴图保存了在某一定点上周围6个截图,用以模拟天空或者环境;
  3. 立方体贴图可以使用texassemble工具,使用6张单独的纹理来生成;然后保存成DDS图像格式,因为它保存了6张纹理,所以会占用较大的内存,所以要应用DDS的压缩格式;
  4. 预先烘焙的立方体贴图不能捕捉到运动的物体,要解决这个问题就需要在运行时创建立方体贴图(动态立方体贴图)。它非常占用性能,所以应该应用到尽可能少的物体上;
  5. 我们可以绑定RTV到纹理数组到OM阶段,并且让每一个纹理数组View渲染到每一个纹理数组中的纹理(使用SV_RenderTargetArrayIndex系统值),可以将绘制调用降低到1。但是这种策略并不总是最优解,因为它不能应用截头锥体剔除。


8 练习