// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#include "VolumetricFogPass.h"
#include "ShadowsPass.h"
#include "GBufferPass.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/GPULimits.h"
#include "Engine/Graphics/RenderTargetPool.h"
#include "Engine/Content/Assets/CubeTexture.h"
#include "Engine/Content/Content.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Graphics/GPUContext.h"

// Must match shader source
int32 VolumetricFogGridInjectionGroupSize = 4;
int32 VolumetricFogIntegrationGroupSize = 8;

VolumetricFogPass::VolumetricFogPass()
    : _shader(nullptr)
{
}

String VolumetricFogPass::ToString() const
{
    return TEXT("VolumetricFogPass");
}

bool VolumetricFogPass::Init()
{
    const auto& limits = GPUDevice::Instance->Limits;
    _isSupported = limits.HasGeometryShaders && limits.HasVolumeTextureRendering && limits.HasCompute && limits.HasInstancing;

    // Create pipeline states
    _psInjectLight.CreatePipelineStates();

    // Load assets
    _shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/VolumetricFog"));
    if (_shader == nullptr)
    {
        return true;
    }
#if COMPILE_WITH_DEV_ENV
    _shader.Get()->OnReloading.Bind<VolumetricFogPass, &VolumetricFogPass::OnShaderReloading>(this);
#endif

    return false;
}

bool VolumetricFogPass::setupResources()
{
    if (!_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;
    }
    if (shader->GetCB(1)->GetSize() != sizeof(PerLight))
    {
        REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 1, PerLight);
        return true;
    }

    // Cache compute shaders
    _csInitialize = shader->GetCS("CS_Initialize");
    _csLightScattering.Get(shader, "CS_LightScattering");
    _csFinalIntegration = shader->GetCS("CS_FinalIntegration");

    // Create pipeline stages
    GPUPipelineState::Description psDesc;
    if (!_psInjectLight.IsValid())
    {
        psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
        psDesc.BlendMode = BlendingMode::Add;
        psDesc.VS = shader->GetVS("VS_WriteToSlice");
        psDesc.GS = shader->GetGS("GS_WriteToSlice");
        if (_psInjectLight.Create(psDesc, shader, "PS_InjectLight"))
            return true;
    }

    return false;
}

void VolumetricFogPass::Dispose()
{
    // Base
    RendererPass::Dispose();

    // Cleanup
    _psInjectLight.Delete();
    _csInitialize = nullptr;
    _csLightScattering.Clear();
    _csFinalIntegration = nullptr;
    SAFE_DELETE_GPU_RESOURCE(_vbCircleRasterize);
    SAFE_DELETE_GPU_RESOURCE(_ibCircleRasterize);
    _shader = nullptr;
}

float ComputeZSliceFromDepth(float sceneDepth, const VolumetricFogOptions& options, int32 gridSizeZ)
{
    return sceneDepth / options.Distance * (float)gridSizeZ;
}

