// Copyright (c) Wojciech Figat. All rights reserved. #include "VolumetricFogPass.h" #include "ShadowsPass.h" #include "GBufferPass.h" #include "DrawCall.h" #include "GI/DynamicDiffuseGlobalIllumination.h" #include "Engine/Graphics/Graphics.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderTargetPool.h" #include "Engine/Graphics/GPULimits.h" #include "Engine/Graphics/GPUContext.h" #include "Engine/Graphics/Shaders/GPUVertexLayout.h" #include "Engine/Content/Assets/CubeTexture.h" #include "Engine/Content/Content.h" #include "Engine/Engine/Engine.h" #include "Engine/Engine/Units.h" // Must match shader source int32 VolumetricFogGridInjectionGroupSize = 4; int32 VolumetricFogIntegrationGroupSize = 8; #define VOLUMETRIC_FOG_GRID_Z_LINEAR 1 GPU_CB_STRUCT(SkyLightData { Float3 MultiplyColor; float VolumetricScatteringIntensity; Float3 AdditiveColor; float Dummy0; }); GPU_CB_STRUCT(Data { ShaderGBufferData GBuffer; Float3 GlobalAlbedo; float GlobalExtinctionScale; Float3 GlobalEmissive; float HistoryWeight; Float3 GridSize; uint32 MissedHistorySamplesCount; uint32 GridSizeIntX; uint32 GridSizeIntY; uint32 GridSizeIntZ; float PhaseG; Float2 Dummy0; float VolumetricFogMaxDistance; float InverseSquaredLightDistanceBiasScale; Float4 FogParameters; Float4 GridSliceParameters; Matrix PrevWorldToClip; Float4 FrameJitterOffsets[8]; ShaderLightData DirectionalLight; SkyLightData SkyLight; DynamicDiffuseGlobalIlluminationPass::ConstantsData DDGI; }); GPU_CB_STRUCT(PerLight { Float2 SliceToDepth; int32 MinZ; float LocalLightScatteringIntensity; Float4 ViewSpaceBoundingSphere; Matrix ViewToVolumeClip; ShaderLightData LocalLight; }); struct FrameCache { Float3 GridSize; int32 GridPixelSize; int32 GridSizeZ; bool FogJitter; float HistoryWeight; int32 MissedHistorySamplesCount; float InverseSquaredLightDistanceBiasScale; float SphereRasterizeRadiusBias; Data Data; }; 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(TEXT("Shaders/VolumetricFog")); if (_shader == nullptr) { return true; } #if COMPILE_WITH_DEV_ENV _shader.Get()->OnReloading.Bind(this); #endif return false; } bool VolumetricFogPass::setupResources() { if (!_shader->IsLoaded()) return true; auto shader = _shader->GetShader(); CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // CB1 is used for per-draw info (ObjectIndex) CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 2, PerLight); // 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; } Float4 GetGridSliceParameters(float fogStart, float fogEnd, int32 gridSizeZ) { float sliceToUV = 1.0f / (float)gridSizeZ; #if VOLUMETRIC_FOG_GRID_Z_LINEAR float sliceToDepth = fogEnd / (float)gridSizeZ; return Float4(sliceToDepth, 1.0f / sliceToDepth, 0.0f, sliceToUV); #else // Use logarithmic distribution for Z slices to have more resolution for close distances and less for far ones (less aliasing near camera) const float distribution = 220.0f; // Manually adjusted to give a good distribution across the range fogStart += UNITS_TO_METERS(10); // Bias start a bit for some more quality float y = (fogEnd - fogStart * Math::Exp2((float)(gridSizeZ - 1) / distribution)) / (fogEnd - fogStart); float x = (1.0f - y) / fogStart; return Float4(x, y, distribution, sliceToUV); #endif } float GetDepthFromSlice(float slice, const Float4& gridSliceParameters) { #if VOLUMETRIC_FOG_GRID_Z_LINEAR return slice * gridSliceParameters.X; #else return (Math::Exp2(slice / gridSliceParameters.Z) - gridSliceParameters.Y) / gridSliceParameters.X; #endif } float GetSliceFromDepth(float sceneDepth, const Float4& gridSliceParameters) { #if VOLUMETRIC_FOG_GRID_Z_LINEAR return sceneDepth * gridSliceParameters.Y; #else return Math::Log2(sceneDepth * gridSliceParameters.X + gridSliceParameters.Y) * gridSliceParameters.Z; #endif } struct alignas(Float4) RasterizeSphere { Float3 Center; float Radius; Float3 ViewSpaceCenter; uint16 VolumeZBoundsMin; uint16 VolumeZBoundsMax; }; bool VolumetricFogPass::Init(FrameCache& cache, RenderContext& renderContext, GPUContext* context) { const auto& fog = renderContext.List->Fog; if (renderContext.Buffers->LastFrameVolumetricFog == Engine::FrameCount) return false; if (fog.Renderer == nullptr || !renderContext.List->Setup.UseVolumetricFog || !_isSupported || !fog.VolumetricFog.UseVolumetricFog() || checkIfSkipPass()) { RenderTargetPool::Release(renderContext.Buffers->VolumetricFog); renderContext.Buffers->VolumetricFog = nullptr; renderContext.Buffers->LastFrameVolumetricFog = 0; return true; } auto& options = fog.VolumetricFog; // Setup configuration cache.FogJitter = true; cache.HistoryWeight = 0.92f; cache.InverseSquaredLightDistanceBiasScale = 1.0f; switch (Graphics::VolumetricFogQuality) { case Quality::Low: cache.GridPixelSize = 24; cache.GridSizeZ = 50; cache.MissedHistorySamplesCount = 1; break; case Quality::Medium: cache.GridPixelSize = 20; cache.GridSizeZ = 54; cache.MissedHistorySamplesCount = 2; break; case Quality::High: cache.GridPixelSize = 16; cache.GridSizeZ = 64; cache.MissedHistorySamplesCount = 4; break; case Quality::Ultra: cache.GridPixelSize = 10; cache.GridSizeZ = 128; cache.MissedHistorySamplesCount = 8; break; } // Calculate volumetric fog size const int32 width = renderContext.Buffers->GetWidth(); const int32 height = renderContext.Buffers->GetHeight(); constexpr int32 resolutionLimit = 1920; // Limit resolution to 1080p to save on perf for players on 2k/4k displays (incl. consoles) if (resolutionLimit > 0) { const int32 limitX = resolutionLimit; // Controls screen width limit, height depends on the aspect ratio const int32 limitY = Math::CeilToInt((float)limitX * (float)height / (float)width); if (width > limitX || height > limitY) { const float scaleX = (float)Math::Max(width, limitX) / limitX; const float scaleY = (float)Math::Max(height, limitY) / limitY; cache.GridPixelSize = Math::CeilToInt(cache.GridPixelSize * Math::Max(scaleX, scaleY)); } } 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; if (renderContext.Task->IsCameraCut || renderContext.View.IsOriginTeleport() || (renderContext.Buffers->VolumetricFog && renderContext.Buffers->VolumetricFog->Size3() != cache.GridSize)) { // Don't blend with history on camera cuts or teleport or resizes cache.HistoryWeight = 0.0f; } // 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.GridSliceParameters = GetGridSliceParameters(renderContext.View.Near, options.Distance, cache.GridSizeZ); /*static bool log = true; if (log) { log = false; for (int slice = 0; slice < cache.GridSizeZ; slice++) LOG(Error, "Slice {} -> {}", slice, GetDepthFromSlice((float)slice, cache.Data.GridSliceParameters)); }*/ 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(renderContext.View.PrevViewProjection, cache.Data.PrevWorldToClip); 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; cache.SphereRasterizeRadiusBias = 0.0f; if (cache.FogJitter) { for (int32 i = 0; i < cache.MissedHistorySamplesCount; i++) { const uint64 frameNumber = renderContext.Task->LastUsedFrame - i; cache.Data.FrameJitterOffsets[i] = Float4( RenderTools::TemporalHalton(frameNumber & 1023, 2), RenderTools::TemporalHalton(frameNumber & 1023, 3), RenderTools::TemporalHalton(frameNumber & 1023, 5), 0); } // Add bias to radius when using jittering to avoid pixelization on the circle borders (cell offset is randomized) float worldUnitsPerDepthCell = options.Distance / cache.GridSize.Z; // TODO: include XY size too? cache.SphereRasterizeRadiusBias = worldUnitsPerDepthCell * 0.25f; } // 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; } bool VolumetricFogPass::InitSphereRasterize(FrameCache& cache, RasterizeSphere& sphere, RenderView& view, const Float3& center, float radius) { ASSERT_LOW_LAYER(!center.IsNanOrInfinity() && !isnan(radius) && !isinf(radius)); sphere.Center = center; sphere.Radius = radius + cache.SphereRasterizeRadiusBias; // Calculate sphere volume bounds in camera frustum depth range (min and max) sphere.ViewSpaceCenter = Float3::Transform(center, view.View); const float furthestSliceIndex = GetSliceFromDepth(sphere.ViewSpaceCenter.Z + sphere.Radius, cache.Data.GridSliceParameters); const float closestSliceIndex = GetSliceFromDepth(sphere.ViewSpaceCenter.Z - sphere.Radius, cache.Data.GridSliceParameters); sphere.VolumeZBoundsMin = (uint16)Math::Clamp(closestSliceIndex, 0.0f, cache.GridSize.Z - 1.0f); sphere.VolumeZBoundsMax = (uint16)Math::Clamp(furthestSliceIndex, 0.0f, cache.GridSize.Z - 1.0f); // Cull if ((view.Position - sphere.Center).LengthSquared() >= Math::Square(cache.Data.VolumetricFogMaxDistance + sphere.Radius) || sphere.VolumeZBoundsMin > sphere.VolumeZBoundsMax) { return true; } return false; } GPUTextureView* VolumetricFogPass::GetLocalShadowedLightScattering(FrameCache& cache, RenderContext& renderContext, GPUContext* context) 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 void VolumetricFogPass::RenderRadialLight(FrameCache& cache, RenderContext& renderContext, GPUContext* context, T& light, PerLight& perLight, GPUConstantBuffer* cb2) { RasterizeSphere sphere; if (InitSphereRasterize(cache, sphere, renderContext.View, light.Position, light.Radius)) return; // Setup data perLight.SliceToDepth.X = cache.Data.GridSize.Z; perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance; perLight.MinZ = sphere.VolumeZBoundsMin; perLight.LocalLightScatteringIntensity = light.VolumetricScatteringIntensity; perLight.ViewSpaceBoundingSphere = Float4(sphere.ViewSpaceCenter, sphere.Radius); Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip); const bool withShadow = light.CastVolumetricShadow && light.HasShadow; light.SetShaderData(perLight.LocalLight, withShadow); // Upload data context->UpdateCB(cb2, &perLight); context->BindCB(2, cb2); // 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 = sphere.VolumeZBoundsMax - sphere.VolumeZBoundsMin + 1; const int32 indexCount = _ibCircleRasterize->GetElementsCount(); context->BindVB(ToSpan(&_vbCircleRasterize, 1)); context->BindIB(_ibCircleRasterize); context->DrawIndexedInstanced(indexCount, instanceCount, 0); } void VolumetricFogPass::Render(RenderContext& renderContext) { auto context = GPUDevice::Instance->GetMainContext(); FrameCache cache; if (Init(cache, renderContext, context)) return; PROFILE_GPU_CPU("Volumetric Fog"); // TODO: test exponential depth distribution (should give better quality near the camera) // TODO: use tiled light culling and render shadowed/unshadowed lights in single pass // Try to get shadows atlas GPUTexture* shadowMap; GPUBufferView* shadowsBuffer; ShadowsPass::GetShadowAtlas(renderContext.Buffers, shadowMap, shadowsBuffer); // Init directional light data Platform::MemoryClear(&cache.Data.DirectionalLight, sizeof(cache.Data.DirectionalLight)); 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 bool useShadow = shadowMap && dirLight.CastVolumetricShadow && dirLight.HasShadow; dirLight.SetShaderData(cache.Data.DirectionalLight, useShadow); cache.Data.DirectionalLight.Color *= brightness; } } // 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; Platform::MemoryClear(&cache.Data.SkyLight, sizeof(cache.Data.SkyLight)); 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.Count() != 0) { PROFILE_GPU_CPU_NAMED("Local Fog"); // Bind the output GPUTextureView* rt[] = { vBufferA->ViewVolume(), vBufferB->ViewVolume() }; context->SetRenderTarget(nullptr, Span(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); CustomData customData; customData.Shader = _shader->GetShader(); customData.GridSize = cache.GridSize; customData.VolumetricFogMaxDistance = cache.Data.VolumetricFogMaxDistance; customData.GridSliceParameters = cache.Data.GridSliceParameters; bindParams.CustomData = &customData; bindParams.BindViewData(); bindParams.DrawCall = renderContext.List->VolumetricFogParticles.begin(); bindParams.BindDrawData(); for (auto& drawCall : renderContext.List->VolumetricFogParticles) { RasterizeSphere sphere; if (InitSphereRasterize(cache, sphere, renderContext.View, drawCall.Particle.VolumetricFog.Position, drawCall.Particle.VolumetricFog.Radius)) continue; // Setup material shader data customData.ParticleIndex = drawCall.Particle.VolumetricFog.ParticleIndex; bindParams.DrawCall = &drawCall; drawCall.Material->Bind(bindParams); // Setup volumetric shader data PerLight perLight; auto cb2 = _shader->GetShader()->GetCB(2); perLight.SliceToDepth.X = cache.Data.GridSize.Z; perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance; perLight.MinZ = sphere.VolumeZBoundsMin; perLight.ViewSpaceBoundingSphere = Float4(sphere.ViewSpaceCenter, sphere.Radius); Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip); // Upload data context->UpdateCB(cb2, &perLight); context->BindCB(2, cb2); // Call rendering to the volume const int32 instanceCount = sphere.VolumeZBoundsMax - sphere.VolumeZBoundsMin + 1; 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> pointLights; Array> spotLights; Float3 viewPosition = renderContext.View.Position; float distance = cache.Data.VolumetricFogMaxDistance; for (int32 i = 0; i < renderContext.List->PointLights.Count(); i++) { const auto& light = renderContext.List->PointLights.Get()[i]; if (light.VolumetricScatteringIntensity > ZeroTolerance && (viewPosition - light.Position).LengthSquared() < Math::Square(distance + light.Radius)) pointLights.Add(i); } for (int32 i = 0; i < renderContext.List->SpotLights.Count(); i++) { const auto& light = renderContext.List->SpotLights.Get()[i]; if (light.VolumetricScatteringIntensity > ZeroTolerance && (viewPosition - light.Position).LengthSquared() < Math::Square(distance + light.Radius)) spotLights.Add(i); } // 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(cache, renderContext, context); // Prepare PerLight perLight; perLight.SliceToDepth.X = cache.Data.GridSize.Z; perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance; auto cb2 = _shader->GetShader()->GetCB(2); // Bind the output context->SetRenderTarget(localShadowedLightScattering); context->SetViewportAndScissors((float)volumeDesc.Width, (float)volumeDesc.Height); // Render them to the volume context->BindSR(0, shadowMap); context->BindSR(1, shadowsBuffer); auto* pointLightsIdxPtr = pointLights.Get(); auto* pointLightsPtr = renderContext.List->PointLights.Get(); for (int32 i = 0; i < pointLights.Count(); i++) RenderRadialLight(cache, renderContext, context, pointLightsPtr[pointLightsIdxPtr[i]], perLight, cb2); auto* spotLightsIdxPtr = spotLights.Get(); auto* spotLightsPtr = renderContext.List->SpotLights.Get(); for (int32 i = 0; i < spotLights.Count(); i++) RenderRadialLight(cache, renderContext, context, spotLightsPtr[spotLightsIdxPtr[i]], perLight, cb2); // Cleanup context->UnBindCB(2); 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 && 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, shadowMap); context->BindSR(5, shadowsBuffer); int32 csIndex; if (useDDGI) { context->BindSR(6, bindingDataDDGI.ProbesData); context->BindSR(7, bindingDataDDGI.ProbesDistance); context->BindSR(8, bindingDataDDGI.ProbesIrradiance); csIndex = 1; } else { context->BindSR(6, 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 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)) { RenderTargetPool::Release(integratedLightScattering); integratedLightScattering = RenderTargetPool::Get(volumeDesc); RENDER_TARGET_POOL_SET_NAME(integratedLightScattering, "VolumetricFog.Integrated"); renderContext.Buffers->VolumetricFog = integratedLightScattering; } renderContext.Buffers->LastFrameVolumetricFog = Engine::FrameCount; // Update fog to be used by other passes const float ditherNoiseScale = 0.2f; // Scales noise in SampleVolumetricFog renderContext.List->Fog.VolumetricFogTexture = integratedLightScattering->ViewVolume(); renderContext.List->Fog.VolumetricFogData.GridSliceParameters = cache.Data.GridSliceParameters; renderContext.List->Fog.VolumetricFogData.ScreenSize = renderContext.Buffers->GetSize(); renderContext.List->Fog.VolumetricFogData.VolumeTexelSize = Float2(1.0f / cache.GridSize.X, 1.0f / cache.GridSize.Y) * ditherNoiseScale; 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(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_LOW_LAYER(_vbCircleRasterize == nullptr && _ibCircleRasterize == nullptr); _vbCircleRasterize = GPUDevice::Instance->CreateBuffer(TEXT("VolumetricFog.CircleRasterize.VB")); _ibCircleRasterize = GPUDevice::Instance->CreateBuffer(TEXT("VolumetricFog.CircleRasterize.IB")); auto layout = GPUVertexLayout::Get({{ VertexElement::Types::TexCoord, 0, 0, 0, PixelFormat::R32G32_Float }}); if (_vbCircleRasterize->Init(GPUBufferDescription::Vertex(layout, sizeof(Float2), vertices, vbData)) || _ibCircleRasterize->Init(GPUBufferDescription::Index(sizeof(uint16), triangles * 3, ibData))) { LOG(Fatal, "Failed to setup volumetric fog buffers."); } }