Add variable rate update for shadow maps atlas based on distance to light

This commit is contained in:
Wojtek Figat
2024-04-08 00:04:57 +02:00
parent 7d92779e99
commit 708fba5136
11 changed files with 328 additions and 151 deletions

View File

@@ -38,6 +38,8 @@ void DirectionalLight::Draw(RenderContext& renderContext)
data.VolumetricScatteringIntensity = VolumetricScatteringIntensity;
data.IndirectLightingIntensity = IndirectLightingIntensity;
data.CastVolumetricShadow = CastVolumetricShadow;
data.ShadowsUpdateRate = ShadowsUpdateRate;
data.ShadowsUpdateRateAtDistance = ShadowsUpdateRateAtDistance;
data.ShadowsMode = ShadowsMode;
data.CascadeCount = CascadeCount;
data.Cascade1Spacing = Cascade1Spacing;

View File

@@ -100,6 +100,8 @@ void LightWithShadow::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE(ShadowsDepthBias);
SERIALIZE(ShadowsNormalOffsetScale);
SERIALIZE(ContactShadowsLength);
SERIALIZE(ShadowsUpdateRate);
SERIALIZE(ShadowsUpdateRateAtDistance);
}
void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
@@ -116,4 +118,6 @@ void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier*
DESERIALIZE(ShadowsDepthBias);
DESERIALIZE(ShadowsNormalOffsetScale);
DESERIALIZE(ContactShadowsLength);
DESERIALIZE(ShadowsUpdateRate);
DESERIALIZE(ShadowsUpdateRateAtDistance);
}

View File

@@ -128,6 +128,18 @@ public:
API_FIELD(Attributes="EditorOrder(99), EditorDisplay(\"Shadow\"), Limit(0.0f, 0.1f, 0.001f)")
float ContactShadowsLength = 0.0f;
/// <summary>
/// Frequency of shadow updates. 1 - every frame, 0.5 - every second frame, 0 - on start or change. It's the inverse value of how many frames should happen in-between shadow map updates (eg. inverse of 0.5 is 2 thus shadow will update every 2nd frame).
/// </summary>
API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Shadow\", \"Update Rate\"), Limit(0.0f, 1.0f)")
float ShadowsUpdateRate = 1.0f;
/// <summary>
/// Frequency of shadow updates at the maximum distance from the view at which shadows are still rendered. This value is multiplied by ShadowsUpdateRate and allows scaling the update rate in-between the shadow range. For example, if light is near view, it will get normal shadow updates but will reduce this rate when far from view. See ShadowsUpdateRate to learn more.
/// </summary>
API_FIELD(Attributes="EditorOrder(105), EditorDisplay(\"Shadow\", \"Update Rate At Distance\"), Limit(0.0f, 1.0f)")
float ShadowsUpdateRateAtDistance = 0.5f;
/// <summary>
/// Describes how a visual element casts shadows.
/// </summary>

View File

@@ -103,6 +103,8 @@ void PointLight::Draw(RenderContext& renderContext)
data.ShadowsSharpness = ShadowsSharpness;
data.VolumetricScatteringIntensity = VolumetricScatteringIntensity;
data.CastVolumetricShadow = CastVolumetricShadow;
data.ShadowsUpdateRate = ShadowsUpdateRate;
data.ShadowsUpdateRateAtDistance = ShadowsUpdateRateAtDistance;
data.ShadowsMode = ShadowsMode;
data.Radius = radius;
data.FallOffExponent = FallOffExponent;

View File

@@ -153,6 +153,8 @@ void SpotLight::Draw(RenderContext& renderContext)
data.ShadowsSharpness = ShadowsSharpness;
data.VolumetricScatteringIntensity = VolumetricScatteringIntensity;
data.CastVolumetricShadow = CastVolumetricShadow;
data.ShadowsUpdateRate = ShadowsUpdateRate;
data.ShadowsUpdateRateAtDistance = ShadowsUpdateRateAtDistance;
data.ShadowsMode = ShadowsMode;
data.Radius = radius;
data.FallOffExponent = FallOffExponent;

View File