bool VolumetricFogPass::Init(RenderContext& renderContext, GPUContext* context, VolumetricFogOptions& options)
{
    auto& view = renderContext.View;
    const auto fog = renderContext.List->Fog;

    // Check if already prepared for this frame
    if (renderContext.Buffers->LastFrameVolumetricFog == Engine::FrameCount)
    {
        if (fog)
            fog->GetVolumetricFogOptions(options);
        return false;
    }

    // Check if skip rendering
    if (fog == nullptr || (view.Flags & ViewFlags::Fog) == ViewFlags::None || !_isSupported || checkIfSkipPass())
    {
        RenderTargetPool::Release(renderContext.Buffers->VolumetricFog);
        renderContext.Buffers->VolumetricFog = nullptr;
        renderContext.Buffers->LastFrameVolumetricFog = 0;
        return true;
    }
    fog->GetVolumetricFogOptions(options);
    if (!options.UseVolumetricFog())
    {
        RenderTargetPool::Release(renderContext.Buffers->VolumetricFog);
        renderContext.Buffers->VolumetricFog = nullptr;
        renderContext.Buffers->LastFrameVolumetricFog = 0;
        return true;
    }

    // Setup configuration
    _cache.HistoryWeight = 0.9f;
    _cache.InverseSquaredLightDistanceBiasScale = 1.0f;
    const auto quality = Graphics::VolumetricFogQuality;
    switch (quality)
    {
    case Quality::Low:
    {
        _cache.GridPixelSize = 16;
        _cache.GridSizeZ = 64;
        _cache.FogJitter = false;
        _cache.MissedHistorySamplesCount = 1;
        break;
    }
    case Quality::Medium:
    {
        _cache.GridPixelSize = 16;
        _cache.GridSizeZ = 64;
        _cache.FogJitter = true;
        _cache.MissedHistorySamplesCount = 4;
        break;
    }
    case Quality::High:
    {
        _cache.GridPixelSize = 16;
        _cache.GridSizeZ = 128;
        _cache.FogJitter = true;
        _cache.MissedHistorySamplesCount = 4;
        break;
    }
    case Quality::Ultra:
    {
        _cache.GridPixelSize = 8;
        _cache.GridSizeZ = 256;
        _cache.FogJitter = true;
        _cache.MissedHistorySamplesCount = 8;
        break;
    }
    }

    // Prepare
    const int32 width = renderContext.Buffers->GetWidth();
    const int32 height = renderContext.Buffers->GetHeight();
    _cache.GridSize = Float3(
        (float)Math::DivideAndRoundUp(width, _cache.GridPixelSize),
        (float)Math::DivideAndRoundUp(height, _cache.GridPixelSize),
        (float)_cache.GridSizeZ);
    auto& fogData = renderContext.Buffers->VolumetricFogData;
    fogData.MaxDistance = options.Distance;

    // Init data (partial, without directional light or sky light data);
    GBufferPass::SetInputs(renderContext.View, _cache.Data.GBuffer);
    _cache.Data.GlobalAlbedo = options.Albedo.ToFloat3() * options.Albedo.A;
    _cache.Data.GlobalExtinctionScale = options.ExtinctionScale;
    _cache.Data.GlobalEmissive = options.Emissive.ToFloat3() * options.Emissive.A;
    _cache.Data.GridSize = _cache.GridSize;
    _cache.Data.GridSizeIntX = (uint32)_cache.GridSize.X;
    _cache.Data.GridSizeIntY = (uint32)_cache.GridSize.Y;
    _cache.Data.GridSizeIntZ = (uint32)_cache.GridSize.Z;
    _cache.Data.HistoryWeight = _cache.HistoryWeight;
    _cache.Data.FogParameters = options.FogParameters;
    _cache.Data.InverseSquaredLightDistanceBiasScale = _cache.InverseSquaredLightDistanceBiasScale;
    _cache.Data.PhaseG = options.ScatteringDistribution;
    _cache.Data.VolumetricFogMaxDistance = options.Distance;
    _cache.Data.MissedHistorySamplesCount = Math::Clamp(_cache.MissedHistorySamplesCount, 1, (int32)ARRAY_COUNT(_cache.Data.FrameJitterOffsets));
    Matrix::Transpose(view.PrevViewProjection, _cache.Data.PrevWorldToClip);
    _cache.Data.DirectionalLightShadow.NumCascades = 0;
    _cache.Data.SkyLight.VolumetricScatteringIntensity = 0;

    // Fill frame jitter history
    const Float4 defaultOffset(0.5f, 0.5f, 0.5f, 0.0f);
    for (int32 i = 0; i < ARRAY_COUNT(_cache.Data.FrameJitterOffsets); i++)
    {
        _cache.Data.FrameJitterOffsets[i] = defaultOffset;
    }
    if (_cache.FogJitter)
    {
        for (int32 i = 0; i < _cache.MissedHistorySamplesCount; i++)
        {
            const uint64 frameNumber = renderContext.Task->LastUsedFrame - i;
            _cache.Data.FrameJitterOffsets[i] = Float4(
                RendererUtils::TemporalHalton(frameNumber & 1023, 2),
                RendererUtils::TemporalHalton(frameNumber & 1023, 3),
                RendererUtils::TemporalHalton(frameNumber & 1023, 5),
                0);
        }
    }

    // Set constant buffer data
    auto cb0 = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb0, &_cache.Data);

    // Clear local lights scattering table if was used and will be probably reused later
    if (renderContext.Buffers->LocalShadowedLightScattering)
    {
        if (Float3::NearEqual(renderContext.Buffers->LocalShadowedLightScattering->Size3(), _cache.GridSize))
        {
            context->Clear(renderContext.Buffers->LocalShadowedLightScattering->ViewVolume(), Color::Transparent);
        }
        else
        {
            RenderTargetPool::Release(renderContext.Buffers->LocalShadowedLightScattering);
            renderContext.Buffers->LocalShadowedLightScattering = nullptr;
        }
    }

    // Render fog this frame
    renderContext.Buffers->LastFrameVolumetricFog = Engine::FrameCount;
    return false;
}

