-
Notifications
You must be signed in to change notification settings - Fork 411
Physically based rendering
Getting Started |
---|
In this lesson we learn the basics of Physically-Based Rendering (PBR) as supported by the DirectX Tool Kit.
A full discussion of Physically-Based Rendering (PBR) is beyond the scope of this lesson, so see the references at the end of the page. Instead I'll provide a short motivation of why PBR is useful. The first thing to acknowledge is that traditional computer graphics lighting algorithms are inspired hacks. They work well at providing many useful lighting clues, and have been inexpensive enough to compute on consumer level hardware for decades. These algorithms, however, all have drawbacks. For example, basically anything you render using Phong shading ends up looking like it's made of smooth plastic. Another challenge is that textures, models, and other assets that look great in some lighting conditions and lighting algorithms don't work at all when moved to a new engine or solution which makes it harder to reuse expensive artwork.
The proponents of PBR rendering have gone back to the foundational rendering equation and built new algorithms that in some way better mimic laws of physics (such as the law of conservation of energy). While there are many ways to formulate a PBR materials & lighting system, the industry has converged on a few workflows. DirectX Tool Kit implements the "Disney-style Roughness/Metalness" workflow as it's well-understood, has reasonably good tooling support, and is the one that was chosen for Khronos' glTF2 asset format and many modern game engines.
PBR rendering essentially requires HDR rendering as there's no physical process that clamps light into a 0 to 1 range. As such, be sure you have worked through the Using HDR rendering tutorial before this one.
Another important aspect of PBR is that real world lighting is not well modeled by trivial point, directional, or spot light sources. Area lighting or other global illumination systems are expensive and/or complex to implement in real-time systems, so for the purposes of DirectX Tool Kit's PBR implementation we make use of image-based lighting. Specifically the ambient lighting environment consists of two specially formulated cubemaps, in addition to direct lighting from up to 3 directional lights.
First create a new project using the instructions from the previous lessons: Using DeviceResources and Adding the DirectX Tool Kit which we will use for this lesson.
Save the files RenderTexture.h, RenderTexture.cpp, SunSubMixer_diffuseIBL.dds, and SunSubMixer_specularIBL.dds to your new project's folder. Using to the top menu and select Project / Add Existing Item.... Select "RenderTexture.h" and hit "OK". Repeat for the other files.
Add to the Game.h file to the #include
section:
#include "RenderTexture.h"
In the Game.h file, add the following variable to the bottom of the Game class's private declarations (right after the m_graphicsMemory
variable you already added as part of setup):
DirectX::SimpleMath::Matrix m_world;
DirectX::SimpleMath::Matrix m_view;
DirectX::SimpleMath::Matrix m_proj;
std::unique_ptr<DirectX::CommonStates> m_states;
std::unique_ptr<DirectX::GeometricPrimitive> m_shape;
std::unique_ptr<DirectX::PBREffect> m_effect;
Microsoft::WRL::ComPtr<ID3D12Resource> m_radiance;
Microsoft::WRL::ComPtr<ID3D12Resource> m_irradiance;
std::unique_ptr<DX::RenderTexture> m_hdrScene;
std::unique_ptr<DirectX::ToneMapPostProcess> m_toneMap;
std::unique_ptr<DirectX::DescriptorHeap> m_resourceDescriptors;
std::unique_ptr<DirectX::DescriptorHeap> m_renderDescriptors;
enum Descriptors
{
SceneTex,
RadianceIBL,
IrradianceIBL,
Count
};
enum RTDescriptors
{
HDRScene,
RTCount
};
In the Game.cpp file, modify the Game class constructor:
m_deviceResources = std::make_unique<DX::DeviceResources>();
m_deviceResources->RegisterDeviceNotify(this);
m_hdrScene = std::make_unique<DX::RenderTexture>(DXGI_FORMAT_R16G16B16A16_FLOAT);
XMVECTORF32 color;
color.v = XMColorSRGBToRGB(Colors::CornflowerBlue);
m_hdrScene->SetClearColor(color);
In Game.cpp, add to the TODO of CreateDeviceDependentResources after where you have created m_graphicsMemory
:
m_states = std::make_unique<CommonStates>(device);
m_shape = GeometricPrimitive::CreateSphere();
m_resourceDescriptors = std::make_unique<DescriptorHeap>(device,
Descriptors::Count);
m_renderDescriptors = std::make_unique<DescriptorHeap>(device,
D3D12_DESCRIPTOR_HEAP_TYPE_RTV,
D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
RTDescriptors::RTCount);
m_hdrScene->SetDevice(device,
m_resourceDescriptors->GetCpuHandle(Descriptors::SceneTex),
m_renderDescriptors->GetCpuHandle(RTDescriptors::HDRScene));
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
DXGI_FORMAT_UNKNOWN);
m_toneMap = std::make_unique<ToneMapPostProcess>(device,
rtState,
ToneMapPostProcess::Reinhard, ToneMapPostProcess::SRGB);
RenderTargetState hdrState(m_hdrScene->GetFormat(),
m_deviceResources->GetDepthBufferFormat());
{
EffectPipelineStateDescription pd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullCounterClockwise,
hdrState);
m_effect = std::make_unique<PBREffect>(device, EffectFlags::None, pd);
m_effect->EnableDefaultLighting();
}
ResourceUploadBatch resourceUpload(device);
resourceUpload.Begin();
m_shape->LoadStaticBuffers(device, resourceUpload);
// Image-based lighting cubemaps.
DX::ThrowIfFailed(
CreateDDSTextureFromFile(device, resourceUpload,
L"SunSubMixer_diffuseIBL.dds",
m_radiance.ReleaseAndGetAddressOf()), true);
CreateShaderResourceView(device, m_radiance.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::RadianceIBL));
DX::ThrowIfFailed(
CreateDDSTextureFromFile(device, resourceUpload,
L"SunSubMixer_specularIBL.dds",
m_irradiance.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_irradiance.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::IrradianceIBL), true);
auto uploadResourcesFinished = resourceUpload.End(
m_deviceResources->GetCommandQueue());
uploadResourcesFinished.wait();
auto desc = m_radiance->GetDesc();
auto radianceTex = m_resourceDescriptors->GetGpuHandle(Descriptors::RadianceIBL);
auto irradianceTex = m_resourceDescriptors->GetGpuHandle(Descriptors::IrradianceIBL);
m_effect->SetIBLTextures(radianceTex, desc.MipLevels, irradianceTex, m_states->AnisotropicClamp());
m_world = Matrix::Identity;
In Game.cpp, add to the TODO of CreateWindowSizeDependentResources:
auto size = m_deviceResources->GetOutputSize();
m_view = Matrix::CreateLookAt(Vector3(2.f, 2.f, 2.f),
Vector3::Zero, Vector3::UnitY);
m_proj = Matrix::CreatePerspectiveFieldOfView(XM_PI / 4.f,
float(size.right) / float(size.bottom), 0.1f, 10.f);
m_effect->SetView(m_view);
m_effect->SetProjection(m_proj);
m_hdrScene->SetWindow(size);
auto sceneTex = m_resourceDescriptors->GetGpuHandle(Descriptors::SceneTex);
m_toneMap->SetHDRSourceTexture(sceneTex);
In Game.cpp, add to the TODO of OnDeviceLost where you added m_graphicsMemory.reset()
:
m_states.reset();
m_shape.reset();
m_effect.reset();
m_radiance.Reset();
m_irradiance.Reset();
m_hdrScene->ReleaseDevice();
m_toneMap.reset();
m_resourceDescriptors.reset();
m_renderDescriptors.reset();
In Game.cpp, modify Clear as follows:
// Clear the views.
auto rtvDescriptor = m_renderDescriptors->GetCpuHandle(RTDescriptors::HDRScene);
auto dsvDescriptor = m_deviceResources->GetDepthStencilView();
commandList->OMSetRenderTargets(1, &rtvDescriptor, FALSE, &dsvDescriptor);
m_hdrScene->Clear(commandList);
commandList->ClearDepthStencilView(dsvDescriptor, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
...
In Game.cpp, modify Render as follows:
// Don't try to render anything before the first Update.
if (m_timer.GetFrameCount() == 0)
{
return;
}
// Prepare the command list to render a new frame.
m_deviceResources->Prepare();
auto commandList = m_deviceResources->GetCommandList();
m_hdrScene->BeginScene(commandList);
Clear();
// TODO: Add your rendering code here.
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap(), m_states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_effect->SetWorld(m_world);
m_effect->Apply(commandList);
m_shape->Draw(commandList);
m_hdrScene->EndScene(commandList);
auto rtvDescriptor = m_deviceResources->GetRenderTargetView();
commandList->OMSetRenderTargets(1, &rtvDescriptor, FALSE, nullptr);
m_toneMap->Process(commandList);
// Show the new frame.
m_deviceResources->Present();
m_graphicsMemory->Commit(m_deviceResources->GetCommandQueue());
In Game.cpp, add to the TODO of Update:
auto time = static_cast<float>(timer.GetTotalSeconds());
m_world = Matrix::CreateRotationY(cosf(time) * 2.f);
Build and run to see the sphere rendered with a reflective metal appearance:
While PBREffect does have a basic 'constant' shader, the real impact of PBR materials requires the use of texture maps. Save the files Sphere2Mat_baseColor.png, Sphere2Mat_normal.png, Sphere2Mat_occlusionRoughnessMetallic.png, and Sphere2Mat_emissive.png to your project directory and add them to your project.
In the Game.h file, add the following variable to the bottom of the Game class's private declarations:
Microsoft::WRL::ComPtr<ID3D12Resource> m_albedoMap;
Microsoft::WRL::ComPtr<ID3D12Resource> m_normalMap;
Microsoft::WRL::ComPtr<ID3D12Resource> m_rmaMap;
Microsoft::WRL::ComPtr<ID3D12Resource> m_emissiveMap;
In the Game.h file, modify the Descriptors enum:
enum Descriptors
{
SceneTex,
RadianceIBL,
IrradianceIBL,
AlbedoMap,
NormalMap,
RMAMap,
EmissiveMap,
Count
};
In Game.cpp, modify the effect creation in CreateDeviceDependentResources:
{
EffectPipelineStateDescription pd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullCounterClockwise,
hdrState);
m_effect = std::make_unique<PBREffect>(device,
EffectFlags::Texture | EffectFlags::Emissive, pd);
m_effect->EnableDefaultLighting();
}
In Game.cpp, add to the TODO of CreateDeviceDependentResources after resourceUpload.Begin
and before the resourceUpload.End
:
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload,
L"Sphere2Mat_baseColor.png",
m_albedoMap.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_albedoMap.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::AlbedoMap));
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload,
L"Sphere2Mat_normal.png",
m_normalMap.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_normalMap.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::NormalMap));
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload,
L"Sphere2Mat_occlusionRoughnessMetallic.png",
m_rmaMap.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_rmaMap.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::RMAMap));
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload,
L"Sphere2Mat_emissive.png",
m_emissiveMap.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_emissiveMap.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::EmissiveMap));
In Game.cpp, add to the TODO of CreateDeviceDependentResources:
m_effect->SetSurfaceTextures(
m_resourceDescriptors->GetGpuHandle(Descriptors::AlbedoMap),
m_resourceDescriptors->GetGpuHandle(Descriptors::NormalMap),
m_resourceDescriptors->GetGpuHandle(Descriptors::RMAMap),
m_states->AnisotropicClamp());
m_effect->SetEmissiveTexture(
m_resourceDescriptors->GetGpuHandle(Descriptors::EmissiveMap));
In Game.cpp, add to the TODO of OnDeviceLost:
m_albedoMap.Reset();
m_normalMap.Reset();
m_rmaMap.Reset();
m_emissiveMap.Reset();
Build and run to see the sphere rendered a more complex material.
The use of the emissive texture is optional and controlled by the EffectFlags::Emissive
flag. Any textured use of PBREffect requires albedo, normal, and roughness/metalness/ambient-occlusion maps.
PBR typically refers to the 'base colors' as albedo rather than the traditional-lighting texture name 'diffuse'.
DirectX Tool Kit supports "SDKMESH version 2", which is the venerable DirectX SDK sample mesh file format updated with PBR-style materials information. Follow the instructions from Rendering a model with these differences:
-
The meshconvert and DirectX SDK Samples Content Exporter utilities both support a
-sdkmesh2
command-line switch to export PBR materials information. -
You need an HDR render setup per Using HDR rendering.
-
Make use of PBREffectFactory instead of
EffectFactory
which will createPBREffect
orSkinnedPBREffect
instances. -
Be sure to set the IBL textures before rendering:
auto radianceTex = m_resourceDescriptors->GetGpuHandle(Descriptors::RadianceIBL);
auto diffuseDesc = m_radiance->GetDesc();
auto irradianceTex = m_resourceDescriptors->GetGpuHandle(Descriptors::IrradianceIBL);
for (auto& it : modelNormal)
{
auto pbr = dynamic_cast<PBREffect*>(it.get());
if (pbr)
{
pbr->SetIBLTextures(radianceTex,
diffuseDesc.MipLevels, irradianceTex,
m_states->AnisotropicClamp());
}
}
-
See the SimplePBR12 sample for UWP / Xbox / Microsoft GDKfor more details on the shader implementation used by the tool kit.
-
PBREffect supports GPU Instancing.
Next lessons: Game controller input, Using the SimpleMath library, Adding the DirectX Tool Kit for Audio
DirectX Tool Kit docs PBREffect
Physically-Based Rendering wikipedia
Basic Theory of Physically-Based Rendering
Burley et al. "Physically-Based Shading at Disney", SIGGRAPH 2012 Course: Practical Physically Based Shading in Film and Game Production. Slides
All content and source code for this package are subject to the terms of the MIT License.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
- Universal Windows Platform apps
- Windows desktop apps
- Windows 11
- Windows 10
- Xbox One
- Xbox Series X|S
- x86
- x64
- ARM64
- Visual Studio 2022
- Visual Studio 2019 (16.11)
- clang/LLVM v12 - v18
- MinGW 12.2, 13.2
- CMake 3.20