Files
FlaxEngine/Source/Engine/Renderer/ShadowsPass.cpp
2022-06-13 00:40:32 +02:00

754 lines
31 KiB
C++

// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
#include "ShadowsPass.h"
#include "GBufferPass.h"
#include "VolumetricFogPass.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Content/Content.h"
#if USE_EDITOR
#include "Engine/Renderer/Lightmaps.h"
#endif
#define NormalOffsetScaleTweak 100.0f
#define SpotLight_NearPlane 10.0f
#define PointLight_NearPlane 10.0f
PACK_STRUCT(struct Data{
GBufferData GBuffer;
LightData Light;
LightShadowData LightShadow;
Matrix WVP;
Matrix ViewProjectionMatrix;
Float2 Dummy0;
float ContactShadowsDistance;
float ContactShadowsLength;
});
ShadowsPass::ShadowsPass()
: _shader(nullptr)
, _shadowMapsSizeCSM(0)
, _shadowMapsSizeCube(0)
, _shadowMapCSM(nullptr)
, _shadowMapCube(nullptr)
, _currentShadowMapsQuality((Quality)((int32)Quality::Ultra + 1))
, _sphereModel(nullptr)
, maxShadowsQuality(0)
{
}
uint64 ShadowsPass::GetShadowMapsMemoryUsage() const
{
uint64 result = 0;
if (_shadowMapCSM)
result += _shadowMapCSM->GetMemoryUsage();
if (_shadowMapCube)
result += _shadowMapCube->GetMemoryUsage();
return result;
}
String ShadowsPass::ToString() const
{
return TEXT("ShadowsPass");
}
bool ShadowsPass::Init()
{
// Create pipeline states
_psShadowDir.CreatePipelineStates();
_psShadowPoint.CreatePipelineStates();
_psShadowSpot.CreatePipelineStates();
// Load assets
_shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/Shadows"));
_sphereModel = Content::LoadAsyncInternal<Model>(TEXT("Engine/Models/SphereLowPoly"));
if (_shader == nullptr || _sphereModel == nullptr)
{
return true;
}
// Create shadow maps
_shadowMapCSM = GPUDevice::Instance->CreateTexture(TEXT("Shadow Map CSM"));
_shadowMapCube = GPUDevice::Instance->CreateTexture(TEXT("Shadow Map Cube"));
#if COMPILE_WITH_DEV_ENV
_shader.Get()->OnReloading.Bind<ShadowsPass, &ShadowsPass::OnShaderReloading>(this);
#endif
// If GPU doesn't support linear sampling for the shadow map then fallback to the single sample on lowest quality
const auto formatTexture = PixelFormatExtensions::FindShaderResourceFormat(SHADOW_MAPS_FORMAT, false);
const auto formatFeaturesDepth = GPUDevice::Instance->GetFormatFeatures(SHADOW_MAPS_FORMAT);
const auto formatFeaturesTexture = GPUDevice::Instance->GetFormatFeatures(formatTexture);
_supportsShadows = FORMAT_FEATURES_ARE_SUPPORTED(formatFeaturesDepth.Support, FormatSupport::DepthStencil | FormatSupport::Texture2D)
&& FORMAT_FEATURES_ARE_SUPPORTED(formatFeaturesTexture.Support, FormatSupport::ShaderSample | FormatSupport::ShaderSampleComparison);
if (!_supportsShadows)
{
LOG(Warning, "GPU doesn't support shadows rendering");
LOG(Warning, "Format: {0}, features support: {1}", (int32)SHADOW_MAPS_FORMAT, (uint32)formatFeaturesDepth.Support);
LOG(Warning, "Format: {0}, features support: {1}", (int32)formatTexture, (uint32)formatFeaturesTexture.Support);
}
return false;
}
bool ShadowsPass::setupResources()
{
// Wait for the assets
if (!_sphereModel->CanBeRendered() || !_shader->IsLoaded())
return true;
auto shader = _shader->GetShader();
// Validate shader constant buffers sizes
if (shader->GetCB(0)->GetSize() != sizeof(Data))
{
REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data);
return true;
}
// Create pipeline stages
GPUPipelineState::Description psDesc;
if (!_psShadowPoint.IsValid())
{
psDesc = GPUPipelineState::Description::DefaultNoDepth;
psDesc.CullMode = CullMode::TwoSided;
psDesc.VS = shader->GetVS("VS_Model");
if (_psShadowPoint.Create(psDesc, shader, "PS_PointLight"))
return true;
}
if (!_psShadowDir.IsValid())
{
psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
if (_psShadowDir.Create(psDesc, shader, "PS_DirLight"))
return true;
}
if (!_psShadowSpot.IsValid())
{
psDesc = GPUPipelineState::Description::DefaultNoDepth;
psDesc.CullMode = CullMode::TwoSided;
psDesc.VS = shader->GetVS("VS_Model");
if (_psShadowSpot.Create(psDesc, shader, "PS_SpotLight"))
return true;
}
return false;
}
void ShadowsPass::updateShadowMapSize()
{
// Temporary data
int32 newSizeCSM = 0;
int32 newSizeCube = 0;
// Select new size
_currentShadowMapsQuality = Graphics::ShadowMapsQuality;
if (_supportsShadows)
{
switch (_currentShadowMapsQuality)
{
case Quality::Ultra:
newSizeCSM = 2048;
newSizeCube = 1024;
break;
case Quality::High:
newSizeCSM = 1024;
newSizeCube = 1024;
break;
case Quality::Medium:
newSizeCSM = 1024;
newSizeCube = 512;
break;
case Quality::Low:
newSizeCSM = 512;
newSizeCube = 256;
break;
}
}
// Check if size will change
if (newSizeCSM > 0 && newSizeCSM != _shadowMapsSizeCSM)
{
if (_shadowMapCSM->Init(GPUTextureDescription::New2D(newSizeCSM, newSizeCSM, SHADOW_MAPS_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil, 1, MAX_CSM_CASCADES)))
{
LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("CSM"), newSizeCSM, (int32)SHADOW_MAPS_FORMAT);
return;
}
_shadowMapsSizeCSM = newSizeCSM;
}
if (newSizeCube > 0 && newSizeCube != _shadowMapsSizeCube)
{
if (_shadowMapCube->Init(GPUTextureDescription::NewCube(newSizeCube, SHADOW_MAPS_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil)))
{
LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("Cube"), newSizeCube, (int32)SHADOW_MAPS_FORMAT);
return;
}
_shadowMapsSizeCube = newSizeCube;
}
}
void ShadowsPass::Dispose()
{
// Base
RendererPass::Dispose();
// Cleanup
_psShadowDir.Delete();
_psShadowPoint.Delete();
_psShadowSpot.Delete();
_shader = nullptr;
_sphereModel = nullptr;
SAFE_DELETE_GPU_RESOURCE(_shadowMapCSM);
SAFE_DELETE_GPU_RESOURCE(_shadowMapCube);
}
bool ShadowsPass::CanRenderShadow(RenderContext& renderContext, const RendererPointLightData& light)
{
const Float3 lightPosition = light.Position;
const float dstLightToView = Float3::Distance(lightPosition, renderContext.View.Position);
// Fade shadow on distance
const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance);
return fade > ZeroTolerance && _supportsShadows;
}
bool ShadowsPass::CanRenderShadow(RenderContext& renderContext, const RendererSpotLightData& light)
{
const Float3 lightPosition = light.Position;
const float dstLightToView = Float3::Distance(lightPosition, renderContext.View.Position);
// Fade shadow on distance
const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance);
return fade > ZeroTolerance && _supportsShadows;
}
bool ShadowsPass::CanRenderShadow(RenderContext& renderContext, const RendererDirectionalLightData& light)
{
return _supportsShadows;
}
void ShadowsPass::Prepare(RenderContext& renderContext, GPUContext* context)
{
ASSERT(IsReady());
auto& view = renderContext.View;
const auto shader = _shader->GetShader();
const auto shadowMapsQuality = Graphics::ShadowMapsQuality;
if (shadowMapsQuality != _currentShadowMapsQuality)
updateShadowMapSize();
auto shadowsQuality = Graphics::ShadowsQuality;
maxShadowsQuality = Math::Clamp(Math::Min<int32>(static_cast<int32>(shadowsQuality), static_cast<int32>(view.MaxShadowsQuality)), 0, static_cast<int32>(Quality::MAX) - 1);
// Use the current render view to sync model LODs with the shadow maps rendering stage
_shadowContext.LodProxyView = &renderContext.View;
// Prepare properties
auto& shadowView = _shadowContext.View;
shadowView.Flags = view.Flags;
shadowView.StaticFlagsMask = view.StaticFlagsMask;
shadowView.RenderLayersMask = view.RenderLayersMask;
shadowView.IsOfflinePass = view.IsOfflinePass;
shadowView.ModelLODBias = view.ModelLODBias + view.ShadowModelLODBias;
shadowView.ModelLODDistanceFactor = view.ModelLODDistanceFactor * view.ShadowModelLODDistanceFactor;
shadowView.Pass = DrawPass::Depth;
_shadowContext.List = &_shadowCache;
}
void ShadowsPass::RenderShadow(RenderContext& renderContext, RendererPointLightData& light, GPUTextureView* shadowMask)
{
const float sphereModelScale = 3.0f;
PROFILE_GPU_CPU("Shadow");
// Cache data
auto device = GPUDevice::Instance;
auto context = device->GetMainContext();
auto& view = renderContext.View;
auto shader = _shader->GetShader();
Data sperLight;
float lightRadius = light.Radius;
Float3 lightPosition = light.Position;
Float3 lightDirection = light.Direction;
float dstLightToView = Float3::Distance(lightPosition, view.Position);
// TODO: here we can use lower shadows quality based on light distance to view (LOD switching) and per light setting for max quality
int32 shadowQuality = maxShadowsQuality;
// Fade shadow on distance
const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance);
// Set up GPU context and render view
const auto shadowMapsSizeCube = (float)_shadowMapsSizeCube;
context->SetViewportAndScissors(shadowMapsSizeCube, shadowMapsSizeCube);
_shadowContext.View.SetUpCube(PointLight_NearPlane, lightRadius, lightPosition);
_shadowContext.View.PrepareCache(_shadowContext, shadowMapsSizeCube, shadowMapsSizeCube, Float2::Zero);
// Render depth to all 6 faces of the cube map
for (int32 faceIndex = 0; faceIndex < 6; faceIndex++)
{
// Set up view
_shadowCache.Clear();
_shadowContext.View.SetFace(faceIndex);
Matrix::Transpose(_shadowContext.View.ViewProjection(), sperLight.LightShadow.ShadowVP[faceIndex]);
// Set render target
auto rt = _shadowMapCube->View(faceIndex);
context->ResetSR();
context->SetRenderTarget(rt, static_cast<GPUTextureView*>(nullptr));
context->ClearDepth(rt);
// Render actors to the shadow map
renderContext.Task->OnCollectDrawCalls(_shadowContext);
_shadowCache.SortDrawCalls(_shadowContext, false, DrawCallsListType::Depth);
_shadowCache.ExecuteDrawCalls(_shadowContext, DrawCallsListType::Depth);
}
// Restore GPU context
context->ResetSR();
context->ResetRenderTarget();
const Viewport viewport = renderContext.Task->GetViewport();
GPUTexture* depthBuffer = renderContext.Buffers->DepthBuffer;
GPUTextureView* depthBufferSRV = depthBuffer->GetDescription().Flags & GPUTextureFlags::ReadOnlyDepthView ? depthBuffer->ViewReadOnlyDepth() : depthBuffer->View();
context->SetViewportAndScissors(viewport);
context->BindSR(0, renderContext.Buffers->GBuffer0);
context->BindSR(1, renderContext.Buffers->GBuffer1);
context->BindSR(2, renderContext.Buffers->GBuffer2);
context->BindSR(3, depthBufferSRV);
context->BindSR(4, renderContext.Buffers->GBuffer3);
// Setup shader data
GBufferPass::SetInputs(view, sperLight.GBuffer);
light.SetupLightData(&sperLight.Light, true);
sperLight.LightShadow.ShadowMapSize = shadowMapsSizeCube;
sperLight.LightShadow.Sharpness = light.ShadowsSharpness;
sperLight.LightShadow.Fade = Math::Saturate(light.ShadowsStrength * fade);
sperLight.LightShadow.NormalOffsetScale = light.ShadowsNormalOffsetScale * NormalOffsetScaleTweak * (1.0f / shadowMapsSizeCube);
sperLight.LightShadow.Bias = light.ShadowsDepthBias;
sperLight.LightShadow.FadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
sperLight.LightShadow.NumCascades = 1;
sperLight.LightShadow.CascadeSplits = Float4::Zero;
Matrix::Transpose(view.ViewProjection(), sperLight.ViewProjectionMatrix);
sperLight.ContactShadowsDistance = light.ShadowsDistance;
sperLight.ContactShadowsLength = view.Flags & ViewFlags::ContactShadows ? light.ContactShadowsLength : 0.0f;
// Calculate world view projection matrix for the light sphere
Matrix world, wvp, matrix;
Matrix::Scaling(lightRadius * sphereModelScale, wvp);
Matrix::Translation(lightPosition, matrix);
Matrix::Multiply(wvp, matrix, world);
Matrix::Multiply(world, view.ViewProjection(), wvp);
Matrix::Transpose(wvp, sperLight.WVP);
// Render shadow in screen space
context->UpdateCB(shader->GetCB(0), &sperLight);
context->BindCB(0, shader->GetCB(0));
context->BindCB(1, shader->GetCB(1));
context->BindSR(5, _shadowMapCube->ViewArray());
context->SetRenderTarget(shadowMask);
context->SetState(_psShadowPoint.Get(shadowQuality + (sperLight.ContactShadowsLength > ZeroTolerance ? 4 : 0)));
_sphereModel->Render(context);
// Cleanup
context->ResetRenderTarget();
context->UnBindSR(5);
// Render volumetric light with shadow
VolumetricFogPass::Instance()->RenderLight(renderContext, context, light, _shadowMapCube->ViewArray(), sperLight.LightShadow);
}
void ShadowsPass::RenderShadow(RenderContext& renderContext, RendererSpotLightData& light, GPUTextureView* shadowMask)
{
const float sphereModelScale = 3.0f;
PROFILE_GPU_CPU("Shadow");
// Cache data
auto device = GPUDevice::Instance;
auto context = device->GetMainContext();
auto& view = renderContext.View;
auto shader = _shader->GetShader();
Data sperLight;
float lightRadius = light.Radius;
Float3 lightPosition = light.Position;
Float3 lightDirection = light.Direction;
float dstLightToView = Float3::Distance(lightPosition, view.Position);
// TODO: here we can use lower shadows quality based on light distance to view (LOD switching) and per light setting for max quality
int32 shadowQuality = maxShadowsQuality;
// Fade shadow on distance
const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance);
// Set up GPU context and render view
const auto shadowMapsSizeCube = (float)_shadowMapsSizeCube;
context->SetViewportAndScissors(shadowMapsSizeCube, shadowMapsSizeCube);
_shadowContext.View.SetProjector(SpotLight_NearPlane, lightRadius, lightPosition, lightDirection, light.UpVector, light.OuterConeAngle * 2.0f);
_shadowContext.View.PrepareCache(_shadowContext, shadowMapsSizeCube, shadowMapsSizeCube, Float2::Zero);
// Render depth to all 1 face of the cube map
const int32 cubeFaceIndex = 0;
{
// Set up view
_shadowCache.Clear();
Matrix::Transpose(_shadowContext.View.ViewProjection(), sperLight.LightShadow.ShadowVP[cubeFaceIndex]);
// Set render target
auto rt = _shadowMapCube->View(cubeFaceIndex);
context->ResetSR();
context->SetRenderTarget(rt, static_cast<GPUTextureView*>(nullptr));
context->ClearDepth(rt);
// Render actors to the shadow map
renderContext.Task->OnCollectDrawCalls(_shadowContext);
_shadowCache.SortDrawCalls(_shadowContext, false, DrawCallsListType::Depth);
_shadowCache.ExecuteDrawCalls(_shadowContext, DrawCallsListType::Depth);
}
// Restore GPU context
context->ResetSR();
context->ResetRenderTarget();
const Viewport viewport = renderContext.Task->GetViewport();
GPUTexture* depthBuffer = renderContext.Buffers->DepthBuffer;
GPUTextureView* depthBufferSRV = depthBuffer->GetDescription().Flags & GPUTextureFlags::ReadOnlyDepthView ? depthBuffer->ViewReadOnlyDepth() : depthBuffer->View();
context->SetViewportAndScissors(viewport);
context->BindSR(0, renderContext.Buffers->GBuffer0);
context->BindSR(1, renderContext.Buffers->GBuffer1);
context->BindSR(2, renderContext.Buffers->GBuffer2);
context->BindSR(3, depthBufferSRV);
context->BindSR(4, renderContext.Buffers->GBuffer3);
// Setup shader data
GBufferPass::SetInputs(view, sperLight.GBuffer);
light.SetupLightData(&sperLight.Light, true);
sperLight.LightShadow.ShadowMapSize = shadowMapsSizeCube;
sperLight.LightShadow.Sharpness = light.ShadowsSharpness;
sperLight.LightShadow.Fade = Math::Saturate(light.ShadowsStrength * fade);
sperLight.LightShadow.NormalOffsetScale = light.ShadowsNormalOffsetScale * NormalOffsetScaleTweak * (1.0f / shadowMapsSizeCube);
sperLight.LightShadow.Bias = light.ShadowsDepthBias;
sperLight.LightShadow.FadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
sperLight.LightShadow.NumCascades = 1;
sperLight.LightShadow.CascadeSplits = Float4::Zero;
Matrix::Transpose(view.ViewProjection(), sperLight.ViewProjectionMatrix);
sperLight.ContactShadowsDistance = light.ShadowsDistance;
sperLight.ContactShadowsLength = view.Flags & ViewFlags::ContactShadows ? light.ContactShadowsLength : 0.0f;
// Calculate world view projection matrix for the light sphere
Matrix world, wvp, matrix;
Matrix::Scaling(lightRadius * sphereModelScale, wvp);
Matrix::Translation(lightPosition, matrix);
Matrix::Multiply(wvp, matrix, world);
Matrix::Multiply(world, view.ViewProjection(), wvp);
Matrix::Transpose(wvp, sperLight.WVP);
// Render shadow in screen space
context->UpdateCB(shader->GetCB(0), &sperLight);
context->BindCB(0, shader->GetCB(0));
context->BindCB(1, shader->GetCB(1));
context->BindSR(5, _shadowMapCube->View(cubeFaceIndex));
context->SetRenderTarget(shadowMask);
context->SetState(_psShadowSpot.Get(shadowQuality + (sperLight.ContactShadowsLength > ZeroTolerance ? 4 : 0)));
_sphereModel->Render(context);
// Cleanup
context->ResetRenderTarget();
context->UnBindSR(5);
// Render volumetric light with shadow
VolumetricFogPass::Instance()->RenderLight(renderContext, context, light, _shadowMapCube->View(cubeFaceIndex), sperLight.LightShadow);
}
void ShadowsPass::RenderShadow(RenderContext& renderContext, RendererDirectionalLightData& light, int32 index, GPUTextureView* shadowMask)
{
PROFILE_GPU_CPU("Shadow");
// Cache data
auto device = GPUDevice::Instance;
auto context = device->GetMainContext();
auto& view = renderContext.View;
auto mainCache = renderContext.List;
auto shader = _shader->GetShader();
Data sperLight;
Float3 lightDirection = light.Direction;
float shadowsDistance = Math::Min(view.Far, light.ShadowsDistance);
int32 csmCount = Math::Clamp(light.CascadeCount, 0, MAX_CSM_CASCADES);
bool blendCSM = Graphics::AllowCSMBlending;
#if USE_EDITOR
if (IsRunningRadiancePass)
blendCSM = false;
#endif
// Views with orthographic cameras cannot use cascades, we force it to 1 shadow map here
if (view.Projection.M44 == 1.0f)
csmCount = 1;
// Calculate cascade splits
auto cameraNear = view.Near;
auto cameraFar = view.Far;
auto cameraRange = cameraFar - cameraNear;
float minDistance;
float maxDistance;
float cascadeSplits[MAX_CSM_CASCADES];
{
// TODO: use HiZ and get view min/max range to fit cascades better
minDistance = cameraNear;
maxDistance = cameraNear + shadowsDistance;
// TODO: expose partition mode?
enum class PartitionMode
{
Manual = 0,
Logarithmic = 1,
PSSM = 2,
};
PartitionMode partitionMode = PartitionMode::Manual;
float pssmFactor = 0.5f;
float splitDistance0 = 0.05f;
float splitDistance1 = 0.15f;
float splitDistance2 = 0.50f;
float splitDistance3 = 1.00f;
// Compute the split distances based on the partitioning mode
if (partitionMode == PartitionMode::Manual)
{
if (csmCount == 1)
{
cascadeSplits[0] = minDistance + splitDistance3 * maxDistance;
}
else if (csmCount == 2)
{
cascadeSplits[0] = minDistance + splitDistance1 * maxDistance;
cascadeSplits[1] = minDistance + splitDistance3 * maxDistance;
}
else if (csmCount == 3)
{
cascadeSplits[0] = minDistance + splitDistance0 * maxDistance;
cascadeSplits[1] = minDistance + splitDistance2 * maxDistance;
cascadeSplits[2] = minDistance + splitDistance3 * maxDistance;
}
else if (csmCount == 4)
{
cascadeSplits[0] = minDistance + splitDistance0 * maxDistance;
cascadeSplits[1] = minDistance + splitDistance1 * maxDistance;
cascadeSplits[2] = minDistance + splitDistance2 * maxDistance;
cascadeSplits[3] = minDistance + splitDistance3 * maxDistance;
}
}
else if (partitionMode == PartitionMode::Logarithmic || partitionMode == PartitionMode::PSSM)
{
const float lambda = partitionMode == PartitionMode::PSSM ? pssmFactor : 1.0f;
const auto range = maxDistance - minDistance;
const auto ratio = maxDistance / minDistance;
const auto logRatio = Math::Clamp(1.0f - lambda, 0.0f, 1.0f);
for (int32 cascadeLevel = 0; cascadeLevel < csmCount; cascadeLevel++)
{
// Compute cascade split (between znear and zfar)
const float distribute = static_cast<float>(cascadeLevel + 1) / csmCount;
float logZ = static_cast<float>(minDistance * powf(ratio, distribute));
float uniformZ = minDistance + range * distribute;
cascadeSplits[cascadeLevel] = Math::Lerp(uniformZ, logZ, logRatio);
}
}
// Convert distance splits to ratios cascade in the range [0, 1]
for (int32 i = 0; i < MAX_CSM_CASCADES; i++)
{
cascadeSplits[i] = (cascadeSplits[i] - cameraNear) / cameraRange;
}
}
// Select best Up vector
Float3 side = Float3::UnitX;
Float3 upDirection = Float3::UnitX;
Float3 vectorUps[] = { Float3::UnitY, Float3::UnitX, Float3::UnitZ };
for (int32 i = 0; i < ARRAY_COUNT(vectorUps); i++)
{
const Float3 vectorUp = vectorUps[i];
if (Math::Abs(Float3::Dot(lightDirection, vectorUp)) < (1.0f - 0.0001f))
{
side = Float3::Normalize(Float3::Cross(vectorUp, lightDirection));
upDirection = Float3::Normalize(Float3::Cross(lightDirection, side));
break;
}
}
// Temporary data
Float3 frustumCorners[8];
Matrix shadowView, shadowProjection, shadowVP;
// Set up GPU context and render view
const auto shadowMapsSizeCSM = (float)_shadowMapsSizeCSM;
context->SetViewportAndScissors(shadowMapsSizeCSM, shadowMapsSizeCSM);
_shadowContext.View.PrepareCache(_shadowContext, shadowMapsSizeCSM, shadowMapsSizeCSM, Float2::Zero);
// Create the different view and projection matrices for each split
float splitMinRatio = 0;
float splitMaxRatio = (minDistance - cameraNear) / cameraRange;
for (int32 cascadeIndex = 0; cascadeIndex < csmCount; cascadeIndex++)
{
// Cascade splits
const auto oldSplitMinRatio = splitMinRatio;
splitMinRatio = splitMaxRatio;
splitMaxRatio = cascadeSplits[cascadeIndex];
// Calculate cascade split frustum corners in view space
for (int32 j = 0; j < 4; j++)
{
float overlap = 0;
if (blendCSM)
overlap = 0.2f * (splitMinRatio - oldSplitMinRatio);
const auto frustumRangeVS = mainCache->FrustumCornersVs[j + 4] - mainCache->FrustumCornersVs[j];
frustumCorners[j] = mainCache->FrustumCornersVs[j] + frustumRangeVS * (splitMinRatio - overlap);
frustumCorners[j + 4] = mainCache->FrustumCornersVs[j] + frustumRangeVS * splitMaxRatio;
}
// Perform stabilization
enum StabilizationMode
{
None,
ProjectionSnapping,
ViewSnapping,
};
const StabilizationMode stabilization = ViewSnapping; // TODO: expose to graphics settings maybe
Float3 cascadeMinBoundLS;
Float3 cascadeMaxBoundLS;
Float3 target;
{
// Make sure we are using the same direction when stabilizing
BoundingSphere boundingVS;
BoundingSphere::FromPoints(frustumCorners, ARRAY_COUNT(frustumCorners), boundingVS);
// Compute bounding box center
Float3::TransformCoordinate(boundingVS.Center, view.IV, target);
float boundingVSRadius = (float)boundingVS.Radius;
cascadeMaxBoundLS = Float3(boundingVSRadius);
cascadeMinBoundLS = -cascadeMaxBoundLS;
if (stabilization == ViewSnapping)
{
// Snap the target to the texel units (reference: ShaderX7 - Practical Cascaded Shadows Maps)
float shadowMapHalfSize = shadowMapsSizeCSM * 0.5f;
float x = Math::Ceil(Float3::Dot(target, upDirection) * shadowMapHalfSize / boundingVSRadius) * boundingVSRadius / shadowMapHalfSize;
float y = Math::Ceil(Float3::Dot(target, side) * shadowMapHalfSize / boundingVSRadius) * boundingVSRadius / shadowMapHalfSize;
float z = Float3::Dot(target, lightDirection);
target = upDirection * x + side * y + lightDirection * z;
}
}
const auto nearClip = 0.0f;
const auto farClip = cascadeMaxBoundLS.Z - cascadeMinBoundLS.Z;
// Create shadow view matrix
Matrix::LookAt(target - lightDirection * cascadeMaxBoundLS.Z, target, upDirection, shadowView);
// Create viewport for culling with extended near/far planes due to culling issues
Matrix cullingVP;
{
const float cullRangeExtent = 100000.0f;
Matrix::OrthoOffCenter(cascadeMinBoundLS.X, cascadeMaxBoundLS.X, cascadeMinBoundLS.Y, cascadeMaxBoundLS.Y, -cullRangeExtent, farClip + cullRangeExtent, shadowProjection);
Matrix::Multiply(shadowView, shadowProjection, cullingVP);
}
// Create shadow projection matrix
Matrix::OrthoOffCenter(cascadeMinBoundLS.X, cascadeMaxBoundLS.X, cascadeMinBoundLS.Y, cascadeMaxBoundLS.Y, nearClip, farClip, shadowProjection);
// Construct shadow matrix (View * Projection)
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
// Stabilize the shadow matrix on the projection
if (stabilization == ProjectionSnapping)
{
Float3 shadowPixelPosition = shadowVP.GetTranslation() * (shadowMapsSizeCSM * 0.5f);
shadowPixelPosition.Z = 0;
const Float3 shadowPixelPositionRounded(Math::Round(shadowPixelPosition.X), Math::Round(shadowPixelPosition.Y), 0.0f);
const Float4 shadowPixelOffset((shadowPixelPositionRounded - shadowPixelPosition) * (2.0f / shadowMapsSizeCSM), 0.0f);
shadowProjection.SetRow4(shadowProjection.GetRow4() + shadowPixelOffset);
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
}
// Transform NDC space [-1,+1]^2 to texture space [0,1]^2
{
const Matrix T(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f);
Matrix m;
Matrix::Multiply(shadowVP, T, m);
Matrix::Transpose(m, sperLight.LightShadow.ShadowVP[cascadeIndex]);
}
// Set up view and cache
_shadowCache.Clear();
_shadowContext.View.Position = -lightDirection * shadowsDistance + view.Position;
_shadowContext.View.Direction = lightDirection;
_shadowContext.View.SetUp(shadowView, shadowProjection);
_shadowContext.View.CullingFrustum.SetMatrix(cullingVP);
// Set render target
const auto rt = _shadowMapCSM->View(cascadeIndex);
context->ResetSR();
context->SetRenderTarget(rt, static_cast<GPUTextureView*>(nullptr));
context->ClearDepth(rt);
// Render actors to the shadow map
renderContext.Task->OnCollectDrawCalls(_shadowContext);
_shadowCache.SortDrawCalls(_shadowContext, false, DrawCallsListType::Depth);
_shadowCache.ExecuteDrawCalls(_shadowContext, DrawCallsListType::Depth);
}
// Restore GPU context
context->ResetSR();
context->ResetRenderTarget();
const Viewport viewport = renderContext.Task->GetViewport();
GPUTexture* depthBuffer = renderContext.Buffers->DepthBuffer;
GPUTextureView* depthBufferSRV = depthBuffer->GetDescription().Flags & GPUTextureFlags::ReadOnlyDepthView ? depthBuffer->ViewReadOnlyDepth() : depthBuffer->View();
context->SetViewportAndScissors(viewport);
context->BindSR(0, renderContext.Buffers->GBuffer0);
context->BindSR(1, renderContext.Buffers->GBuffer1);
context->BindSR(2, renderContext.Buffers->GBuffer2);
context->BindSR(3, depthBufferSRV);
context->BindSR(4, renderContext.Buffers->GBuffer3);
// Setup shader data
GBufferPass::SetInputs(view, sperLight.GBuffer);
light.SetupLightData(&sperLight.Light, true);
sperLight.LightShadow.ShadowMapSize = shadowMapsSizeCSM;
sperLight.LightShadow.Sharpness = light.ShadowsSharpness;
sperLight.LightShadow.Fade = Math::Saturate(light.ShadowsStrength);
sperLight.LightShadow.NormalOffsetScale = light.ShadowsNormalOffsetScale * NormalOffsetScaleTweak * (1.0f / shadowMapsSizeCSM);
sperLight.LightShadow.Bias = light.ShadowsDepthBias;
sperLight.LightShadow.FadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
sperLight.LightShadow.NumCascades = csmCount;
sperLight.LightShadow.CascadeSplits = view.Near + Float4(cascadeSplits) * cameraRange;
Matrix::Transpose(view.ViewProjection(), sperLight.ViewProjectionMatrix);
sperLight.ContactShadowsDistance = light.ShadowsDistance;
sperLight.ContactShadowsLength = view.Flags & ViewFlags::ContactShadows ? light.ContactShadowsLength : 0.0f;
// Render shadow in screen space
context->UpdateCB(shader->GetCB(0), &sperLight);
context->BindCB(0, shader->GetCB(0));
context->BindCB(1, shader->GetCB(1));
context->BindSR(5, _shadowMapCSM->ViewArray());
context->SetRenderTarget(shadowMask);
context->SetState(_psShadowDir.Get(maxShadowsQuality + static_cast<int32>(Quality::MAX) * blendCSM + (sperLight.ContactShadowsLength > ZeroTolerance ? 8 : 0)));
context->DrawFullscreenTriangle();
// Cleanup
context->ResetRenderTarget();
context->UnBindSR(5);
// Cache params for the volumetric fog or other effects that use dir light shadow sampling
LastDirLightIndex = index;
LastDirLightShadowMap = _shadowMapCSM->ViewArray();
LastDirLight = sperLight.LightShadow;
}