GPUTextureView* VolumetricFogPass::GetLocalShadowedLightScattering(RenderContext& renderContext, GPUContext* context, VolumetricFogOptions& options) const
{
    if (renderContext.Buffers->LocalShadowedLightScattering == nullptr)
    {
        ASSERT(renderContext.Buffers->LastFrameVolumetricFog == Engine::FrameCount);
        const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(_cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess);
        const auto texture = RenderTargetPool::Get(volumeDescRGB);
        RENDER_TARGET_POOL_SET_NAME(texture, "VolumetricFog.LocalShadowedLightScattering");
        renderContext.Buffers->LocalShadowedLightScattering = texture;
        context->Clear(texture->ViewVolume(), Color::Transparent);
    }

    return renderContext.Buffers->LocalShadowedLightScattering->ViewVolume();
}

template<typename T>
void VolumetricFogPass::RenderRadialLight(RenderContext& renderContext, GPUContext* context, T& light, LightShadowData& shadow)
{
    // Prepare
    VolumetricFogOptions options;
    if (Init(renderContext, context, options))
        return;
    auto& view = renderContext.View;

    // Calculate light volume bounds in camera frustum depth range (min and max)
    const Float3 center = light.Position;
    const float radius = light.Radius;
    Float3 viewSpaceLightBoundsOrigin = Float3::Transform(center, view.View);
    float furthestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z + radius, options, _cache.GridSizeZ);
    float closestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z - radius, options, _cache.GridSizeZ);
    int32 volumeZBoundsMin = (int32)Math::Clamp(closestSliceIndexUnclamped, 0.0f, _cache.GridSize.Z - 1.0f);
    int32 volumeZBoundsMax = (int32)Math::Clamp(furthestSliceIndexUnclamped, 0.0f, _cache.GridSize.Z - 1.0f);

    // Cull light
    if ((view.Position - center).LengthSquared() >= (options.Distance + radius) * (options.Distance + radius) || volumeZBoundsMin >= volumeZBoundsMax)
        return;

    PROFILE_GPU_CPU("Volumetric Fog Light");

    // Allocate temporary buffer for light scattering injection
    auto localShadowedLightScattering = GetLocalShadowedLightScattering(renderContext, context, options);

    // Prepare
    PerLight perLight;
    auto cb0 = _shader->GetShader()->GetCB(0);
    auto cb1 = _shader->GetShader()->GetCB(1);

    // Bind the output
    context->SetRenderTarget(localShadowedLightScattering);
    context->SetViewportAndScissors(_cache.Data.GridSize.X, _cache.Data.GridSize.Y);

    // Setup data
    perLight.SliceToDepth.X = _cache.Data.GridSize.Z;
    perLight.SliceToDepth.Y = _cache.Data.VolumetricFogMaxDistance;
    perLight.MinZ = volumeZBoundsMin;
    perLight.LocalLightScatteringIntensity = light.VolumetricScatteringIntensity;
    perLight.ViewSpaceBoundingSphere = Float4(viewSpaceLightBoundsOrigin, radius);
    Matrix::Transpose(view.Projection, perLight.ViewToVolumeClip);
    light.SetupLightData(&perLight.LocalLight, true);
    perLight.LocalLightShadow = shadow;

    // Upload data
    context->UpdateCB(cb1, &perLight);
    context->BindCB(0, cb0);
    context->BindCB(1, cb1);

    // Ensure to have valid buffers created
    if (_vbCircleRasterize == nullptr || _ibCircleRasterize == nullptr)
        InitCircleBuffer();

    // Call rendering to the volume
    const int32 psIndex = 1;
    context->SetState(_psInjectLight.Get(psIndex));
    const int32 instanceCount = volumeZBoundsMax - volumeZBoundsMin;
    const int32 indexCount = _ibCircleRasterize->GetElementsCount();
    ASSERT(instanceCount > 0);
    context->BindVB(ToSpan(&_vbCircleRasterize, 1));
    context->BindIB(_ibCircleRasterize);
    context->DrawIndexedInstanced(indexCount, instanceCount, 0);

    // Cleanup
    context->UnBindCB(0);
    context->UnBindCB(1);
    auto viewport = renderContext.Task->GetViewport();
    context->SetViewportAndScissors(viewport);
    context->ResetRenderTarget();
    context->FlushState();

    // Mark as rendered
    light.RenderedVolumetricFog = 1;
}