@@ -391,7 +391,9 @@ void ParticleEmitterGraphCPUExecutor::Draw(ParticleEmitter* emitter, ParticleEff
RenderPointLightData lightData;
lightData.MinRoughness = 0.04f;
lightData.ShadowsDistance = 2000.0f;
lightData.ShadowsStrength = 1.0f;
lightData.ShadowsStrength = 0.0f;
lightData.ShadowsUpdateRate = 1.0f;
lightData.ShadowsUpdateRateAtDistance = 0.5f;
lightData.Direction = Float3::Forward;
lightData.ShadowsFadeDistance = 50.0f;
lightData.ShadowsNormalOffsetScale = 10.0f;

View File

@@ -80,7 +80,7 @@ bool RenderLocalLightData::CanRenderShadow(const RenderView& view) const
const float fadeDistance = Math::Max(ShadowsFadeDistance, 0.1f);
const float dstLightToView = Float3::Distance(Position, view.Position);
const float fade = 1 - Math::Saturate((dstLightToView - Radius - ShadowsDistance + fadeDistance) / fadeDistance);
return fade > ZeroTolerance && RenderLightData::CanRenderShadow(view);
return fade > ZeroTolerance && Radius > 10 && RenderLightData::CanRenderShadow(view);
}
void RenderSpotLightData::SetShaderData(ShaderLightData& data, bool useShadow) const

View File

