// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. #include "GlobalSurfaceAtlasPass.h" #include "GlobalSignDistanceFieldPass.h" #include "RenderList.h" #include "ShadowsPass.h" #include "Engine/Core/Math/Matrix3x3.h" #include "Engine/Core/Math/OrientedBoundingBox.h" #include "Engine/Engine/Engine.h" #include "Engine/Content/Content.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderTargetPool.h" #include "Engine/Graphics/Shaders/GPUShader.h" #include "Engine/Level/Actors/StaticModel.h" #include "Engine/Level/Scene/SceneRendering.h" #include "Engine/Utilities/RectPack.h" // This must match HLSL #define GLOBAL_SURFACE_ATLAS_OBJECT_SIZE (5 + 6 * 5) // Amount of float4s per-object #define GLOBAL_SURFACE_ATLAS_TILE_PADDING 1 // 1px padding to prevent color bleeding between tiles #define GLOBAL_SURFACE_ATLAS_TILE_PROJ_PLANE_OFFSET 0.1f // Small offset to prevent clipping with the closest triangles (shifts near and far planes) #define GLOBAL_SURFACE_ATLAS_DEBUG_FORCE_REDRAW_TILES 0 // Forces to redraw all object tiles every frame #define GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_OBJECTS 0 // Debug draws object bounds on redraw (and tile draw projection locations) #if GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_OBJECTS #include "Engine/Debug/DebugDraw.h" #endif PACK_STRUCT(struct Data0 { Vector3 ViewWorldPos; float ViewNearPlane; Vector2 Padding00; float LightShadowsStrength; float ViewFarPlane; Vector4 ViewFrustumWorldRays[4]; GlobalSignDistanceFieldPass::GlobalSDFData GlobalSDF; GlobalSurfaceAtlasPass::GlobalSurfaceAtlasData GlobalSurfaceAtlas; LightData Light; }); PACK_STRUCT(struct AtlasTileVertex { Half2 Position; Half2 TileUV; uint16 ObjectIndex; uint16 TileIndex; }); struct GlobalSurfaceAtlasTile : RectPack { Vector3 ViewDirection; Vector3 ViewPosition; Vector3 ViewBoundsSize; Matrix ViewMatrix; GlobalSurfaceAtlasTile(uint16 x, uint16 y, uint16 width, uint16 height) : RectPack(x, y, width, height) { } void OnInsert(class GlobalSurfaceAtlasCustomBuffer* buffer, Actor* actor, int32 tileIndex); void OnFree() { } }; struct GlobalSurfaceAtlasObject { uint64 LastFrameUsed; uint64 LastFrameDirty; GlobalSurfaceAtlasTile* Tiles[6]; uint32 Index; float Radius; OrientedBoundingBox Bounds; GlobalSurfaceAtlasObject() { Platform::MemoryClear(this, sizeof(GlobalSurfaceAtlasObject)); } GlobalSurfaceAtlasObject(const GlobalSurfaceAtlasObject& other) { Platform::MemoryCopy(this, &other, sizeof(GlobalSurfaceAtlasObject)); } GlobalSurfaceAtlasObject(GlobalSurfaceAtlasObject&& other) noexcept { Platform::MemoryCopy(this, &other, sizeof(GlobalSurfaceAtlasObject)); } GlobalSurfaceAtlasObject& operator=(const GlobalSurfaceAtlasObject& other) { Platform::MemoryCopy(this, &other, sizeof(GlobalSurfaceAtlasObject)); return *this; } GlobalSurfaceAtlasObject& operator=(GlobalSurfaceAtlasObject&& other) noexcept { Platform::MemoryCopy(this, &other, sizeof(GlobalSurfaceAtlasObject)); return *this; } }; class GlobalSurfaceAtlasCustomBuffer : public RenderBuffers::CustomBuffer { public: int32 Resolution = 0; uint64 LastFrameAtlasInsertFail = 0; uint64 LastFrameAtlasDefragmentation = 0; GPUTexture* AtlasDepth = nullptr; GPUTexture* AtlasEmissive = nullptr; GPUTexture* AtlasGBuffer0 = nullptr; GPUTexture* AtlasGBuffer1 = nullptr; GPUTexture* AtlasGBuffer2 = nullptr; GPUTexture* AtlasDirectLight = nullptr; DynamicTypedBuffer ObjectsBuffer; uint32 ObjectIndexCounter; GlobalSurfaceAtlasPass::BindingData Result; GlobalSurfaceAtlasTile* AtlasTiles = nullptr; // TODO: optimize with a single allocation for atlas tiles Dictionary Objects; GlobalSurfaceAtlasCustomBuffer() : ObjectsBuffer(256 * GLOBAL_SURFACE_ATLAS_OBJECT_SIZE, PixelFormat::R32G32B32A32_Float, false, TEXT("GlobalSurfaceAtlas.ObjectsBuffer")) { } FORCE_INLINE void ClearObjects() { LastFrameAtlasDefragmentation = Engine::FrameCount; SAFE_DELETE(AtlasTiles); ObjectsBuffer.Clear(); Objects.Clear(); } FORCE_INLINE void Clear() { RenderTargetPool::Release(AtlasDepth); RenderTargetPool::Release(AtlasEmissive); RenderTargetPool::Release(AtlasGBuffer0); RenderTargetPool::Release(AtlasGBuffer1); RenderTargetPool::Release(AtlasGBuffer2); RenderTargetPool::Release(AtlasDirectLight); ClearObjects(); } ~GlobalSurfaceAtlasCustomBuffer() { Clear(); } }; void GlobalSurfaceAtlasTile::OnInsert(GlobalSurfaceAtlasCustomBuffer* buffer, Actor* actor, int32 tileIndex) { buffer->Objects[actor].Tiles[tileIndex] = this; } String GlobalSurfaceAtlasPass::ToString() const { return TEXT("GlobalSurfaceAtlasPass"); } bool GlobalSurfaceAtlasPass::Init() { // Check platform support const auto device = GPUDevice::Instance; _supported = device->GetFeatureLevel() >= FeatureLevel::SM5 && device->Limits.HasCompute && device->Limits.HasTypedUAVLoad; return false; } bool GlobalSurfaceAtlasPass::setupResources() { if (!_supported) return true; // Load shader if (!_shader) { _shader = Content::LoadAsyncInternal(TEXT("Shaders/GlobalSurfaceAtlas")); if (_shader == nullptr) return true; #if COMPILE_WITH_DEV_ENV _shader.Get()->OnReloading.Bind(this); #endif } if (!_shader->IsLoaded()) return true; const auto device = GPUDevice::Instance; const auto shader = _shader->GetShader(); _cb0 = shader->GetCB(0); if (!_cb0) return true; // Create pipeline state GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; if (!_psDebug) { _psDebug = device->CreatePipelineState(); psDesc.PS = shader->GetPS("PS_Debug"); if (_psDebug->Init(psDesc)) return true; } if (!_psClear) { _psClear = device->CreatePipelineState(); psDesc.DepthTestEnable = true; psDesc.DepthWriteEnable = true; psDesc.DepthFunc = ComparisonFunc::Always; psDesc.VS = shader->GetVS("VS_Atlas"); psDesc.PS = shader->GetPS("PS_Clear"); if (_psClear->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_DirectLighting", 0); if (_psDirectLighting0->Init(psDesc)) return true; _psDirectLighting1 = device->CreatePipelineState(); psDesc.PS = shader->GetPS("PS_DirectLighting", 1); if (_psDirectLighting1->Init(psDesc)) return true; } return false; } #if COMPILE_WITH_DEV_ENV void GlobalSurfaceAtlasPass::OnShaderReloading(Asset* obj) { SAFE_DELETE_GPU_RESOURCE(_psClear); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting0); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting1); SAFE_DELETE_GPU_RESOURCE(_psDebug); invalidateResources(); } #endif void GlobalSurfaceAtlasPass::Dispose() { RendererPass::Dispose(); // Cleanup SAFE_DELETE(_vertexBuffer); SAFE_DELETE_GPU_RESOURCE(_psClear); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting0); SAFE_DELETE_GPU_RESOURCE(_psDirectLighting1); SAFE_DELETE_GPU_RESOURCE(_psDebug); _cb0 = nullptr; _shader = nullptr; } bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* context, BindingData& result) { // Skip if not supported if (checkIfSkipPass()) return true; if (renderContext.List->Scenes.Count() == 0) return true; auto& surfaceAtlasData = *renderContext.Buffers->GetCustomBuffer(TEXT("GlobalSurfaceAtlas")); // Render Global SDF (used for direct shadowing) GlobalSignDistanceFieldPass::BindingData bindingDataSDF; if (GlobalSignDistanceFieldPass::Instance()->Render(renderContext, context, bindingDataSDF)) return true; // Skip if already done in the current frame const auto currentFrame = Engine::FrameCount; if (surfaceAtlasData.LastFrameUsed == currentFrame) { result = surfaceAtlasData.Result; return false; } surfaceAtlasData.LastFrameUsed = currentFrame; PROFILE_GPU_CPU("Global Surface Atlas"); // TODO: configurable via graphics settings const int32 resolution = 2048; const float resolutionInv = 1.0f / resolution; // TODO: configurable via postFx settings (maybe use Global SDF distance?) const float distance = 20000; // Initialize buffers bool noCache = surfaceAtlasData.Resolution != resolution; if (noCache) { surfaceAtlasData.Clear(); auto desc = GPUTextureDescription::New2D(resolution, resolution, PixelFormat::Unknown); uint64 memUsage = 0; // TODO: try using BC4/BC5/BC7 block compression for Surface Atlas (eg. for Tiles material properties) #define INIT_ATLAS_TEXTURE(texture, format) desc.Format = format; surfaceAtlasData.texture = RenderTargetPool::Get(desc); if (!surfaceAtlasData.texture) return true; memUsage += surfaceAtlasData.texture->GetMemoryUsage() INIT_ATLAS_TEXTURE(AtlasEmissive, LIGHT_BUFFER_FORMAT); INIT_ATLAS_TEXTURE(AtlasGBuffer0, GBUFFER0_FORMAT); INIT_ATLAS_TEXTURE(AtlasGBuffer1, GBUFFER1_FORMAT); INIT_ATLAS_TEXTURE(AtlasGBuffer2, GBUFFER2_FORMAT); INIT_ATLAS_TEXTURE(AtlasDirectLight, LIGHT_BUFFER_FORMAT); desc.Flags = GPUTextureFlags::DepthStencil | GPUTextureFlags::ShaderResource; INIT_ATLAS_TEXTURE(AtlasDepth, PixelFormat::D16_UNorm); #undef INIT_ATLAS_TEXTURE surfaceAtlasData.Resolution = resolution; LOG(Info, "Global Surface Atlas resolution: {0}, memory usage: {1} MB", resolution, memUsage / 1024 / 1024); } else { // Perform atlas defragmentation if needed // TODO: track atlas used vs free ratio to skip defragmentation if it's nearly full (then maybe auto resize up?) if (currentFrame - surfaceAtlasData.LastFrameAtlasInsertFail < 10 && currentFrame - surfaceAtlasData.LastFrameAtlasDefragmentation > 60) { surfaceAtlasData.ClearObjects(); } } if (!surfaceAtlasData.AtlasTiles) surfaceAtlasData.AtlasTiles = New(0, 0, resolution, resolution); if (!_vertexBuffer) _vertexBuffer = New(0u, (uint32)sizeof(AtlasTileVertex), TEXT("GlobalSurfaceAtlas.VertexBuffer")); const Vector2 posToClipMul(2.0f * resolutionInv, -2.0f * resolutionInv); const Vector2 posToClipAdd(-1.0f, 1.0f); #define VB_WRITE_TILE_POS_ONLY(tile) \ Vector2 minPos((float)tile->X, (float)tile->Y), maxPos((float)(tile->X + tile->Width), (float)(tile->Y + tile->Height)); \ Half2 min(minPos * posToClipMul + posToClipAdd), max(maxPos * posToClipMul + posToClipAdd); \ auto* quad = _vertexBuffer->WriteReserve(6); \ quad[0].Position = max; \ quad[1].Position = { min.X, max.Y }; \ quad[2].Position = min; \ quad[3].Position = quad[2].Position; \ quad[4].Position = { max.X, min.Y }; \ quad[5].Position = quad[0].Position #define VB_WRITE_TILE(tile) \ Vector2 minPos((float)tile->X, (float)tile->Y), maxPos((float)(tile->X + tile->Width), (float)(tile->Y + tile->Height)); \ Half2 min(minPos * posToClipMul + posToClipAdd), max(maxPos * posToClipMul + posToClipAdd); \ Vector2 minUV(0, 0), maxUV(1, 1); \ auto* quad = _vertexBuffer->WriteReserve(6); \ quad[0] = { { max }, { maxUV }, (uint16)object.Index, (uint16)tileIndex }; \ quad[1] = { { min.X, max.Y }, { minUV.X, maxUV.Y }, (uint16)object.Index, (uint16)tileIndex }; \ quad[2] = { { min }, { minUV }, (uint16)object.Index, (uint16)tileIndex }; \ quad[3] = quad[2]; \ quad[4] = { { max.X, min.Y }, { maxUV.X, minUV.Y }, (uint16)object.Index, (uint16)tileIndex }; \ quad[5] = quad[0] #define VB_DRAW() \ _vertexBuffer->Flush(context); \ auto vb = _vertexBuffer->GetBuffer(); \ context->BindVB(ToSpan(&vb, 1)); \ context->DrawInstanced(_vertexBuffer->Data.Count() / sizeof(AtlasTileVertex), 1); // Add objects into the atlas surfaceAtlasData.ObjectsBuffer.Clear(); surfaceAtlasData.ObjectIndexCounter = 0; _dirtyObjectsBuffer.Clear(); { PROFILE_CPU_NAMED("Draw"); const uint32 viewMask = renderContext.View.RenderLayersMask; const Vector3 viewPosition = renderContext.View.Position; const uint16 minTileResolution = 8; // Minimum size (in texels) of the tile in atlas const uint16 maxTileResolution = 128; // Maximum size (in texels) of the tile in atlas const uint16 tileResolutionAlignment = 8; // Alignment to snap (down) tiles resolution which allows to reuse atlas slots once object gets resizes/replaced by other object const float minObjectRadius = 20.0f; // Skip too small objects const float tileTexelsPerWorldUnit = 1.0f / 10.0f; // Scales the tiles resolution const float distanceScalingStart = 2000.0f; // Distance from camera at which the tiles resolution starts to be scaled down const float distanceScalingEnd = 5000.0f; // Distance from camera at which the tiles resolution end to be scaled down const float distanceScaling = 0.1f; // The scale for tiles at distanceScalingEnd and further away static_assert(GLOBAL_SURFACE_ATLAS_TILE_PADDING < minTileResolution, "Invalid tile size configuration."); for (auto* scene : renderContext.List->Scenes) { // TODO: optimize for static objects (SceneRendering could have separate and optimized caching for static actors) for (auto& e : scene->Actors) { if (viewMask & e.LayerMask && e.Bounds.Radius >= minObjectRadius && CollisionsHelper::DistanceSpherePoint(e.Bounds, viewPosition) < distance) { // TODO: move into actor-specific Draw() impl (eg. via GlobalSurfaceAtlas pass) auto* staticModel = ScriptingObject::Cast(e.Actor); if (staticModel && staticModel->Model && staticModel->Model->IsLoaded() && staticModel->Model->CanBeRendered()) { Matrix localToWorld; staticModel->GetWorld(&localToWorld); bool anyTile = false, dirty = false; GlobalSurfaceAtlasObject* object = surfaceAtlasData.Objects.TryGet(e.Actor); auto& lod = staticModel->Model->LODs.Last(); BoundingBox localBounds = lod.GetBox(); Vector3 boundsSize = localBounds.GetSize() * staticModel->GetScale(); const float distanceScale = Math::Lerp(1.0f, distanceScaling, Math::InverseLerp(distanceScalingStart, distanceScalingEnd, CollisionsHelper::DistanceSpherePoint(e.Bounds, viewPosition))); const float tilesScale = tileTexelsPerWorldUnit * distanceScale; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { // Calculate optimal tile resolution for the object side Vector3 boundsSizeTile = boundsSize; boundsSizeTile.Raw[tileIndex / 2] = MAX_float; // Ignore depth size boundsSizeTile.Absolute(); uint16 tileResolution = (uint16)(boundsSizeTile.MinValue() * tilesScale); if (tileResolution < 4) { // Skip too small surfaces if (object && object->Tiles[tileIndex]) { object->Tiles[tileIndex]->Free(); object->Tiles[tileIndex] = nullptr; } continue; } // Clamp and snap to reduce atlas fragmentation tileResolution = Math::Clamp(tileResolution, minTileResolution, maxTileResolution); tileResolution = Math::AlignDown(tileResolution, tileResolutionAlignment); // Reuse current tile (refit only on a significant resolution change) if (object && object->Tiles[tileIndex]) { const uint16 tileRefitResolutionStep = 32; const uint16 currentSize = object->Tiles[tileIndex]->Width; if (Math::Abs(tileResolution - currentSize) < tileRefitResolutionStep) { anyTile = true; continue; } object->Tiles[tileIndex]->Free(); } // Insert tile into atlas auto* tile = surfaceAtlasData.AtlasTiles->Insert(tileResolution, tileResolution, 0, &surfaceAtlasData, e.Actor, tileIndex); if (tile) { if (!object) object = &surfaceAtlasData.Objects[e.Actor]; object->Tiles[tileIndex] = tile; anyTile = true; dirty = true; } else { if (object) object->Tiles[tileIndex] = nullptr; surfaceAtlasData.LastFrameAtlasInsertFail = currentFrame; } } if (anyTile) { // Redraw objects from time-to-time (dynamic objects can be animated, static objects can have textures streamed) uint32 redrawFramesCount = staticModel->HasStaticFlag(StaticFlags::Lightmap) ? 120 : 4; if (currentFrame - object->LastFrameDirty >= (redrawFramesCount + (e.Actor->GetID().D & redrawFramesCount))) dirty = true; // Mark object as used object->LastFrameUsed = currentFrame; object->Bounds = OrientedBoundingBox(localBounds); object->Bounds.Transform(localToWorld); object->Radius = e.Bounds.Radius; if (dirty || GLOBAL_SURFACE_ATLAS_DEBUG_FORCE_REDRAW_TILES) { object->LastFrameDirty = currentFrame; _dirtyObjectsBuffer.Add(e.Actor); } // Write to objects buffer (this must match unpacking logic in HLSL) Matrix worldToLocalBounds; Matrix::Invert(object->Bounds.Transformation, worldToLocalBounds); // TODO: cache data for static objects to optimize CPU perf (move ObjectsBuffer into surfaceAtlasData) object->Index = surfaceAtlasData.ObjectIndexCounter++; auto* objectData = surfaceAtlasData.ObjectsBuffer.WriteReserve(GLOBAL_SURFACE_ATLAS_OBJECT_SIZE); objectData[0] = *(Vector4*)&e.Bounds; objectData[1] = Vector4(worldToLocalBounds.M11, worldToLocalBounds.M12, worldToLocalBounds.M13, worldToLocalBounds.M41); objectData[2] = Vector4(worldToLocalBounds.M21, worldToLocalBounds.M22, worldToLocalBounds.M23, worldToLocalBounds.M42); objectData[3] = Vector4(worldToLocalBounds.M31, worldToLocalBounds.M32, worldToLocalBounds.M33, worldToLocalBounds.M43); objectData[4] = Vector4(object->Bounds.Extents, 0.0f); // TODO: try to optimize memory footprint (eg. merge scale into extents and use rotation+offset but reconstruct rotation from two axes with sign) for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object->Tiles[tileIndex]; const int32 tileStart = 5 + tileIndex * 5; if (!tile) { // Disable tile objectData[tileStart + 4] = Vector4::Zero; continue; } // Setup view to render object from the side Vector3 xAxis, yAxis, zAxis = Vector3::Zero; zAxis.Raw[tileIndex / 2] = tileIndex & 1 ? 1.0f : -1.0f; yAxis = tileIndex == 2 || tileIndex == 3 ? Vector3::Right : Vector3::Up; Vector3::Cross(yAxis, zAxis, xAxis); Vector3 localSpaceOffset = -zAxis * object->Bounds.Extents; Vector3::TransformNormal(xAxis, object->Bounds.Transformation, xAxis); Vector3::TransformNormal(yAxis, object->Bounds.Transformation, yAxis); Vector3::TransformNormal(zAxis, object->Bounds.Transformation, zAxis); xAxis.NormalizeFast(); yAxis.NormalizeFast(); zAxis.NormalizeFast(); Vector3::Transform(localSpaceOffset, object->Bounds.Transformation, tile->ViewPosition); tile->ViewDirection = zAxis; // Create view matrix tile->ViewMatrix.SetColumn1(Vector4(xAxis, -Vector3::Dot(xAxis, tile->ViewPosition))); tile->ViewMatrix.SetColumn2(Vector4(yAxis, -Vector3::Dot(yAxis, tile->ViewPosition))); tile->ViewMatrix.SetColumn3(Vector4(zAxis, -Vector3::Dot(zAxis, tile->ViewPosition))); tile->ViewMatrix.SetColumn4(Vector4(0, 0, 0, 1)); // Calculate object bounds size in the view OrientedBoundingBox viewBounds(object->Bounds); viewBounds.Transform(tile->ViewMatrix); Vector3 viewExtent; Vector3::TransformNormal(viewBounds.Extents, viewBounds.Transformation, viewExtent); tile->ViewBoundsSize = viewExtent.GetAbsolute() * 2.0f; // Per-tile data const float tileWidth = (float)tile->Width - GLOBAL_SURFACE_ATLAS_TILE_PADDING; const float tileHeight = (float)tile->Height - GLOBAL_SURFACE_ATLAS_TILE_PADDING; objectData[tileStart + 0] = Vector4(tile->X, tile->Y, tileWidth, tileHeight) * resolutionInv; objectData[tileStart + 1] = Vector4(tile->ViewMatrix.M11, tile->ViewMatrix.M12, tile->ViewMatrix.M13, tile->ViewMatrix.M41); objectData[tileStart + 2] = Vector4(tile->ViewMatrix.M21, tile->ViewMatrix.M22, tile->ViewMatrix.M23, tile->ViewMatrix.M42); objectData[tileStart + 3] = Vector4(tile->ViewMatrix.M31, tile->ViewMatrix.M32, tile->ViewMatrix.M33, tile->ViewMatrix.M43); objectData[tileStart + 4] = Vector4(tile->ViewBoundsSize, 1.0f); } } } } } } } // Remove unused objects for (auto it = surfaceAtlasData.Objects.Begin(); it.IsNotEnd(); ++it) { if (it->Value.LastFrameUsed != currentFrame) { for (auto& tile : it->Value.Tiles) { if (tile) tile->Free(); } surfaceAtlasData.Objects.Remove(it); } } // Send objects data to the GPU { PROFILE_GPU_CPU("Update Objects"); surfaceAtlasData.ObjectsBuffer.Flush(context); } // Rasterize world geometry material properties into Global Surface Atlas if (_dirtyObjectsBuffer.Count() != 0) { PROFILE_GPU_CPU("Rasterize Tiles"); RenderContext renderContextTiles = renderContext; renderContextTiles.List = RenderList::GetFromPool(); renderContextTiles.View.Pass = DrawPass::GBuffer; renderContextTiles.View.Mode = ViewMode::Default; renderContextTiles.View.ModelLODBias += 100000; renderContextTiles.View.ShadowModelLODBias += 100000; renderContextTiles.View.IsSingleFrame = true; renderContextTiles.View.Near = 0.0f; renderContextTiles.View.Prepare(renderContextTiles); GPUTextureView* depthBuffer = surfaceAtlasData.AtlasDepth->View(); GPUTextureView* targetBuffers[4] = { surfaceAtlasData.AtlasEmissive->View(), surfaceAtlasData.AtlasGBuffer0->View(), surfaceAtlasData.AtlasGBuffer1->View(), surfaceAtlasData.AtlasGBuffer2->View(), }; context->SetRenderTarget(depthBuffer, ToSpan(targetBuffers, ARRAY_COUNT(targetBuffers))); { PROFILE_GPU_CPU("Clear"); if (noCache || GLOBAL_SURFACE_ATLAS_DEBUG_FORCE_REDRAW_TILES) { // Full-atlas hardware clear context->ClearDepth(depthBuffer); context->Clear(targetBuffers[0], Color::Transparent); context->Clear(targetBuffers[1], Color::Transparent); context->Clear(targetBuffers[2], Color::Transparent); context->Clear(targetBuffers[3], Color(1, 0, 0, 0)); } else { // Per-tile clear (with a single draw call) _vertexBuffer->Clear(); _vertexBuffer->Data.EnsureCapacity(_dirtyObjectsBuffer.Count() * 6 * sizeof(AtlasTileVertex)); for (Actor* actor : _dirtyObjectsBuffer) { const auto& object = ((const Dictionary&)surfaceAtlasData.Objects)[actor]; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; if (!tile) continue; VB_WRITE_TILE_POS_ONLY(tile); } } context->SetState(_psClear); context->SetViewportAndScissors(Viewport(0, 0, resolution, resolution)); VB_DRAW(); } } // TODO: limit dirty objects count on a first frame (eg. collect overflown objects to be redirty next frame) auto& drawCallsListGBuffer = renderContextTiles.List->DrawCallsLists[(int32)DrawCallsListType::GBuffer]; auto& drawCallsListGBufferNoDecals = renderContextTiles.List->DrawCallsLists[(int32)DrawCallsListType::GBufferNoDecals]; drawCallsListGBuffer.CanUseInstancing = false; drawCallsListGBufferNoDecals.CanUseInstancing = false; for (Actor* actor : _dirtyObjectsBuffer) { // Clear draw calls list renderContextTiles.List->DrawCalls.Clear(); renderContextTiles.List->BatchedDrawCalls.Clear(); drawCallsListGBuffer.Indices.Clear(); drawCallsListGBuffer.PreBatchedDrawCalls.Clear(); drawCallsListGBufferNoDecals.Indices.Clear(); drawCallsListGBufferNoDecals.PreBatchedDrawCalls.Clear(); // Fake projection matrix to disable Screen Size culling based on RenderTools::ComputeBoundsScreenRadiusSquared renderContextTiles.View.Projection.Values[0][0] = 10000.0f; // Collect draw calls for the object actor->Draw(renderContextTiles); // Render all tiles into the atlas const auto& object = ((const Dictionary&)surfaceAtlasData.Objects)[actor]; #if GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_OBJECTS DebugDraw::DrawBox(object.Bounds, Color::Red.AlphaMultiplied(0.4f)); #endif for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; if (!tile) continue; const float tileWidth = (float)tile->Width - GLOBAL_SURFACE_ATLAS_TILE_PADDING; const float tileHeight = (float)tile->Height - GLOBAL_SURFACE_ATLAS_TILE_PADDING; // Setup projection to capture object from the side renderContextTiles.View.Position = tile->ViewPosition; renderContextTiles.View.Direction = tile->ViewDirection; renderContextTiles.View.Near = -GLOBAL_SURFACE_ATLAS_TILE_PROJ_PLANE_OFFSET; renderContextTiles.View.Far = tile->ViewBoundsSize.Z + 2 * GLOBAL_SURFACE_ATLAS_TILE_PROJ_PLANE_OFFSET; Matrix projectionMatrix; Matrix::Ortho(tile->ViewBoundsSize.X, tile->ViewBoundsSize.Y, renderContextTiles.View.Near, renderContextTiles.View.Far, projectionMatrix); renderContextTiles.View.SetUp(tile->ViewMatrix, projectionMatrix); #if GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_OBJECTS DebugDraw::DrawLine(renderContextTiles.View.Position, renderContextTiles.View.Position + renderContextTiles.View.Direction * 20.0f, Color::Orange); DebugDraw::DrawWireSphere(BoundingSphere(renderContextTiles.View.Position, 10.0f), Color::Green); #endif // Draw context->SetViewportAndScissors(Viewport(tile->X, tile->Y, tileWidth, tileHeight)); renderContextTiles.List->ExecuteDrawCalls(renderContextTiles, drawCallsListGBuffer); renderContextTiles.List->ExecuteDrawCalls(renderContextTiles, drawCallsListGBufferNoDecals); } } context->ResetRenderTarget(); RenderList::ReturnToPool(renderContextTiles.List); } // Copy results result.Atlas[0] = surfaceAtlasData.AtlasDepth; result.Atlas[1] = surfaceAtlasData.AtlasGBuffer0; result.Atlas[2] = surfaceAtlasData.AtlasGBuffer1; result.Atlas[3] = surfaceAtlasData.AtlasGBuffer2; result.Atlas[4] = surfaceAtlasData.AtlasDirectLight; result.Objects = surfaceAtlasData.ObjectsBuffer.GetBuffer(); result.GlobalSurfaceAtlas.Resolution = (float)resolution; result.GlobalSurfaceAtlas.ObjectsCount = surfaceAtlasData.Objects.Count(); surfaceAtlasData.Result = result; // Render direct lighting into atlas 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 { PROFILE_GPU_CPU("Copy Emissive"); context->CopyTexture(surfaceAtlasData.AtlasDirectLight, 0, 0, 0, 0, surfaceAtlasData.AtlasEmissive, 0); } context->SetViewportAndScissors(Viewport(0, 0, resolution, resolution)); context->SetRenderTarget(surfaceAtlasData.AtlasDirectLight->View()); context->BindSR(0, surfaceAtlasData.AtlasGBuffer0->View()); context->BindSR(1, surfaceAtlasData.AtlasGBuffer1->View()); context->BindSR(2, surfaceAtlasData.AtlasGBuffer2->View()); context->BindSR(3, surfaceAtlasData.AtlasDepth->View()); context->BindSR(4, surfaceAtlasData.ObjectsBuffer.GetBuffer()->View()); for (int32 i = 0; i < 4; i++) { context->BindSR(i + 5, bindingDataSDF.Cascades[i]->ViewVolume()); context->BindSR(i + 9, bindingDataSDF.CascadeMips[i]->ViewVolume()); } context->BindCB(0, _cb0); Data0 data; data.ViewWorldPos = renderContext.View.Position; data.GlobalSDF = bindingDataSDF.GlobalSDF; data.GlobalSurfaceAtlas = result.GlobalSurfaceAtlas; // 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 _vertexBuffer->Clear(); for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; if (!tile || Vector3::Dot(tile->ViewDirection, light.Direction) < ZeroTolerance) continue; VB_WRITE_TILE(tile); } } // Draw draw 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); data.LightShadowsStrength = 1.0f - light.ShadowsStrength; context->UpdateCB(_cb0, &data); context->SetState(_psDirectLighting0); VB_DRAW(); } for (auto& light : renderContext.List->PointLights) { // Collect tiles to shade _vertexBuffer->Clear(); for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; Vector3 lightToObject = object.Bounds.GetCenter() - light.Position; if (lightToObject.LengthSquared() >= Math::Square(object.Radius + light.Radius)) continue; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; if (!tile) continue; VB_WRITE_TILE(tile); } } // Draw draw light const bool useShadow = CanRenderShadow(renderContext.View, light); light.SetupLightData(&data.Light, useShadow); data.LightShadowsStrength = 1.0f - light.ShadowsStrength; context->UpdateCB(_cb0, &data); context->SetState(_psDirectLighting1); VB_DRAW(); } for (auto& light : renderContext.List->SpotLights) { // Collect tiles to shade _vertexBuffer->Clear(); for (const auto& e : surfaceAtlasData.Objects) { const auto& object = e.Value; Vector3 lightToObject = object.Bounds.GetCenter() - light.Position; if (lightToObject.LengthSquared() >= Math::Square(object.Radius + light.Radius)) continue; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; if (!tile || Vector3::Dot(tile->ViewDirection, light.Direction) < ZeroTolerance) continue; VB_WRITE_TILE(tile); } } // Draw draw light const bool useShadow = CanRenderShadow(renderContext.View, light); light.SetupLightData(&data.Light, useShadow); data.LightShadowsStrength = 1.0f - light.ShadowsStrength; context->UpdateCB(_cb0, &data); context->SetState(_psDirectLighting1); VB_DRAW(); } context->ResetRenderTarget(); } // TODO: indirect lighting apply to get infinite bounces for GI // TODO: explore atlas tiles optimization with feedback from renderer (eg. when tile is sampled by GI/Reflections mark it as used, then sort tiles by importance and prioritize updates for ones frequently used) #undef WRITE_TILE return false; } void GlobalSurfaceAtlasPass::RenderDebug(RenderContext& renderContext, GPUContext* context, GPUTexture* output) { GlobalSignDistanceFieldPass::BindingData bindingDataSDF; BindingData bindingData; if (GlobalSignDistanceFieldPass::Instance()->Render(renderContext, context, bindingDataSDF) || Render(renderContext, context, bindingData)) { context->Draw(output, renderContext.Buffers->GBuffer0); return; } PROFILE_GPU_CPU("Global Surface Atlas Debug"); const Vector2 outputSize(output->Size()); { Data0 data; data.ViewWorldPos = renderContext.View.Position; data.ViewNearPlane = renderContext.View.Near; data.ViewFarPlane = renderContext.View.Far; for (int32 i = 0; i < 4; i++) data.ViewFrustumWorldRays[i] = Vector4(renderContext.List->FrustumCornersWs[i + 4], 0); data.GlobalSDF = bindingDataSDF.GlobalSDF; data.GlobalSurfaceAtlas = bindingData.GlobalSurfaceAtlas; context->UpdateCB(_cb0, &data); context->BindCB(0, _cb0); } for (int32 i = 0; i < 4; i++) { context->BindSR(i, bindingDataSDF.Cascades[i]->ViewVolume()); context->BindSR(i + 4, bindingDataSDF.CascadeMips[i]->ViewVolume()); } context->BindSR(8, bindingData.Objects ? bindingData.Objects->View() : nullptr); context->BindSR(9, bindingData.Atlas[0]->View()); { //GPUTexture* tex = bindingData.Atlas[1]; // Preview diffuse //GPUTexture* tex = bindingData.Atlas[2]; // Preview normals //GPUTexture* tex = bindingData.Atlas[3]; // Preview roughness/metalness/ao GPUTexture* tex = bindingData.Atlas[4]; // Preview direct light context->BindSR(10, tex->View()); } context->SetState(_psDebug); context->SetRenderTarget(output->View()); context->SetViewportAndScissors(outputSize.X, outputSize.Y); context->DrawFullscreenTriangle(); }