template<typename T>
void VolumetricFogPass::RenderRadialLight(RenderContext& renderContext, GPUContext* context, RenderView& view, VolumetricFogOptions& options, T& light, PerLight& perLight, GPUConstantBuffer* cb1)
{
    const Float3 center = light.Position;
    const float radius = light.Radius;
    ASSERT(!center.IsNanOrInfinity() && !isnan(radius) && !isinf(radius));
    auto& cache = _cache;

    // Calculate light volume bounds in camera frustum depth range (min and max)
    const Float3 viewSpaceLightBoundsOrigin = Float3::Transform(center, view.View);
    const float furthestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z + radius, options, cache.GridSizeZ);
    const float closestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z - radius, options, cache.GridSizeZ);
    const int32 volumeZBoundsMin = (int32)Math::Clamp(closestSliceIndexUnclamped, 0.0f, cache.GridSize.Z - 1.0f);
    const int32 volumeZBoundsMax = (int32)Math::Clamp(furthestSliceIndexUnclamped, 0.0f, cache.GridSize.Z - 1.0f);

    if (volumeZBoundsMin < volumeZBoundsMax)
    {
        // TODO: use full scene shadows atlas and render point/spot lights with shadow into a fog volume
        bool withShadow = false;

        // Setup data
        perLight.SliceToDepth.X = cache.Data.GridSize.Z;
        perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance;
        perLight.MinZ = volumeZBoundsMin;
        perLight.LocalLightScatteringIntensity = light.VolumetricScatteringIntensity;
        perLight.ViewSpaceBoundingSphere = Float4(viewSpaceLightBoundsOrigin, radius);
        Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip);
        light.SetupLightData(&perLight.LocalLight, withShadow);

        // Upload data
        context->UpdateCB(cb1, &perLight);
        context->BindCB(1, cb1);

        // Ensure to have valid buffers created
        if (_vbCircleRasterize == nullptr || _ibCircleRasterize == nullptr)
            InitCircleBuffer();

        // Call rendering to the volume
        const int32 psIndex = withShadow ? 1 : 0;
        context->SetState(_psInjectLight.Get(psIndex));
        const int32 instanceCount = volumeZBoundsMax - volumeZBoundsMin;
        const int32 indexCount = _ibCircleRasterize->GetElementsCount();
        context->BindVB(ToSpan(&_vbCircleRasterize, 1));
        context->BindIB(_ibCircleRasterize);
        context->DrawIndexedInstanced(indexCount, instanceCount, 0);
    }
}

void VolumetricFogPass::RenderLight(RenderContext& renderContext, GPUContext* context, RendererPointLightData& light, GPUTextureView* shadowMap, LightShadowData& shadow)
{
    // Skip lights with no volumetric light influence or not casting volumetric shadow
    if (light.VolumetricScatteringIntensity <= ZeroTolerance || !light.CastVolumetricShadow)
        return;
    ASSERT(shadowMap);

    context->BindSR(5, shadowMap);

    RenderRadialLight(renderContext, context, light, shadow);

    context->UnBindSR(5);
}

