BRE Architecture Series Part 9 – Skybox Pass

In this opportunity, we are going to talk about the skybox pass. Its previous pass is EnvironmentLightPass and its next pass is ToneMappingPass. You can check BRE Architecture Series Part 8 – Environment Light Pass and BRE Architecture Series Part 10 – Tone Mapping Pass and Post Process Pass. Before giving more details we are going to talk a little about skybox mapping.

What is a skybox?

The idea with the skybox technique is to have a big box encase the camera as it moves around. If the box appears to be the same distance even when the camera is moving, then we get a parallax effect – the illusion of it being relatively very far away. We do not rotate it as the camera rotates though – this allows us to pan around. If the texture has been well constructed then we will not notice the seams and it will have been projected onto the box so that it appears as if we are inside a sphere when we sample it with our view 3d vector. We can actually use a dome or a sphere instead of a box. The size of the chosen geometry is not going to affect how big it looks. If it is moving with the camera it will look exactly the same close up as far away. The size only matters if it intersects with the clip planes, in this case, you will see a plane cutting off part of your geometry. You can see a skybox in BRE in the following video

How do we implement skybox mapping in BRE?

In BRE, we have a type of command list recorder named SkyBoxCommandListRecorder that generates a command list related with skybox mapping and pushes it to the CommandListExecutor to be executed. This command list recorder needs an input texture which is the output texture of the EnvironmentLightPass. Also, we need the cube map texture that we will use for the skybox. Its output is consumed by the ToneMappingPass. Its implementation is the following

SkyBoxCommandListRecorder.h

#pragma once

#include <DirectXMath.h>

#include <CommandManager\CommandListPerFrame.h>
#include <MathUtils\MathUtils.h>
#include <ResourceManager\FrameUploadCBufferPerFrame.h>
#include <ResourceManager/VertexAndIndexBufferCreator.h>

namespace BRE {
struct FrameCBuffer;

///
/// @brief Responsible to generate command list recorders for the sky box pass
///
class SkyBoxCommandListRecorder {
public:
    SkyBoxCommandListRecorder() = default;
    ~SkyBoxCommandListRecorder() = default;
    SkyBoxCommandListRecorder(const SkyBoxCommandListRecorder&) = delete;
    const SkyBoxCommandListRecorder& operator=(const SkyBoxCommandListRecorder&) = delete;
    SkyBoxCommandListRecorder(SkyBoxCommandListRecorder&&) = delete;
    SkyBoxCommandListRecorder& operator=(SkyBoxCommandListRecorder&&) = delete;

    ///
    /// @brief Initializes pipeline state object and root signature
    ///
    /// This method must be called at the beginning of the application, and once
    ///
    static void InitSharedPSOAndRootSignature() noexcept;

    ///
    /// @brief Initializes the command list recorder
    ///
    /// InitSharedPSOAndRootSignature() must be called first
    ///
    /// @param vertexBufferData Vertex buffer data of the sky box
    /// @param indexBufferData Index buffer data of the sky box
    /// @param worldMatrix World matrix of the sky box
    /// @param skyBoxCubeMap Sky box cube map
    /// @param renderTargetView Render target view
    /// @param depthBufferView Depth buffer view
    ///
    void Init(const VertexAndIndexBufferCreator::VertexBufferData& vertexBufferData,
              const VertexAndIndexBufferCreator::IndexBufferData indexBufferData,
              const DirectX::XMFLOAT4X4& worldMatrix,
              ID3D12Resource& skyBoxCubeMap,
              const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView,
              const D3D12_CPU_DESCRIPTOR_HANDLE& depthBufferView) noexcept;

    ///
    /// @brief Records and pushes command lists to CommandListExecutor
    /// @param frameCBuffer Constant buffer per frame, for current frame
    ///
    void RecordAndPushCommandLists(const FrameCBuffer& frameCBuffer) noexcept;

    ///
    /// @brief Checks if internal data is valid. Typically, used for assertions
    /// @return True if valid. Otherwise, false
    ///
    bool IsDataValid() const noexcept;

private:
    ///
    /// @brief Initializes constant buffers
    /// @param worldMatrix World matrix
    ///
    void InitConstantBuffers(const DirectX::XMFLOAT4X4& worldMatrix) noexcept;

