环境贴图

发布时间 2023-03-23 11:23:10作者: 爱莉希雅

前言

​ 本篇总结了环境贴图,立方体贴图,如何绘制天空纹理,如何模拟反射,如何实现动态立方体贴图

立方体贴图

  • 定义:存储六个纹理,将他们分别看作立方体的六个面。此立方体的中心点位于某一坐标系的原点,且立方体对齐坐标系的主轴,也就是说立方体的每个面都对应于坐标系的某个主轴

  • 在D3D中,立方体图被表示为一个由6个元素构成的纹理数组

    1. 索引0引用与\(+X\)相交的面
    2. 索引1引用与\(-X\)相交的面
    3. 索引2引用与\(+Y\)相交的面
    4. 索引3引用与\(-Y\)相交的面
    5. 索引4引用与\(+Z\)相交的面
    6. 索引5引用与\(-Z\)相交的面
  • 与之前的2D纹理不同,寻找对应的纹素需要使用3D纹理坐标:与2D纹理查找不同的是,该方法定义了一个起点位于原点的查找向量v

    如下图所示,图中的正方形是一个中心位于原点,且轴对齐与某坐标系主轴的立方体图,而v和立方体图相交处即为目标纹素
    image-20230321233158158

  • 实现:

    //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°
    image-20230322140522004

绘制天空纹理

利用立方体贴图我们可以实现天空纹理。思路是:

  1. 围绕当前场景创建一个巨大的球体。该球体距离相机无限远
  2. 该球体始终以相机为中心,跟随相机运动而运动
  3. 将立方体贴图上的纹理映射到球面上。示意图如下:
    image-20230322144507178

实现:

  • 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点为反射点
image-20230322152648532

实现:

//光泽度
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’\)两个方向观察,会发现这两点的纹素是相同的.对于曲面物体而言这不易察觉,但对于平滑的物体来说这很容易露陷
image-20230322171701416

解决方案:为进行环境贴图的物体关联代理几何体(proxy geometry),这样就可以求取它的位置.比如下图,我们为立方体关联一个紧密包裹的AABB,再对它们进行相交检测,若确定有相交,即可求得对应的相交位置,这时查找向量将不再是\(r\),而是\(p + t_0r\)(p点的选取和AABB的中心点相关)
image-20230322175031890

该方法的实现:

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次。主要步骤如下:

  1. 为整个纹理数组创建一个RTV,不再是6个
  2. 深度缓冲区增长为6个,每个对应一个面
  3. 将RTV和DSV绑定至输出合并阶段(OMSetRenderTargets())
  4. 依然需要6个观察矩阵,GS将输入的三角形赋值六次,并一次赋予一个渲染目标数组切片。设置系统值 SV_RenderTargetArrayIndex(仅用于设置GS输出的索引值,且只有当RTV为数组时才能使用)即可将三角形赋予到渲染目标数组切片中

​ 缺陷:

  1. 不适用于数据量大的数据集.当GS输出大量数据时,效率极低
  2. 一般来说,某个三角形不会和第二个立方体面重叠,也就是说,复制三角形并将其渲染至每个立方体面很浪费资源(因为6个面中有5个面的三角形会进行裁剪)

reference

Directx12 3D 游戏开发实战