From 8cca7f884be96e11b526122078402bc4677615d8 Mon Sep 17 00:00:00 2001 From: Wojciech Figat Date: Fri, 18 Mar 2022 16:18:00 +0100 Subject: [PATCH] Add **Global Sign Distance Field** rendering (work in progress) --- Content/Shaders/GlobalSignDistanceField.flax | 3 + Source/Editor/Cooker/Steps/DeployDataStep.cpp | 1 + Source/Editor/Viewport/EditorViewport.cs | 1 + Source/Engine/Foliage/Foliage.cpp | 2 + Source/Engine/Foliage/FoliageType.cpp | 4 + Source/Engine/Graphics/Enums.h | 14 +- Source/Engine/Level/Actors/AnimatedModel.cpp | 6 + Source/Engine/Level/Actors/SplineModel.cpp | 6 + Source/Engine/Level/Actors/StaticModel.cpp | 5 + Source/Engine/Particles/ParticleEffect.cpp | 6 + .../Renderer/GlobalSignDistanceFieldPass.cpp | 595 ++++++++++++++++++ .../Renderer/GlobalSignDistanceFieldPass.h | 75 +++ Source/Engine/Renderer/Renderer.cpp | 13 +- Source/Engine/Terrain/Terrain.cpp | 6 + Source/Engine/UI/SpriteRender.cpp | 2 + Source/Engine/UI/TextRender.cpp | 6 + Source/Shaders/GlobalSignDistanceField.hlsl | 153 +++++ Source/Shaders/GlobalSignDistanceField.shader | 216 +++++++ 18 files changed, 1109 insertions(+), 5 deletions(-) create mode 100644 Content/Shaders/GlobalSignDistanceField.flax create mode 100644 Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp create mode 100644 Source/Engine/Renderer/GlobalSignDistanceFieldPass.h create mode 100644 Source/Shaders/GlobalSignDistanceField.hlsl create mode 100644 Source/Shaders/GlobalSignDistanceField.shader diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax new file mode 100644 index 000000000..571c8bf25 --- /dev/null +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f78c2ca540a80769090fe8e855c3a61671556845d2751a31125f42204d34b1b4 +size 8246 diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index 4e511b0ac..8ff70bb4e 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -68,6 +68,7 @@ bool DeployDataStep::Perform(CookingData& data) data.AddRootEngineAsset(TEXT("Shaders/MotionBlur")); data.AddRootEngineAsset(TEXT("Shaders/BitonicSort")); data.AddRootEngineAsset(TEXT("Shaders/GPUParticlesSorting")); + data.AddRootEngineAsset(TEXT("Shaders/GlobalSignDistanceField")); data.AddRootEngineAsset(TEXT("Shaders/Quad")); data.AddRootEngineAsset(TEXT("Shaders/Reflections")); data.AddRootEngineAsset(TEXT("Shaders/Shadows")); diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 548da2e56..c618dd568 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -1407,6 +1407,7 @@ namespace FlaxEditor.Viewport new ViewModeOptions(ViewMode.LODPreview, "LOD Preview"), new ViewModeOptions(ViewMode.MaterialComplexity, "Material Complexity"), new ViewModeOptions(ViewMode.QuadOverdraw, "Quad Overdraw"), + new ViewModeOptions(ViewMode.GlobalSDF, "Global SDF"), }; private void WidgetCamSpeedShowHide(Control cm) diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 1d367c5ef..c784178c6 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -845,6 +845,8 @@ bool Foliage::Intersects(const Ray& ray, float& distance, Vector3& normal, int32 void Foliage::Draw(RenderContext& renderContext) { + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; // TODO: Foliage rendering to Global SDF if (Instances.IsEmpty()) return; auto& view = renderContext.View; diff --git a/Source/Engine/Foliage/FoliageType.cpp b/Source/Engine/Foliage/FoliageType.cpp index 75bd33191..bdb57b922 100644 --- a/Source/Engine/Foliage/FoliageType.cpp +++ b/Source/Engine/Foliage/FoliageType.cpp @@ -208,4 +208,8 @@ void FoliageType::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE(PlacementRandomRollAngle); DESERIALIZE_BIT(PlacementAlignToNormal); DESERIALIZE_BIT(PlacementRandomYaw); + + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; } diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 8b9057f75..c1fca1224 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -702,6 +702,11 @@ API_ENUM(Attributes="Flags") enum class DrawPass : int32 /// MotionVectors = 1 << 4, + /// + /// The Global Sign Distance Field (SDF) rendering pass. + /// + GlobalSDF = 1 << 5, + /// /// The debug quad overdraw rendering (editor-only). /// @@ -712,13 +717,13 @@ API_ENUM(Attributes="Flags") enum class DrawPass : int32 /// The default set of draw passes for the scene objects. /// API_ENUM(Attributes="HideInEditor") - Default = Depth | GBuffer | Forward | Distortion | MotionVectors, + Default = Depth | GBuffer | Forward | Distortion | MotionVectors | GlobalSDF, /// /// The all draw passes combined into a single mask. /// API_ENUM(Attributes="HideInEditor") - All = Depth | GBuffer | Forward | Distortion | MotionVectors, + All = Depth | GBuffer | Forward | Distortion | MotionVectors | GlobalSDF, }; DECLARE_ENUM_OPERATORS(DrawPass); @@ -847,6 +852,11 @@ API_ENUM() enum class ViewMode /// Draw geometry overdraw to visualize performance of pixels rendering. /// QuadOverdraw = 23, + + /// + /// Draw global Sign Distant Field (SDF) preview. + /// + GlobalSDF = 24, }; /// diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index c6a770de7..e2d5a5668 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -689,6 +689,8 @@ void AnimatedModel::Update() void AnimatedModel::Draw(RenderContext& renderContext) { + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; // TODO: Animated Model rendering to Global SDF GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, _world); const DrawPass drawModes = (DrawPass)(DrawModes & renderContext.View.Pass & (int32)renderContext.View.GetShadowsDrawPassMask(ShadowsMode)); @@ -806,6 +808,10 @@ void AnimatedModel::Deserialize(DeserializeStream& stream, ISerializeModifier* m DESERIALIZE(RootMotionTarget); Entries.DeserializeIfExists(stream, "Buffer", modifier); + + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; } bool AnimatedModel::IntersectsEntry(int32 entryIndex, const Ray& ray, float& distance, Vector3& normal) diff --git a/Source/Engine/Level/Actors/SplineModel.cpp b/Source/Engine/Level/Actors/SplineModel.cpp index f432e1da5..c84226928 100644 --- a/Source/Engine/Level/Actors/SplineModel.cpp +++ b/Source/Engine/Level/Actors/SplineModel.cpp @@ -350,6 +350,8 @@ void SplineModel::Draw(RenderContext& renderContext) if (!_spline || !Model || !Model->IsLoaded() || !Model->CanBeRendered() || actorDrawModes == DrawPass::None) return; auto model = Model.Get(); + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; // TODO: Spline Model rendering to Global SDF if (!Entries.IsValidFor(model)) Entries.Setup(model); @@ -469,6 +471,10 @@ void SplineModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE(DrawModes); Entries.DeserializeIfExists(stream, "Buffer", modifier); + + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; } void SplineModel::OnTransformChanged() diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index cbb700dda..6e50fb174 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -209,6 +209,8 @@ bool StaticModel::HasContentLoaded() const void StaticModel::Draw(RenderContext& renderContext) { + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; // TODO: Static Model rendering to Global SDF GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, _world); const DrawPass drawModes = (DrawPass)(DrawModes & renderContext.View.Pass); @@ -409,6 +411,9 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DrawModes = DrawPass::Depth; } } + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; { const auto member = stream.FindMember("RenderPasses"); diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 7cf2420ca..6275509e5 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -494,6 +494,8 @@ bool ParticleEffect::HasContentLoaded() const void ParticleEffect::Draw(RenderContext& renderContext) { + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; _lastMinDstSqr = Math::Min(_lastMinDstSqr, Vector3::DistanceSquared(GetPosition(), renderContext.View.Position)); Particles::DrawParticles(renderContext, this); } @@ -677,6 +679,10 @@ void ParticleEffect::Deserialize(DeserializeStream& stream, ISerializeModifier* { ApplyModifiedParameters(); } + + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; } void ParticleEffect::EndPlay() diff --git a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp new file mode 100644 index 000000000..881d8dd6e --- /dev/null +++ b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp @@ -0,0 +1,595 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#include "GlobalSignDistanceFieldPass.h" +#include "GBufferPass.h" +#include "RenderList.h" +#include "Engine/Core/Math/Int3.h" +#include "Engine/Core/Collections/HashSet.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/Scene/SceneRendering.h" +#include "Engine/Level/Actors/StaticModel.h" + +// Some of those constants must match in shader +// TODO: try using R8 format for Global SDF +#define GLOBAL_SDF_FORMAT PixelFormat::R16_Float +#define GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT 28 +#define GLOBAL_SDF_RASTERIZE_GROUP_SIZE 8 +#define GLOBAL_SDF_RASTERIZE_CHUNK_SIZE 32 +#define GLOBAL_SDF_RASTERIZE_CHUNK_MARGIN 4 +#define GLOBAL_SDF_MIP_GROUP_SIZE 4 +#define GLOBAL_SDF_MIP_FLOODS 5 +#define GLOBAL_SDF_DEBUG_CHUNKS 0 + +static_assert(GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT % 4 == 0, "Must be multiple of 4 due to data packing for GPU constant buffer."); +#if GLOBAL_SDF_DEBUG_CHUNKS +#include "Engine/Debug/DebugDraw.h" +#endif + +PACK_STRUCT(struct ModelRasterizeData + { + Matrix WorldToVolume; // TODO: use 3x4 matrix + Matrix VolumeToWorld; // TODO: use 3x4 matrix + Vector3 VolumeToUVWMul; + float MipOffset; + Vector3 VolumeToUVWAdd; + float MaxDistance; + Vector3 VolumeLocalBoundsExtent; + float Padding0; + }); + +PACK_STRUCT(struct Data + { + Vector3 ViewWorldPos; + float ViewNearPlane; + Vector3 Padding00; + float ViewFarPlane; + Vector4 ViewFrustumWorldRays[4]; + GlobalSignDistanceFieldPass::GlobalSDFData GlobalSDF; + }); + +PACK_STRUCT(struct ModelsRasterizeData + { + Int3 ChunkCoord; + float MaxDistance; + Vector3 CascadeCoordToPosMul; + int ModelsCount; + Vector3 CascadeCoordToPosAdd; + int32 CascadeResolution; + Vector2 Padding0; + int32 CascadeMipResolution; + int32 CascadeMipFactor; + uint32 Models[GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT]; + }); + +struct RasterizeModel +{ + Matrix WorldToVolume; + Matrix VolumeToWorld; + Vector3 VolumeToUVWMul; + Vector3 VolumeToUVWAdd; + Vector3 VolumeLocalBoundsExtent; + float MipOffset; + const ModelBase::SDFData* SDF; +}; + +struct RasterizeChunk +{ + int32 ModelsCount = 0; + int32 Models[GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT]; +}; + +constexpr int32 RasterizeChunkKeyHashResolution = GLOBAL_SDF_RASTERIZE_CHUNK_SIZE; + +struct RasterizeChunkKey +{ + uint32 Hash; + int32 Layer; + Int3 Coord; + + friend bool operator==(const RasterizeChunkKey& a, const RasterizeChunkKey& b) + { + return a.Hash == b.Hash && a.Coord == b.Coord && a.Layer == b.Layer; + } +}; + +uint32 GetHash(const RasterizeChunkKey& key) +{ + return key.Hash; +} + +class GlobalSignDistanceFieldCustomBuffer : public RenderBuffers::CustomBuffer +{ +public: + GPUTexture* Cascades[4] = {}; + GPUTexture* CascadeMips[4] = {}; + Vector3 Positions[4]; + HashSet NonEmptyChunks[4]; + + ~GlobalSignDistanceFieldCustomBuffer() + { + for (GPUTexture* cascade : Cascades) + RenderTargetPool::Release(cascade); + for (GPUTexture* mip : CascadeMips) + RenderTargetPool::Release(mip); + } +}; + +namespace +{ + Dictionary ChunksCache; +} + +String GlobalSignDistanceFieldPass::ToString() const +{ + return TEXT("GlobalSignDistanceFieldPass"); +} + +bool GlobalSignDistanceFieldPass::Init() +{ + // Check platform support + auto device = GPUDevice::Instance; + if (device->GetFeatureLevel() < FeatureLevel::SM5 || !device->Limits.HasCompute || !device->Limits.HasTypedUAVLoad) + return false; + if (FORMAT_FEATURES_ARE_NOT_SUPPORTED(device->GetFormatFeatures(GLOBAL_SDF_FORMAT).Support, FormatSupport::ShaderSample | FormatSupport::Texture3D)) + return false; + + // Create pipeline states + _psDebug = device->CreatePipelineState(); + + // Load shader + _shader = Content::LoadAsyncInternal(TEXT("Shaders/GlobalSignDistanceField")); + if (_shader == nullptr) + return false; +#if COMPILE_WITH_DEV_ENV + _shader.Get()->OnReloading.Bind(this); +#endif + + // Init buffer + _modelsBuffer = New(64u * (uint32)sizeof(ModelRasterizeData), (uint32)sizeof(ModelRasterizeData), false, TEXT("GlobalSDF.ModelsBuffer")); + + return false; +} + +bool GlobalSignDistanceFieldPass::setupResources() +{ + // Check shader + if (!_shader || !_shader->IsLoaded()) + return true; + const auto shader = _shader->GetShader(); + _cb0 = shader->GetCB(0); + _cb1 = shader->GetCB(1); + _csRasterizeModel0 = shader->GetCS("CS_RasterizeModel", 0); + _csRasterizeModel1 = shader->GetCS("CS_RasterizeModel", 1); + _csClearChunk = shader->GetCS("CS_ClearChunk"); + _csGenerateMip0 = shader->GetCS("CS_GenerateMip", 0); + _csGenerateMip1 = shader->GetCS("CS_GenerateMip", 1); + + // Create pipeline state + GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; + if (!_psDebug->IsValid()) + { + psDesc.PS = shader->GetPS("PS_Debug"); + if (_psDebug->Init(psDesc)) + return true; + } + + return false; +} + +#if USE_EDITOR + +void GlobalSignDistanceFieldPass::OnShaderReloading(Asset* obj) +{ + _psDebug->ReleaseGPU(); + _csRasterizeModel0 = nullptr; + _csRasterizeModel1 = nullptr; + _csClearChunk = nullptr; + _csGenerateMip0 = nullptr; + _csGenerateMip1 = nullptr; + _cb0 = nullptr; + _cb1 = nullptr; + invalidateResources(); +} + +#endif + +void GlobalSignDistanceFieldPass::Dispose() +{ + RendererPass::Dispose(); + + // Cleanup + Delete(_modelsBuffer); + _modelsTextures.Resize(0); + SAFE_DELETE_GPU_RESOURCE(_psDebug); + _shader = nullptr; + ChunksCache.Clear(); + ChunksCache.SetCapacity(0); +} + +bool GlobalSignDistanceFieldPass::Render(RenderContext& renderContext, GPUContext* context, BindingData& result) +{ + // Skip if not supported + if (setupResources()) + return true; + if (renderContext.List->Scenes.Count() == 0) + return true; + auto& sdfData = *renderContext.Buffers->GetCustomBuffer(TEXT("GlobalSignDistanceField")); + + // TODO: configurable via graphics settings + const int32 resolution = 256; + const int32 mipFactor = 4; + const int32 resolutionMip = Math::DivideAndRoundUp(resolution, mipFactor); + // TODO: configurable via postFx settings + const float distanceExtent = 2500.0f; + const float cascadesDistances[] = { distanceExtent, distanceExtent * 2.0f, distanceExtent * 4.0f, distanceExtent * 8.0f }; + + // Skip if already done in the current frame + const auto currentFrame = Engine::FrameCount; + if (sdfData.LastFrameUsed != currentFrame) + { + PROFILE_GPU_CPU("Global SDF"); + + // Initialize buffers + sdfData.LastFrameUsed = currentFrame; + auto desc = GPUTextureDescription::New3D(resolution, resolution, resolution, GLOBAL_SDF_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess, 1); + bool updated = false; + for (GPUTexture*& cascade : sdfData.Cascades) + { + if (cascade && cascade->Width() != desc.Width) + { + RenderTargetPool::Release(cascade); + cascade = nullptr; + } + if (!cascade) + { + cascade = RenderTargetPool::Get(desc); + if (!cascade) + return true; + updated = true; + PROFILE_GPU_CPU("Init"); + context->ClearUA(cascade, Vector4::One); + } + } + desc = GPUTextureDescription::New3D(resolutionMip, resolutionMip, resolutionMip, GLOBAL_SDF_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess, 1); + for (GPUTexture*& cascadeMip : sdfData.CascadeMips) + { + if (cascadeMip && cascadeMip->Width() != desc.Width) + { + RenderTargetPool::Release(cascadeMip); + cascadeMip = nullptr; + } + if (!cascadeMip) + { + cascadeMip = RenderTargetPool::Get(desc); + if (!cascadeMip) + return true; + updated = true; + } + } + GPUTexture* tmpMip = nullptr; + if (updated) + LOG(Info, "Global SDF memory usage: {0} MB", (sdfData.Cascades[0]->GetMemoryUsage() + sdfData.CascadeMips[0]->GetMemoryUsage()) * ARRAY_COUNT(sdfData.Cascades) / 1024 / 1024); + + // Rasterize world geometry into Global SDF + renderContext.View.Pass = DrawPass::GlobalSDF; + uint32 viewMask = renderContext.View.RenderLayersMask; + const bool useCache = !updated && !renderContext.Task->IsCameraCut; + static_assert(GLOBAL_SDF_RASTERIZE_CHUNK_SIZE % GLOBAL_SDF_RASTERIZE_GROUP_SIZE == 0, "Invalid chunk size for Global SDF rasterization group size."); + const int32 rasterizeChunks = Math::CeilToInt((float)resolution / (float)GLOBAL_SDF_RASTERIZE_CHUNK_SIZE); + auto& chunks = ChunksCache; + chunks.EnsureCapacity(rasterizeChunks * rasterizeChunks, false); + bool anyDraw = false; + const uint64 cascadeFrequencies[] = { 2, 3, 5, 11 }; + //const uint64 cascadeFrequencies[] = { 1, 1, 1, 1 }; + for (int32 cascade = 0; cascade < 4; cascade++) + { + // Reduce frequency of the updates + if (useCache && (Engine::FrameCount % cascadeFrequencies[cascade]) != 0) + continue; + const float distance = cascadesDistances[cascade]; + const float maxDistance = distance * 2; + const float voxelSize = maxDistance / resolution; + const float snapping = voxelSize * mipFactor; + const Vector3 center = Vector3::Floor(renderContext.View.Position / snapping) * snapping; + // TODO: cascade scrolling on movement to reduce dirty chunks? + //const Vector3 center = Vector3::Zero; + sdfData.Positions[cascade] = center; + BoundingBox cascadeBounds(center - distance, center + distance); + // TODO: add scene detail scale factor to PostFx settings (eg. to increase or decrease scene details and quality) + const float minObjectRadius = Math::Max(20.0f, voxelSize * 0.5f); // Skip too small objects for this cascade + const float chunkSize = voxelSize * GLOBAL_SDF_RASTERIZE_CHUNK_SIZE; + GPUTextureView* cascadeView = sdfData.Cascades[cascade]->ViewVolume(); + GPUTextureView* cascadeMipView = sdfData.CascadeMips[cascade]->ViewVolume(); + + // Clear cascade before rasterization + { + PROFILE_CPU_NAMED("Clear"); + chunks.Clear(); + _modelsBuffer->Clear(); + _modelsTextures.Clear(); + } + int32 modelsBufferCount = 0; + + // Draw all objects from all scenes into the cascade + for (auto* scene : renderContext.List->Scenes) + { + // TODO: optimize for moving camera (copy sdf) + // TODO: if chunk is made of static objects only then mark it as static and skip from rendering during the next frame (will need to track objects dirty state in the SceneRendering) + for (auto& e : scene->Actors) + { + if (viewMask & e.LayerMask && e.Bounds.Radius >= minObjectRadius && CollisionsHelper::BoxIntersectsSphere(cascadeBounds, e.Bounds)) + { + e.Actor->Draw(renderContext); + + // TODO: move to be properly implemented per object type + auto staticModel = ScriptingObject::Cast(e.Actor); + if (staticModel && staticModel->Model && staticModel->Model->IsLoaded() && staticModel->Model->CanBeRendered() && staticModel->DrawModes & DrawPass::GlobalSDF) + { + // okay so firstly we need SDF for this model + // TODO: implement SDF generation on model import + if (!staticModel->Model->SDF.Texture) + staticModel->Model->GenerateSDF(); + ModelBase::SDFData& sdf = staticModel->Model->SDF; + if (sdf.Texture) + { + // Setup object data + BoundingBox objectBounds = staticModel->GetBox(); + BoundingBox objectBoundsCascade; + const float objectMargin = voxelSize * GLOBAL_SDF_RASTERIZE_CHUNK_MARGIN; + Vector3::Clamp(objectBounds.Minimum - objectMargin, cascadeBounds.Minimum, cascadeBounds.Maximum, objectBoundsCascade.Minimum); + Vector3::Subtract(objectBoundsCascade.Minimum, cascadeBounds.Minimum, objectBoundsCascade.Minimum); + Vector3::Clamp(objectBounds.Maximum + objectMargin, cascadeBounds.Minimum, cascadeBounds.Maximum, objectBoundsCascade.Maximum); + Vector3::Subtract(objectBoundsCascade.Maximum, cascadeBounds.Minimum, objectBoundsCascade.Maximum); + Int3 objectChunkMin(objectBoundsCascade.Minimum / chunkSize); + Int3 objectChunkMax(objectBoundsCascade.Maximum / chunkSize); + Matrix localToWorld, worldToLocal, volumeToWorld; + staticModel->GetWorld(&localToWorld); + Matrix::Invert(localToWorld, worldToLocal); + BoundingBox localVolumeBounds(sdf.LocalBoundsMin, sdf.LocalBoundsMax); + Vector3 volumeLocalBoundsExtent = localVolumeBounds.GetSize() * 0.5f; + Matrix worldToVolume = worldToLocal * Matrix::Translation(-(localVolumeBounds.Minimum + volumeLocalBoundsExtent)); + Matrix::Invert(worldToVolume, volumeToWorld); + + // Pick the SDF mip for the cascade + int32 mipLevelIndex = 1; + float worldUnitsPerVoxel = sdf.WorldUnitsPerVoxel * localToWorld.GetScaleVector().MaxValue() * 2; + while (voxelSize > worldUnitsPerVoxel && mipLevelIndex < sdf.Texture->MipLevels()) + { + mipLevelIndex++; + worldUnitsPerVoxel *= 2.0f; + } + mipLevelIndex--; + + // Volume -> Local -> UVW + Vector3 volumeToUVWMul = sdf.LocalToUVWMul; + Vector3 volumeToUVWAdd = sdf.LocalToUVWAdd + (localVolumeBounds.Minimum + volumeLocalBoundsExtent) * sdf.LocalToUVWMul; + + // Add model data for the GPU buffer + int32 modelIndex = modelsBufferCount++; + ModelRasterizeData modelData; + Matrix::Transpose(worldToVolume, modelData.WorldToVolume); + Matrix::Transpose(volumeToWorld, modelData.VolumeToWorld); + modelData.VolumeLocalBoundsExtent = volumeLocalBoundsExtent; + modelData.VolumeToUVWMul = volumeToUVWMul; + modelData.VolumeToUVWAdd = volumeToUVWAdd; + modelData.MipOffset = (float)mipLevelIndex; + modelData.MaxDistance = sdf.MaxDistance; + _modelsBuffer->Write(modelData); + _modelsTextures.Add(sdf.Texture->ViewVolume()); + + // Inject object into the intersecting cascade chunks + RasterizeChunkKey key; + for (key.Coord.Z = objectChunkMin.Z; key.Coord.Z <= objectChunkMax.Z; key.Coord.Z++) + { + for (key.Coord.Y = objectChunkMin.Y; key.Coord.Y <= objectChunkMax.Y; key.Coord.Y++) + { + for (key.Coord.X = objectChunkMin.X; key.Coord.X <= objectChunkMax.X; key.Coord.X++) + { + key.Layer = 0; + key.Hash = key.Coord.Z * (RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution) + key.Coord.Y * RasterizeChunkKeyHashResolution + key.Coord.X; + RasterizeChunk* chunk = &chunks[key]; + + // Move to the next layer if chunk has overflown + while (chunk->ModelsCount == GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT) + { + key.Layer++; + key.Hash += RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution; + chunk = &chunks[key]; + } + + chunk->Models[chunk->ModelsCount++] = modelIndex; + } + } + } + } + } + } + } + } + + // Send models data to the GPU + { + PROFILE_GPU_CPU("Update Models"); + _modelsBuffer->Flush(context); + } + + // Perform batched chunks rasterization + if (!anyDraw) + { + anyDraw = true; + context->ResetSR(); + tmpMip = RenderTargetPool::Get(desc); + if (!tmpMip) + return true; + } + ModelsRasterizeData data; + data.CascadeCoordToPosMul = cascadeBounds.GetSize() / resolution; + data.CascadeCoordToPosAdd = cascadeBounds.Minimum + voxelSize * 0.5f; + data.MaxDistance = maxDistance; + data.CascadeResolution = resolution; + data.CascadeMipResolution = resolutionMip; + data.CascadeMipFactor = mipFactor; + context->BindUA(0, cascadeView); + context->BindSR(0, _modelsBuffer->GetBuffer() ? _modelsBuffer->GetBuffer()->View() : nullptr); + if (_cb1) + context->BindCB(1, _cb1); + const int32 chunkDispatchGroups = GLOBAL_SDF_RASTERIZE_CHUNK_SIZE / GLOBAL_SDF_RASTERIZE_GROUP_SIZE; + auto& nonEmptyChunks = sdfData.NonEmptyChunks[cascade]; + { + PROFILE_GPU_CPU("Clear Chunks"); + for (auto it = nonEmptyChunks.Begin(); it.IsNotEnd(); ++it) + { + auto& key = it->Item; + if (chunks.ContainsKey(key)) + continue; + + // Clear empty chunk + nonEmptyChunks.Remove(it); + data.ChunkCoord = key.Coord * GLOBAL_SDF_RASTERIZE_CHUNK_SIZE; + if (_cb1) + context->UpdateCB(_cb1, &data); + context->Dispatch(_csClearChunk, chunkDispatchGroups, chunkDispatchGroups, chunkDispatchGroups); + // TODO: don't stall with UAV barrier on D3D12 if UAVs don't change between dispatches + } + } + // TODO: rasterize models into global sdf relative to the cascade origin to prevent fp issues on large worlds + { + PROFILE_GPU_CPU("Rasterize Chunks"); + for (auto& e : chunks) + { + // Rasterize non-empty chunk + auto& key = e.Key; + auto& chunk = e.Value; + for (int32 i = 0; i < chunk.ModelsCount; i++) + { + int32 model = chunk.Models[i]; + data.Models[i] = model; + context->BindSR(i + 1, _modelsTextures[model]); + } + ASSERT_LOW_LAYER(chunk.ModelsCount != 0); + data.ChunkCoord = key.Coord * GLOBAL_SDF_RASTERIZE_CHUNK_SIZE; + data.ModelsCount = chunk.ModelsCount; + if (_cb1) + context->UpdateCB(_cb1, &data); + GPUShaderProgramCS* cs; + if (key.Layer == 0) + { + // First layer so can override existing chunk data + cs = _csRasterizeModel0; + nonEmptyChunks.Add(key); + } + else + { + // Another layer so need combine with existing chunk data + cs = _csRasterizeModel1; + } + context->Dispatch(cs, chunkDispatchGroups, chunkDispatchGroups, chunkDispatchGroups); + // TODO: don't stall with UAV barrier on D3D12 if UAVs don't change between dispatches - only for a sequence of _csRasterizeModel0 dispatches (maybe cache per-shader write/read flags for all UAVs?) + +#if GLOBAL_SDF_DEBUG_CHUNKS + // Debug draw chunk bounds in world space with number of models in it + if (cascade + 1 == GLOBAL_SDF_DEBUG_CHUNKS) + { + Vector3 chunkMin = cascadeBounds.Minimum + Vector3(key.Coord) * chunkSize; + BoundingBox chunkBounds(chunkMin, chunkMin + chunkSize); + DebugDraw::DrawWireBox(chunkBounds, Color::Red, 0, false); + DebugDraw::DrawText(StringUtils::ToString(chunk.ModelsCount), chunkBounds.GetCenter() + Vector3(0, 50.0f * key.Layer, 0), Color::Red); + } +#endif + } + } + + // Generate mip out of cascade (empty chunks have distance value 1 which is incorrect so mip will be used as a fallback - lower res) + { + PROFILE_GPU_CPU("Generate Mip"); + if (_cb1) + context->UpdateCB(_cb1, &data); + context->ResetUA(); + context->BindSR(0, cascadeView); + context->BindUA(0, cascadeMipView); + const int32 mipDispatchGroups = Math::DivideAndRoundUp(resolutionMip, GLOBAL_SDF_MIP_GROUP_SIZE); + int32 floodFillIterations = chunks.Count() == 0 ? 1 : GLOBAL_SDF_MIP_FLOODS; + context->Dispatch(_csGenerateMip0, mipDispatchGroups, mipDispatchGroups, mipDispatchGroups); + context->UnBindSR(0); + GPUTextureView* tmpMipView = tmpMip->ViewVolume(); + for (int32 i = 1; i < floodFillIterations; i++) + { + context->ResetUA(); + context->BindSR(0, cascadeMipView); + context->BindUA(0, tmpMipView); + context->Dispatch(_csGenerateMip1, mipDispatchGroups, mipDispatchGroups, mipDispatchGroups); + Swap(tmpMipView, cascadeMipView); + } + if (floodFillIterations % 2 == 0) + Swap(tmpMipView, cascadeMipView); + } + } + + RenderTargetPool::Release(tmpMip); + if (anyDraw) + { + context->UnBindCB(1); + context->ResetUA(); + context->FlushState(); + context->ResetSR(); + context->FlushState(); + } + } + + // Copy results + static_assert(ARRAY_COUNT(result.Cascades) == ARRAY_COUNT(sdfData.Cascades), "Invalid cascades count."); + static_assert(ARRAY_COUNT(result.CascadeMips) == ARRAY_COUNT(sdfData.CascadeMips), "Invalid cascades count."); + Platform::MemoryCopy(result.Cascades, sdfData.Cascades, sizeof(result.Cascades)); + Platform::MemoryCopy(result.CascadeMips, sdfData.CascadeMips, sizeof(result.CascadeMips)); + for (int32 cascade = 0; cascade < 4; cascade++) + { + const float distance = cascadesDistances[cascade]; + const float maxDistance = distance * 2; + const float voxelSize = maxDistance / resolution; + const Vector3 center = sdfData.Positions[cascade]; + result.GlobalSDF.CascadePosDistance[cascade] = Vector4(center, distance); + result.GlobalSDF.CascadeVoxelSize.Raw[cascade] = voxelSize; + } + result.GlobalSDF.Resolution = (float)resolution; + return false; +} + +void GlobalSignDistanceFieldPass::RenderDebug(RenderContext& renderContext, GPUContext* context, GPUTexture* output) +{ + BindingData bindingData; + if (Render(renderContext, context, bindingData)) + { + context->Draw(output, renderContext.Buffers->GBuffer0); + return; + } + + PROFILE_GPU_CPU("Global SDF Debug"); + const Vector2 outputSize(output->Size()); + if (_cb0) + { + Data 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 = bindingData.GlobalSDF; + context->UpdateCB(_cb0, &data); + context->BindCB(0, _cb0); + } + for (int32 i = 0; i < 4; i++) + { + context->BindSR(i, bindingData.Cascades[i]->ViewVolume()); + context->BindSR(i + 4, bindingData.CascadeMips[i]->ViewVolume()); + } + context->SetState(_psDebug); + context->SetRenderTarget(output->View()); + context->SetViewportAndScissors(outputSize.X, outputSize.Y); + context->DrawFullscreenTriangle(); +} diff --git a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.h b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.h new file mode 100644 index 000000000..e6eac2ab2 --- /dev/null +++ b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.h @@ -0,0 +1,75 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#pragma once + +#include "RendererPass.h" + +/// +/// Global Sign Distance Field (SDF) rendering pass. Composites scene geometry into series of 3D volume textures that cover the world around the camera for global distance field sampling. +/// +class GlobalSignDistanceFieldPass : public RendererPass +{ +public: + // Constant buffer data for Global SDF access on a GPU. + PACK_STRUCT(struct GlobalSDFData + { + Vector4 CascadePosDistance[4]; + Vector4 CascadeVoxelSize; + Vector3 Padding; + float Resolution; + }); + + // Binding data for the GPU. + struct BindingData + { + GPUTexture* Cascades[4]; + GPUTexture* CascadeMips[4]; + GlobalSDFData GlobalSDF; + }; + +private: + AssetReference _shader; + GPUPipelineState* _psDebug = nullptr; + GPUShaderProgramCS* _csRasterizeModel0 = nullptr; + GPUShaderProgramCS* _csRasterizeModel1 = nullptr; + GPUShaderProgramCS* _csClearChunk = nullptr; + GPUShaderProgramCS* _csGenerateMip0 = nullptr; + GPUShaderProgramCS* _csGenerateMip1 = nullptr; + GPUConstantBuffer* _cb0 = nullptr; + GPUConstantBuffer* _cb1 = nullptr; + class DynamicStructuredBuffer* _modelsBuffer = nullptr; + Array _modelsTextures; + +public: + /// + /// Renders the Global SDF. + /// + /// The rendering context. + /// The GPU context. + /// The result Global SDF data for binding to the shaders. + /// True if failed to render (platform doesn't support it, out of video memory, disabled feature or effect is not ready), otherwise false. + bool Render(RenderContext& renderContext, GPUContext* context, BindingData& result); + + /// + /// Renders the debug view. + /// + /// The rendering context. + /// The GPU context. + /// The output buffer. + void RenderDebug(RenderContext& renderContext, GPUContext* context, GPUTexture* output); + +private: +#if COMPILE_WITH_DEV_ENV + void OnShaderReloading(Asset* obj); +#endif + +public: + // [RendererPass] + String ToString() const override; + bool Init() override; + void Dispose() override; + +protected: + // [RendererPass] + bool setupResources() override; +}; diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index caf72d088..8174ebeb5 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -21,6 +21,7 @@ #include "VolumetricFogPass.h" #include "HistogramPass.h" #include "AtmospherePreCompute.h" +#include "GlobalSignDistanceFieldPass.h" #include "Utils/MultiScaler.h" #include "Utils/BitonicSort.h" #include "AntiAliasing/FXAA.h" @@ -45,7 +46,6 @@ Array PassList(64); class RendererService : public EngineService { public: - RendererService() : EngineService(TEXT("Renderer"), 20) { @@ -81,6 +81,7 @@ bool RendererService::Init() PassList.Add(TAA::Instance()); PassList.Add(SMAA::Instance()); PassList.Add(HistogramPass::Instance()); + PassList.Add(GlobalSignDistanceFieldPass::Instance()); #if USE_EDITOR PassList.Add(QuadOverdrawPass::Instance()); #endif @@ -340,8 +341,14 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext) // Fill GBuffer GBufferPass::Instance()->Fill(renderContext, lightBuffer->View()); - // Check if debug emissive light - if (renderContext.View.Mode == ViewMode::Emissive || renderContext.View.Mode == ViewMode::LightmapUVsDensity) + // Debug drawing + if (renderContext.View.Mode == ViewMode::GlobalSDF) + { + GlobalSignDistanceFieldPass::Instance()->RenderDebug(renderContext, context, lightBuffer); + } + if (renderContext.View.Mode == ViewMode::Emissive || + renderContext.View.Mode == ViewMode::LightmapUVsDensity || + renderContext.View.Mode == ViewMode::GlobalSDF) { context->ResetRenderTarget(); context->SetRenderTarget(task->GetOutputView()); diff --git a/Source/Engine/Terrain/Terrain.cpp b/Source/Engine/Terrain/Terrain.cpp index 76c02e75d..40916a095 100644 --- a/Source/Engine/Terrain/Terrain.cpp +++ b/Source/Engine/Terrain/Terrain.cpp @@ -505,6 +505,8 @@ void Terrain::Draw(RenderContext& renderContext) DrawPass drawModes = (DrawPass)(DrawModes & renderContext.View.Pass); if (drawModes == DrawPass::None) return; + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; // TODO: Terrain rendering to Global SDF PROFILE_CPU(); @@ -727,6 +729,10 @@ void Terrain::Deserialize(DeserializeStream& stream, ISerializeModifier* modifie } #endif } + + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; } RigidBody* Terrain::GetAttachedRigidBody() const diff --git a/Source/Engine/UI/SpriteRender.cpp b/Source/Engine/UI/SpriteRender.cpp index 2e9ac8392..3b4f9d3b8 100644 --- a/Source/Engine/UI/SpriteRender.cpp +++ b/Source/Engine/UI/SpriteRender.cpp @@ -106,6 +106,8 @@ bool SpriteRender::HasContentLoaded() const void SpriteRender::Draw(RenderContext& renderContext) { + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; if (!Material || !Material->IsLoaded() || !_quadModel || !_quadModel->IsLoaded()) return; auto model = _quadModel.As(); diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index 0ec85ff5d..93051d500 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -340,6 +340,8 @@ bool TextRender::HasContentLoaded() const void TextRender::Draw(RenderContext& renderContext) { + if (renderContext.View.Pass == DrawPass::GlobalSDF) + return; // TODO: Text rendering to Global SDF if (_isDirty) { UpdateLayout(); @@ -478,6 +480,10 @@ void TextRender::Deserialize(DeserializeStream& stream, ISerializeModifier* modi DESERIALIZE_MEMBER(Scale, _layoutOptions.Scale); DESERIALIZE_MEMBER(GapScale, _layoutOptions.BaseLinesGapScale); + // [Deprecated on 07.02.2022, expires on 07.02.2024] + if (modifier->EngineBuild <= 6330) + DrawModes |= DrawPass::GlobalSDF; + _isDirty = true; } diff --git a/Source/Shaders/GlobalSignDistanceField.hlsl b/Source/Shaders/GlobalSignDistanceField.hlsl new file mode 100644 index 000000000..83a98ae00 --- /dev/null +++ b/Source/Shaders/GlobalSignDistanceField.hlsl @@ -0,0 +1,153 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#include "./Flax/Common.hlsl" +#include "./Flax/Collisions.hlsl" + +#define GLOBAL_SDF_RASTERIZE_CHUNK_SIZE 32 +#define GLOBAL_SDF_RASTERIZE_CHUNK_MARGIN 4 +#define GLOBAL_SDF_MIP_FLOODS 5 + +// Global SDF data for a constant buffer +struct GlobalSDFData +{ + float4 CascadePosDistance[4]; + float4 CascadeVoxelSize; + float3 Padding; + float Resolution; +}; + +// Global SDF ray trace settings. +struct GlobalSDFTrace +{ + float3 WorldPosition; + float MinDistance; + float3 WorldDirection; + float MaxDistance; + float StepScale; + + void Init(float3 worldPosition, float3 worldDirection, float minDistance, float maxDistance, float stepScale = 1.0f) + { + WorldPosition = worldPosition; + WorldDirection = worldDirection; + MinDistance = minDistance; + MaxDistance = maxDistance; + StepScale = stepScale; + } +}; + +// Global SDF ray trace hit information. +struct GlobalSDFHit +{ + float HitTime; + uint HitCascade; + uint StepsCount; + + bool IsHit() + { + return HitTime >= 0.0f; + } + + float3 GetHitPosition(const GlobalSDFTrace trace) + { + return trace.WorldPosition + trace.WorldDirection * HitTime; + } +}; + +// Samples the Global SDF and returns the distance to the closest surface (in world units) at the given world location. +float SampleGlobalSDF(const GlobalSDFData data, Texture3D tex[4], float3 worldPosition, uint minCascade = 0) +{ + float distance = data.CascadePosDistance[3].w * 2.0f; + for (uint cascade = minCascade; cascade < 4; cascade++) + { + float4 cascadePosDistance = data.CascadePosDistance[cascade]; + float cascadeMaxDistance = cascadePosDistance.w * 2; + float3 posInCascade = worldPosition - cascadePosDistance.xyz; + float3 cascadeUV = posInCascade / cascadeMaxDistance + 0.5f; + if (any(cascadeUV < 0) || any(cascadeUV > 1)) + continue; + // TODO: sample mip first + float cascadeDistance = tex[cascade].SampleLevel(SamplerLinearClamp, cascadeUV, 0); + if (cascadeDistance < 1.0f) + { + distance = cascadeDistance * cascadeMaxDistance; + break; + } + } + return distance; +} + +// Ray traces the Global SDF. +GlobalSDFHit RayTraceGlobalSDF(const GlobalSDFData data, Texture3D tex[4], Texture3D mips[4], const GlobalSDFTrace trace, uint minCascade = 0) +{ + GlobalSDFHit hit; + hit.HitTime = -1.0f; + hit.HitCascade = 0; + hit.StepsCount = 0; + float chunkSizeDistance = (float)GLOBAL_SDF_RASTERIZE_CHUNK_SIZE / data.Resolution; // Size of the chunk in SDF distance (0-1) + float chunkMarginDistance = (float)GLOBAL_SDF_RASTERIZE_CHUNK_MARGIN / data.Resolution; // Size of the chunk margin in SDF distance (0-1) + float nextIntersectionStart = 0.0f; + float traceMaxDistance = min(trace.MaxDistance, data.CascadePosDistance[3].w * 2); + float3 traceEndPosition = trace.WorldPosition + trace.WorldDirection * traceMaxDistance; + UNROLL + for (uint cascade = minCascade; cascade < 4 && hit.HitTime < 0.0f; cascade++) + { + float4 cascadePosDistance = data.CascadePosDistance[cascade]; + float cascadeMaxDistance = cascadePosDistance.w * 2; + float voxelSize = data.CascadeVoxelSize[cascade]; + float voxelExtent = voxelSize * 0.5f; + float cascadeMinStep = voxelSize; + + // Hit the cascade bounds to find the intersection points + float2 intersections = LineHitBox(trace.WorldPosition, traceEndPosition, cascadePosDistance.xyz - cascadePosDistance.www, cascadePosDistance.xyz + cascadePosDistance.www); + intersections.xy *= traceMaxDistance; + intersections.x = max(intersections.x, nextIntersectionStart); + if (intersections.x >= intersections.y) + break; + + // Skip the current cascade tracing on the next cascade + nextIntersectionStart = intersections.y; + + // Walk over the cascade SDF + uint step = 0; + float stepTime = intersections.x; + LOOP + for (; step < 250 && stepTime < intersections.y; step++) + { + float3 stepPosition = trace.WorldPosition + trace.WorldDirection * stepTime; + + // Sample SDF + float3 posInCascade = stepPosition - cascadePosDistance.xyz; + float3 cascadeUV = posInCascade / cascadeMaxDistance + 0.5f; + float stepDistance = mips[cascade].SampleLevel(SamplerLinearClamp, cascadeUV, 0); + if (stepDistance < chunkSizeDistance) + { + float stepDistanceTex = tex[cascade].SampleLevel(SamplerLinearClamp, cascadeUV, 0); + if (stepDistanceTex < chunkMarginDistance * 2) + { + stepDistance = stepDistanceTex; + } + } + else + { + // Assume no SDF nearby so perform a jump + stepDistance = chunkSizeDistance; + } + stepDistance *= cascadeMaxDistance; + + // Detect surface hit + float minSurfaceThickness = voxelExtent * saturate(stepTime / (voxelExtent * 2.0f)); + if (stepDistance < minSurfaceThickness) + { + // Surface hit + hit.HitTime = max(stepTime + stepDistance - minSurfaceThickness, 0.0f); + hit.HitCascade = cascade; + break; + } + + // Move forward + stepTime += max(stepDistance * trace.StepScale, cascadeMinStep); + } + hit.StepsCount += step; + } + return hit; +} diff --git a/Source/Shaders/GlobalSignDistanceField.shader b/Source/Shaders/GlobalSignDistanceField.shader new file mode 100644 index 000000000..058e13749 --- /dev/null +++ b/Source/Shaders/GlobalSignDistanceField.shader @@ -0,0 +1,216 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#include "./Flax/Common.hlsl" +#include "./Flax/Math.hlsl" +#include "./Flax/GlobalSignDistanceField.hlsl" + +#define GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT 28 +#define GLOBAL_SDF_RASTERIZE_GROUP_SIZE 8 +#define GLOBAL_SDF_MIP_GROUP_SIZE 4 + +struct ModelRasterizeData +{ + float4x4 WorldToVolume; // TODO: use 3x4 matrix + float4x4 VolumeToWorld; // TODO: use 3x4 matrix + float3 VolumeToUVWMul; + float MipOffset; + float3 VolumeToUVWAdd; + float MaxDistance; + float3 VolumeLocalBoundsExtent; + float Padding0; +}; + +META_CB_BEGIN(0, Data) +float3 ViewWorldPos; +float ViewNearPlane; +float3 Padding00; +float ViewFarPlane; +float4 ViewFrustumWorldRays[4]; +GlobalSDFData GlobalSDF; +META_CB_END + +META_CB_BEGIN(1, ModelsRasterizeData) +int3 ChunkCoord; +float MaxDistance; +float3 CascadeCoordToPosMul; +int ModelsCount; +float3 CascadeCoordToPosAdd; +int CascadeResolution; +float2 Padding0; +int CascadeMipResolution; +int CascadeMipFactor; +uint4 Models[GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT / 4]; +META_CB_END + +float CombineDistanceToSDF(float sdf, float distanceToSDF) +{ + // Simple sum (aprox) + //return sdf + distanceToSDF; + + // Negative distinace inside the SDF + if (sdf <= 0 && distanceToSDF <= 0) return sdf; + + // Worst-case scenario with triangle edge (C^2 = A^2 + B^2) + return sqrt(Square(max(sdf, 0)) + Square(distanceToSDF)); +} + +#if defined(_CS_RasterizeModel) + +RWTexture3D GlobalSDFTex : register(u0); +StructuredBuffer ModelsBuffer : register(t0); +Texture3D ModelSDFTex[GLOBAL_SDF_RASTERIZE_MODEL_MAX_COUNT] : register(t1); + +float DistanceToModelSDF(float minDistance, ModelRasterizeData modelData, Texture3D modelSDFTex, float3 worldPos) +{ + // Compute SDF volume UVs and distance in world-space to the volume bounds + float3 volumePos = mul(float4(worldPos, 1), modelData.WorldToVolume).xyz; + float3 volumeUV = volumePos * modelData.VolumeToUVWMul + modelData.VolumeToUVWAdd; + float3 volumePosClamped = clamp(volumePos, -modelData.VolumeLocalBoundsExtent, modelData.VolumeLocalBoundsExtent); + float3 worldPosClamped = mul(float4(volumePosClamped, 1), modelData.VolumeToWorld).xyz; + float distanceToVolume = distance(worldPos, worldPosClamped); + + // Skip sampling SDF if there is already a better result + BRANCH if (minDistance <= distanceToVolume) return distanceToVolume; + + // Sample SDF + float volumeDistance = modelSDFTex.SampleLevel(SamplerLinearClamp, volumeUV, modelData.MipOffset).x * modelData.MaxDistance; + + // Combine distance to the volume with distance to the surface inside the model + float result = CombineDistanceToSDF(volumeDistance, distanceToVolume); + if (distanceToVolume > 0) + { + // Prevent negative distance outside the model + result = max(distanceToVolume, result); + } + return result; +} + +// Compute shader for rasterizing model SDF into Global SDF +META_CS(true, FEATURE_LEVEL_SM5) +META_PERMUTATION_1(READ_SDF=0) +META_PERMUTATION_1(READ_SDF=1) +[numthreads(GLOBAL_SDF_RASTERIZE_GROUP_SIZE, GLOBAL_SDF_RASTERIZE_GROUP_SIZE, GLOBAL_SDF_RASTERIZE_GROUP_SIZE)] +void CS_RasterizeModel(uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) +{ + uint3 voxelCoord = ChunkCoord + DispatchThreadId; + float3 voxelWorldPos = voxelCoord * CascadeCoordToPosMul + CascadeCoordToPosAdd; + float minDistance = MaxDistance; +#if READ_SDF + minDistance *= GlobalSDFTex[voxelCoord]; +#endif + for (int i = 0; i < ModelsCount; i++) + { + ModelRasterizeData modelData = ModelsBuffer[Models[i / 4][i % 4]]; + float modelDistance = DistanceToModelSDF(minDistance, modelData, ModelSDFTex[i], voxelWorldPos); + minDistance = min(minDistance, modelDistance); + } + GlobalSDFTex[voxelCoord] = saturate(minDistance / MaxDistance); +} + +#endif + +#if defined(_CS_ClearChunk) + +RWTexture3D GlobalSDFTex : register(u0); + +// Compute shader for clearing Global SDF chunk +META_CS(true, FEATURE_LEVEL_SM5) +[numthreads(GLOBAL_SDF_RASTERIZE_GROUP_SIZE, GLOBAL_SDF_RASTERIZE_GROUP_SIZE, GLOBAL_SDF_RASTERIZE_GROUP_SIZE)] +void CS_ClearChunk(uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) +{ + uint3 voxelCoord = ChunkCoord + DispatchThreadId; + GlobalSDFTex[voxelCoord] = 1.0f; +} + +#endif + +#if defined(_CS_GenerateMip) + +RWTexture3D GlobalSDFMip : register(u0); +Texture3D GlobalSDFTex : register(t0); + +float SampleSDF(uint3 voxelCoordMip, int3 offset) +{ +#if SAMPLE_MIP + // Sampling Global SDF Mip + float resolution = CascadeMipResolution; +#else + // Sampling Global SDF Tex + voxelCoordMip *= CascadeMipFactor; + float resolution = CascadeResolution; +#endif + + // Sample SDF + voxelCoordMip = (uint3)clamp((int3)voxelCoordMip + offset, 0, resolution - 1); + float result = GlobalSDFTex[voxelCoordMip].r; + + // Extend by distance to the sampled texel location + float distanceInWorldUnits = length(offset) * (MaxDistance / resolution); + float distanceToVoxel = distanceInWorldUnits / MaxDistance; + result = CombineDistanceToSDF(result, distanceToVoxel); + + return result; +} + +// Compute shader for generating mip for Global SDF (uses flood fill algorithm) +META_CS(true, FEATURE_LEVEL_SM5) +META_PERMUTATION_1(SAMPLE_MIP=0) +META_PERMUTATION_1(SAMPLE_MIP=1) +[numthreads(GLOBAL_SDF_MIP_GROUP_SIZE, GLOBAL_SDF_MIP_GROUP_SIZE, GLOBAL_SDF_MIP_GROUP_SIZE)] +void CS_GenerateMip(uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) +{ + uint3 voxelCoordMip = DispatchThreadId; + float minDistance = SampleSDF(voxelCoordMip, int3(0, 0, 0)); + + // Find the distance to the closest surface by sampling the nearby area (flood fill) + minDistance = min(minDistance, SampleSDF(voxelCoordMip, int3(1, 0, 0))); + minDistance = min(minDistance, SampleSDF(voxelCoordMip, int3(0, 1, 0))); + minDistance = min(minDistance, SampleSDF(voxelCoordMip, int3(0, 0, 1))); + minDistance = min(minDistance, SampleSDF(voxelCoordMip, int3(-1, 0, 0))); + minDistance = min(minDistance, SampleSDF(voxelCoordMip, int3(0, -1, 0))); + minDistance = min(minDistance, SampleSDF(voxelCoordMip, int3(0, 0, -1))); + + GlobalSDFMip[voxelCoordMip] = minDistance; +} + +#endif + +#ifdef _PS_Debug + +Texture3D GlobalSDFTex[4] : register(t0); +Texture3D GlobalSDFMip[4] : register(t4); + +// Pixel shader for Global SDF debug drawing +META_PS(true, FEATURE_LEVEL_SM5) +float4 PS_Debug(Quad_VS2PS input) : SV_Target +{ +#if 0 + // Preview Global SDF slice + float zSlice = 0.6f; + float mip = 0; + uint cascade = 0; + float distance01 = GlobalSDFTex[cascade].SampleLevel(SamplerLinearClamp, float3(input.TexCoord, zSlice), mip).x; + //float distance01 = GlobalSDFMip[cascade].SampleLevel(SamplerLinearClamp, float3(input.TexCoord, zSlice), mip).x; + float distance = distance01 * GlobalSDF.CascadePosDistance[cascade].w; + if (abs(distance) < 1) + return float4(1, 0, 0, 1); + if (distance01 < 0) + return float4(0, 0, 1 - distance01, 1); + return float4(0, 1 - distance01, 0, 1); +#endif + + // Shot a ray from camera into the Global SDF + GlobalSDFTrace trace; + float3 viewRay = lerp(lerp(ViewFrustumWorldRays[3], ViewFrustumWorldRays[0], input.TexCoord.x), lerp(ViewFrustumWorldRays[2], ViewFrustumWorldRays[1], input.TexCoord.x), 1 - input.TexCoord.y).xyz; + viewRay = normalize(viewRay - ViewWorldPos); + trace.Init(ViewWorldPos, viewRay, ViewNearPlane, ViewFarPlane); + GlobalSDFHit hit = RayTraceGlobalSDF(GlobalSDF, GlobalSDFTex, GlobalSDFMip, trace); + + // Debug draw + float3 color = saturate(hit.StepsCount / 80.0f).xxx; + if (!hit.IsHit()) + color.rg *= 0.4f; + return float4(color, 1); +} + +#endif