前言
本篇总结了环境贴图,立方体贴图,如何绘制天空纹理,如何模拟反射,如何实现动态立方体贴图
立方体贴图
-
定义:存储六个纹理,将他们分别看作立方体的六个面。此立方体的中心点位于某一坐标系的原点,且立方体对齐坐标系的主轴,也就是说立方体的每个面都对应于坐标系的某个主轴
-
在D3D中,立方体图被表示为一个由6个元素构成的纹理数组
- 索引0引用与\(+X\)相交的面
- 索引1引用与\(-X\)相交的面
- 索引2引用与\(+Y\)相交的面
- 索引3引用与\(-Y\)相交的面
- 索引4引用与\(+Z\)相交的面
- 索引5引用与\(-Z\)相交的面
-
与之前的2D纹理不同,寻找对应的纹素需要使用3D纹理坐标:与2D纹理查找不同的是,该方法定义了一个起点位于原点的查找向量v
如下图所示,图中的正方形是一个中心位于原点,且轴对齐与某坐标系主轴的立方体图,而v和立方体图相交处即为目标纹素
-
实现:
//HLSL TextureCube gCubeMap; SamplerState gsamLinearWrap : register(s2); … //PS float3 v = float3(x,y,z); // 查找向量 float4 color = gCubeMap.Sample(gsamLinearWrap, v);
-
创建立方体图
依然使用texassemble工具生成立方体图
texassemble -cube -w 256 -h 256 -o cubemap.dds lobbyxpos.jpg lobbyxneg.jpg lobbyypos.jpg lobbyyneg.jpg lobbyzpos.jpg lobbyzneg.jpg
实现:
-
加载纹理
auto skyTex = std::make_unique<Texture>(); skyTex->Name = “skyTex”; skyTex->Filename = L”Textures/grasscube1024.dds”; ThrowIfFailed( DirectX::CreateDDSTextureFromFile12(md3dDevice.Get(), mCommandList.Get(), skyTex->Filename.c_str(), skyTex->Resource, skyTex->UploadHeap) );
-
为立方体图纹理创建SRV
//创建描述符句柄堆 D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {}; srvHeapDesc.NumDescriptors = 5; srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&mSrvDescriptorHeap))); //句柄 CD3DX12_CPU_DESCRIPTOR_HANDLE hDescriptor(mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart()); //srv 描述符 D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; 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);
-
环境贴图
-
环境贴图(Environment Maps):将6张周围环境图存在立方体贴图中.该方法是立方体贴图的主要运用
-
看起来,我们需要为每一个启用环境贴图的物体都创建一个环境图,但不必浪费更多的内存。我们只需在场景中的关键处截取少量环境图,为每个物体采集场景中离他们最近的环境图
使用Terragen工具可以实现以视场角为90°渲染六张环境图,它可以较完美地还原照片级真实感的场景。不过需要注意的是,还需要将zoom(缩放因子)设为1.0,图像的输出宽高是相等的使得两个方向角为90°
绘制天空纹理
利用立方体贴图我们可以实现天空纹理。思路是:
- 围绕当前场景创建一个巨大的球体。该球体距离相机无限远
- 该球体始终以相机为中心,跟随相机运动而运动
- 将立方体贴图上的纹理映射到球面上。示意图如下:
实现:
-
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; // 使用局部空间的顶点坐标作为查找向量 vout.PosL = vin.PosL; float4 posW = mul(float4(vin.PosL, 1.0f), gWorld); // 天空球总是以相机为中心 posW.xyz += gEyePosW; // 让z = w,使得球面总是远于相机远平面 vout.PosH = mul(posW, gViewProj).xyww; return vout; } float4 PS(VertexOut pin) : SV_Target { return gCubeMap.Sample(gsamLinearWrap, pin.PosL); }
-
PSO
由于绘制天空球需要用另一个shader,因此PSO也需使用新的
//设置PSO D3D12_GRAPHICS_PIPELINE_STATE_DESC skyPsoDesc = opaquePsoDesc; // 球体远于远平面 skyPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; // 该深度测试函数为LESS_EQUAL而非LESS,否则zbuffer中都为1,这会导致z = 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"]))); //绘制 mCommandList->SetPipelineState(mPSOs["sky"].Get()); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Sky]);
以环境贴图模拟反射
我们可以在立方体贴图上存储环境光,也就是说可以将纹素看成光源,如此无需进行光照的数学计算,直接进行纹理贴图即可.在此我们将实现镜面反射
如下图所示,\(I\)为入射光线,\(E\)为观察点,\(v\)为观察向量\((E - p)\),\(r\)为反射向量\(reflect(-v, n)\),p点为反射点
实现:
//光泽度
const float shininess = 1.0f - roughness;
//镜面反射数据
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; //最终的光强
但目前还存在一个问题:向量只具有方向,不表示具体的位置信息,而光线具有方向和位置信息,我们需要的正是反射光线和环境贴图的交点
如下图所示,左右两束光线方向相同但位置不同,按照我们所想的是它们分别交于两个不同的纹素,但事实是这是因为采用方向向量,从\(E\)和\(E’\)两个方向观察,会发现这两点的纹素是相同的.对于曲面物体而言这不易察觉,但对于平滑的物体来说这很容易露陷
解决方案:为进行环境贴图的物体关联代理几何体(proxy geometry),这样就可以求取它的位置.比如下图,我们为立方体关联一个紧密包裹的AABB,再对它们进行相交检测,若确定有相交,即可求得对应的相交位置,这时查找向量
将不再是\(r\),而是\(p + t_0r\)(p点的选取和AABB的中心点相关)
该方法的实现:
float3 BoxCubeMapLookup(float3 rayOrigin, float3unitRayDir, float3 boxCenter, float3 boxExtents)
{
//射线位置和AABB的中心点有关
float3 p = rayOrigin - boxCenter;
//相交检测求得的参数t
float3 t1 = (-p+boxExtents)/unitRayDir;
float3 t2 = (-p-boxExtents)/unitRayDir;
//由于射线处于AABB内部,因此我们只要最大值
float3 tmax = max(t1, t2);
//当射线离开任一平面,就算是真正离开了AABB,因此需要求最小值min
float t = min(min(tmax.x, tmax.y), tmax.z);
return p + t*unitRayDir;
}
动态立方体图
-
静态环境贴图存在的问题:目前这种方法存储的是
根据相机位置预先绘制
好的图像,虽然这种方式开销小,但若贴图对象是一个会移动的对象,该方法必然是会失效的 -
解决之道:再运行时动态地构建立方体图,也就是说每一帧都需要将相机置于场景内,立方体图始终以它作为原点,沿着坐标轴3个轴6个方向将场景分六次逐个渲染到立方体图的对应面
-
需要注意的:该方法开销是比较大的,因此在渲染时尽量少使用动态立方体图,且使用低分辨率的贴图。如,只有场景中某些特殊物体需要使用动态反射时我们才使用该方法
-
实现
-
动态立方体图的辅助class
该class中封装立方体图的ID3D12Resource对象、该资源使用的描述符、渲染立方体图的其他相关数据
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; };
-
构建立方体图纹理
创建包含六个元素的纹理数组,想要渲染立方体图必须设立标志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))); }
-
描述符堆
为渲染立方体图,还需额外添加六个
RTV
,使之和立方体图各个面一一对应;一个深度/模板缓冲区
;一个SRV。这些描述符句柄都需传入BuildDescriptors函数void DynamicCubeMapApp::CreateRtvAndDsvDescriptorHeaps() { // 添加6个RTV 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()))); // 添加一个DSV 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); } auto srvCpuStart = mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart(); auto srvGpuStart = mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart(); auto rtvCpuStart = mRtvHeap->GetCPUDescriptorHandleForHeapStart(); // 位于交换链描述符后 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); //SRV句柄 mDynamicCubeMap->BuildDescriptors( CD3DX12_CPU_DESCRIPTOR_HANDLE(srvCpuStart, mDynamicTexHeapIndex, mCbvSrvUavDescriptorSize), CD3DX12_GPU_DESCRIPTOR_HANDLE(srvGpuStart, mDynamicTexHeapIndex, mCbvSrvUavDescriptorSize), cubeRtvHandles); void CubeRenderTarget::BuildDescriptors(CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuSrv, CD3DX12_GPU_DESCRIPTOR_HANDLE hGpuSrv, CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuRtv[6]) { // 保存描述符句柄副本 mhCpuSrv = hCpuSrv; mhGpuSrv = hGpuSrv; for(int i = 0; i < 6; ++i) mhCpuRtv[i] = hCpuRtv[i]; // 创建描述符 BuildDescriptors(); }
-
深度缓冲区
由于每次只渲染立方体一个面,因此对于立方体图的渲染只需一个深度缓冲区
void DynamicCubeMapApp::BuildCubeDepthStencil() { // 创建深度/模板缓冲区及其视图 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()))); // 以资源自身的格式为整个资源的mip 0级创建描述符 md3dDevice->CreateDepthStencilView(mCubeDepthStencilBuffer.Get(), nullptr, mCubeDSV); // 将资源状态转换到深度缓冲区 mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mCubeDepthStencilBuffer.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE)); }
-
立方体图的视口和裁剪矩阵
若立方体图和后台缓冲区的分辨率不一样,需要定义一个视口变换和裁剪矩阵
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, (int)width, (int)height }; BuildResource(); } D3D12_VIEWPORT CubeRenderTarget::Viewport()const { return mViewport; } D3D12_RECT CubeRenderTarget::ScissorRect()const { return mScissorRect; }
-
设置立方体图相机
以\((x,y,z)\)为中心生成六台相机,每一台负责其中一个面的图像截取工作,每台相机拥有它独自的PassConstants
void DynamicCubeMapApp::BuildCubeFaceCamera(float x, float y, float z) { //生成(x,y,z)位置的立方体图 XMFLOAT3 center(x, y, z); XMFLOAT3 worldUp(0.0f, 1.0f, 0.0f); // 沿每个坐标轴方向看去 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 }; // 除开+Y/-Y,其他方向的向上向量均用(0,1,0)表示 // 因为是往y轴看去,向上向量不可能依然使用(0,1,0) 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 XMFLOAT3(0.0f, 1.0f, 0.0f), // +Z XMFLOAT3(0.0f, 1.0f, 0.0f) // -Z }; 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(); } } //6个PassConstants void DynamicCubeMapApp::BuildFrameResources() { for(int i = 0; i < gNumFrameResources; ++i) { mFrameResources.push_back(std::make_unique<FrameResource>( md3dDevice.Get(), 7, //0:对应主渲染过程,1~6:六台相机 (UINT)mAllRitems.size(), (UINT)mMaterials.size())); } } //更新六个相机PassConstants 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(); currPassCB->CopyData(1 + i, cubeFacePassCB); } }
-
绘制立方体图
//三个渲染层 enum class RenderLayer : int { Opaque = 0, OpaqueDynamicReflectors, Sky, Count }; void DynamicCubeMapApp::DrawSceneToCubeMap() { mCommandList->RSSetViewports(1, &mDynamicCubeMap->Viewport()); mCommandList->RSSetScissorRects(1, &mDynamicCubeMap->ScissorRect()); // 将立方体图资源状态转换至 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(int i = 0; i < 6; ++i) { // 清空后台缓冲区和深度缓冲区 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); // 指定将要渲染的缓冲区 mCommandList->OMSetRenderTargets(1, &mDynamicCubeMap->Rtv(i), true, &mCubeDSV); // 为当前的立方体图(相机)绑定对应的渲染过程常量缓冲区 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()); } // 将立方体图资源状态转至GENERIC_READ,便于在shader中读取纹理数据 mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDynamicCubeMap->Resource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_GENERIC_READ)); } void DynamicCubeMapApp::Draw(const GameTimer& gt) { auto cmdListAlloc = mCurrFrameResource->CmdListAlloc; // Reuse the memory associated with command recording. // We can only reset when the associated command lists have finished execution on the GPU. ThrowIfFailed(cmdListAlloc->Reset()); // A command list can be reset after it has been added to the command queue via ExecuteCommandList. // Reusing the command list reuses memory. ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["opaque"].Get())); ID3D12DescriptorHeap* descriptorHeaps[] = { mSrvDescriptorHeap.Get() }; mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); mCommandList->SetGraphicsRootSignature(mRootSignature.Get()); // Bind all the materials used in this scene. For structured buffers, we can bypass the heap and // set as a root descriptor. auto matBuffer = mCurrFrameResource->MaterialBuffer->Resource(); mCommandList->SetGraphicsRootShaderResourceView(2, matBuffer->GetGPUVirtualAddress()); // Bind the sky cube map. For our demos, we just use one "world" cube map representing the environment // from far away, so all objects will use the same cube map and we only need to set it once per-frame. // If we wanted to use "local" cube maps, we would have to change them per-object, or dynamically // index into an array of cube maps. CD3DX12_GPU_DESCRIPTOR_HANDLE skyTexDescriptor(mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart()); skyTexDescriptor.Offset(mSkyTexHeapIndex, mCbvSrvUavDescriptorSize); mCommandList->SetGraphicsRootDescriptorTable(3, skyTexDescriptor); // Bind all the textures used in this scene. Observe // that we only have to specify the first descriptor in the table. // The root signature knows how many descriptors are expected in the table. mCommandList->SetGraphicsRootDescriptorTable(4, mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart()); DrawSceneToCubeMap(); mCommandList->RSSetViewports(1, &mScreenViewport); mCommandList->RSSetScissorRects(1, &mScissorRect); mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr); mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr); mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView()); auto passCB = mCurrFrameResource->PassCB->Resource(); mCommandList->SetGraphicsRootConstantBufferView(1, passCB->GetGPUVirtualAddress()); // 为OpaqueDynamicReflectors使用动态立方体贴图 CD3DX12_GPU_DESCRIPTOR_HANDLE dynamicTexDescriptor(mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart()); dynamicTexDescriptor.Offset(mSkyTexHeapIndex + 1, mCbvSrvUavDescriptorSize); mCommandList->SetGraphicsRootDescriptorTable(3, dynamicTexDescriptor); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::OpaqueDynamicReflectors]); // 为其它物体使用静态立方体贴图 mCommandList->SetGraphicsRootDescriptorTable(3, skyTexDescriptor); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]); mCommandList->SetPipelineState(mPSOs["sky"].Get()); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Sky]); mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); // Done recording commands. ThrowIfFailed(mCommandList->Close()); // Add the command list to the queue for execution. ID3D12CommandList* cmdsLists[] = { mCommandList.Get() }; mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists); // Swap the back and front buffers ThrowIfFailed(mSwapChain->Present(0, 0)); mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount; // Advance the fence value to mark commands up to this fence point. mCurrFrameResource->Fence = ++mCurrentFence; // Add an instruction to the command queue to set a new fence point. // Because we are on the GPU timeline, the new fence point won't be // set until the GPU finishes processing all the commands prior to this Signal(). mCommandQueue->Signal(mFence.Get(), mCurrentFence); }
-
使用GS绘制动态立方体贴图
在刚刚的绘制动态立方体贴图中,我们使用六个相机对场景绘制六次,依次渲染至每个立方体图上,但draw call是有开销的,本节将使用GS将场景绘制降为1次。主要步骤如下:
- 为整个纹理数组创建一个RTV,不再是6个
- 深度缓冲区增长为6个,每个对应一个面
- 将RTV和DSV绑定至输出合并阶段(OMSetRenderTargets())
- 依然需要6个观察矩阵,GS将输入的三角形赋值六次,并一次赋予一个渲染目标数组切片。设置系统值
SV_RenderTargetArrayIndex(仅用于设置GS输出的索引值,且只有当RTV为数组时才能使用)
即可将三角形赋予到渲染目标数组切片中
缺陷:
- 不适用于数据量大的数据集.当GS输出大量数据时,效率极低
- 一般来说,某个三角形不会和第二个立方体面重叠,也就是说,复制三角形并将其渲染至每个立方体面很浪费资源(因为6个面中有5个面的三角形会进行裁剪)
reference
Directx12 3D 游戏开发实战