1. 程式人生 > >Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十八章:立方體貼圖

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十八章:立方體貼圖

byte position use nts ike 幾何 epo memory 調用

原文: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紋理坐標(代表看向的方向)來采樣。在第九章中介紹的紋理濾波器對於立方體紋理依然適用。
技術分享圖片
對於立方體紋理采樣,查找向量的長度是不重要的,只有方向重要;如果兩個具有相同方向但是不同長度的向量,采樣出來的結果是一致的。

在HLSL中立方體紋理是TextureCube類型,下面的代碼段展示了如何采樣:

TextureCube gCubeMap;
SamplerState gsamLinearWrap : register(s2);
…
// in pixel shader
float3 v = float3(x,y,z); // some lookup vector

查找向量應該和立方體紋理關聯的坐標系是一致的,否則采樣結果會不正確。



2 環境貼圖

對於立方體貼圖最主要的應用就是環境紋理映射(environment mapping)。它的思路就是將相機固定在一個位置,然後朝6個方向分別拍攝圖片,然後組成立方體紋理來模擬環境:
技術分享圖片
根據上面的描述,我們需要對場景中的每個物體創建用以環境紋理采樣的環境貼圖,這樣會更準確,但是也需要更多的紋理內存。有一種折中的辦法是,在場景中幾個重要的點上創建環境紋理,然後物體從最近的環境紋理上進行采樣;這個方法在應用中效果不錯,因為對於曲面物體,不正確的采樣很難讓玩家察覺。還有一種簡化的方法是省略場景中一些特定的物品:比如只拍攝遠處的山和天空,近處的物體直接省略掉。如果要拍攝近處的物體,我們就需要使用Direct3D來渲染6個圖片,這個在本章第五節中講解。
技術分享圖片


如果相機向下拍攝的圖片是是在世界坐標的軸,那麽這個環境貼圖我們就說是與世界坐標系相關。

因為立方體貼圖只是保存了紋理數據,所以它可以讓藝術家提前制作,而不需要在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 紋理映射一個天空

創建一個大的球體然後映射一個環境貼圖:
技術分享圖片
我們假設天空球是無限遠的,並且把它在世界坐標系下的位置設置為何攝像機一致。
著色器文件代碼如下:

//*********************************************************************
// 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)對環境貼圖紋理采樣的。這讓平面產生一種鏡面的感覺:
技術分享圖片
我們逐像素計算反射向量,然後用它來對環境貼圖進行采樣:

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;

因為我們討論的是反射,所以我們需要應用菲涅爾效果,我們通過材質的光澤度對反射的光照強度進行縮放–粗糙的材質反射的光線應該很小。
技術分享圖片
因為環境貼圖采樣時不關系位置,只關心方向,上圖中兩個反射向量的方向是相同的,所以他們采樣出來的結果是一樣的。但實際上正確的結果應該從不同位置采樣的不同的結果。對於平滑的平面,這個問題更容易被玩家發覺,而對於曲面就不容易發覺,因為反射向量差別就比較大。
其中一種方法是對環境貼圖關聯一些代理幾何體。比如,假設我們有一個四方的房間的環境貼圖。如下圖,如果包圍盒關聯的立方體貼圖輸入到著色器程序中,那麽射線/盒子相交檢測可以在像素著色器中進行,並且我們可以在像素著色器中進一步計算查找向量來進行立方體紋理采樣:
技術分享圖片
下面的函數展示了查找向量如何被計算:

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 動態立方體貼圖

之前已經討論過靜態立方體貼圖,它是提前制作完成的。動態立方體貼圖就是指在運行時,每幀進行創建,這樣就可以捕捉到場景中運動的物體:
技術分享圖片
渲染動態立方體貼圖非常占用性能,它需要進行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 練習

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十八章:立方體貼圖