    ///
    /// @brief Initializes shader resource views
    /// @param skyBoxCubeMap Sky box cube map resource
    ///
    void InitShaderResourceViews(ID3D12Resource& skyBoxCubeMap) noexcept;

    CommandListPerFrame mCommandListPerFrame;

    VertexAndIndexBufferCreator::VertexBufferData mVertexBufferData;
    VertexAndIndexBufferCreator::IndexBufferData mIndexBufferData;

    FrameUploadCBufferPerFrame mFrameUploadCBufferPerFrame;

    UploadBuffer* mObjectUploadCBuffer{ nullptr };
    D3D12_GPU_DESCRIPTOR_HANDLE mObjectCBufferView;

    D3D12_GPU_DESCRIPTOR_HANDLE mStartPixelShaderResourceView;

    D3D12_CPU_DESCRIPTOR_HANDLE mRenderTargetView{ 0UL };
    D3D12_CPU_DESCRIPTOR_HANDLE mDepthBufferView{ 0UL };
};
}

SkyBoxCommandListRecorder.cpp

#include "SkyBoxCommandListRecorder.h"

#include <d3d12.h>

#include <CommandListExecutor\CommandListExecutor.h>
#include <DescriptorManager\CbvSrvUavDescriptorManager.h>
#include <DirectXManager\DirectXManager.h>
#include <PSOManager/PSOManager.h>
#include <ResourceManager/UploadBufferManager.h>
#include <RootSignatureManager\RootSignatureManager.h>
#include <ShaderManager\ShaderManager.h>
#include <ShaderUtils\CBuffers.h>
#include <Utils/DebugUtils.h>

using namespace DirectX;

