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