Skip to content

Using advanced shaders

Chuck Walbourn edited this page Apr 26, 2022 · 34 revisions
Getting Started

In this lesson we learn about additional built-in shader types and some of their uses.

Setup

First create a new project using the instructions from the earlier lessons: Using DeviceResources and Adding the DirectX Tool Kit which we will use for this lesson.

Environment mapping

Environment mapping (also known as reflection mapping) is a common technique for adding reflections of the surrounding environment to 3D rendered materials using a cubemap.

Start by saving wood.dds and cubemap.dds into your new project's directory, and then from the top menu select Project / Add Existing Item.... Select "wood.dds" and click "OK". Repeat for "cubemap.dds"

In the Game.h file, add the following variables 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::EnvironmentMapEffect> m_effect;
std::unique_ptr<DirectX::DescriptorHeap> m_resourceDescriptors;

Microsoft::WRL::ComPtr<ID3D12Resource> m_texture;
Microsoft::WRL::ComPtr<ID3D12Resource> m_cubemap;

enum Descriptors
{
    Wood,
    EnvMap,
    Count
};

In Game.cpp, add to the TODO of CreateDeviceDependentResources after where you have created m_graphicsMemory:

RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
    m_deviceResources->GetDepthBufferFormat());

m_resourceDescriptors = std::make_unique<DescriptorHeap>(device,
    Descriptors::Count);

ResourceUploadBatch resourceUpload(device);

resourceUpload.Begin();

DX::ThrowIfFailed(
    CreateDDSTextureFromFile(device, resourceUpload, L"wood.dds",
        m_texture.ReleaseAndGetAddressOf(), false));

CreateShaderResourceView(device, m_texture.Get(),
    m_resourceDescriptors->GetCpuHandle(Descriptors::Wood));

bool isCubeMap = false;
DX::ThrowIfFailed(
    CreateDDSTextureFromFile(device, resourceUpload, L"cubemap.dds",
        m_cubemap.ReleaseAndGetAddressOf(), false, 0, nullptr, &isCubeMap));

CreateShaderResourceView(device, m_cubemap.Get(),
    m_resourceDescriptors->GetCpuHandle(Descriptors::EnvMap), isCubeMap);

m_states = std::make_unique<CommonStates>(device);

EffectPipelineStateDescription pd(
    &GeometricPrimitive::VertexType::InputLayout,
    CommonStates::Opaque,
    CommonStates::DepthDefault,
    CommonStates::CullNone,
    rtState);

m_effect = std::make_unique<EnvironmentMapEffect>(device,
    EffectFlags::Lighting | EffectFlags::Fresnel, pd);
m_effect->EnableDefaultLighting();

m_effect->SetTexture(m_resourceDescriptors->GetGpuHandle(Descriptors::Wood),
    m_states->LinearWrap());
m_effect->SetEnvironmentMap(m_resourceDescriptors->GetGpuHandle(Descriptors::EnvMap),
    m_states->LinearWrap());

m_shape = GeometricPrimitive::CreateTeapot();

m_world = Matrix::Identity;

auto uploadResourcesFinished = resourceUpload.End(
    m_deviceResources->GetCommandQueue());

uploadResourcesFinished.wait();

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);

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_texture.Reset();
m_cubemap.Reset();
m_resourceDescriptors.reset();

In Game.cpp, add to the TODO of Render:

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);

In Game.cpp, add to the TODO of Update:

auto time = static_cast<float>(timer.GetTotalSeconds());

m_world = Matrix::CreateRotationZ(cosf(time) * 2.f);

Build and run to see the teapot rendered with a 'glossy' material.

Screenshot of teapot

In Game.cpp add the following to the TODO section of Update:

m_effect->SetFresnelFactor(cosf(time * 2.f));

Build and run to see the effect of animating the Fresnel factor.

Screenshot of teapot

Technical Notes

The EnvironmentMapEffect shader is computing a 3D reflection vector from the eye direction determined from the view matrix and the normal vector of the pixel. The result is used to index into the cubemap. This results in the 'mirror-like' appearance of the material.

For dynamic environments, you can generate the cubemap at runtime by rendering the scene using six camera directions around a sample-point and capturing the result in a low-resolution cubemap. Since this requires additional rendering passes, this is typically done occasionally rather than every frame.

In versions of the tookit before August 2020, the creation of the environment map effect would be m_effect = std::make_unique<EnvironmentMapEffect>(device, EffectFlags::Lighting, pd); or m_effect = std::make_unique<EnvironmentMapEffect>(device, EffectFlags::Lighting, pd, true);. In current versions, the defaulted bool parameters have been replaced with EffectFlags::Fresnel and EffectFlags::Specular.