namespace BRE {
// Root Signature:
// "DescriptorTable(CBV(b0), visibility = SHADER_VISIBILITY_VERTEX), " \ 0 -> Object CBuffers
// "CBV(b1, visibility = SHADER_VISIBILITY_VERTEX), " \ 1 > Frame CBuffer
// "DescriptorTable(SRV(t0), visibility = SHADER_VISIBILITY_PIXEL), " \ 2 -> Cube Map texture

namespace {
ID3D12PipelineState* sPSO{ nullptr };
ID3D12RootSignature* sRootSignature{ nullptr };
}

void
SkyBoxCommandListRecorder::InitSharedPSOAndRootSignature() noexcept
{
    BRE_ASSERT(sPSO == nullptr);
    BRE_ASSERT(sRootSignature == nullptr);

    PSOManager::PSOCreationData psoData{};

    // The camera is inside the sky sphere, so just turn off culling.
    psoData.mRasterizerDescriptor.CullMode = D3D12_CULL_MODE_NONE;

    // Make sure the depth function is LESS_EQUAL and not just GREATER.
    // Otherwise, the normalized depth values at z = 1 (NDC) will
    // fail the depth test if the depth buffer was cleared to 1.
    psoData.mDepthStencilDescriptor.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
    psoData.mInputLayoutDescriptors = D3DFactory::GetPositionNormalTangentTexCoordInputLayout();

    psoData.mPixelShaderBytecode = ShaderManager::LoadShaderFileAndGetBytecode("SkyBoxPass/Shaders/PS.cso");
    psoData.mVertexShaderBytecode = ShaderManager::LoadShaderFileAndGetBytecode("SkyBoxPass/Shaders/VS.cso");

    ID3DBlob* rootSignatureBlob = &ShaderManager::LoadShaderFileAndGetBlob("SkyBoxPass/Shaders/RS.cso");
    psoData.mRootSignature = &RootSignatureManager::CreateRootSignatureFromBlob(*rootSignatureBlob);
    sRootSignature = psoData.mRootSignature;

    psoData.mNumRenderTargets = 1U;
    psoData.mRenderTargetFormats[0U] = ApplicationSettings::sColorBufferFormat;
    for (std::size_t i = psoData.mNumRenderTargets; i < _countof(psoData.mRenderTargetFormats); ++i) {         psoData.mRenderTargetFormats[i] = DXGI_FORMAT_UNKNOWN;     }     psoData.mPrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;     sPSO = &PSOManager::CreateGraphicsPSO(psoData);     BRE_ASSERT(sPSO != nullptr);     BRE_ASSERT(sRootSignature != nullptr); } void SkyBoxCommandListRecorder::Init(const VertexAndIndexBufferCreator::VertexBufferData& vertexBufferData,                                 const VertexAndIndexBufferCreator::IndexBufferData indexBufferData,                                 const XMFLOAT4X4& worldMatrix,                                 ID3D12Resource& skyBoxCubeMap,                                 const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView,                                 const D3D12_CPU_DESCRIPTOR_HANDLE& depthBufferView) noexcept {     BRE_ASSERT(IsDataValid() == false);     mVertexBufferData = vertexBufferData;     mIndexBufferData = indexBufferData;     mRenderTargetView = renderTargetView;     mDepthBufferView = depthBufferView;     InitConstantBuffers(worldMatrix);     InitShaderResourceViews(skyBoxCubeMap);     BRE_ASSERT(IsDataValid()); } void SkyBoxCommandListRecorder::RecordAndPushCommandLists(const FrameCBuffer& frameCBuffer) noexcept {     BRE_ASSERT(IsDataValid());     BRE_ASSERT(sPSO != nullptr);     BRE_ASSERT(sRootSignature != nullptr);     // Update frame constants     UploadBuffer& uploadFrameCBuffer(mFrameUploadCBufferPerFrame.GetNextFrameCBuffer());     uploadFrameCBuffer.CopyData(0U, &frameCBuffer, sizeof(frameCBuffer));     ID3D12GraphicsCommandList& commandList = mCommandListPerFrame.ResetCommandListWithNextCommandAllocator(sPSO);     commandList.RSSetViewports(1U, &ApplicationSettings::sScreenViewport);     commandList.RSSetScissorRects(1U, &ApplicationSettings::sScissorRect);     commandList.OMSetRenderTargets(1U, &mRenderTargetView, false, &mDepthBufferView);     ID3D12DescriptorHeap* heaps[] = { &CbvSrvUavDescriptorManager::GetDescriptorHeap() };     commandList.SetDescriptorHeaps(_countof(heaps), heaps);     commandList.SetGraphicsRootSignature(sRootSignature);     D3D12_GPU_VIRTUAL_ADDRESS frameCBufferGpuVAddress(uploadFrameCBuffer.GetResource()->GetGPUVirtualAddress());
    commandList.SetGraphicsRootDescriptorTable(0U, mObjectCBufferView);
    commandList.SetGraphicsRootConstantBufferView(1U, frameCBufferGpuVAddress);
    commandList.SetGraphicsRootDescriptorTable(2U, mStartPixelShaderResourceView);

    commandList.IASetVertexBuffers(0U, 1U, &mVertexBufferData.mBufferView);
    commandList.IASetIndexBuffer(&mIndexBufferData.mBufferView);
    commandList.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    commandList.DrawIndexedInstanced(mIndexBufferData.mElementCount, 1U, 0U, 0U, 0U);

    commandList.Close();
    CommandListExecutor::Get().AddCommandList(commandList);
}

bool
SkyBoxCommandListRecorder::IsDataValid() const noexcept
{
    const bool result =
        mObjectUploadCBuffer != nullptr &&
        mObjectCBufferView.ptr != 0UL &&
        mStartPixelShaderResourceView.ptr != 0UL &&
        mRenderTargetView.ptr != 0UL &&
        mDepthBufferView.ptr != 0UL;

    return result;
}

void
SkyBoxCommandListRecorder::InitConstantBuffers(const XMFLOAT4X4& worldMatrix) noexcept
{
    BRE_ASSERT(mObjectUploadCBuffer == nullptr);

    // Create object cbuffer and fill it
    const std::size_t objCBufferElemSize{ UploadBuffer::GetRoundedConstantBufferSizeInBytes(sizeof(ObjectCBuffer)) };
    mObjectUploadCBuffer = &UploadBufferManager::CreateUploadBuffer(objCBufferElemSize, 1U);
    ObjectCBuffer objCBuffer;
    MathUtils::StoreTransposeMatrix(worldMatrix, objCBuffer.mWorldMatrix);
    mObjectUploadCBuffer->CopyData(0U, &objCBuffer, sizeof(objCBuffer));

    // Create object cbufferview
    D3D12_CONSTANT_BUFFER_VIEW_DESC cBufferDesc{};
    const D3D12_GPU_VIRTUAL_ADDRESS objCBufferGpuAddress{ mObjectUploadCBuffer->GetResource()->GetGPUVirtualAddress() };
    cBufferDesc.BufferLocation = objCBufferGpuAddress;
    cBufferDesc.SizeInBytes = static_cast<std::uint32_t>(objCBufferElemSize);
    mObjectCBufferView = CbvSrvUavDescriptorManager::CreateConstantBufferView(cBufferDesc);
}

void
SkyBoxCommandListRecorder::InitShaderResourceViews(ID3D12Resource& skyBoxCubeMap) noexcept
{
    D3D12_SHADER_RESOURCE_VIEW_DESC srvDescriptor{};
    srvDescriptor.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
    srvDescriptor.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
    srvDescriptor.TextureCube.MostDetailedMip = 0;
    srvDescriptor.TextureCube.MipLevels = skyBoxCubeMap.GetDesc().MipLevels;
    srvDescriptor.TextureCube.ResourceMinLODClamp = 0.0f;
    srvDescriptor.Format = skyBoxCubeMap.GetDesc().Format;
    mStartPixelShaderResourceView = CbvSrvUavDescriptorManager::CreateShaderResourceView(skyBoxCubeMap, srvDescriptor);
}
}