@@ -56,6 +56,9 @@ struct RenderLightData
float ScreenSize;
uint32 ShadowsBufferAddress;
float ShadowsUpdateRate;
float ShadowsUpdateRateAtDistance;
RenderLightData()
{
Platform::MemoryClear(this, sizeof(RenderLightData));

View File

@@ -18,7 +18,8 @@
#include "Engine/Renderer/Lightmaps.h"
#endif
#define MaxTiles 6
#define SHADOWS_MAX_TILES 6
#define SHADOWS_MIN_RESOLUTION 16
#define NormalOffsetScaleTweak 100.0f
#define LocalLightNearPlane 10.0f
@@ -33,10 +34,10 @@ PACK_STRUCT(struct Data{
float ContactShadowsLength;
});
struct ShadowsAtlasTile : RectPack<ShadowsAtlasTile, uint16>
struct ShadowsAtlasRectTile : RectPack<ShadowsAtlasRectTile, uint16>
{
ShadowsAtlasTile(uint16 x, uint16 y, uint16 width, uint16 height)
: RectPack<ShadowsAtlasTile, uint16>(x, y, width, height)
ShadowsAtlasRectTile(uint16 x, uint16 y, uint16 width, uint16 height)
: RectPack<ShadowsAtlasRectTile, uint16>(x, y, width, height)
{
}
@@ -56,26 +57,25 @@ uint16 QuantizeResolution(float input)
return output;
}
struct ShadowAtlasLight
// State for shadow projection
struct ShadowAtlasLightTile
{
uint64 LastFrameUsed;
int32 ContextIndex;
int32 ContextCount;
uint16 Resolution;
uint16 TilesNeeded;
float Sharpness, Fade, NormalOffsetScale, Bias, FadeDistance;
Float4 CascadeSplits;
ShadowsAtlasTile* Tiles[MaxTiles];
Matrix WorldToShadow[MaxTiles];
ShadowsAtlasRectTile* RectTile;
Matrix WorldToShadow;
float FramesToUpdate; // Amount of frames (with fraction) until the next shadow update can happen
bool SkipUpdate;
Viewport CachedViewport; // The viewport used the last time to render shadow to the atlas
ShadowAtlasLight()
void Free(ShadowsCustomBuffer* buffer)
{
Platform::MemoryClear(this, sizeof(ShadowAtlasLight));
if (RectTile)
{
RectTile->Free(buffer);
RectTile = nullptr;
}
}
POD_COPYABLE(ShadowAtlasLight);
void SetWorldToShadow(int32 index, const Matrix& shadowViewProjection)
void SetWorldToShadow(const Matrix& shadowViewProjection)
{
// Transform Clip Space [-1,+1]^2 to UV Space [0,1]^2 (saves MAD instruction in shader)
const Matrix ClipToUV(
@@ -85,7 +85,120 @@ struct ShadowAtlasLight
0.5f, 0.5f, 0.0f, 1.0f);
Matrix m;
Matrix::Multiply(shadowViewProjection, ClipToUV, m);
Matrix::Transpose(m, WorldToShadow[index]);
Matrix::Transpose(m, WorldToShadow);
}
};
// State for shadow cache sed to invalidate any prerendered shadow depths
struct ShadowAtlasLightCache
{
bool Valid;
float ShadowsUpdateRate;
float ShadowsUpdateRateAtDistance;
Float3 Position;
float Radius;
Float3 Direction;
float Distance;
Float4 CascadeSplits;
void Set(const RenderView& view, const RenderLightData& light, const Float4& cascadeSplits = Float4::Zero)
{
Valid = true;
Distance = light.ShadowsDistance;
ShadowsUpdateRate = light.ShadowsUpdateRate;
ShadowsUpdateRateAtDistance = light.ShadowsUpdateRateAtDistance;
if (light.IsDirectionalLight)
{
// Sun
Position = view.Position;
Direction = light.Direction;
CascadeSplits = cascadeSplits;
}
else
{
// Local light
const auto& localLight = (const RenderLocalLightData&)light;
Position = light.Position;
Radius = localLight.Radius;
}
}
};
// State for light's shadows rendering
struct ShadowAtlasLight
{
uint64 LastFrameUsed;
int32 ContextIndex;
int32 ContextCount;
uint16 Resolution;
uint8 TilesNeeded;
uint8 TilesCount;
float Sharpness, Fade, NormalOffsetScale, Bias, FadeDistance, Distance;
Float4 CascadeSplits;
ShadowAtlasLightTile Tiles[SHADOWS_MAX_TILES];
ShadowAtlasLightCache Cache;
ShadowAtlasLight()
{
Platform::MemoryClear(this, sizeof(ShadowAtlasLight));
}
POD_COPYABLE(ShadowAtlasLight);
float CalculateUpdateRateInv(const RenderLightData& light, float distanceFromView, bool& freezeUpdate) const
{
const float shadowsUpdateRate = light.ShadowsUpdateRate;
const float shadowsUpdateRateAtDistance = shadowsUpdateRate * light.ShadowsUpdateRateAtDistance;
float updateRate = Math::Lerp(shadowsUpdateRate, shadowsUpdateRateAtDistance, Math::Saturate(distanceFromView / Distance));
// TODO: add global shadows update rate scale to be adjusted per-platform
freezeUpdate = updateRate <= ZeroTolerance;
if (freezeUpdate)
return 0.0f;
return 1.0f / updateRate;
}
void ValidateCache(const RenderView& view, const RenderLightData& light)
{
if (!Cache.Valid)
return;
if (!Math::NearEqual(Cache.Distance, light.ShadowsDistance) ||
!Math::NearEqual(Cache.ShadowsUpdateRate, light.ShadowsUpdateRate) ||
!Math::NearEqual(Cache.ShadowsUpdateRateAtDistance, light.ShadowsUpdateRateAtDistance))
{
// Invalidate
Cache.Valid = false;
}
if (light.IsDirectionalLight)
{
// Sun
if (Float3::Dot(Cache.Direction, light.Direction) < 0.999999f ||
!Float3::NearEqual(Cache.Position, view.Position, 1.0f) ||
!Float4::NearEqual(Cache.CascadeSplits, CascadeSplits))
{
// Invalidate
Cache.Valid = false;
}
}
else
{
// Local light
const auto& localLight = (const RenderLocalLightData&)light;
if (!Float3::NearEqual(Cache.Position, light.Position, 1.0f) ||
!Math::NearEqual(Cache.Radius, localLight.Radius))
{
// Invalidate
Cache.Valid = false;
}
}
for (int32 i = 0; i < TilesCount && Cache.Valid; i++)
{
auto& tile = Tiles[i];
if (tile.CachedViewport != Viewport(tile.RectTile->X, tile.RectTile->Y, tile.RectTile->Width, tile.RectTile->Height))
{
// Invalidate
Cache.Valid = false;
}
}
}
};
@@ -99,7 +212,7 @@ public:
GPUTexture* ShadowMapAtlas = nullptr;
DynamicTypedBuffer ShadowsBuffer;
GPUBufferView* ShadowsBufferView = nullptr;
ShadowsAtlasTile* AtlasTiles = nullptr; // TODO: optimize with a single allocation for atlas tiles
ShadowsAtlasRectTile* AtlasTiles = nullptr; // TODO: optimize with a single allocation for atlas tiles
Dictionary<Guid, ShadowAtlasLight> Lights;
ShadowsCustomBuffer()
@@ -117,6 +230,7 @@ public:
{
auto& atlasLight = it->Value;
Platform::MemoryClear(atlasLight.Tiles, sizeof(atlasLight.Tiles));
Platform::MemoryClear(&atlasLight.Cache, sizeof(atlasLight.Cache));
}
}
@@ -134,12 +248,12 @@ public:
}
};
void ShadowsAtlasTile::OnInsert(ShadowsCustomBuffer* buffer)
void ShadowsAtlasRectTile::OnInsert(ShadowsCustomBuffer* buffer)
{
buffer->AtlasPixelsUsed += (int32)Width * (int32)Height;
}
void ShadowsAtlasTile::OnFree(ShadowsCustomBuffer* buffer)
void ShadowsAtlasRectTile::OnFree(ShadowsCustomBuffer* buffer)
{
buffer->AtlasPixelsUsed -= (int32)Width * (int32)Height;
}
@@ -262,6 +376,7 @@ void ShadowsPass::SetupRenderContext(RenderContext& renderContext, RenderContext
shadowContext.List = RenderList::GetFromPool();
shadowContext.Buffers = renderContext.Buffers;
shadowContext.Task = renderContext.Task;
shadowContext.List->Clear();
}
void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLightData& light, ShadowAtlasLight& atlasLight)
@@ -272,17 +387,47 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
atlasLight.NormalOffsetScale = light.ShadowsNormalOffsetScale * NormalOffsetScaleTweak * (1.0f / (float)atlasLight.Resolution);
atlasLight.Bias = light.ShadowsDepthBias;
atlasLight.FadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
atlasLight.Distance = Math::Min(renderContext.View.Far, light.ShadowsDistance);
}
void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLocalLightData& light, ShadowAtlasLight& atlasLight)
bool ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLocalLightData& light, ShadowAtlasLight& atlasLight)
{
SetupLight(renderContext, renderContextBatch, (RenderLightData&)light, atlasLight);
// Fade shadow on distance
const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
const float dstLightToView = Float3::Distance(light.Position, renderContext.View.Position);
const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance);
const float dstLightToView = Float3::Distance(light.Position, renderContext.View.Position) - light.Radius;
const float fade = 1 - Math::Saturate((dstLightToView - atlasLight.Distance + fadeDistance) / fadeDistance);
atlasLight.Fade *= fade;
// Update cached state (invalidate it if the light changed)
atlasLight.ValidateCache(renderContext.View, light);
// Calculate update rate based on the distance to the view
bool freezeUpdate;
const float updateRateInv = atlasLight.CalculateUpdateRateInv(light, dstLightToView, freezeUpdate);
float& framesToUpdate = atlasLight.Tiles[0].FramesToUpdate; // Use the first tile for all local light projections to be in sync
if ((framesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.Valid)
{
// Light state matches the cached state and the update rate allows us to reuse the cached shadow map so skip update
if (!freezeUpdate)
framesToUpdate -= 1.0f;
for (auto& tile : atlasLight.Tiles)
tile.SkipUpdate = true;
return true;
}
framesToUpdate += updateRateInv - 1.0f;
// Cache the current state
atlasLight.Cache.Set(renderContext.View, light);
for (int32 i = 0; i < atlasLight.TilesCount; i++)
{
auto& tile = atlasLight.Tiles[i];
tile.SkipUpdate = false;
tile.CachedViewport = Viewport(tile.RectTile->X, tile.RectTile->Y, tile.RectTile->Width, tile.RectTile->Height);
}
return false;
}
void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderDirectionalLightData& light, ShadowAtlasLight& atlasLight)
@@ -290,29 +435,16 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
SetupLight(renderContext, renderContextBatch, (RenderLightData&)light, atlasLight);
const RenderView& view = renderContext.View;
auto mainCache = renderContext.List;
Float3 lightDirection = light.Direction;
float shadowsDistance = Math::Min(view.Far, light.ShadowsDistance);
int32 csmCount = Math::Clamp(light.CascadeCount, 0, MAX_CSM_CASCADES);
const int32 csmCount = atlasLight.TilesCount;
const auto shadowMapsSize = (float)atlasLight.Resolution;
// 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;
const float minDistance = view.Near;
const float maxDistance = view.Near + atlasLight.Distance;
const float viewRange = view.Far - view.Near;
float cascadeSplits[MAX_CSM_CASCADES];
{
minDistance = cameraNear;
maxDistance = cameraNear + shadowsDistance;
PartitionMode partitionMode = light.PartitionMode;
float pssmFactor = 0.5f;
float splitDistance0 = light.Cascade1Spacing;
float splitDistance1 = Math::Max(splitDistance0, light.Cascade2Spacing);
float splitDistance2 = Math::Max(splitDistance1, light.Cascade3Spacing);
@@ -346,8 +478,8 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
}
else if (partitionMode == PartitionMode::Logarithmic || partitionMode == PartitionMode::PSSM)
{
const float pssmFactor = 0.5f;
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);
@@ -355,7 +487,7 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
{
// 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 logZ = minDistance * Math::Pow(ratio, distribute);
float uniformZ = minDistance + range * distribute;
cascadeSplits[cascadeLevel] = Math::Lerp(uniformZ, logZ, logRatio);
}
@@ -363,9 +495,44 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
// 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;
cascadeSplits[i] = (cascadeSplits[i] - view.Near) / viewRange;
}
atlasLight.CascadeSplits = view.Near + Float4(cascadeSplits) * cameraRange;
atlasLight.CascadeSplits = view.Near + Float4(cascadeSplits) * viewRange;
// Update cached state (invalidate it if the light changed)
atlasLight.ValidateCache(renderContext.View, light);
// Update cascades to check which should be updated this frame
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = 0;
for (int32 cascadeIndex = 0; cascadeIndex < csmCount; cascadeIndex++)
{
const float dstToCascade = atlasLight.CascadeSplits.Raw[cascadeIndex];
bool freezeUpdate;
const float updateRateInv = atlasLight.CalculateUpdateRateInv(light, dstToCascade, freezeUpdate);
auto& tile = atlasLight.Tiles[cascadeIndex];
if ((tile.FramesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.Valid)
{
// Light state matches the cached state and the update rate allows us to reuse the cached shadow map so skip update
if (!freezeUpdate)
tile.FramesToUpdate -= 1.0f;
tile.SkipUpdate = true;
continue;
}
tile.FramesToUpdate += updateRateInv - 1.0f;
// Cache the current state
tile.SkipUpdate = false;
tile.CachedViewport = Viewport(tile.RectTile->X, tile.RectTile->Y, tile.RectTile->Width, tile.RectTile->Height);
atlasLight.ContextCount++;
}
// Init shadow data
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
if (atlasLight.ContextCount == 0)
return;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
atlasLight.Cache.Set(renderContext.View, light, atlasLight.CascadeSplits);
// Select best Up vector
Float3 side = Float3::UnitX;
@@ -374,37 +541,34 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
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))
if (Math::Abs(Float3::Dot(light.Direction, vectorUp)) < (1.0f - 0.0001f))
{
side = Float3::Normalize(Float3::Cross(vectorUp, lightDirection));
upDirection = Float3::Normalize(Float3::Cross(lightDirection, side));
side = Float3::Normalize(Float3::Cross(vectorUp, light.Direction));
upDirection = Float3::Normalize(Float3::Cross(light.Direction, side));
break;
}
}
// Temporary data
Float3 frustumCorners[8];
Matrix shadowView, shadowProjection, shadowVP;
// Init shadow data
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = csmCount;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
// Create the different view and projection matrices for each split
float splitMinRatio = 0;
float splitMaxRatio = (minDistance - cameraNear) / cameraRange;
float splitMaxRatio = (minDistance - view.Near) / viewRange;
int32 contextIndex = 0;
for (int32 cascadeIndex = 0; cascadeIndex < csmCount; cascadeIndex++)
{
// Cascade splits
const auto oldSplitMinRatio = splitMinRatio;
splitMinRatio = splitMaxRatio;
splitMaxRatio = cascadeSplits[cascadeIndex];
auto& tile = atlasLight.Tiles[cascadeIndex];
if (tile.SkipUpdate)
continue;
// Calculate cascade split frustum corners in view space
Float3 frustumCorners[8];
for (int32 j = 0; j < 4; j++)
{
float overlap = 0.1f * (splitMinRatio - oldSplitMinRatio); // CSM blending overlap
const RenderList* mainCache = renderContext.List;
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;
@@ -438,8 +602,8 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
float shadowMapHalfSize = shadowMapsSize * 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;
float z = Float3::Dot(target, light.Direction);
target = upDirection * x + side * y + light.Direction * z;
}
}
@@ -447,7 +611,8 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
const auto farClip = cascadeMaxBoundLS.Z - cascadeMinBoundLS.Z;
// Create shadow view matrix
Matrix::LookAt(target - lightDirection * cascadeMaxBoundLS.Z, target, upDirection, shadowView);
Matrix shadowView, shadowProjection, shadowVP;
Matrix::LookAt(target - light.Direction * cascadeMaxBoundLS.Z, target, upDirection, shadowView);
// Create viewport for culling with extended near/far planes due to culling issues
Matrix cullingVP;
@@ -474,14 +639,13 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
Matrix::Multiply(shadowView, shadowProjection, shadowVP);
}
atlasLight.SetWorldToShadow(cascadeIndex, shadowVP);
tile.SetWorldToShadow(shadowVP);
// Setup context for cascade
auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + cascadeIndex];
auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++];
SetupRenderContext(renderContext, shadowContext);
shadowContext.List->Clear();
shadowContext.View.Position = -lightDirection * shadowsDistance + view.Position;
shadowContext.View.Direction = lightDirection;
shadowContext.View.Position = light.Direction * -atlasLight.Distance + view.Position;
shadowContext.View.Direction = light.Direction;
shadowContext.View.SetUp(shadowView, shadowProjection);
shadowContext.View.CullingFrustum.SetMatrix(cullingVP);
shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &view);
@@ -490,54 +654,40 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderPointLightData& light, ShadowAtlasLight& atlasLight)
{
SetupLight(renderContext, renderContextBatch, (RenderLocalLightData&)light, atlasLight);
if (SetupLight(renderContext, renderContextBatch, (RenderLocalLightData&)light, atlasLight))
return;
// Init shadow data
// Render depth to all 6 faces of the cube map
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = 6;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
const auto& view = renderContext.View;
const auto shadowMapsSize = (float)atlasLight.Resolution;
// Fade shadow on distance
const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f);
const float dstLightToView = Float3::Distance(light.Position, view.Position);
const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance);
atlasLight.Fade *= fade;
// Render depth to all 6 faces of the cube map
for (int32 faceIndex = 0; faceIndex < 6; faceIndex++)
{
auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + faceIndex];
SetupRenderContext(renderContext, shadowContext);
shadowContext.List->Clear();
shadowContext.View.SetUpCube(LocalLightNearPlane, light.Radius, light.Position);
shadowContext.View.SetFace(faceIndex);
shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &view);
atlasLight.SetWorldToShadow(faceIndex, shadowContext.View.ViewProjection());
const auto shadowMapsSize = (float)atlasLight.Resolution;
shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &renderContext.View);
atlasLight.Tiles[faceIndex].SetWorldToShadow(shadowContext.View.ViewProjection());
}
}
void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderSpotLightData& light, ShadowAtlasLight& atlasLight)
{
SetupLight(renderContext, renderContextBatch, (RenderLocalLightData&)light, atlasLight);
if (SetupLight(renderContext, renderContextBatch, (RenderLocalLightData&)light, atlasLight))
return;
// Init shadow data
// Render depth to a single projection
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = 1;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
const auto& view = renderContext.View;
const auto shadowMapsSize = (float)atlasLight.Resolution;
// Render depth to a single projection
auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex];
SetupRenderContext(renderContext, shadowContext);
shadowContext.List->Clear();
shadowContext.View.SetProjector(LocalLightNearPlane, light.Radius, light.Position, light.Direction, light.UpVector, light.OuterConeAngle * 2.0f);
shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &view);
atlasLight.SetWorldToShadow(0, shadowContext.View.ViewProjection());
const auto shadowMapsSize = (float)atlasLight.Resolution;
shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &renderContext.View);
atlasLight.Tiles[0].SetWorldToShadow(shadowContext.View.ViewProjection());
}
void ShadowsPass::Dispose()
@@ -624,7 +774,7 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
shadows.ViewOrigin = renderContext.View.Origin;
}
if (!shadows.AtlasTiles)
shadows.AtlasTiles = New<ShadowsAtlasTile>(0, 0, atlasResolution, atlasResolution);
shadows.AtlasTiles = New<ShadowsAtlasRectTile>(0, 0, atlasResolution, atlasResolution);
// Update/add lights
for (const RenderLightData* light : shadowedLights)
@@ -637,12 +787,17 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
atlasLight.Resolution = QuantizeResolution(lightResolutionFloat);
// Cull too small lights
constexpr uint16 MinResolution = 16;
if (atlasLight.Resolution < MinResolution)
if (atlasLight.Resolution < SHADOWS_MIN_RESOLUTION)
continue;
if (light->IsDirectionalLight)
atlasLight.TilesNeeded = Math::Clamp(((const RenderDirectionalLightData*)light)->CascadeCount, 0, MAX_CSM_CASCADES);
{
atlasLight.TilesNeeded = Math::Clamp(((const RenderDirectionalLightData*)light)->CascadeCount, 1, MAX_CSM_CASCADES);
// Views with orthographic cameras cannot use cascades, we force it to 1 shadow map here
if (renderContext.View.IsOrthographicProjection())
atlasLight.TilesNeeded = 1;
}
else if (light->IsPointLight)
atlasLight.TilesNeeded = 6;
else
@@ -655,11 +810,8 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
{
if (it->Value.LastFrameUsed != currentFrame)
{
for (auto& tile : it->Value.Tiles)
{
if (tile)
tile->Free(&shadows);
}
for (ShadowAtlasLightTile& tile : it->Value.Tiles)
tile.Free(&shadows);
shadows.Lights.Remove(it);
}
}
@@ -696,50 +848,41 @@ RETRY_ATLAS_SETUP:
}
// Macro checks if light has proper amount of tiles already assigned and the resolution is matching
#define IS_LIGHT_TILE_REUSABLE (atlasLight.ContextCount == atlasLight.TilesNeeded && atlasLight.Tiles[0] && atlasLight.Tiles[0]->Width == atlasLight.Resolution)
#define IS_LIGHT_TILE_REUSABLE (atlasLight.TilesCount == atlasLight.TilesNeeded && atlasLight.Tiles[0].RectTile && atlasLight.Tiles[0].RectTile->Width == atlasLight.Resolution)
// Remove incorrect tiles before allocating new ones
for (RenderLightData* light : shadowedLights)
{
auto& atlasLight = shadows.Lights[light->ID];
ShadowAtlasLight& atlasLight = shadows.Lights[light->ID];
if (IS_LIGHT_TILE_REUSABLE)
continue;
// Remove existing tiles
for (auto& tile : atlasLight.Tiles)
{
if (tile)
{
tile->Free(&shadows);
tile = nullptr;
}
}
for (ShadowAtlasLightTile& tile : atlasLight.Tiles)
tile.Free(&shadows);
}
// Insert tiles into the atlas (already sorted to favor the first ones)
for (RenderLightData* light : shadowedLights)
{
auto& atlasLight = shadows.Lights[light->ID];
if (IS_LIGHT_TILE_REUSABLE || atlasLight.Resolution < 16)
if (IS_LIGHT_TILE_REUSABLE || atlasLight.Resolution < SHADOWS_MIN_RESOLUTION)
continue;
// Try to insert tiles
bool failedToInsert = false;
for (int32 tileIndex = 0; tileIndex < atlasLight.TilesNeeded; tileIndex++)
{
auto tile = shadows.AtlasTiles->Insert(atlasLight.Resolution, atlasLight.Resolution, 0, &shadows);
if (!tile)
auto rectTile = shadows.AtlasTiles->Insert(atlasLight.Resolution, atlasLight.Resolution, 0, &shadows);
if (!rectTile)
{
// Free any previous tiles that were added
for (int32 i = 0; i < tileIndex; i++)
{
atlasLight.Tiles[i]->Free(&shadows);
atlasLight.Tiles[i] = nullptr;
}
atlasLight.Tiles[i].Free(&shadows);
failedToInsert = true;
break;
}
atlasLight.Tiles[tileIndex] = tile;
atlasLight.Tiles[tileIndex].RectTile = rectTile;
}
if (failedToInsert)
{
@@ -757,7 +900,7 @@ RETRY_ATLAS_SETUP:
// Rebuild atlas
shadows.ClearTiles();
shadows.AtlasTiles = New<ShadowsAtlasTile>(0, 0, atlasResolution, atlasResolution);
shadows.AtlasTiles = New<ShadowsAtlasRectTile>(0, 0, atlasResolution, atlasResolution);
goto RETRY_ATLAS_SETUP;
}
}
@@ -766,9 +909,14 @@ RETRY_ATLAS_SETUP:
for (RenderLightData* light : shadowedLights)
{
auto& atlasLight = shadows.Lights[light->ID];
if (atlasLight.Tiles[0] && atlasLight.Tiles[0]->Width == atlasLight.Resolution)
if (atlasLight.Tiles[0].RectTile && atlasLight.Tiles[0].RectTile->Width == atlasLight.Resolution)
{
// Invalidate cache when whole atlas will be cleared
if (shadows.ClearShadowMapAtlas)
atlasLight.Cache.Valid = false;
light->HasShadow = true;
atlasLight.TilesCount = atlasLight.TilesNeeded;
if (light->IsPointLight)
SetupLight(renderContext, renderContextBatch, *(RenderPointLightData*)light, atlasLight);
else if (light->IsSpotLight)
@@ -787,7 +935,7 @@ RETRY_ATLAS_SETUP:
for (RenderLightData* light : shadowedLights)
{
auto& atlasLight = shadows.Lights[light->ID];
if (atlasLight.Tiles[0] == nullptr)
if (atlasLight.Tiles[0].RectTile == nullptr)
{
light->ShadowsBufferAddress = 0; // Clear to indicate no shadow
continue;
@@ -797,26 +945,24 @@ RETRY_ATLAS_SETUP:
light->ShadowsBufferAddress = shadows.ShadowsBuffer.Data.Count() / sizeof(Float4);
// Write shadow data (this must match HLSL)
const int32 tilesCount = atlasLight.ContextCount;
{
// Shadow info
auto* packed = shadows.ShadowsBuffer.WriteReserve<Float4>(2);
Color32 packed0x((byte)(atlasLight.Sharpness * (255.0f / 10.0f)), (byte)(atlasLight.Fade * 255.0f), tilesCount, 0);
Color32 packed0x((byte)(atlasLight.Sharpness * (255.0f / 10.0f)), (byte)(atlasLight.Fade * 255.0f), (byte)atlasLight.TilesCount, 0);
packed[0] = Float4(*(const float*)&packed0x, atlasLight.FadeDistance, atlasLight.NormalOffsetScale, atlasLight.Bias);
packed[1] = atlasLight.CascadeSplits;
}
for (int32 tileIndex = 0; tileIndex < tilesCount; tileIndex++)
for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++)
{
// Shadow projection info
const ShadowsAtlasTile* tile = atlasLight.Tiles[tileIndex];
ASSERT(tile);
const Matrix& worldToShadow = atlasLight.WorldToShadow[tileIndex];
const ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex];
ASSERT(tile.RectTile);
auto* packed = shadows.ShadowsBuffer.WriteReserve<Float4>(5);
packed[0] = Float4(tile->Width, tile->Height, tile->X, tile->Y) * atlasResolutionInv; // UV to AtlasUV via a single MAD instruction
packed[1] = worldToShadow.GetColumn1();
packed[2] = worldToShadow.GetColumn2();
packed[3] = worldToShadow.GetColumn3();
packed[4] = worldToShadow.GetColumn4();
packed[0] = Float4(tile.RectTile->Width, tile.RectTile->Height, tile.RectTile->X, tile.RectTile->Y) * atlasResolutionInv; // UV to AtlasUV via a single MAD instruction
packed[1] = tile.WorldToShadow.GetColumn1();
packed[2] = tile.WorldToShadow.GetColumn2();
packed[3] = tile.WorldToShadow.GetColumn3();
packed[4] = tile.WorldToShadow.GetColumn4();
}
}
GPUContext* context = GPUDevice::Instance->GetMainContext();
@@ -852,14 +998,18 @@ void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch)
for (auto& e : shadows.Lights)
{
const ShadowAtlasLight& atlasLight = e.Value;
for (int32 tileIndex = 0; tileIndex < atlasLight.ContextCount; tileIndex++)
int32 contextIndex = 0;
for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++)
{
const ShadowsAtlasTile* tile = atlasLight.Tiles[tileIndex];
if (!tile)
const ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex];
if (!tile.RectTile)
break;
if (tile.SkipUpdate)
continue;
// Set viewport for tile
context->SetViewportAndScissors(Viewport(tile->X, tile->Y, tile->Width, tile->Height));
ASSERT_LOW_LAYER(tile.CachedViewport == Viewport(tile.RectTile->X, tile.RectTile->Y, tile.RectTile->Width, tile.RectTile->Height));
context->SetViewportAndScissors(tile.CachedViewport);
if (!shadows.ClearShadowMapAtlas)
{
@@ -870,7 +1020,7 @@ void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch)
}
// Draw objects depth
auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + tileIndex];
auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++];
shadowContext.List->ExecuteDrawCalls(shadowContext, DrawCallsListType::Depth);
shadowContext.List->ExecuteDrawCalls(shadowContext, shadowContext.List->ShadowDepthDrawCallsList, renderContext.List->DrawCalls, nullptr);
}