void VolumetricFogPass::RenderLight(RenderContext& renderContext, GPUContext* context, RendererSpotLightData& light, GPUTextureView* shadowMap, LightShadowData& shadow)
{
    // Skip lights with no volumetric light influence or not casting volumetric shadow
    if (light.VolumetricScatteringIntensity <= ZeroTolerance || !light.CastVolumetricShadow)
        return;
    ASSERT(shadowMap);

    context->BindSR(6, shadowMap);

    RenderRadialLight(renderContext, context, light, shadow);

    context->UnBindSR(6);
}

void VolumetricFogPass::Render(RenderContext& renderContext)
{
    // Prepare
    VolumetricFogOptions options;
    auto context = GPUDevice::Instance->GetMainContext();
    if (Init(renderContext, context, options))
        return;
    auto& view = renderContext.View;
    auto& cache = _cache;

    PROFILE_GPU_CPU("Volumetric Fog");

    // TODO: test exponential depth distribution (should give better quality near the camera)
    // TODO: use tiled light culling and render unshadowed lights in single pass

    // Init directional light data
    GPUTextureView* dirLightShadowMap = nullptr;
    if (renderContext.List->DirectionalLights.HasItems())
    {
        const int32 dirLightIndex = (int32)renderContext.List->DirectionalLights.Count() - 1;
        const auto& dirLight = renderContext.List->DirectionalLights[dirLightIndex];
        const float brightness = dirLight.VolumetricScatteringIntensity;

        if (brightness > ZeroTolerance)
        {
            const auto shadowPass = ShadowsPass::Instance();
            const bool useShadow = dirLight.CastVolumetricShadow && shadowPass->LastDirLightIndex == dirLightIndex;
            dirLight.SetupLightData(&_cache.Data.DirectionalLight, useShadow);
            _cache.Data.DirectionalLight.Color *= brightness;
            if (useShadow)
            {
                _cache.Data.DirectionalLightShadow = shadowPass->LastDirLight;
                dirLightShadowMap = shadowPass->LastDirLightShadowMap;
            }
            else
            {
                _cache.Data.DirectionalLightShadow.NumCascades = 0;
            }
        }
    }

    // Init GI data
    bool useDDGI = false;
    DynamicDiffuseGlobalIlluminationPass::BindingData bindingDataDDGI;
    if (EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI))
    {
        switch (renderContext.List->Settings.GlobalIllumination.Mode)
        {
        case GlobalIlluminationMode::DDGI:
            if (!DynamicDiffuseGlobalIlluminationPass::Instance()->Get(renderContext.Buffers, bindingDataDDGI))
            {
                _cache.Data.DDGI = bindingDataDDGI.Constants;
                useDDGI = true;
            }
            break;
        }
    }

    // Init sky light data
    GPUTexture* skyLightImage = nullptr;
    if (renderContext.List->SkyLights.HasItems() && !useDDGI)
    {
        const auto& skyLight = renderContext.List->SkyLights.Last();
        if (skyLight.VolumetricScatteringIntensity > ZeroTolerance)
        {
            _cache.Data.SkyLight.MultiplyColor = skyLight.Color;
            _cache.Data.SkyLight.AdditiveColor = skyLight.AdditiveColor;
            _cache.Data.SkyLight.VolumetricScatteringIntensity = skyLight.VolumetricScatteringIntensity;
            const auto source = skyLight.Image;
            skyLightImage = source ? source->GetTexture() : nullptr;
        }
    }

    // Set constant buffer data
    auto cb0 = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb0, &_cache.Data);
    context->BindCB(0, cb0);

    // Allocate buffers
    const GPUTextureDescription volumeDesc = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R16G16B16A16_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess);
    const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess);
    auto vBufferA = RenderTargetPool::Get(volumeDesc);
    RENDER_TARGET_POOL_SET_NAME(vBufferA, "VolumetricFog.VBufferA");
    auto vBufferB = RenderTargetPool::Get(volumeDescRGB);
    RENDER_TARGET_POOL_SET_NAME(vBufferB, "VolumetricFog.VBufferB");
    const auto lightScattering = RenderTargetPool::Get(volumeDesc);
    RENDER_TARGET_POOL_SET_NAME(lightScattering, "VolumetricFog.LightScattering");

    int32 groupCountX = Math::DivideAndRoundUp((int32)cache.GridSize.X, VolumetricFogGridInjectionGroupSize);
    int32 groupCountY = Math::DivideAndRoundUp((int32)cache.GridSize.Y, VolumetricFogGridInjectionGroupSize);
    int32 groupCountZ = Math::DivideAndRoundUp((int32)cache.GridSize.Z, VolumetricFogGridInjectionGroupSize);

    // Initialize fog volume properties
    {
        PROFILE_GPU("Initialize");

        context->ResetRenderTarget();
        context->BindUA(0, vBufferA->ViewVolume());
        context->BindUA(1, vBufferB->ViewVolume());

        context->Dispatch(_csInitialize, groupCountX, groupCountY, groupCountZ);

        context->ResetUA();
    }

    // Render local fog particles
    if (renderContext.List->VolumetricFogParticles.HasItems())
    {
        PROFILE_GPU_CPU_NAMED("Local Fog");

        // Bind the output
        GPUTextureView* rt[] = { vBufferA->ViewVolume(), vBufferB->ViewVolume() };
        context->SetRenderTarget(nullptr, Span<GPUTextureView*>(rt, 2));
        context->SetViewportAndScissors((float)volumeDesc.Width, (float)volumeDesc.Height);

        // Ensure to have valid buffers created
        if (_vbCircleRasterize == nullptr || _ibCircleRasterize == nullptr)
            InitCircleBuffer();

        MaterialBase::BindParameters bindParams(context, renderContext);
        bindParams.DrawCallsCount = 1;
        CustomData customData;
        customData.Shader = _shader->GetShader();
        customData.GridSize = cache.GridSize;
        customData.VolumetricFogMaxDistance = cache.Data.VolumetricFogMaxDistance;
        bindParams.CustomData = &customData;
        bindParams.BindViewData();

        for (auto& drawCall : renderContext.List->VolumetricFogParticles)
        {
            const Float3 center = drawCall.Particle.VolumetricFog.Position;
            const float radius = drawCall.Particle.VolumetricFog.Radius;
            ASSERT(!center.IsNanOrInfinity() && !isnan(radius) && !isinf(radius));

            // Calculate light volume bounds in camera frustum depth range (min and max)
            const Float3 viewSpaceLightBoundsOrigin = Float3::Transform(center, view.View);
            const float furthestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z + radius, options, cache.GridSizeZ);
            const float closestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z - radius, options, cache.GridSizeZ);
            const int32 volumeZBoundsMin = (int32)Math::Clamp(closestSliceIndexUnclamped, 0.0f, cache.GridSize.Z - 1.0f);
            const int32 volumeZBoundsMax = (int32)Math::Clamp(furthestSliceIndexUnclamped, 0.0f, cache.GridSize.Z - 1.0f);

            // Culling
            if ((view.Position - center).LengthSquared() >= (options.Distance + radius) * (options.Distance + radius) || volumeZBoundsMin >= volumeZBoundsMax)
                continue;

            // Setup material shader data
            customData.ParticleIndex = drawCall.Particle.VolumetricFog.ParticleIndex;
            bindParams.FirstDrawCall = &drawCall;
            drawCall.Material->Bind(bindParams);

            // Setup volumetric shader data
            PerLight perLight;
            auto cb1 = _shader->GetShader()->GetCB(1);
            perLight.SliceToDepth.X = cache.Data.GridSize.Z;
            perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance;
            perLight.MinZ = volumeZBoundsMin;
            perLight.ViewSpaceBoundingSphere = Float4(viewSpaceLightBoundsOrigin, radius);
            Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip);

            // Upload data
            context->UpdateCB(cb1, &perLight);
            context->BindCB(1, cb1);

            // Call rendering to the volume
            const int32 instanceCount = volumeZBoundsMax - volumeZBoundsMin;
            const int32 indexCount = _ibCircleRasterize->GetElementsCount();
            context->BindVB(ToSpan(&_vbCircleRasterize, 1));
            context->BindIB(_ibCircleRasterize);
            context->DrawIndexedInstanced(indexCount, instanceCount, 0);
        }

        context->ResetRenderTarget();
        context->BindCB(0, cb0);
    }

    // Render Lights
    GPUTextureView* localShadowedLightScattering = nullptr;
    {
        // Get lights to render
        Array<const RendererPointLightData*, InlinedAllocation<64, RendererAllocation>> pointLights;
        Array<const RendererSpotLightData*, InlinedAllocation<64, RendererAllocation>> spotLights;
        for (int32 i = 0; i < renderContext.List->PointLights.Count(); i++)
        {
            const auto& light = renderContext.List->PointLights[i];
            if (light.VolumetricScatteringIntensity > ZeroTolerance && !light.RenderedVolumetricFog)
            {
                if ((view.Position - light.Position).LengthSquared() < (options.Distance + light.Radius) * (options.Distance + light.Radius))
                {
                    pointLights.Add(&light);
                }
            }
        }
        for (int32 i = 0; i < renderContext.List->SpotLights.Count(); i++)
        {
            const auto& light = renderContext.List->SpotLights[i];
            if (light.VolumetricScatteringIntensity > ZeroTolerance && !light.RenderedVolumetricFog)
            {
                if ((view.Position - light.Position).LengthSquared() < (options.Distance + light.Radius) * (options.Distance + light.Radius))
                {
                    spotLights.Add(&light);
                }
            }
        }

        // Skip if no lights to render
        if (pointLights.Count() + spotLights.Count())
        {
            PROFILE_GPU_CPU_NAMED("Lights Injection");

            // Allocate temporary buffer for light scattering injection
            localShadowedLightScattering = GetLocalShadowedLightScattering(renderContext, context, options);

            // Prepare
            PerLight perLight;
            perLight.SliceToDepth.X = cache.Data.GridSize.Z;
            perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance;
            auto cb1 = _shader->GetShader()->GetCB(1);

            // Bind the output
            context->SetRenderTarget(localShadowedLightScattering);
            context->SetViewportAndScissors((float)volumeDesc.Width, (float)volumeDesc.Height);

            // Render them to the volume
            for (int32 i = 0; i < pointLights.Count(); i++)
                RenderRadialLight(renderContext, context, view, options, *pointLights[i], perLight, cb1);
            for (int32 i = 0; i < spotLights.Count(); i++)
                RenderRadialLight(renderContext, context, view, options, *spotLights[i], perLight, cb1);

            // Cleanup
            context->UnBindCB(1);
            context->ResetRenderTarget();
            context->FlushState();
        }
        else if (renderContext.Buffers->LocalShadowedLightScattering)
        {
            localShadowedLightScattering = renderContext.Buffers->LocalShadowedLightScattering->ViewVolume();
        }
    }

    // Light Scattering
    {
        PROFILE_GPU("Light Scattering");

        const bool temporalHistoryIsValid = renderContext.Buffers->VolumetricFogHistory && !renderContext.Task->IsCameraCut && Float3::NearEqual(renderContext.Buffers->VolumetricFogHistory->Size3(), cache.GridSize);
        const auto lightScatteringHistory = temporalHistoryIsValid ? renderContext.Buffers->VolumetricFogHistory : nullptr;

        context->BindUA(0, lightScattering->ViewVolume());
        context->BindSR(0, vBufferA->ViewVolume());
        context->BindSR(1, vBufferB->ViewVolume());
        context->BindSR(2, lightScatteringHistory ? lightScatteringHistory->ViewVolume() : nullptr);
        context->BindSR(3, localShadowedLightScattering);
        context->BindSR(4, dirLightShadowMap);

        int32 csIndex;
        if (useDDGI)
        {
            context->BindSR(5, bindingDataDDGI.ProbesData);
            context->BindSR(6, bindingDataDDGI.ProbesDistance);
            context->BindSR(7, bindingDataDDGI.ProbesIrradiance);
            csIndex = 1;
        }
        else
        {
            context->BindSR(5, skyLightImage);
            csIndex = 0;
        }
        context->Dispatch(_csLightScattering.Get(csIndex), groupCountX, groupCountY, groupCountZ);

        context->ResetSR();
        context->ResetUA();
    }

    // Release resources
    RenderTargetPool::Release(vBufferA);
    RenderTargetPool::Release(vBufferB);

    // Update the temporal history buffer
    if (renderContext.Buffers->VolumetricFogHistory)
    {
        RenderTargetPool::Release(renderContext.Buffers->VolumetricFogHistory);
    }
    renderContext.Buffers->VolumetricFogHistory = lightScattering;

    // Get buffer for the integrated light scattering (try to reuse the previous frame if it's valid)
    GPUTexture* integratedLightScattering = renderContext.Buffers->VolumetricFog;
    if (integratedLightScattering == nullptr || !Float3::NearEqual(integratedLightScattering->Size3(), cache.GridSize))
    {
        if (integratedLightScattering)
        {
            RenderTargetPool::Release(integratedLightScattering);
        }
        integratedLightScattering = RenderTargetPool::Get(volumeDesc);
        RENDER_TARGET_POOL_SET_NAME(integratedLightScattering, "VolumetricFog.Integrated");
        renderContext.Buffers->VolumetricFog = integratedLightScattering;
    }
    renderContext.Buffers->LastFrameVolumetricFog = Engine::FrameCount;

    groupCountX = Math::DivideAndRoundUp((int32)cache.GridSize.X, VolumetricFogIntegrationGroupSize);
    groupCountY = Math::DivideAndRoundUp((int32)cache.GridSize.Y, VolumetricFogIntegrationGroupSize);

    // Final Integration
    {
        PROFILE_GPU("Final Integration");

        context->BindUA(0, integratedLightScattering->ViewVolume());
        context->BindSR(0, lightScattering->ViewVolume());

        context->Dispatch(_csFinalIntegration, groupCountX, groupCountY, 1);
    }

    // Cleanup
    context->ResetUA();
    context->ResetSR();
    context->ResetRenderTarget();
    auto viewport = renderContext.Task->GetViewport();
    context->SetViewportAndScissors(viewport);
}