As you can see, we make sure the depth function is LESS_EQUAL and not just GREATER. Otherwise, the normalized depth values at z = 1 (NDC) will fail the depth test if the depth buffer was cleared to 1. Also, as the camera is inside the sky sphere we just turn off culling.

Regarding shaders implementation, we use vertex and pixel shaders. We send a sphere instead of a box, and in the vertex shader, we always center the sphere around the camera. Its implementation is the following

VertexShader

#include <ShaderUtils/CBuffers.hlsli>

#include "RS.hlsl"

struct Input {
    float3 mPositionObjectSpace : POSITION;
    float3 mNormalObjectSpace : NORMAL;
    float3 mTangentObjectSpace : TANGENT;
    float2 mUV : TEXCOORD;
};

ConstantBuffer<ObjectCBuffer> gObjCBuffer : register(b0);
ConstantBuffer<FrameCBuffer> gFrameCBuffer : register(b1);

struct Output {
    float4 mPositionClipSpace : SV_POSITION;
    float3 mPositionObjectSpace : POS_VIEW;
};

[RootSignature(RS)]
Output main(in const Input input)
{
    const float4x4 viewProjectionMatrix = mul(gFrameCBuffer.mViewMatrix,
                                              gFrameCBuffer.mProjectionMatrix);

    Output output;

    // Use local vertex position as cubemap lookup vector.
    output.mPositionObjectSpace = input.mPositionObjectSpace;

    // Always center sky about camera.
    float3 positionWorldSpace = mul(float4(input.mPositionObjectSpace, 1.0f),
                                    gObjCBuffer.mWorldMatrix).xyz;
    positionWorldSpace += gFrameCBuffer.mEyePositionWorldSpace.xyz;

    // Set z = w so that z/w = 1 (i.e., skydome always on far plane).
    output.mPositionClipSpace = mul(float4(positionWorldSpace, 1.0f),
                                    viewProjectionMatrix).xyww;

    return output;
}

In the pixel shader, we simply sample the cube map texture with the position of the fragment in object space. Its implementation is the following

PixelShader

#include "RS.hlsl"

//#define SKIP_SKYBOX 

struct Input {
    float4 mPositionClipSpace : SV_POSITION;
    float3 mPositionObjectSpace : POS_VIEW;
};

