diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli index 37e8634543..65cbeb5874 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli @@ -8,6 +8,8 @@ #pragma once +#include "ReceiverPlaneDepthBias.azsli" + // Pcf filtering taken from legacy Cry/Lumberyard ShadowCommon.cfi. // // Approximate an expensive but smooth bicubic filter by taking multiple bilinear samples and then weighting the samples appropriately. @@ -28,8 +30,12 @@ struct SampleShadowMapBicubicParameters float invShadowMapSize; SamplerComparisonState samplerState; float comparisonValue; + + // Optionally pass in a non-zero value into the receiverPlaneDepthBias to reduce shadow acne with large Pcf kernels + float2 receiverPlaneDepthBias; }; + float SampleShadowMapBicubic_4Tap(SampleShadowMapBicubicParameters param) { // Compute the shadow map UV and fractionals. @@ -47,13 +53,19 @@ float SampleShadowMapBicubic_4Tap(SampleShadowMapBicubicParameters param) float2 uv1 = (fractionalUV / weights1) + 1.0; // Accumulate the shadow results and return the resolve value. - float shadow; + float shadow = 0; + + const float2 uvOffsets[4] = {float2(uv0.x, uv0.y), float2(uv1.x, uv0.y), float2(uv0.x, uv1.y), float2(uv1.x, uv1.y)}; + const float weights[4] = {weights0.x * weights0.y, weights1.x * weights0.y, weights0.x * weights1.y, weights1.x * weights1.y}; + + [unroll] + for(int i = 0 ; i < 4; ++i) + { + const float2 texCoordOffset = uvOffsets[i] * param.invShadowMapSize; + const float fragmentDistance = param.comparisonValue + ApplyReceiverPlaneDepthBias(param.receiverPlaneDepthBias, texCoordOffset); + shadow += weights[i] * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + texCoordOffset, param.shadowPos.z), fragmentDistance); + } - shadow = weights0.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights1.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights0.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights1.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow /= 16.0; return shadow * shadow; } @@ -80,20 +92,26 @@ float SampleShadowMapBicubic_9Tap(SampleShadowMapBicubicParameters param) float2 uv1 = (3.0 + fractionalUV) / weights1; float2 uv2 = (fractionalUV / weights2) + 2.0; + + const float2 uvOffsets[9] = {float2(uv0.x, uv0.y), float2(uv1.x, uv0.y), float2(uv2.x, uv0.y), + float2(uv0.x, uv1.y), float2(uv1.x, uv1.y), float2(uv2.x, uv1.y), + float2(uv0.x, uv2.y), float2(uv1.x, uv2.y), float2(uv2.x, uv2.y) }; + + const float weights[9] = {weights0.x * weights0.y, weights1.x * weights0.y, weights2.x * weights0.y, + weights0.x * weights1.y, weights1.x * weights1.y, weights2.x * weights1.y, + weights0.x * weights2.y, weights1.x * weights2.y, weights2.x * weights2.y}; + // Accumulate the shadow results and return the resolve value. - float shadowResult; - - shadowResult = weights0.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadowResult += weights1.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadowResult += weights2.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); + float shadowResult = 0; - shadowResult += weights0.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadowResult += weights1.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadowResult += weights2.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - - shadowResult += weights0.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadowResult += weights1.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadowResult += weights2.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); + [unroll] + for(int i = 0 ; i < 9 ; ++i) + { + const float2 texCoordOffset = uvOffsets[i] * param.invShadowMapSize; + const float fragmentDistance = param.comparisonValue + ApplyReceiverPlaneDepthBias(param.receiverPlaneDepthBias, texCoordOffset); + + shadowResult += weights[i] * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + texCoordOffset, param.shadowPos.z), fragmentDistance); + } shadowResult /= 144.0; return shadowResult * shadowResult; @@ -123,28 +141,27 @@ float SampleShadowMapBicubic_16Tap(SampleShadowMapBicubicParameters param) float2 uv2 = -((7.0 * fractionalUV + 5.0) / weights2) + 1.0; float2 uv3 = -(fractionalUV / weights3) + 3.0; - // Accumulate the shadow results and return the resolve value. - float shadow; + const float2 uvOffsets[16] = {float2(uv0.x, uv0.y), float2(uv1.x, uv0.y), float2(uv2.x, uv0.y), float2(uv3.x, uv0.y), + float2(uv0.x, uv1.y), float2(uv1.x, uv1.y), float2(uv2.x, uv1.y), float2(uv3.x, uv1.y), + float2(uv0.x, uv2.y), float2(uv1.x, uv2.y), float2(uv2.x, uv2.y), float2(uv3.x, uv2.y), + float2(uv0.x, uv3.y), float2(uv1.x, uv3.y), float2(uv2.x, uv3.y), float2(uv3.x, uv3.y)}; - shadow = weights0.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights1.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights2.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights3.x * weights0.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv3.x, uv0.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); + const float weights[16] = {weights0.x * weights0.y, weights1.x * weights0.y, weights2.x * weights0.y, weights3.x * weights0.y, + weights0.x * weights1.y, weights1.x * weights1.y, weights2.x * weights1.y, weights3.x * weights1.y, + weights0.x * weights2.y, weights1.x * weights2.y, weights2.x * weights2.y, weights3.x * weights2.y, + weights0.x * weights3.y, weights1.x * weights3.y, weights2.x * weights3.y, weights3.x * weights3.y}; - shadow += weights0.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights1.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights2.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights3.x * weights1.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv3.x, uv1.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); + // Accumulate the shadow results and return the resolve value. + float shadow = 0; - shadow += weights0.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights1.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights2.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights3.x * weights2.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv3.x, uv2.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); + [unroll] + for(int i = 0 ; i < 16 ; ++i) + { + const float2 texCoordOffset = uvOffsets[i] * param.invShadowMapSize; + const float fragmentDistance = param.comparisonValue + ApplyReceiverPlaneDepthBias(param.receiverPlaneDepthBias, texCoordOffset); - shadow += weights0.x * weights3.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv0.x, uv3.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights1.x * weights3.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv1.x, uv3.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights2.x * weights3.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv2.x, uv3.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); - shadow += weights3.x * weights3.y * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + float2(uv3.x, uv3.y) * param.invShadowMapSize, param.shadowPos.z), param.comparisonValue); + shadow += weights[i] * param.shadowMap.SampleCmpLevelZero(param.samplerState, float3(texelCenter + texCoordOffset, param.shadowPos.z), fragmentDistance); + } shadow /= 2704; return shadow * shadow; diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli index 8b3eedf2aa..8df13bd19a 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli @@ -14,6 +14,7 @@ #include "Shadow.azsli" #include "ShadowmapAtlasLib.azsli" #include "BicubicPcfFilters.azsli" +#include "ReceiverPlaneDepthBias.azsli" // Before including this azsli file, a PassSrg must be defined with the following members: // Texture2DArray m_directionalLightShadowmap; @@ -23,6 +24,7 @@ // This matchs ShadowFilterMethod in ShadowConstants.h enum class ShadowFilterMethod {None, Pcf, Esm, EsmPcf}; option ShadowFilterMethod o_directional_shadow_filtering_method = ShadowFilterMethod::None; +option bool o_directional_shadow_receiver_plane_bias_enable = true; // DirectionalLightShadow calculates lit ratio for a directional light. class DirectionalLightShadow @@ -107,6 +109,8 @@ class DirectionalLightShadow float m_slopeBias[ViewSrg::MaxCascadeCount]; float3 m_normalVector; DebugInfo m_debugInfo; + float3 m_shadowPosDX[ViewSrg::MaxCascadeCount]; + float3 m_shadowPosDY[ViewSrg::MaxCascadeCount]; }; // This outputs the coordinate in shadowmap Texture space of the given point. @@ -438,6 +442,7 @@ float DirectionalLightShadow::SamplePcfBicubic(float3 shadowCoord, uint indexOfC { const uint filteringSampleCount = ViewSrg::m_directionalLightShadows[m_lightIndex].m_filteringSampleCount; const uint size = ViewSrg::m_directionalLightShadows[m_lightIndex].m_shadowmapSize; + const uint cascadeCount = ViewSrg::m_directionalLightShadows[m_lightIndex].m_cascadeCount; Texture2DArray shadowmap = PassSrg::m_directionalLightShadowmap; @@ -448,7 +453,8 @@ float DirectionalLightShadow::SamplePcfBicubic(float3 shadowCoord, uint indexOfC param.invShadowMapSize = rcp(size); param.comparisonValue = shadowCoord.z; param.samplerState = SceneSrg::m_hwPcfSampler; - + param.receiverPlaneDepthBias = o_directional_shadow_receiver_plane_bias_enable ? ComputeReceiverPlaneDepthBias(m_shadowPosDX[indexOfCascade], m_shadowPosDY[indexOfCascade]) : 0; + if (filteringSampleCount <= 4) { return SampleShadowMapBicubic_4Tap(param); @@ -476,6 +482,16 @@ float DirectionalLightShadow::GetVisibility( shadow.m_debugInfo.m_cascadeIndex = 0; shadow.m_debugInfo.m_usePcfFallback = false; + if (o_directional_shadow_receiver_plane_bias_enable) + { + [unroll] + for(int i = 0 ; i < ViewSrg::MaxCascadeCount ; ++i) + { + shadow.m_shadowPosDX[i] = ddx_fine(shadowCoords[i]); + shadow.m_shadowPosDY[i] = ddy_fine(shadowCoords[i]); + } + } + // Calculate slope bias const float3 lightDirection = normalize(SceneSrg::m_directionalLights[lightIndex].m_direction); diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/ReceiverPlaneDepthBias.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/ReceiverPlaneDepthBias.azsli new file mode 100644 index 0000000000..6225bb3434 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/ReceiverPlaneDepthBias.azsli @@ -0,0 +1,38 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +// Use ddx/ddy to attempt to predict the slope of the receiver plane (the triangle we are shading) and adjust the pcf comparision depth accordingly +// See Section 20.5 Large Pcf Kernels in "Introduction to 3D Game Programming with DirectX 12" for a good explanation and diagrams of the problem +// Solution from: +// https://web.archive.org/web/20120220010450/http://developer.amd.com/media/gpu_assets/Isidoro-ShadowMapping.pdf +// Another implementation example: https://github.com/TheRealMJP/Shadows +float2 ComputeReceiverPlaneDepthBias(float3 projCoordsDDX, float3 projCoordsDDY) +{ + const float invDet = 1.0f / ((projCoordsDDX.x * projCoordsDDY.y) - (projCoordsDDX.y * projCoordsDDY.x)); + + float2 receiverPlaneDepthBias; + + receiverPlaneDepthBias.x = projCoordsDDY.y * projCoordsDDX.z - projCoordsDDX.y * projCoordsDDY.z; + receiverPlaneDepthBias.y = projCoordsDDX.x * projCoordsDDY.z - projCoordsDDY.x * projCoordsDDX.z; + receiverPlaneDepthBias *= invDet; + + return receiverPlaneDepthBias; +} + +// For each sample in the Pcf kernel, offset the z comparison depth value by the amount returned by this function. +// receiverPlaneDepthBias should be the input from ComputeReceiverPlaneDepthBias() +float ApplyReceiverPlaneDepthBias(const float2 receiverPlaneDepthBias, const float2 texCoordOffset) +{ + float fragmentDepthBias = dot(receiverPlaneDepthBias, texCoordOffset); + // Because ddx/ddy can only give us a very rough approximation of the underlying surface, we might introduce acne by biasing away from the camera. + // Clamping this will remove this effect + fragmentDepthBias = min(fragmentDepthBias, 0.0); + return fragmentDepthBias; +} \ No newline at end of file diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h index 8dc8f1bade..3244c8249e 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h @@ -168,6 +168,10 @@ namespace AZ //! Sets the shadowmap Pcf method. virtual void SetPcfMethod(LightHandle handle, PcfMethod method) = 0; + + //! Sets whether the directional shadowmap should use receiver plane bias. + //! This attempts to reduce shadow acne when using large pcf filters. + virtual void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) = 0; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp index e0812f633a..44f95a8b85 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp @@ -202,6 +202,7 @@ namespace AZ { uint32_t shadowFilterMethod = m_shadowData.at(nullptr).GetData(m_shadowingLightHandle.GetIndex()).m_shadowFilterMethod; RPI::ShaderSystemInterface::Get()->SetGlobalShaderOption(m_directionalShadowFilteringMethodName, AZ::RPI::ShaderOptionValue{shadowFilterMethod}); + RPI::ShaderSystemInterface::Get()->SetGlobalShaderOption(m_directionalShadowReceiverPlaneBiasEnableName, AZ::RPI::ShaderOptionValue{ m_shadowProperties.GetData(m_shadowingLightHandle.GetIndex()).m_isReceiverPlaneBiasEnabled }); const uint32_t cascadeCount = m_shadowData.at(nullptr).GetData(m_shadowingLightHandle.GetIndex()).m_cascadeCount; ShadowProperty& property = m_shadowProperties.GetData(m_shadowingLightHandle.GetIndex()); @@ -616,6 +617,10 @@ namespace AZ m_shadowBufferNeedsUpdate = true; } + void DirectionalLightFeatureProcessor::SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) + { + m_shadowProperties.GetData(handle.GetIndex()).m_isReceiverPlaneBiasEnabled = enable; + } void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h index 4c486c4540..d57b3aaf2b 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h @@ -177,6 +177,10 @@ namespace AZ // Shadow filter method of the light ShadowFilterMethod m_shadowFilterMethod = ShadowFilterMethod::None; + + // If true, this will reduce the shadow acne introduced by large pcf kernels by estimating the angle of the triangle being shaded + // with the ddx/ddy functions. + bool m_isReceiverPlaneBiasEnabled = true; }; static void Reflect(ReflectContext* context); @@ -218,6 +222,7 @@ namespace AZ void SetFilteringSampleCount(LightHandle handle, uint16_t count) override; void SetShadowBoundaryWidth(LightHandle handle, float boundaryWidth) override; void SetPcfMethod(LightHandle handle, PcfMethod method) override; + void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) override; const Data::Instance GetLightBuffer() const; uint32_t GetLightCount() const; @@ -371,6 +376,7 @@ namespace AZ Name m_lightTypeName = Name("directional"); Name m_directionalShadowFilteringMethodName = Name("o_directional_shadow_filtering_method"); + Name m_directionalShadowReceiverPlaneBiasEnableName = Name("o_directional_shadow_receiver_plane_bias_enable"); static constexpr const char* FeatureProcessorName = "DirectionalLightFeatureProcessor"; }; } // namespace Render diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h index 086f68f80f..610578ee2d 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h @@ -185,6 +185,14 @@ namespace AZ //! This sets the type of Pcf (percentage-closer filtering) to use. //! @param method The Pcf method to use. virtual void SetPcfMethod(PcfMethod method) = 0; + + //! Gets whether the directional shadowmap should use receiver plane bias. + //! This attempts to reduce shadow acne when using large pcf filters. + virtual bool GetShadowReceiverPlaneBiasEnabled() const = 0; + + //! Sets whether the directional shadowmap should use receiver plane bias. + //! @param enable flag specifying whether to enable the receiver plane bias feature + virtual void SetShadowReceiverPlaneBiasEnabled(bool enable) = 0; }; using DirectionalLightRequestBus = EBus; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h index e7fe692213..e9e5778086 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h @@ -115,6 +115,10 @@ namespace AZ PcfMethod m_pcfMethod = PcfMethod::Bicubic; + //! Whether not to enable the receiver plane bias. + //! This uses partial derivatives to reduce shadow acne when using large pcf kernels. + bool m_receiverPlaneBiasEnabled = true; + bool IsSplitManual() const; bool IsSplitAutomatic() const; bool IsCascadeCorrectionDisabled() const; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp index 86fb97aeba..24ce566fb4 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp @@ -21,7 +21,7 @@ namespace AZ if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(7) + ->Version(8) ->Field("Color", &DirectionalLightComponentConfig::m_color) ->Field("IntensityMode", &DirectionalLightComponentConfig::m_intensityMode) ->Field("Intensity", &DirectionalLightComponentConfig::m_intensity) @@ -41,7 +41,7 @@ namespace AZ ->Field("PcfPredictionSampleCount", &DirectionalLightComponentConfig::m_predictionSampleCount) ->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount) ->Field("Pcf Method", &DirectionalLightComponentConfig::m_pcfMethod) - ; + ->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled); } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp index 642e50d104..6bf449803b 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp @@ -88,6 +88,8 @@ namespace AZ ->Event("SetFilteringSampleCount", &DirectionalLightRequestBus::Events::SetFilteringSampleCount) ->Event("GetPcfMethod", &DirectionalLightRequestBus::Events::GetPcfMethod) ->Event("SetPcfMethod", &DirectionalLightRequestBus::Events::SetPcfMethod) + ->Event("GetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::GetShadowReceiverPlaneBiasEnabled) + ->Event("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled) ->VirtualProperty("Color", "GetColor", "SetColor") ->VirtualProperty("Intensity", "GetIntensity", "SetIntensity") ->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter") @@ -104,7 +106,8 @@ namespace AZ ->VirtualProperty("SofteningBoundaryWidth", "GetSofteningBoundaryWidth", "SetSofteningBoundaryWidth") ->VirtualProperty("PredictionSampleCount", "GetPredictionSampleCount", "SetPredictionSampleCount") ->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount") - ->VirtualProperty("PcfMethod", "GetPcfMethod", "SetPcfMethod"); + ->VirtualProperty("PcfMethod", "GetPcfMethod", "SetPcfMethod") + ->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled"); ; } } @@ -539,6 +542,7 @@ namespace AZ SetPredictionSampleCount(m_configuration.m_predictionSampleCount); SetFilteringSampleCount(m_configuration.m_filteringSampleCount); SetPcfMethod(m_configuration.m_pcfMethod); + SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled); // [GFX TODO][ATOM-1726] share config for multiple light (e.g., light ID). // [GFX TODO][ATOM-2416] adapt to multiple viewports. @@ -637,6 +641,17 @@ namespace AZ m_configuration.m_pcfMethod = method; m_featureProcessor->SetPcfMethod(m_lightHandle, method); } - + + bool DirectionalLightComponentController::GetShadowReceiverPlaneBiasEnabled() const + { + return m_configuration.m_receiverPlaneBiasEnabled; + } + + void DirectionalLightComponentController::SetShadowReceiverPlaneBiasEnabled(bool enable) + { + m_configuration.m_receiverPlaneBiasEnabled = enable; + m_featureProcessor->SetShadowReceiverPlaneBiasEnabled(m_lightHandle, enable); + } + } // namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h index 4684852ff1..6b788c241c 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h @@ -84,6 +84,8 @@ namespace AZ void SetFilteringSampleCount(uint32_t count) override; PcfMethod GetPcfMethod() const override; void SetPcfMethod(PcfMethod method) override; + bool GetShadowReceiverPlaneBiasEnabled() const override; + void SetShadowReceiverPlaneBiasEnabled(bool enable) override; private: friend class EditorDirectionalLightComponent; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp index 9f0e06f203..18d9ad70e0 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp @@ -164,9 +164,13 @@ namespace AZ ->EnumAttribute(PcfMethod::Bicubic, "Bicubic") ->EnumAttribute(PcfMethod::BoundarySearch, "Boundary search") ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) - ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled); - ; - + ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled) + ->DataElement( + Edit::UIHandlers::CheckBox, &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled, + "Shadow Receiver Plane Bias Enable", + "This reduces shadow acne when using large pcf kernels.") + ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) + ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled); } }