void VolumetricFogPass::InitCircleBuffer()
{
    const int32 vertices = 8;
    const int32 triangles = vertices - 2;
    const int32 rings = vertices;
    const float radiansPerRingSegment = PI / (float)rings;
    Float2 vbData[vertices];
    uint16 ibData[triangles * 3];

    const float radiusScale = 1.0f / Math::Cos(radiansPerRingSegment);
    for (int32 vertexIndex = 0; vertexIndex < vertices; vertexIndex++)
    {
        const float angle = vertexIndex / static_cast<float>(vertices - 1) * 2 * PI;
        vbData[vertexIndex] = Float2(radiusScale * Math::Cos(angle) * 0.5f + 0.5f, radiusScale * Math::Sin(angle) * 0.5f + 0.5f);
    }
    int32 ibIndex = 0;
    for (int32 triangleIndex = 0; triangleIndex < triangles; triangleIndex++)
    {
        const int32 firstVertexIndex = triangleIndex + 2;
        ibData[ibIndex++] = 0;
        ibData[ibIndex++] = firstVertexIndex - 1;
        ibData[ibIndex++] = firstVertexIndex;
    }

    // Create buffers
    ASSERT(_vbCircleRasterize == nullptr && _ibCircleRasterize == nullptr);
    _vbCircleRasterize = GPUDevice::Instance->CreateBuffer(TEXT("VolumetricFog.CircleRasterize.VB"));
    _ibCircleRasterize = GPUDevice::Instance->CreateBuffer(TEXT("VolumetricFog.CircleRasterize.IB"));
    if (_vbCircleRasterize->Init(GPUBufferDescription::Vertex(sizeof(Float2), vertices, vbData))
        || _ibCircleRasterize->Init(GPUBufferDescription::Index(sizeof(uint16), triangles * 3, ibData)))
    {
        LOG(Fatal, "Failed to setup volumetric fog buffers.");
    }
}