Normal mapping

NormalMapEffect is similar to the BasicEffect with the addition of a normal texture map and an optional specular texture map.

Starting with the project from the exercise above, download normalMap.dds into your project's directory, and then from the top menu select Project / Add Existing Item.... Select "normalMap.dds" and click "OK".

In the Game.h file, add the following variables to the bottom of the Game class's private declarations:

Microsoft::WRL::ComPtr<ID3D12Resource> m_normalTexture;

Modify the Descriptors enum:

enum Descriptors : size_t
{
    Wood,
    EnvMap,
    NormalMap,
    Count
};

Also change the definition of m_effect:

std::unique_ptr<DirectX::NormalMapEffect> m_effect;

In Game.cpp, add to the TODO of CreateDeviceDependentResources with the other texture loading:

DX::ThrowIfFailed(
    CreateDDSTextureFromFile(device, resourceUpload, L"normalMap.dds",
        m_normalTexture.ReleaseAndGetAddressOf(), false));

CreateShaderResourceView(device, m_normalTexture.Get(),
    m_resourceDescriptors->GetCpuHandle(Descriptors::NormalMap));

And change the creation of the effect:

m_effect = std::make_unique<NormalMapEffect>(device, EffectFlags::None, pd);

m_effect->SetNormalTexture(
    m_resourceDescriptors->GetGpuHandle(Descriptors::NormalMap));

You'll also need to comment out the calls to SetEnvironmentMap and SetFresnelFactor.

In Game.cpp, add to the TODO of OnDeviceLost:

m_normalTexture.Reset();

Build and run to see the normal mapped effect rendering a bumpy teapot:

Screenshot of teapot

Hemisphere lighting

DebugEffect is intended primarily to visualize normals/tangents, but it also supports hemispherical ambient lighting. This basic form of lighting is useful for seeing 3D shapes, as well as providing a gradient color for skydomes, etc.

Once again, we start with the project from the previous exercises.

In the Game.h file, change the definition of m_effect:

std::unique_ptr<DirectX::DebugEffect> m_effect;

In Game.cpp, change the creation of the effect:

m_effect = std::make_unique<DebugEffect>(device);

m_effect->SetHemisphericalAmbientColor(Colors::DarkBlue, Colors::Purple);

You'll also need to comment out the calls to EnableDefaultLighting, SetTexture, SetNormalTexture, SetEnvironmentMap and SetFresnelFactor.

Build and run to see the debug effect rendering a blue/purple gradient teapot:

Screenshot of teapot

More to explore

  • The EnvironmentMapEffect also supports spherical environment maps (a DirectX 9 feature) and dual-parabolic environment maps.

  • PBREffect is a Disney-style Physically-based rendering effect which uses albedo maps, normal map, and roughness/metalness/ambient-occlusion map along with two cubemaps for Image-Based Lighting.

  • DualTextureEffect is used to render a material with two textures applied. This requires the input layout to contain a second set of texture coordinates. This does not perform vertex or per-pixel lighting, as the second texture is most often a lightmap with statically computed lighting information. .SDKMESH and the Content Exporter support exporting light-mapped models which utilize this effect (see -lightmaps).

  • The AlphaTestEffect is used to perform pixel rejection based on an alpha reference value and function selection. It's primarily to implement techniques that relied on legacy Direct3D 9 alpha testing render state. This effect is independent of the depth/stencil tests set in D3D12_DEPTH_STENCIL_DESC.DepthFunc and StencilFunc.

DGSLEffect is not supported for DirectX Tool Kit for DirectX 12 since it's specific to Shader Model 4.0.

Next lessons: Multistream rendering and instancing

Further reading

DirectX Tool Kit docs Effects, EffectPipelineStateDescription, RenderTargetState

For Use

  • Universal Windows Platform apps
  • Windows desktop apps
  • Windows 11
  • Windows 10
  • Xbox One
  • Xbox Series X|S

Architecture

  • x86
  • x64
  • ARM64

For Development

  • Visual Studio 2022
  • Visual Studio 2019 (16.11)
  • clang/LLVM v12 - v18
  • MinGW 12.2, 13.2
  • CMake 3.20

Related Projects

DirectX Tool Kit for DirectX 11

DirectXMesh

DirectXTex

DirectXMath

Tools

Test Suite

Model Viewer

Content Exporter

DxCapsViewer

See also

DirectX Landing Page

Clone this wiki locally