From 443ce106e2258d4f3b54f8fb46dc4652290fb7c7 Mon Sep 17 00:00:00 2001 From: Wojciech Figat Date: Tue, 28 Jun 2022 10:38:51 +0200 Subject: [PATCH] Optimize Global Surface Atlas objects lighting to be less frequent with caching for static lights and objects --- .../Engine/Level/Actors/DirectionalLight.cpp | 2 + Source/Engine/Level/Actors/PointLight.cpp | 2 + Source/Engine/Level/Actors/SkyLight.cpp | 2 + Source/Engine/Level/Actors/SpotLight.cpp | 2 + .../Renderer/GI/GlobalSurfaceAtlasPass.cpp | 186 ++++++++++++++++-- .../Renderer/GI/GlobalSurfaceAtlasPass.h | 1 + Source/Engine/Renderer/RenderList.h | 18 +- Source/Shaders/GI/GlobalSurfaceAtlas.shader | 18 +- 8 files changed, 205 insertions(+), 26 deletions(-) diff --git a/Source/Engine/Level/Actors/DirectionalLight.cpp b/Source/Engine/Level/Actors/DirectionalLight.cpp index 5b8d1d51f..dc24df922 100644 --- a/Source/Engine/Level/Actors/DirectionalLight.cpp +++ b/Source/Engine/Level/Actors/DirectionalLight.cpp @@ -41,6 +41,8 @@ void DirectionalLight::Draw(RenderContext& renderContext) data.ShadowsMode = ShadowsMode; data.CascadeCount = CascadeCount; data.ContactShadowsLength = ContactShadowsLength; + data.StaticFlags = GetStaticFlags(); + data.ID = GetID(); renderContext.List->DirectionalLights.Add(data); } } diff --git a/Source/Engine/Level/Actors/PointLight.cpp b/Source/Engine/Level/Actors/PointLight.cpp index 02eb4b517..36a92e8ae 100644 --- a/Source/Engine/Level/Actors/PointLight.cpp +++ b/Source/Engine/Level/Actors/PointLight.cpp @@ -133,6 +133,8 @@ void PointLight::Draw(RenderContext& renderContext) data.ContactShadowsLength = ContactShadowsLength; data.IndirectLightingIntensity = IndirectLightingIntensity; data.IESTexture = IESTexture ? IESTexture->GetTexture() : nullptr; + data.StaticFlags = GetStaticFlags(); + data.ID = GetID(); renderContext.List->PointLights.Add(data); } } diff --git a/Source/Engine/Level/Actors/SkyLight.cpp b/Source/Engine/Level/Actors/SkyLight.cpp index 9a86ff704..1da09de47 100644 --- a/Source/Engine/Level/Actors/SkyLight.cpp +++ b/Source/Engine/Level/Actors/SkyLight.cpp @@ -115,6 +115,8 @@ void SkyLight::Draw(RenderContext& renderContext) data.IndirectLightingIntensity = IndirectLightingIntensity; data.Radius = GetScaledRadius(); data.Image = GetSource(); + data.StaticFlags = GetStaticFlags(); + data.ID = GetID(); renderContext.List->SkyLights.Add(data); } } diff --git a/Source/Engine/Level/Actors/SpotLight.cpp b/Source/Engine/Level/Actors/SpotLight.cpp index a01a6e584..f5da52c98 100644 --- a/Source/Engine/Level/Actors/SpotLight.cpp +++ b/Source/Engine/Level/Actors/SpotLight.cpp @@ -186,6 +186,8 @@ void SpotLight::Draw(RenderContext& renderContext) data.IESTexture = IESTexture ? IESTexture->GetTexture() : nullptr; Float3::Transform(Float3::Up, GetOrientation(), data.UpVector); data.OuterConeAngle = outerConeAngle; + data.StaticFlags = GetStaticFlags(); + data.ID = GetID(); renderContext.List->SpotLights.Add(data); } } diff --git a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp index c8814c7a6..f34de0629 100644 --- a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp +++ b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp @@ -86,7 +86,8 @@ struct GlobalSurfaceAtlasTile : RectPack struct GlobalSurfaceAtlasObject { uint64 LastFrameUsed; - uint64 LastFrameDirty; + uint64 LastFrameUpdated; + uint64 LightingUpdateFrame; // Index of the frame to update lighting for this object (calculated when object gets dirty or overriden by dynamic lights) Actor* Actor; GlobalSurfaceAtlasTile* Tiles[6]; float Radius; @@ -120,6 +121,12 @@ struct GlobalSurfaceAtlasObject } }; +struct GlobalSurfaceAtlasLight +{ + uint64 LastFrameUsed = 0; + uint64 LastFrameUpdated = 0; +}; + class GlobalSurfaceAtlasCustomBuffer : public RenderBuffers::CustomBuffer, public ISceneRenderingListener { public: @@ -139,6 +146,7 @@ public: GlobalSurfaceAtlasPass::BindingData Result; GlobalSurfaceAtlasTile* AtlasTiles = nullptr; // TODO: optimize with a single allocation for atlas tiles Dictionary Objects; + Dictionary Lights; // Cached data to be reused during RasterizeActor uint64 CurrentFrame; @@ -160,6 +168,7 @@ public: LastFrameAtlasDefragmentation = Engine::FrameCount; SAFE_DELETE(AtlasTiles); Objects.Clear(); + Lights.Clear(); } FORCE_INLINE void Clear() @@ -194,7 +203,13 @@ public: if (object) { // Dirty object to redraw - object->LastFrameDirty = 0; + object->LastFrameUpdated = 0; + } + GlobalSurfaceAtlasLight* light = Lights.TryGet(a->GetID()); + if (light) + { + // Dirty light to redraw + light->LastFrameUpdated = 0; } } } @@ -271,12 +286,20 @@ bool GlobalSurfaceAtlasPass::setupResources() if (_psClear->Init(psDesc)) return true; } + psDesc.DepthTestEnable = false; + psDesc.DepthWriteEnable = false; + psDesc.DepthFunc = ComparisonFunc::Never; + if (!_psClearLighting) + { + _psClearLighting = device->CreatePipelineState(); + psDesc.VS = shader->GetVS("VS_Atlas"); + psDesc.PS = shader->GetPS("PS_ClearLighting"); + if (_psClearLighting->Init(psDesc)) + return true; + } if (!_psDirectLighting0) { _psDirectLighting0 = device->CreatePipelineState(); - psDesc.DepthTestEnable = false; - psDesc.DepthWriteEnable = false; - psDesc.DepthFunc = ComparisonFunc::Never; psDesc.BlendMode = BlendingMode::Add; psDesc.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB; psDesc.PS = shader->GetPS("PS_Lighting", 0); @@ -300,6 +323,7 @@ bool GlobalSurfaceAtlasPass::setupResources() void GlobalSurfaceAtlasPass::OnShaderReloading(Asset* obj) { SAFE_DELETE_GPU_RESOURCE(_psClear); + SAFE_DELETE_GPU_RESOURCE(_psClearLighting); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting0); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting1); SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting); @@ -317,6 +341,7 @@ void GlobalSurfaceAtlasPass::Dispose() SAFE_DELETE(_vertexBuffer); SAFE_DELETE_GPU_RESOURCE(_culledObjectsSizeBuffer); SAFE_DELETE_GPU_RESOURCE(_psClear); + SAFE_DELETE_GPU_RESOURCE(_psClearLighting); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting0); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting1); SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting); @@ -744,14 +769,6 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co if (surfaceAtlasData.Objects.Count() != 0) { PROFILE_GPU_CPU("Direct Lighting"); - - // Copy emissive light into the final direct lighting atlas - // TODO: test perf diff when manually copying only dirty object tiles and dirty light tiles together with indirect lighting - { - PROFILE_GPU_CPU("Copy Emissive"); - context->CopyTexture(surfaceAtlasData.AtlasLighting, 0, 0, 0, 0, surfaceAtlasData.AtlasEmissive, 0); - } - context->SetViewportAndScissors(Viewport(0, 0, (float)resolution, (float)resolution)); context->SetRenderTarget(surfaceAtlasData.AtlasLighting->View()); context->BindSR(0, surfaceAtlasData.AtlasGBuffer0->View()); @@ -767,8 +784,114 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co data.GlobalSDF = bindingDataSDF.Constants; data.GlobalSurfaceAtlas = result.Constants; + // Collect objects to update lighting this frame (dirty objects and dirty lights) + bool allLightingDirty = false; + for (auto& light : renderContext.List->DirectionalLights) + { + GlobalSurfaceAtlasLight& lightData = surfaceAtlasData.Lights[light.ID]; + lightData.LastFrameUsed = currentFrame; + uint32 redrawFramesCount = (light.StaticFlags & StaticFlags::Lightmap) ? 120 : 4; + if (surfaceAtlasData.CurrentFrame - lightData.LastFrameUpdated < (redrawFramesCount + (light.ID.D & redrawFramesCount))) + continue; + lightData.LastFrameUpdated = currentFrame; + + // Mark all objects to shade + allLightingDirty = true; + } + if (renderContext.View.Flags & ViewFlags::GI && (renderContext.List->DirectionalLights.Count() != 1 || renderContext.List->DirectionalLights[0].StaticFlags & StaticFlags::Lightmap)) + { + switch (renderContext.List->Settings.GlobalIllumination.Mode) + { + case GlobalIlluminationMode::DDGI: + { + DynamicDiffuseGlobalIlluminationPass::BindingData bindingDataDDGI; + if (!DynamicDiffuseGlobalIlluminationPass::Instance()->Get(renderContext.Buffers, bindingDataDDGI)) + { + GlobalSurfaceAtlasLight& lightData = surfaceAtlasData.Lights[Guid(0, 0, 0, 1)]; + lightData.LastFrameUsed = currentFrame; + uint32 redrawFramesCount = 4; // GI Bounce redraw minimum frequency + if (surfaceAtlasData.CurrentFrame - lightData.LastFrameUpdated < redrawFramesCount) + break; + lightData.LastFrameUpdated = currentFrame; + + // Mark all objects to shade + allLightingDirty = true; + } + break; + } + } + } + for (auto& light : renderContext.List->PointLights) + { + GlobalSurfaceAtlasLight& lightData = surfaceAtlasData.Lights[light.ID]; + lightData.LastFrameUsed = currentFrame; + uint32 redrawFramesCount = (light.StaticFlags & StaticFlags::Lightmap) ? 120 : 4; + if (surfaceAtlasData.CurrentFrame - lightData.LastFrameUpdated < (redrawFramesCount + (light.ID.D & redrawFramesCount))) + continue; + lightData.LastFrameUpdated = currentFrame; + + if (!allLightingDirty) + { + // Mark objects to shade + for (auto& e : surfaceAtlasData.Objects) + { + auto& object = e.Value; + Float3 lightToObject = object.Bounds.GetCenter() - light.Position; + if (lightToObject.LengthSquared() >= Math::Square(object.Radius + light.Radius)) + continue; + object.LightingUpdateFrame = currentFrame; + } + } + } + for (auto& light : renderContext.List->SpotLights) + { + GlobalSurfaceAtlasLight& lightData = surfaceAtlasData.Lights[light.ID]; + lightData.LastFrameUsed = currentFrame; + uint32 redrawFramesCount = (light.StaticFlags & StaticFlags::Lightmap) ? 120 : 4; + if (surfaceAtlasData.CurrentFrame - lightData.LastFrameUpdated < (redrawFramesCount + (light.ID.D & redrawFramesCount))) + continue; + lightData.LastFrameUpdated = currentFrame; + + if (!allLightingDirty) + { + // Mark objects to shade + for (auto& e : surfaceAtlasData.Objects) + { + auto& object = e.Value; + Float3 lightToObject = object.Bounds.GetCenter() - light.Position; + if (lightToObject.LengthSquared() >= Math::Square(object.Radius + light.Radius)) + continue; + object.LightingUpdateFrame = currentFrame; + } + } + } + + // Copy emissive light into the final direct lighting atlas + { + PROFILE_GPU_CPU("Copy Emissive"); + _vertexBuffer->Clear(); + for (const auto& e : surfaceAtlasData.Objects) + { + const auto& object = e.Value; + if (!allLightingDirty && object.LightingUpdateFrame != currentFrame) + continue; + for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) + { + auto* tile = object.Tiles[tileIndex]; + if (!tile) + continue; + VB_WRITE_TILE(tile); + } + } + if (_vertexBuffer->Data.Count() != 0) + { + context->BindSR(7, surfaceAtlasData.AtlasEmissive); + context->SetState(_psClearLighting); + VB_DRAW(); + } + } + // Shade object tiles influenced by lights to calculate direct lighting - // TODO: reduce redraw frequency for static lights (StaticFlags::Lightmap) for (auto& light : renderContext.List->DirectionalLights) { // Collect tiles to shade @@ -776,6 +899,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; + if (!allLightingDirty && object.LightingUpdateFrame != currentFrame) + continue; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; @@ -784,8 +909,11 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co VB_WRITE_TILE(tile); } } + if (_vertexBuffer->Data.Count() == 0) + continue; // Draw draw light + PROFILE_GPU_CPU("Directional Light"); const bool useShadow = CanRenderShadow(renderContext.View, light); // TODO: test perf/quality when using Shadow Map for directional light (ShadowsPass::Instance()->LastDirLightShadowMap) instead of Global SDF trace light.SetupLightData(&data.Light, useShadow); @@ -802,6 +930,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; + if (!allLightingDirty && object.LightingUpdateFrame != currentFrame) + continue; Float3 lightToObject = object.Bounds.GetCenter() - light.Position; if (lightToObject.LengthSquared() >= Math::Square(object.Radius + light.Radius)) continue; @@ -813,8 +943,11 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co VB_WRITE_TILE(tile); } } + if (_vertexBuffer->Data.Count() == 0) + continue; // Draw draw light + PROFILE_GPU_CPU("Point Light"); const bool useShadow = CanRenderShadow(renderContext.View, light); light.SetupLightData(&data.Light, useShadow); data.Light.Color *= light.IndirectLightingIntensity; @@ -830,6 +963,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; + if (!allLightingDirty && object.LightingUpdateFrame != currentFrame) + continue; Float3 lightToObject = object.Bounds.GetCenter() - light.Position; if (lightToObject.LengthSquared() >= Math::Square(object.Radius + light.Radius)) continue; @@ -841,8 +976,11 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co VB_WRITE_TILE(tile); } } + if (_vertexBuffer->Data.Count() == 0) + continue; // Draw draw light + PROFILE_GPU_CPU("Spot Light"); const bool useShadow = CanRenderShadow(renderContext.View, light); light.SetupLightData(&data.Light, useShadow); data.Light.Color *= light.IndirectLightingIntensity; @@ -851,9 +989,17 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co context->SetState(_psDirectLighting1); VB_DRAW(); } + + // Remove unused lights + for (auto it = surfaceAtlasData.Lights.Begin(); it.IsNotEnd(); ++it) + { + if (it->Value.LastFrameUsed != currentFrame) + surfaceAtlasData.Lights.Remove(it); + } + + // Draw draw indirect light from Global Illumination if (renderContext.View.Flags & ViewFlags::GI) { - // Draw draw indirect light from Global Illumination switch (renderContext.List->Settings.GlobalIllumination.Mode) { case GlobalIlluminationMode::DDGI: @@ -865,6 +1011,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; + if (!allLightingDirty && object.LightingUpdateFrame != currentFrame) + continue; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; @@ -873,6 +1021,9 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co VB_WRITE_TILE(tile); } } + if (_vertexBuffer->Data.Count() == 0) + break; + PROFILE_GPU_CPU("DDGI"); data.DDGI = bindingDataDDGI.Constants; context->BindSR(5, bindingDataDDGI.ProbesState); context->BindSR(6, bindingDataDDGI.ProbesDistance); @@ -1065,7 +1216,7 @@ void GlobalSurfaceAtlasPass::RasterizeActor(Actor* actor, void* actorObject, con // Redraw objects from time-to-time (dynamic objects can be animated, static objects can have textures streamed) uint32 redrawFramesCount = actor->HasStaticFlag(StaticFlags::Lightmap) ? 120 : 4; - if (surfaceAtlasData.CurrentFrame - object->LastFrameDirty >= (redrawFramesCount + (actor->GetID().D & redrawFramesCount))) + if (surfaceAtlasData.CurrentFrame - object->LastFrameUpdated >= (redrawFramesCount + (actor->GetID().D & redrawFramesCount))) dirty = true; // Mark object as used @@ -1076,7 +1227,8 @@ void GlobalSurfaceAtlasPass::RasterizeActor(Actor* actor, void* actorObject, con object->Radius = (float)actorObjectBounds.Radius; if (dirty || GLOBAL_SURFACE_ATLAS_DEBUG_FORCE_REDRAW_TILES) { - object->LastFrameDirty = surfaceAtlasData.CurrentFrame; + object->LastFrameUpdated = surfaceAtlasData.CurrentFrame; + object->LightingUpdateFrame = surfaceAtlasData.CurrentFrame; _dirtyObjectsBuffer.Add(actorObject); } diff --git a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.h b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.h index 0e971af64..70a0d7296 100644 --- a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.h +++ b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.h @@ -46,6 +46,7 @@ private: bool _supported = false; AssetReference _shader; GPUPipelineState* _psClear = nullptr; + GPUPipelineState* _psClearLighting = nullptr; GPUPipelineState* _psDirectLighting0 = nullptr; GPUPipelineState* _psDirectLighting1 = nullptr; GPUPipelineState* _psIndirectLighting = nullptr; diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index 7401f77b5..7c81eacf4 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -34,6 +34,7 @@ struct RendererDirectionalLightData float ShadowsSharpness; float VolumetricScatteringIntensity; + StaticFlags StaticFlags; float IndirectLightingIntensity; int8 CastVolumetricShadow : 1; int8 RenderedVolumetricFog : 1; @@ -43,6 +44,8 @@ struct RendererDirectionalLightData float ContactShadowsLength; ShadowsCastingMode ShadowsMode; + Guid ID; + void SetupLightData(LightData* data, bool useShadow) const; }; @@ -76,11 +79,13 @@ struct RendererSpotLightData float IndirectLightingIntensity; ShadowsCastingMode ShadowsMode; + StaticFlags StaticFlags; int8 CastVolumetricShadow : 1; int8 RenderedVolumetricFog : 1; int8 UseInverseSquaredFalloff : 1; GPUTexture* IESTexture; + Guid ID; void SetupLightData(LightData* data, bool useShadow) const; }; @@ -111,11 +116,13 @@ struct RendererPointLightData float IndirectLightingIntensity; ShadowsCastingMode ShadowsMode; + StaticFlags StaticFlags; int8 CastVolumetricShadow : 1; int8 RenderedVolumetricFog : 1; int8 UseInverseSquaredFalloff : 1; GPUTexture* IESTexture; + Guid ID; void SetupLightData(LightData* data, bool useShadow) const; }; @@ -131,10 +138,12 @@ struct RendererSkyLightData Float3 AdditiveColor; float IndirectLightingIntensity; + StaticFlags StaticFlags; int8 CastVolumetricShadow : 1; int8 RenderedVolumetricFog : 1; CubeTexture* Image; + Guid ID; void SetupLightData(LightData* data, bool useShadow) const; }; @@ -211,7 +220,6 @@ struct DrawBatch class RenderListAllocation { public: - static FLAXENGINE_API void* Allocate(uintptr size); static FLAXENGINE_API void Free(void* ptr, uintptr size); @@ -222,7 +230,6 @@ public: uintptr _size; public: - FORCE_INLINE Data() { } @@ -329,7 +336,7 @@ struct DrawCallsList /// API_CLASS(Sealed) class FLAXENGINE_API RenderList : public ScriptingObject { -DECLARE_SCRIPTING_TYPE(RenderList); + DECLARE_SCRIPTING_TYPE(RenderList); /// /// Allocates the new renderer list object or reuses already allocated one. @@ -349,7 +356,6 @@ DECLARE_SCRIPTING_TYPE(RenderList); static void CleanupCache(); public: - /// /// All scenes for rendering. /// @@ -458,11 +464,9 @@ public: Float3 FrustumCornersVs[8]; private: - DynamicVertexBuffer _instanceBuffer; public: - /// /// Blends the postprocessing settings into the final options. /// @@ -527,7 +531,6 @@ public: } public: - /// /// Init cache for given task /// @@ -540,7 +543,6 @@ public: void Clear(); public: - /// /// Adds the draw call to the draw lists. /// diff --git a/Source/Shaders/GI/GlobalSurfaceAtlas.shader b/Source/Shaders/GI/GlobalSurfaceAtlas.shader index 07f48d2ae..9d1f201a8 100644 --- a/Source/Shaders/GI/GlobalSurfaceAtlas.shader +++ b/Source/Shaders/GI/GlobalSurfaceAtlas.shader @@ -62,6 +62,22 @@ void PS_Clear(out float4 Light : SV_Target0, out float4 RT0 : SV_Target1, out fl RT2 = float4(1, 0, 0, 0); } +#ifdef _PS_ClearLighting + +Buffer GlobalSurfaceAtlasObjects : register(t4); +Texture2D Texture : register(t7); + +// Pixel shader for Global Surface Atlas clearing +META_PS(true, FEATURE_LEVEL_SM5) +float4 PS_ClearLighting(AtlasVertexOutput input) : SV_Target +{ + GlobalSurfaceTile tile = LoadGlobalSurfaceAtlasTile(GlobalSurfaceAtlasObjects, input.TileAddress); + float2 atlasUV = input.TileUV * tile.AtlasRectUV.zw + tile.AtlasRectUV.xy; + return Texture.Sample(SamplerPointClamp, atlasUV); +} + +#endif + #ifdef _PS_Lighting #include "./Flax/GBuffer.hlsl" @@ -79,7 +95,7 @@ Texture3D GlobalSDFTex : register(t5); Texture3D GlobalSDFMip : register(t6); #endif -// Pixel shader for Global Surface Atlas shading with direct light contribution +// Pixel shader for Global Surface Atlas shading META_PS(true, FEATURE_LEVEL_SM5) META_PERMUTATION_1(RADIAL_LIGHT=0) META_PERMUTATION_1(RADIAL_LIGHT=1)