SamplerState TextureSampler : register (s0);
TextureCube CubeMapTexture : register(t0);

struct Output {
    float4 mColor : SV_Target0;
};

[RootSignature(RS)]
Output main(const in Input input)
{
    Output output = (Output)0;

#ifdef SKIP_SKYBOX
    output.mColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
#else
    output.mColor = CubeMapTexture.Sample(TextureSampler,
                                          input.mPositionObjectSpace);
#endif
    return output;
}

The pass responsible to create SkyBoxCommandListRecorder, to execute it and to set resource barriers is SkyBoxPass, and its implementation is the following

SkyBoxPass.h

#pragma once

#include <memory>

#include <CommandManager\CommandListPerFrame.h>
#include <SkyBoxPass\SkyBoxCommandListRecorder.h>

namespace BRE {
struct FrameCBuffer;

///
/// @brief Responsible of execute command lists to generate a sky box
///
class SkyBoxPass {
public:
    SkyBoxPass() = default;
    ~SkyBoxPass() = default;
    SkyBoxPass(const SkyBoxPass&) = delete;
    const SkyBoxPass& operator=(const SkyBoxPass&) = delete;
    SkyBoxPass(SkyBoxPass&&) = delete;
    SkyBoxPass& operator=(SkyBoxPass&&) = delete;

    ///
    /// @brief Initializes the sky box pass
    /// @param skyBoxCubeMap Sky box cube map resource
    /// @param depthBuffer Depth buffer resource
    /// @param renderTargetView Render target view
    /// @param Depth buffer view
    ///
    void Init(ID3D12Resource& skyBoxCubeMap,
              ID3D12Resource& depthBuffer,
              const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView,
              const D3D12_CPU_DESCRIPTOR_HANDLE& depthBufferView) noexcept;

    ///
    /// @brief Executes the pass
    /// @param frameCBuffer Constant buffer per frame, for current frame
    ///
    void Execute(const FrameCBuffer& frameCBuffer) noexcept;

private:
    ///
    /// @brief Checks if internal data is valid. Typically, used for assertions
    /// @return True if valid. Otherwise, false
    ///
    bool IsDataValid() const noexcept;

    ///
    /// @brief Execute begin task of skybox pass
    ///
    void ExecuteBeginTask() noexcept;

    std::unique_ptr<SkyBoxCommandListRecorder> mCommandListRecorder;

    ID3D12Resource* mDepthBuffer{ nullptr };

    CommandListPerFrame mBeginCommandListPerFrame;
};
}

SkyBoxPass.cpp

#include "SkyBoxPass.h"

#include <d3d12.h>

#include <CommandListExecutor/CommandListExecutor.h>
#include <CommandManager\CommandAllocatorManager.h>
#include <CommandManager\CommandListManager.h>
#include <DXUtils/d3dx12.h>
#include <ModelManager\Mesh.h>
#include <ModelManager\Model.h>
#include <ModelManager\ModelManager.h>
#include <ResourceStateManager\ResourceStateManager.h>
#include <SkyBoxPass\SkyBoxCommandListRecorder.h>
#include <ShaderUtils\CBuffers.h>
#include <Utils\DebugUtils.h>

using namespace DirectX;

namespace BRE {
namespace {
///
/// @brief Create command objects
/// @param commandAllocator Output command allocator
/// @param commandList Output command list
///
void
CreateCommandObjects(ID3D12CommandAllocator* &commandAllocator,
                     ID3D12GraphicsCommandList* &commandList) noexcept
{
    // Create command allocators and command list
    commandAllocator = &CommandAllocatorManager::CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT);
    commandList = &CommandListManager::CreateCommandList(D3D12_COMMAND_LIST_TYPE_DIRECT, *commandAllocator);
    commandList->Close();
}

///
/// @brief Creates and gets sky box sphere model
/// @param commandAllocator Command allocator to use with the command list
/// @param commandList Command list to execute the sphere model creation
/// @return Sphere model
///
Model&
CreateAndGetSkyBoxSphereModel(ID3D12CommandAllocator& commandAllocator,
                              ID3D12GraphicsCommandList& commandList)
{
    BRE_CHECK_HR(commandList.Reset(&commandAllocator, nullptr));

    Microsoft::WRL::ComPtr<ID3D12Resource> uploadVertexBuffer;
    Microsoft::WRL::ComPtr<ID3D12Resource> uploadIndexBuffer;
    Model* model = &ModelManager::CreateSphere(3000, 50, 50, commandList, uploadVertexBuffer, uploadIndexBuffer);

    commandList.Close();
    CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);