View File

@@ -52,12 +52,12 @@ public:
static void GetShadowAtlas(const RenderBuffers* renderBuffers, GPUTexture*& shadowMapAtlas, GPUBufferView*& shadowsBuffer);
private:
void SetupRenderContext(RenderContext& renderContext, RenderContext& shadowContext);
void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLightData& light, struct ShadowAtlasLight& atlasLight);
void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLocalLightData& light, ShadowAtlasLight& atlasLight);
void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderDirectionalLightData& light, ShadowAtlasLight& atlasLight);
void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderPointLightData& light, ShadowAtlasLight& atlasLight);
void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderSpotLightData& light, ShadowAtlasLight& atlasLight);
static void SetupRenderContext(RenderContext& renderContext, RenderContext& shadowContext);
static void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLightData& light, struct ShadowAtlasLight& atlasLight);
static bool SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLocalLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderDirectionalLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderPointLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderSpotLightData& light, ShadowAtlasLight& atlasLight);
#if COMPILE_WITH_DEV_ENV
void OnShaderReloading(Asset* obj)

View File

@@ -146,8 +146,8 @@ ShadowSample SampleDirectionalLightShadow(LightData light, Buffer<float4> shadow
result.SurfaceShadow = SampleShadowMap(shadowMap, shadowMapUV, shadowPosition.z);
// Increase the sharpness for higher cascades to match the filter radius
const float SharpnessScale[MaxNumCascades] = { 1.0f, 1.5f, 3.0f, 3.5f };
shadow.Sharpness *= SharpnessScale[cascadeIndex];
//const float SharpnessScale[MaxNumCascades] = { 1.0f, 1.5f, 3.0f, 3.5f };
//shadow.Sharpness *= SharpnessScale[cascadeIndex];
#if defined(USE_GBUFFER_CUSTOM_DATA)
// Subsurface shadowing