    return *model;
}
}

void
SkyBoxPass::Init(ID3D12Resource& skyBoxCubeMap,
                 ID3D12Resource& depthBuffer,
                 const D3D12_CPU_DESCRIPTOR_HANDLE& renderTargetView,
                 const D3D12_CPU_DESCRIPTOR_HANDLE& depthBufferView) noexcept
{
    BRE_ASSERT(IsDataValid() == false);

    mDepthBuffer = &depthBuffer;

    ID3D12CommandAllocator* commandAllocator;
    ID3D12GraphicsCommandList* commandList;
    CreateCommandObjects(commandAllocator, commandList);

    Model& model = CreateAndGetSkyBoxSphereModel(*commandAllocator, *commandList);
    const std::vector<Mesh>& meshes(model.GetMeshes());
    BRE_ASSERT(meshes.size() == 1UL);

    // Build world matrix
    const Mesh& mesh{ meshes[0] };
    XMFLOAT4X4 worldMatrix;
    MathUtils::ComputeMatrix(worldMatrix,
                             0.0f,
                             0.0f,
                             0.0f,
                             1.0f,
                             1.0f,
                             1.0f,
                             0.0f,
                             0.0f,
                             0.0f);

    SkyBoxCommandListRecorder::InitSharedPSOAndRootSignature();

    mCommandListRecorder.reset(new SkyBoxCommandListRecorder());
    mCommandListRecorder->Init(mesh.GetVertexBufferData(),
                               mesh.GetIndexBufferData(),
                               worldMatrix,
                               skyBoxCubeMap,
                               renderTargetView,
                               depthBufferView);

    BRE_ASSERT(IsDataValid());
}

void
SkyBoxPass::Execute(const FrameCBuffer& frameCBuffer) noexcept
{
    BRE_ASSERT(IsDataValid());

    ExecuteBeginTask();

    CommandListExecutor::Get().ResetExecutedCommandListCount();
    mCommandListRecorder->RecordAndPushCommandLists(frameCBuffer);

    // Wait until all previous tasks command lists are executed
    while (CommandListExecutor::Get().GetExecutedCommandListCount() < 1U) {         Sleep(0U);     } } bool SkyBoxPass::IsDataValid() const noexcept {     const bool b =         mCommandListRecorder.get() != nullptr &&         mDepthBuffer != nullptr;     return b; } void SkyBoxPass::ExecuteBeginTask() noexcept {     BRE_ASSERT(IsDataValid());     CD3DX12_RESOURCE_BARRIER barriers[1U];     std::uint32_t barrierCount = 0UL;     if (ResourceStateManager::GetResourceState(*mDepthBuffer) != D3D12_RESOURCE_STATE_DEPTH_WRITE) {         barriers[barrierCount] = ResourceStateManager::ChangeResourceStateAndGetBarrier(*mDepthBuffer,                                                                                         D3D12_RESOURCE_STATE_DEPTH_WRITE);         ++barrierCount;     }     if (barrierCount > 0UL) {
        ID3D12GraphicsCommandList& commandList = mBeginCommandListPerFrame.ResetCommandListWithNextCommandAllocator(nullptr);
        commandList.ResourceBarrier(barrierCount, barriers);
        BRE_CHECK_HR(commandList.Close());
        CommandListExecutor::Get().ExecuteCommandListAndWaitForCompletion(commandList);
    }
}
}

Future Work

  • Make this pass independent so it can be executed in parallel with geometry pass or environment light pass
Advertisements

2 thoughts on “BRE Architecture Series Part 9 – Skybox Pass

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s