diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs
index 23a15157a..207e133ea 100644
--- a/Source/Editor/Content/Import/ModelImportEntry.cs
+++ b/Source/Editor/Content/Import/ModelImportEntry.cs
@@ -291,6 +291,18 @@ namespace FlaxEditor.Content.Import
[EditorOrder(420), DefaultValue(true), EditorDisplay("Materials", "Restore Materials On Reimport"), Tooltip("If checked, the importer will try to restore the assigned materials to the model slots.")]
public bool RestoreMaterialsOnReimport { get; set; } = true;
+ ///
+ /// If checked, enables generation of Signed Distance Field (SDF).
+ ///
+ [EditorOrder(1500), DefaultValue(false), EditorDisplay("SDF"), VisibleIf(nameof(Type_Model))]
+ public bool GenerateSDF { get; set; } = false;
+
+ ///
+ /// Resolution scale for generated Signed Distance Field (SDF) texture. Higher values improve accuracy but increase memory usage and reduce performance.
+ ///
+ [EditorOrder(1510), DefaultValue(1.0f), Limit(0.0001f, 100.0f), EditorDisplay("SDF"), VisibleIf(nameof(Type_Model))]
+ public float SDFResolution { get; set; } = 1.0f;
+
///
/// If checked, the imported mesh/animations are splitted into separate assets. Used if ObjectIndex is set to -1.
///
@@ -303,6 +315,8 @@ namespace FlaxEditor.Content.Import
[EditorOrder(2010), DefaultValue(-1), EditorDisplay("Splitting"), Tooltip("The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desire object. Default -1 imports all objects.")]
public int ObjectIndex { get; set; } = -1;
+ private bool Type_Model => Type == ModelType.Model;
+
[StructLayout(LayoutKind.Sequential)]
internal struct InternalOptions
{
@@ -350,6 +364,10 @@ namespace FlaxEditor.Content.Import
public byte ImportTextures;
public byte RestoreMaterialsOnReimport;
+ // SDF
+ public byte GenerateSDF;
+ public float SDFResolution;
+
// Splitting
public byte SplitObjects;
public int ObjectIndex;
@@ -392,6 +410,8 @@ namespace FlaxEditor.Content.Import
ImportMaterials = (byte)(ImportMaterials ? 1 : 0),
ImportTextures = (byte)(ImportTextures ? 1 : 0),
RestoreMaterialsOnReimport = (byte)(RestoreMaterialsOnReimport ? 1 : 0),
+ GenerateSDF = (byte)(GenerateSDF ? 1 : 0),
+ SDFResolution = SDFResolution,
SplitObjects = (byte)(SplitObjects ? 1 : 0),
ObjectIndex = ObjectIndex,
};
@@ -431,25 +451,22 @@ namespace FlaxEditor.Content.Import
ImportMaterials = options.ImportMaterials != 0;
ImportTextures = options.ImportTextures != 0;
RestoreMaterialsOnReimport = options.RestoreMaterialsOnReimport != 0;
+ GenerateSDF = options.GenerateSDF != 0;
+ SDFResolution = options.SDFResolution;
SplitObjects = options.SplitObjects != 0;
ObjectIndex = options.ObjectIndex;
}
///
- /// Tries the restore the asset import options from the target resource file.
+ /// Tries the restore the asset import options from the target resource file. Applies the project default options too.
///
/// The options.
/// The asset path.
/// True settings has been restored, otherwise false.
- public static bool TryRestore(ref ModelImportSettings options, string assetPath)
+ public static void TryRestore(ref ModelImportSettings options, string assetPath)
{
- if (ModelImportEntry.Internal_GetModelImportOptions(assetPath, out var internalOptions))
- {
- // Restore settings
- options.FromInternal(ref internalOptions);
- return true;
- }
- return false;
+ ModelImportEntry.Internal_GetModelImportOptions(assetPath, out var internalOptions);
+ options.FromInternal(ref internalOptions);
}
}
@@ -495,7 +512,7 @@ namespace FlaxEditor.Content.Import
#region Internal Calls
[MethodImpl(MethodImplOptions.InternalCall)]
- internal static extern bool Internal_GetModelImportOptions(string path, out ModelImportSettings.InternalOptions result);
+ internal static extern void Internal_GetModelImportOptions(string path, out ModelImportSettings.InternalOptions result);
#endregion
}
diff --git a/Source/Editor/CustomEditors/Elements/ButtonElement.cs b/Source/Editor/CustomEditors/Elements/ButtonElement.cs
index 69340238d..f7485e542 100644
--- a/Source/Editor/CustomEditors/Elements/ButtonElement.cs
+++ b/Source/Editor/CustomEditors/Elements/ButtonElement.cs
@@ -16,26 +16,6 @@ namespace FlaxEditor.CustomEditors.Elements
///
public readonly Button Button = new Button();
- ///
- /// Initializes the element.
- ///
- /// The text.
- public void Init(string text)
- {
- Button.Text = text;
- }
-
- ///
- /// Initializes the element.
- ///
- /// The text.
- /// The color.
- public void Init(string text, Color color)
- {
- Button.Text = text;
- Button.SetColors(color);
- }
-
///
public override Control Control => Button;
}
diff --git a/Source/Editor/CustomEditors/LayoutElementsContainer.cs b/Source/Editor/CustomEditors/LayoutElementsContainer.cs
index 0c9b01942..894e9defd 100644
--- a/Source/Editor/CustomEditors/LayoutElementsContainer.cs
+++ b/Source/Editor/CustomEditors/LayoutElementsContainer.cs
@@ -133,11 +133,13 @@ namespace FlaxEditor.CustomEditors
/// Adds new button element.
///
/// The text.
+ /// The tooltip text.
/// The created element.
- public ButtonElement Button(string text)
+ public ButtonElement Button(string text, string tooltip = null)
{
var element = new ButtonElement();
- element.Init(text);
+ element.Button.Text = text;
+ element.Button.TooltipText = tooltip;
OnAddElement(element);
return element;
}
@@ -147,11 +149,14 @@ namespace FlaxEditor.CustomEditors
///
/// The text.
/// The color.
+ /// The tooltip text.
/// The created element.
- public ButtonElement Button(string text, Color color)
+ public ButtonElement Button(string text, Color color, string tooltip = null)
{
ButtonElement element = new ButtonElement();
- element.Init(text, color);
+ element.Button.Text = text;
+ element.Button.TooltipText = tooltip;
+ element.Button.SetColors(color);
OnAddElement(element);
return element;
}
diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp
index 248aa757f..acfe71ad6 100644
--- a/Source/Editor/Managed/ManagedEditor.Internal.cpp
+++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp
@@ -29,6 +29,7 @@
#include "Engine/Level/Actor.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Core/Config/GameSettings.h"
+#include "Engine/Core/Config/GraphicsSettings.h"
#include "Engine/Core/Cache.h"
#include "Engine/CSG/CSGBuilder.h"
#include "Engine/Debug/DebugLog.h"
@@ -196,6 +197,10 @@ struct InternalModelOptions
byte ImportTextures;
byte RestoreMaterialsOnReimport;
+ // SDF
+ byte GenerateSDF;
+ float SDFResolution;
+
// Splitting
byte SplitObjects;
int32 ObjectIndex;
@@ -235,6 +240,8 @@ struct InternalModelOptions
to->ImportMaterials = from->ImportMaterials;
to->ImportTextures = from->ImportTextures;
to->RestoreMaterialsOnReimport = from->RestoreMaterialsOnReimport;
+ to->GenerateSDF = from->GenerateSDF;
+ to->SDFResolution = from->SDFResolution;
to->SplitObjects = from->SplitObjects;
to->ObjectIndex = from->ObjectIndex;
}
@@ -274,6 +281,8 @@ struct InternalModelOptions
to->ImportMaterials = from->ImportMaterials;
to->ImportTextures = from->ImportTextures;
to->RestoreMaterialsOnReimport = from->RestoreMaterialsOnReimport;
+ to->GenerateSDF = from->GenerateSDF;
+ to->SDFResolution = from->SDFResolution;
to->SplitObjects = from->SplitObjects;
to->ObjectIndex = from->ObjectIndex;
}
@@ -626,22 +635,23 @@ public:
return false;
}
- static bool GetModelImportOptions(MonoString* pathObj, InternalModelOptions* result)
+ static void GetModelImportOptions(MonoString* pathObj, InternalModelOptions* result)
{
+ // Initialize defaults
+ ImportModelFile::Options options;
+ if (const auto* graphicsSettings = GraphicsSettings::Get())
+ {
+ options.GenerateSDF = graphicsSettings->GenerateSDFOnModelImport;
+ }
+
+ // Get options from model
String path;
MUtils::ToString(pathObj, path);
FileSystem::NormalizePath(path);
+ ImportModelFile::TryGetImportOptions(path, options);
- ImportModelFile::Options options;
- if (ImportModelFile::TryGetImportOptions(path, options))
- {
- // Convert into managed storage
- InternalModelOptions::Convert(&options, result);
-
- return true;
- }
-
- return false;
+ // Convert into managed storage
+ InternalModelOptions::Convert(&options, result);
}
static bool GetAudioImportOptions(MonoString* pathObj, InternalAudioOptions* result)
@@ -852,7 +862,7 @@ public:
continue;
switch (e.Type)
{
- // Keyboard events
+ // Keyboard events
case InputDevice::EventType::Char:
window->OnCharInput(e.CharData.Char);
break;
@@ -862,7 +872,7 @@ public:
case InputDevice::EventType::KeyUp:
window->OnKeyUp(e.KeyData.Key);
break;
- // Mouse events
+ // Mouse events
case InputDevice::EventType::MouseDown:
window->OnMouseDown(window->ScreenToClient(e.MouseData.Position), e.MouseData.Button);
break;
diff --git a/Source/Editor/Windows/Assets/ModelWindow.cs b/Source/Editor/Windows/Assets/ModelWindow.cs
index f47901038..d493cd0e6 100644
--- a/Source/Editor/Windows/Assets/ModelWindow.cs
+++ b/Source/Editor/Windows/Assets/ModelWindow.cs
@@ -38,7 +38,7 @@ namespace FlaxEditor.Windows.Assets
PreviewLight.ShadowsDistance = 2000.0f;
Task.ViewFlags |= ViewFlags.Shadows;
}
-
+
public override void Draw()
{
base.Draw();
@@ -75,10 +75,7 @@ namespace FlaxEditor.Windows.Assets
base.OnClean();
}
- ///
- /// Updates the highlight/isolate effects on UI.
- ///
- public void UpdateEffectsOnUI()
+ private void UpdateEffectsOnUI()
{
Window._skipEffectsGuiEvents = true;
@@ -97,10 +94,7 @@ namespace FlaxEditor.Windows.Assets
Window._skipEffectsGuiEvents = false;
}
- ///
- /// Updates the material slots UI parts. Should be called after material slot rename.
- ///
- public void UpdateMaterialSlotsUI()
+ private void UpdateMaterialSlotsUI()
{
Window._skipEffectsGuiEvents = true;
@@ -123,12 +117,7 @@ namespace FlaxEditor.Windows.Assets
Window._skipEffectsGuiEvents = false;
}
- ///
- /// Sets the material slot index to the mesh.
- ///
- /// The mesh.
- /// New index of the material slot to use.
- public void SetMaterialSlot(Mesh mesh, int newSlotIndex)
+ private void SetMaterialSlot(Mesh mesh, int newSlotIndex)
{
if (Window._skipEffectsGuiEvents)
return;
@@ -139,11 +128,7 @@ namespace FlaxEditor.Windows.Assets
Window.MarkAsEdited();
}
- ///
- /// Sets the material slot to isolate.
- ///
- /// The mesh.
- public void SetIsolate(Mesh mesh)
+ private void SetIsolate(Mesh mesh)
{
if (Window._skipEffectsGuiEvents)
return;
@@ -153,11 +138,7 @@ namespace FlaxEditor.Windows.Assets
UpdateEffectsOnUI();
}
- ///
- /// Sets the material slot index to highlight.
- ///
- /// The mesh.
- public void SetHighlight(Mesh mesh)
+ private void SetHighlight(Mesh mesh)
{
if (Window._skipEffectsGuiEvents)
return;
@@ -169,6 +150,8 @@ namespace FlaxEditor.Windows.Assets
private class ProxyEditor : ProxyEditorBase
{
+ private CustomEditors.Elements.IntegerValueElement _sdfModelLodIndex;
+
public override void Initialize(LayoutElementsContainer layout)
{
var proxy = (MeshesPropertiesProxy)Values[0];
@@ -198,6 +181,46 @@ namespace FlaxEditor.Windows.Assets
};
}
+ // SDF
+ {
+ var group = layout.Group("SDF");
+
+ var sdf = proxy.Asset.SDF;
+ if (sdf.Texture != null)
+ {
+ var size = sdf.Texture.Size3;
+ group.Label($"SDF Texture {size.X}x{size.Y}x{size.Z} ({Utilities.Utils.FormatBytesCount(sdf.Texture.MemoryUsage)})").AddCopyContextMenu();
+ }
+ else
+ {
+ group.Label("No SDF");
+ }
+
+ var resolution = group.FloatValue("Resolution Scale", proxy.Window.Editor.CodeDocs.GetTooltip(typeof(ModelImportSettings), nameof(ModelImportSettings.SDFResolution)));
+ resolution.FloatValue.MinValue = 0.0001f;
+ resolution.FloatValue.MaxValue = 100.0f;
+ resolution.FloatValue.Value = sdf.Texture != null ? sdf.ResolutionScale : 1.0f;
+ resolution.FloatValue.BoxValueChanged += b => { proxy.Window._importSettings.SDFResolution = b.Value; };
+
+ var lodIndex = group.IntegerValue("LOD Index", "Index of the model Level of Detail to use for SDF data building. By default uses the lowest quality LOD for fast building.");
+ lodIndex.IntValue.MinValue = 0;
+ lodIndex.IntValue.MaxValue = lods.Length - 1;
+ lodIndex.IntValue.Value = sdf.Texture != null ? sdf.LOD : 6;
+ _sdfModelLodIndex = lodIndex;
+
+ var buttons = group.CustomContainer();
+ var gridControl = buttons.CustomControl;
+ gridControl.ClipChildren = false;
+ gridControl.Height = Button.DefaultHeight;
+ gridControl.SlotsHorizontally = 2;
+ gridControl.SlotsVertically = 1;
+ var rebuildButton = buttons.Button("Rebuild", "Rebuilds the model SDF.").Button;
+ rebuildButton.Clicked += OnRebuildSDF;
+ var removeButton = buttons.Button("Remove", "Removes the model SDF data from the asset.").Button;
+ removeButton.Enabled = sdf.Texture != null;
+ removeButton.Clicked += OnRemoveSDF;
+ }
+
// Group per LOD
for (int lodIndex = 0; lodIndex < lods.Length; lodIndex++)
{
@@ -260,6 +283,22 @@ namespace FlaxEditor.Windows.Assets
proxy.UpdateMaterialSlotsUI();
}
+ private void OnRebuildSDF()
+ {
+ var proxy = (MeshesPropertiesProxy)Values[0];
+ proxy.Asset.GenerateSDF(proxy.Window._importSettings.SDFResolution, _sdfModelLodIndex.Value);
+ proxy.Window.MarkAsEdited();
+ Presenter.BuildLayoutOnUpdate();
+ }
+
+ private void OnRemoveSDF()
+ {
+ var proxy = (MeshesPropertiesProxy)Values[0];
+ proxy.Asset.SetSDF(new ModelBase.SDFData());
+ proxy.Window.MarkAsEdited();
+ Presenter.BuildLayoutOnUpdate();
+ }
+
internal override void RefreshInternal()
{
// Skip updates when model is not loaded
@@ -645,14 +684,14 @@ namespace FlaxEditor.Windows.Assets
[CustomEditor(typeof(ProxyEditor))]
private sealed class ImportPropertiesProxy : PropertiesProxyBase
{
- private ModelImportSettings ImportSettings = new ModelImportSettings();
+ private ModelImportSettings ImportSettings;
///
public override void OnLoad(ModelWindow window)
{
base.OnLoad(window);
- ModelImportSettings.TryRestore(ref ImportSettings, window.Item.Path);
+ ImportSettings = window._importSettings;
}
public void Reimport()
@@ -675,7 +714,7 @@ namespace FlaxEditor.Windows.Assets
{
var group = layout.Group("Import Settings");
- var importSettingsField = typeof(ImportPropertiesProxy).GetField("ImportSettings", BindingFlags.NonPublic | BindingFlags.Instance);
+ var importSettingsField = typeof(ImportPropertiesProxy).GetField(nameof(ImportSettings), BindingFlags.NonPublic | BindingFlags.Instance);
var importSettingsValues = new ValueContainer(new ScriptMemberInfo(importSettingsField)) { proxy.ImportSettings };
group.Object(importSettingsValues);
@@ -730,6 +769,7 @@ namespace FlaxEditor.Windows.Assets
private readonly ModelPreview _preview;
private StaticModel _highlightActor;
private MeshDataCache _meshData;
+ private ModelImportSettings _importSettings = new ModelImportSettings();
///
public ModelWindow(Editor editor, AssetItem item)
@@ -868,6 +908,7 @@ namespace FlaxEditor.Windows.Assets
{
_refreshOnLODsLoaded = true;
_preview.ViewportCamera.SetArcBallView(Asset.GetBox());
+ ModelImportSettings.TryRestore(ref _importSettings, Item.Path);
UpdateEffectsOnAsset();
// TODO: disable streaming for this model
diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp
index 07071a921..79a7d869c 100644
--- a/Source/Engine/Content/Assets/Model.cpp
+++ b/Source/Engine/Content/Assets/Model.cpp
@@ -2,14 +2,11 @@
#include "Model.h"
#include "Engine/Core/Log.h"
-#include "Engine/Core/Math/Int3.h"
-#include "Engine/Core/RandomStream.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Content/WeakAssetReference.h"
#include "Engine/Content/Upgraders/ModelAssetUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
-#include "Engine/Core/Math/Int2.h"
#include "Engine/Debug/DebugDraw.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderTask.h"
@@ -18,10 +15,11 @@
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
#include "Engine/Graphics/Async/GPUTask.h"
#include "Engine/Graphics/Textures/GPUTexture.h"
+#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Renderer/DrawCall.h"
-#include "Engine/Threading/JobSystem.h"
#include "Engine/Threading/Threading.h"
+#include "Engine/Tools/ModelTool/ModelTool.h"
#include "Engine/Tools/ModelTool/MeshAccelerationStructure.h"
#if GPU_ENABLE_ASYNC_RESOURCES_CREATION
#include "Engine/Threading/ThreadPoolTask.h"
@@ -526,17 +524,50 @@ bool Model::Save(bool withMeshDataFromGpu, const StringView& path)
lodChunk->Data.Copy(meshesStream.GetHandle(), meshesStream.GetPosition());
}
}
+
+ // Download SDF data
+ if (SDF.Texture)
+ {
+ auto sdfChunk = GET_CHUNK(15);
+ if (sdfChunk == nullptr)
+ return true;
+ MemoryWriteStream sdfStream;
+ sdfStream.WriteInt32(1); // Version
+ ModelSDFHeader data(SDF, SDF.Texture->GetDescription());
+ sdfStream.Write(&data);
+ TextureData sdfTextureData;
+ if (SDF.Texture->DownloadData(sdfTextureData))
+ return true;
+ for (int32 mipLevel = 0; mipLevel < sdfTextureData.Items[0].Mips.Count(); mipLevel++)
+ {
+ auto& mip = sdfTextureData.Items[0].Mips[mipLevel];
+ ModelSDFMip mipData(mipLevel, mip);
+ sdfStream.Write(&mipData);
+ sdfStream.Write(mip.Data.Get(), mip.Data.Length());
+ }
+ sdfChunk->Data.Copy(sdfStream.GetHandle(), sdfStream.GetPosition());
+ }
}
else
{
- ASSERT(!IsVirtual());
-
// Load all chunks with a mesh data
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
if (LoadChunk(MODEL_LOD_TO_CHUNK_INDEX(lodIndex)))
return true;
}
+
+ if (SDF.Texture)
+ {
+ // SDF data from file (only if has no cached texture data)
+ if (LoadChunk(15))
+ return true;
+ }
+ else
+ {
+ // No SDF texture
+ ReleaseChunk(15);
+ }
}
// Set mesh header data
@@ -565,8 +596,9 @@ bool Model::Save(bool withMeshDataFromGpu, const StringView& path)
#endif
-bool Model::GenerateSDF(float resolutionScale, int32 lodIndex)
+bool Model::GenerateSDF(float resolutionScale, int32 lodIndex, bool cacheData)
{
+ ScopeLock lock(Locker);
if (!HasAnyLODInitialized())
return true;
if (IsInMainThread() && IsVirtual())
@@ -575,219 +607,31 @@ bool Model::GenerateSDF(float resolutionScale, int32 lodIndex)
LOG(Warning, "Cannot generate SDF for virtual models on a main thread.");
return true;
}
- PROFILE_CPU();
- auto startTime = Platform::GetTimeSeconds();
- ScopeLock lock(Locker);
-
- // Setup SDF texture properties
+ cacheData &= Storage != nullptr; // Cache only if has storage linked
lodIndex = Math::Clamp(lodIndex, HighestResidentLODIndex(), LODs.Count() - 1);
- auto& lod = LODs[lodIndex];
- BoundingBox bounds = lod.GetBox();
- Vector3 size = bounds.GetSize();
- SDF.WorldUnitsPerVoxel = 10 / Math::Max(resolutionScale, 0.0001f);
- Int3 resolution(Vector3::Ceil(Vector3::Clamp(size / SDF.WorldUnitsPerVoxel, 4, 256)));
- Vector3 uvwToLocalMul = size;
- Vector3 uvwToLocalAdd = bounds.Minimum;
- SDF.LocalToUVWMul = Vector3::One / uvwToLocalMul;
- SDF.LocalToUVWAdd = -uvwToLocalAdd / uvwToLocalMul;
- SDF.MaxDistance = size.MaxValue();
- SDF.LocalBoundsMin = bounds.Minimum;
- SDF.LocalBoundsMax = bounds.Maximum;
- // TODO: maybe apply 1 voxel margin around the geometry?
- const int32 maxMips = 3;
- const int32 mipCount = Math::Min(MipLevelsCount(resolution.X, resolution.Y, resolution.Z, true), maxMips);
- if (!SDF.Texture)
- SDF.Texture = GPUTexture::New();
- PixelFormat format = PixelFormat::R16_UNorm;
- int32 formatStride = 2;
- float formatMaxValue = MAX_uint16;
- typedef float (*FormatRead)(void* ptr);
- typedef void (*FormatWrite)(void* ptr, float v);
- FormatRead formatRead = [](void* ptr)
- {
- return (float)*(uint16*)ptr;
- };
- FormatWrite formatWrite = [](void* ptr, float v)
- {
- *(uint16*)ptr = (uint16)v;
- };
- if (resolution.MaxValue() < 8)
- {
- // For smaller meshes use more optimized format (gives small perf and memory gain but introduces artifacts on larger meshes)
- format = PixelFormat::R8_UNorm;
- formatStride = 1;
- formatMaxValue = MAX_uint8;
- formatRead = [](void* ptr)
- {
- return (float)*(uint8*)ptr;
- };
- formatWrite = [](void* ptr, float v)
- {
- *(uint8*)ptr = (uint8)v;
- };
- }
- if (SDF.Texture->Init(GPUTextureDescription::New3D(resolution.X, resolution.Y, resolution.Z, format, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess, mipCount)))
- {
- SAFE_DELETE_GPU_RESOURCE(SDF.Texture);
+
+ // Generate SDF
+ MemoryWriteStream sdfStream;
+ if (ModelTool::GenerateModelSDF(this, nullptr, resolutionScale, lodIndex, &SDF, cacheData ? &sdfStream : nullptr, GetPath()))
return true;
- }
- // TODO: support GPU to generate model SDF on-the-fly (if called during rendering)
+ // Set asset data
+ if (cacheData)
+ GetOrCreateChunk(15)->Data.Copy(sdfStream.GetHandle(), sdfStream.GetPosition());
- // Setup acceleration structure for fast ray tracing the mesh triangles
- MeshAccelerationStructure scene;
- scene.Add(this, lodIndex);
- scene.BuildBVH();
-
- // Allocate memory for the distant field
- const int32 voxelsSize = resolution.X * resolution.Y * resolution.Z * formatStride;
- void* voxels = Allocator::Allocate(voxelsSize);
- Vector3 xyzToLocalMul = uvwToLocalMul / Vector3(resolution);
- Vector3 xyzToLocalAdd = uvwToLocalAdd;
- const Vector2 encodeMAD(0.5f / SDF.MaxDistance * formatMaxValue, 0.5f * formatMaxValue);
- const Vector2 decodeMAD(2.0f * SDF.MaxDistance / formatMaxValue, -SDF.MaxDistance);
-
- // TODO: use optimized sparse storage for SDF data as hierarchical bricks as in papers below:
- // https://graphics.pixar.com/library/IrradianceAtlas/paper.pdf
- // http://maverick.inria.fr/Membres/Cyril.Crassin/thesis/CCrassinThesis_EN_Web.pdf
- // http://ramakarl.com/pdfs/2016_Hoetzlein_GVDB.pdf
- // https://www.cse.chalmers.se/~uffe/HighResolutionSparseVoxelDAGs.pdf
-
- // Brute-force for each voxel to calculate distance to the closest triangle with point query and distance sign by raycasting around the voxel
- const int32 sampleCount = 12;
- Array sampleDirections;
- sampleDirections.Resize(sampleCount);
- {
- RandomStream rand;
- sampleDirections.Get()[0] = Vector3::Up;
- sampleDirections.Get()[1] = Vector3::Down;
- sampleDirections.Get()[2] = Vector3::Left;
- sampleDirections.Get()[3] = Vector3::Right;
- sampleDirections.Get()[4] = Vector3::Forward;
- sampleDirections.Get()[5] = Vector3::Backward;
- for (int32 i = 6; i < sampleCount; i++)
- sampleDirections.Get()[i] = rand.GetUnitVector();
- }
- Function sdfJob = [this, &resolution, &sampleDirections, &scene, &voxels, &xyzToLocalMul, &xyzToLocalAdd, &encodeMAD, &formatStride, &formatWrite](int32 z)
- {
- PROFILE_CPU_NAMED("Model SDF Job");
- float hitDistance;
- Vector3 hitNormal, hitPoint;
- Triangle hitTriangle;
- const int32 zAddress = resolution.Y * resolution.X * z;
- for (int32 y = 0; y < resolution.Y; y++)
- {
- const int32 yAddress = resolution.X * y + zAddress;
- for (int32 x = 0; x < resolution.X; x++)
- {
- float minDistance = SDF.MaxDistance;
- Vector3 voxelPos = Vector3((float)x, (float)y, (float)z) * xyzToLocalMul + xyzToLocalAdd;
-
- // Point query to find the distance to the closest surface
- scene.PointQuery(voxelPos, minDistance, hitPoint, hitTriangle);
-
- // Raycast samples around voxel to count triangle backfaces hit
- int32 hitBackCount = 0, hitCount = 0;
- for (int32 sample = 0; sample < sampleDirections.Count(); sample++)
- {
- Ray sampleRay(voxelPos, sampleDirections[sample]);
- if (scene.RayCast(sampleRay, hitDistance, hitNormal, hitTriangle))
- {
- hitCount++;
- const bool backHit = Vector3::Dot(sampleRay.Direction, hitTriangle.GetNormal()) > 0;
- if (backHit)
- hitBackCount++;
- }
- }
-
- float distance = minDistance;
- // TODO: surface thickness threshold? shift reduce distance for all voxels by something like 0.01 to enlarge thin geometry
- //if ((float)hitBackCount > )hitCount * 0.3f && hitCount != 0)
- if ((float)hitBackCount > (float)sampleDirections.Count() * 0.6f && hitCount != 0)
- {
- // Voxel is inside the geometry so turn it into negative distance to the surface
- distance *= -1;
- }
- const int32 xAddress = x + yAddress;
- formatWrite((byte*)voxels + xAddress * formatStride, distance * encodeMAD.X + encodeMAD.Y);
- }
- }
- };
- JobSystem::Execute(sdfJob, resolution.Z);
-
- // Upload data to the GPU
- BytesContainer data;
- data.Link((byte*)voxels, voxelsSize);
- auto task = SDF.Texture->UploadMipMapAsync(data, 0, resolution.X * formatStride, data.Length(), true);
- if (task)
- task->Start();
-
- // Generate mip maps
- void* voxelsMip = nullptr;
- for (int32 mipLevel = 1; mipLevel < mipCount; mipLevel++)
- {
- Int3 resolutionMip = Int3::Max(resolution / 2, Int3::One);
- const int32 voxelsMipSize = resolutionMip.X * resolutionMip.Y * resolutionMip.Z * formatStride;
- if (voxelsMip == nullptr)
- voxelsMip = Allocator::Allocate(voxelsMipSize);
-
- // Downscale mip
- Function mipJob = [this, &voxelsMip, &voxels, &resolution, &resolutionMip, &encodeMAD, &decodeMAD, &formatStride, &formatRead, &formatWrite](int32 z)
- {
- PROFILE_CPU_NAMED("Model SDF Mip Job");
- const int32 zAddress = resolutionMip.Y * resolutionMip.X * z;
- for (int32 y = 0; y < resolutionMip.Y; y++)
- {
- const int32 yAddress = resolutionMip.X * y + zAddress;
- for (int32 x = 0; x < resolutionMip.X; x++)
- {
- // Linear box filter around the voxel
- // TODO: use min distance for nearby texels (texel distance + distance to texel)
- float distance = 0;
- for (int32 dz = 0; dz < 2; dz++)
- {
- const int32 dzAddress = (z * 2 + dz) * (resolution.Y * resolution.X);
- for (int32 dy = 0; dy < 2; dy++)
- {
- const int32 dyAddress = (y * 2 + dy) * (resolution.X) + dzAddress;
- for (int32 dx = 0; dx < 2; dx++)
- {
- const int32 dxAddress = (x * 2 + dx) + dyAddress;
- const float d = formatRead((byte*)voxels + dxAddress * formatStride) * decodeMAD.X + decodeMAD.Y;
- distance += d;
- }
- }
- }
- distance *= 1.0f / 8.0f;
-
- const int32 xAddress = x + yAddress;
- formatWrite((byte*)voxelsMip + xAddress * formatStride, distance * encodeMAD.X + encodeMAD.Y);
- }
- }
- };
- JobSystem::Execute(mipJob, resolutionMip.Z);
-
- // Upload to the GPU
- data.Link((byte*)voxelsMip, voxelsMipSize);
- task = SDF.Texture->UploadMipMapAsync(data, mipLevel, resolutionMip.X * formatStride, data.Length(), true);
- if (task)
- task->Start();
-
- // Go down
- Swap(voxelsMip, voxels);
- resolution = resolutionMip;
- }
-
- Allocator::Free(voxelsMip);
- Allocator::Free(voxels);
-
-#if !BUILD_RELEASE
- auto endTime = Platform::GetTimeSeconds();
- LOG(Info, "Generated SDF {}x{}x{} ({} kB) in {}ms for {}", resolution.X, resolution.Y, resolution.Z, SDF.Texture->GetMemoryUsage() / 1024, (int32)((endTime - startTime) * 1000.0), GetPath());
-#endif
return false;
}
+void Model::SetSDF(const SDFData& sdf)
+{
+ ScopeLock lock(Locker);
+ if (SDF.Texture == sdf.Texture)
+ return;
+ SAFE_DELETE_GPU_RESOURCE(SDF.Texture);
+ SDF = sdf;
+ ReleaseChunk(15);
+}
+
bool Model::Init(const Span& meshesCountPerLod)
{
if (meshesCountPerLod.IsInvalid() || meshesCountPerLod.Length() > MODEL_MAX_LODS)
@@ -1039,6 +883,50 @@ Asset::LoadResult Model::load()
}
}
+ // Load SDF
+ auto chunk15 = GetChunk(15);
+ if (chunk15 && chunk15->IsLoaded())
+ {
+ MemoryReadStream sdfStream(chunk15->Get(), chunk15->Size());
+ int32 version;
+ sdfStream.ReadInt32(&version);
+ switch (version)
+ {
+ case 1:
+ {
+ ModelSDFHeader data;
+ sdfStream.Read(&data);
+ if (!SDF.Texture)
+ SDF.Texture = GPUTexture::New();
+ if (SDF.Texture->Init(GPUTextureDescription::New3D(data.Width, data.Height, data.Depth, data.Format, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess, data.MipLevels)))
+ return LoadResult::Failed;
+ SDF.LocalToUVWMul = data.LocalToUVWMul;
+ SDF.LocalToUVWAdd = data.LocalToUVWAdd;
+ SDF.WorldUnitsPerVoxel = data.WorldUnitsPerVoxel;
+ SDF.MaxDistance = data.MaxDistance;
+ SDF.LocalBoundsMin = data.LocalBoundsMin;
+ SDF.LocalBoundsMax = data.LocalBoundsMax;
+ SDF.ResolutionScale = data.ResolutionScale;
+ SDF.LOD = data.LOD;
+ for (int32 mipLevel = 0; mipLevel < data.MipLevels; mipLevel++)
+ {
+ ModelSDFMip mipData;
+ sdfStream.Read(&mipData);
+ void* mipBytes = sdfStream.Read(mipData.SlicePitch);
+ BytesContainer mipBytesData;
+ mipBytesData.Link((byte*)mipBytes, mipData.SlicePitch);
+ auto task = SDF.Texture->UploadMipMapAsync(mipBytesData, mipData.MipIndex, mipData.RowPitch, mipData.SlicePitch, false);
+ if (task)
+ task->Start();
+ }
+ break;
+ }
+ default:
+ LOG(Warning, "Unknown SDF data version {0} in {1}", version, ToString());
+ break;
+ }
+ }
+
#if BUILD_DEBUG || BUILD_DEVELOPMENT
// Validate LODs
for (int32 lodIndex = 1; lodIndex < LODs.Count(); lodIndex++)
@@ -1092,7 +980,7 @@ bool Model::init(AssetInitData& initData)
AssetChunksFlag Model::getChunksToPreload() const
{
// Note: we don't preload any LODs here because it's done by the Streaming Manager
- return GET_CHUNK_FLAG(0);
+ return GET_CHUNK_FLAG(0) | GET_CHUNK_FLAG(15);
}
void ModelBase::SetupMaterialSlots(int32 slotsCount)
diff --git a/Source/Engine/Content/Assets/Model.h b/Source/Engine/Content/Assets/Model.h
index a4d6711eb..d58fb469b 100644
--- a/Source/Engine/Content/Assets/Model.h
+++ b/Source/Engine/Content/Assets/Model.h
@@ -212,8 +212,14 @@ public:
/// Can be called in async in case of SDF generation on a CPU (assuming model is not during rendering).
/// The SDF texture resolution scale. Use higher values for more precise data but with significant performance and memory overhead.
/// The index of the LOD to use for the SDF building.
+ /// If true, the generated SDF texture data will be cached on CPU (in asset chunk storage) to allow saving it later, otherwise it will be runtime for GPU-only. Ignored for virtual assets.
/// True if failed, otherwise false.
- API_FUNCTION() bool GenerateSDF(float resolutionScale = 1.0f, int32 lodIndex = 6);
+ API_FUNCTION() bool GenerateSDF(float resolutionScale = 1.0f, int32 lodIndex = 6, bool cacheData = true);
+
+ ///
+ /// Sets set SDF data (releases the current one).
+ ///
+ API_FUNCTION() void SetSDF(const SDFData& sdf);
private:
diff --git a/Source/Engine/Content/Assets/ModelBase.h b/Source/Engine/Content/Assets/ModelBase.h
index 6f2065abf..44e8e8242 100644
--- a/Source/Engine/Content/Assets/ModelBase.h
+++ b/Source/Engine/Content/Assets/ModelBase.h
@@ -14,7 +14,7 @@
// Chunk 1: LOD0
// Chunk 2: LOD1
// ..
-//
+// Chunk 15: SDF
#define MODEL_LOD_TO_CHUNK_INDEX(lod) (lod + 1)
class MeshBase;
@@ -63,10 +63,20 @@ public:
///
API_FIELD() Vector3 LocalBoundsMin;
+ ///
+ /// The SDF texture resolution scale used for building texture.
+ ///
+ API_FIELD() float ResolutionScale = 1.0f;
+
///
/// The bounding box of the SDF texture in the model local-space.
///
API_FIELD() Vector3 LocalBoundsMax;
+
+ ///
+ /// The model LOD index used for the building.
+ ///
+ API_FIELD() int32 LOD = 6;
};
protected:
diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h
index 9e4b521a6..6d49ca826 100644
--- a/Source/Engine/ContentImporters/ImportModel.h
+++ b/Source/Engine/ContentImporters/ImportModel.h
@@ -48,9 +48,9 @@ public:
private:
- static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData);
- static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData);
- static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData);
+ static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
+ static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
+ static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
};
#endif
diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp
index 3c85084db..cd56f0025 100644
--- a/Source/Engine/ContentImporters/ImportModelFile.cpp
+++ b/Source/Engine/ContentImporters/ImportModelFile.cpp
@@ -150,13 +150,13 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
switch (options.Type)
{
case ModelTool::ModelType::Model:
- result = ImportModel(context, modelData);
+ result = ImportModel(context, modelData, &options);
break;
case ModelTool::ModelType::SkinnedModel:
- result = ImportSkinnedModel(context, modelData);
+ result = ImportSkinnedModel(context, modelData, &options);
break;
case ModelTool::ModelType::Animation:
- result = ImportAnimation(context, modelData);
+ result = ImportAnimation(context, modelData, &options);
break;
}
if (result != CreateAssetResult::Ok)
@@ -199,7 +199,7 @@ CreateAssetResult ImportModelFile::Create(CreateAssetContext& context)
return ImportModel(context, modelData);
}
-CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData)
+CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(Model, Model::SerializedVersion);
@@ -235,10 +235,22 @@ CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, Mode
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
+ // Generate SDF
+ if (options && options->GenerateSDF)
+ {
+ stream.SetPosition(0);
+ if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath))
+ {
+ if (context.AllocateChunk(15))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+ }
+ }
+
return CreateAssetResult::Ok;
}
-CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData)
+CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
@@ -277,7 +289,7 @@ CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& contex
return CreateAssetResult::Ok;
}
-CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData)
+CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(Animation, Animation::SerializedVersion);
diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h
index 6411d78e6..6e2074870 100644
--- a/Source/Engine/Core/Config/GraphicsSettings.h
+++ b/Source/Engine/Core/Config/GraphicsSettings.h
@@ -11,9 +11,9 @@
///
API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API GraphicsSettings : public SettingsBase
{
-DECLARE_SCRIPTING_TYPE_MINIMAL(GraphicsSettings);
+ API_AUTO_SERIALIZATION();
+ DECLARE_SCRIPTING_TYPE_MINIMAL(GraphicsSettings);
public:
-
///
/// Enables rendering synchronization with the refresh rate of the display device to avoid "tearing" artifacts.
///
@@ -62,8 +62,15 @@ public:
API_FIELD(Attributes="EditorOrder(1320), DefaultValue(false), EditorDisplay(\"Quality\", \"Allow CSM Blending\")")
bool AllowCSMBlending = false;
-public:
+#if USE_EDITOR
+ ///
+ /// If checked, the 'Generate SDF' option will be checked on model import options by default. Use it if your project uses Global SDF (eg. for Global Illumination or particles).
+ ///
+ API_FIELD(Attributes="EditorOrder(2000), EditorDisplay(\"Global SDF\")")
+ bool GenerateSDFOnModelImport = false;
+#endif
+public:
///
/// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use.
///
@@ -71,15 +78,4 @@ public:
// [SettingsBase]
void Apply() override;
- void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override
- {
- DESERIALIZE(UseVSync);
- DESERIALIZE(AAQuality);
- DESERIALIZE(SSRQuality);
- DESERIALIZE(SSAOQuality);
- DESERIALIZE(VolumetricFogQuality);
- DESERIALIZE(ShadowsQuality);
- DESERIALIZE(ShadowMapsQuality);
- DESERIALIZE(AllowCSMBlending);
- }
};
diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp
index d1e867c6c..cf7657039 100644
--- a/Source/Engine/Graphics/Models/ModelData.cpp
+++ b/Source/Engine/Graphics/Models/ModelData.cpp
@@ -600,6 +600,21 @@ bool MaterialSlotEntry::UsesProperties() const
Normals.TextureIndex != -1;
}
+BoundingBox ModelLodData::GetBox() const
+{
+ if (Meshes.IsEmpty())
+ return BoundingBox::Empty;
+ BoundingBox bounds;
+ Meshes[0]->CalculateBox(bounds);
+ for (int32 i = 1; i < Meshes.Count(); i++)
+ {
+ BoundingBox b;
+ Meshes[i]->CalculateBox(b);
+ BoundingBox::Merge(bounds, b, bounds);
+ }
+ return bounds;
+}
+
void ModelData::CalculateLODsScreenSizes()
{
const float autoComputeLodPowerBase = 0.5f;
diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h
index 4d4733fb7..2a7164123 100644
--- a/Source/Engine/Graphics/Models/ModelData.h
+++ b/Source/Engine/Graphics/Models/ModelData.h
@@ -409,6 +409,11 @@ public:
{
Meshes.ClearDelete();
}
+
+ ///
+ /// Gets the bounding box combined for all meshes in this model LOD.
+ ///
+ BoundingBox GetBox() const;
};
///
diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp
index e3b760912..132202dac 100644
--- a/Source/Engine/Level/Actors/StaticModel.cpp
+++ b/Source/Engine/Level/Actors/StaticModel.cpp
@@ -215,8 +215,6 @@ void StaticModel::Draw(RenderContext& renderContext)
return;
if (renderContext.View.Pass == DrawPass::GlobalSDF)
{
- if (!Model->SDF.Texture)
- Model->GenerateSDF();
GlobalSignDistanceFieldPass::Instance()->RasterizeModelSDF(Model->SDF, _world, _box);
return;
}
diff --git a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp
index 1212b293e..b6fe6fa9d 100644
--- a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp
+++ b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp
@@ -5,6 +5,7 @@
#include "MeshAccelerationStructure.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Content/Assets/Model.h"
+#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Profiler/ProfilerCPU.h"
void MeshAccelerationStructure::BuildBVH(int32 node, int32 maxLeafSize, Array& scratch)
@@ -326,6 +327,34 @@ void MeshAccelerationStructure::Add(Model* model, int32 lodIndex)
}
}
+void MeshAccelerationStructure::Add(ModelData* modelData, int32 lodIndex, bool copy)
+{
+ PROFILE_CPU();
+ lodIndex = Math::Clamp(lodIndex, 0, modelData->LODs.Count() - 1);
+ ModelLodData& lod = modelData->LODs[lodIndex];
+ const int32 meshesStart = _meshes.Count();
+ _meshes.AddDefault(lod.Meshes.Count());
+ for (int32 i = 0; i < lod.Meshes.Count(); i++)
+ {
+ MeshData* mesh = lod.Meshes[i];
+ auto& meshData = _meshes[meshesStart + i];
+ meshData.Indices = mesh->Indices.Count();
+ meshData.Vertices = mesh->Positions.Count();
+ if (copy)
+ {
+ meshData.IndexBuffer.Copy((const byte*)mesh->Indices.Get(), meshData.Indices * sizeof(uint32));
+ meshData.VertexBuffer.Copy((const byte*)mesh->Positions.Get(), meshData.Vertices * sizeof(Vector3));
+ }
+ else
+ {
+ meshData.IndexBuffer.Link((const byte*)mesh->Indices.Get(), meshData.Indices * sizeof(uint32));
+ meshData.VertexBuffer.Link((const byte*)mesh->Positions.Get(), meshData.Vertices * sizeof(Vector3));
+ }
+ meshData.Use16BitIndexBuffer = false;
+ mesh->CalculateBox(meshData.Bounds);
+ }
+}
+
void MeshAccelerationStructure::Add(Vector3* vb, int32 vertices, void* ib, int32 indices, bool use16BitIndex, bool copy)
{
auto& meshData = _meshes.AddOne();
diff --git a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h
index 8e3684d63..1063b457c 100644
--- a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h
+++ b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.h
@@ -10,6 +10,7 @@
#include "Engine/Core/Collections/Array.h"
class Model;
+class ModelData;
///
/// Acceleration Structure utility for robust ray tracing mesh geometry with optimized data structure.
@@ -59,6 +60,9 @@ public:
// Adds the model geometry for the build to the structure.
void Add(Model* model, int32 lodIndex);
+ // Adds the model geometry for the build to the structure.
+ void Add(ModelData* modelData, int32 lodIndex, bool copy = false);
+
// Adds the triangles geometry for the build to the structure.
void Add(Vector3* vb, int32 vertices, void* ib, int32 indices, bool use16BitIndex, bool copy = false);
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Build.cs b/Source/Engine/Tools/ModelTool/ModelTool.Build.cs
index f65db0243..7a4eb59c3 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.Build.cs
+++ b/Source/Engine/Tools/ModelTool/ModelTool.Build.cs
@@ -74,5 +74,6 @@ public class ModelTool : EngineModule
public override void GetFilesToDeploy(List files)
{
files.Add(Path.Combine(FolderPath, "ModelTool.h"));
+ files.Add(Path.Combine(FolderPath, "MeshAccelerationStructure.h"));
}
}
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp
index 2b3620616..964df9d6e 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp
@@ -1,6 +1,6 @@
// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
-#if COMPILE_WITH_MODEL_TOOL
+#if COMPILE_WITH_MODEL_TOOL && USE_EDITOR
#include "ModelTool.h"
#include "Engine/Core/Log.h"
@@ -62,6 +62,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(ImportMaterials);
SERIALIZE(ImportTextures);
SERIALIZE(RestoreMaterialsOnReimport);
+ SERIALIZE(GenerateSDF);
+ SERIALIZE(SDFResolution);
SERIALIZE(SplitObjects);
SERIALIZE(ObjectIndex);
}
@@ -100,6 +102,8 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(ImportMaterials);
DESERIALIZE(ImportTextures);
DESERIALIZE(RestoreMaterialsOnReimport);
+ DESERIALIZE(GenerateSDF);
+ DESERIALIZE(SDFResolution);
DESERIALIZE(SplitObjects);
DESERIALIZE(ObjectIndex);
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index f014bd571..85776e5d6 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -3,7 +3,19 @@
#if COMPILE_WITH_MODEL_TOOL
#include "ModelTool.h"
+#include "MeshAccelerationStructure.h"
#include "Engine/Core/Log.h"
+#include "Engine/Core/RandomStream.h"
+#include "Engine/Core/Math/Int3.h"
+#include "Engine/Core/Math/Ray.h"
+#include "Engine/Profiler/ProfilerCPU.h"
+#include "Engine/Threading/JobSystem.h"
+#include "Engine/Graphics/RenderTools.h"
+#include "Engine/Graphics/Async/GPUTask.h"
+#include "Engine/Graphics/Textures/TextureData.h"
+#include "Engine/Content/Assets/Model.h"
+#include "Engine/Serialization/MemoryWriteStream.h"
+#if USE_EDITOR
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Types/Pair.h"
@@ -19,6 +31,297 @@
#include "Engine/ContentImporters/CreateCollisionData.h"
#include "Editor/Utilities/EditorUtilities.h"
#include
+#endif
+
+ModelSDFHeader::ModelSDFHeader(const ModelBase::SDFData& sdf, const GPUTextureDescription& desc)
+ : LocalToUVWMul(sdf.LocalToUVWMul)
+ , WorldUnitsPerVoxel(sdf.WorldUnitsPerVoxel)
+ , LocalToUVWAdd(sdf.LocalToUVWAdd)
+ , MaxDistance(sdf.MaxDistance)
+ , LocalBoundsMin(sdf.LocalBoundsMin)
+ , MipLevels(desc.MipLevels)
+ , LocalBoundsMax(sdf.LocalBoundsMax)
+ , Width(desc.Width)
+ , Height(desc.Height)
+ , Depth(desc.Depth)
+ , Format(desc.Format)
+ , ResolutionScale(sdf.ResolutionScale)
+ , LOD(sdf.LOD)
+{
+}
+
+ModelSDFMip::ModelSDFMip(int32 mipIndex, uint32 rowPitch, uint32 slicePitch)
+ : MipIndex(mipIndex)
+ , RowPitch(rowPitch)
+ , SlicePitch(slicePitch)
+{
+}
+
+ModelSDFMip::ModelSDFMip(int32 mipIndex, const TextureMipData& mip)
+ : MipIndex(mipIndex)
+ , RowPitch(mip.RowPitch)
+ , SlicePitch(mip.Data.Length())
+{
+}
+
+bool ModelTool::GenerateModelSDF(Model* inputModel, ModelData* modelData, float resolutionScale, int32 lodIndex, ModelBase::SDFData* outputSDF, MemoryWriteStream* outputStream, const StringView& assetName)
+{
+ PROFILE_CPU();
+ auto startTime = Platform::GetTimeSeconds();
+
+ // Setup SDF texture properties
+ BoundingBox bounds;
+ if (inputModel)
+ bounds = inputModel->LODs[lodIndex].GetBox();
+ else if (modelData)
+ bounds = modelData->LODs[lodIndex].GetBox();
+ else
+ return true;
+ Vector3 size = bounds.GetSize();
+ ModelBase::SDFData sdf;
+ sdf.WorldUnitsPerVoxel = 10 / Math::Max(resolutionScale, 0.0001f);
+ Int3 resolution(Vector3::Ceil(Vector3::Clamp(size / sdf.WorldUnitsPerVoxel, 4, 256)));
+ Vector3 uvwToLocalMul = size;
+ Vector3 uvwToLocalAdd = bounds.Minimum;
+ sdf.LocalToUVWMul = Vector3::One / uvwToLocalMul;
+ sdf.LocalToUVWAdd = -uvwToLocalAdd / uvwToLocalMul;
+ sdf.MaxDistance = size.MaxValue();
+ sdf.LocalBoundsMin = bounds.Minimum;
+ sdf.LocalBoundsMax = bounds.Maximum;
+ sdf.ResolutionScale = resolutionScale;
+ sdf.LOD = lodIndex;
+ // TODO: maybe apply 1 voxel margin around the geometry?
+ const int32 maxMips = 3;
+ const int32 mipCount = Math::Min(MipLevelsCount(resolution.X, resolution.Y, resolution.Z, true), maxMips);
+ PixelFormat format = PixelFormat::R16_UNorm;
+ int32 formatStride = 2;
+ float formatMaxValue = MAX_uint16;
+ typedef float (*FormatRead)(void* ptr);
+ typedef void (*FormatWrite)(void* ptr, float v);
+ FormatRead formatRead = [](void* ptr)
+ {
+ return (float)*(uint16*)ptr;
+ };
+ FormatWrite formatWrite = [](void* ptr, float v)
+ {
+ *(uint16*)ptr = (uint16)v;
+ };
+ if (resolution.MaxValue() < 8)
+ {
+ // For smaller meshes use more optimized format (gives small perf and memory gain but introduces artifacts on larger meshes)
+ format = PixelFormat::R8_UNorm;
+ formatStride = 1;
+ formatMaxValue = MAX_uint8;
+ formatRead = [](void* ptr)
+ {
+ return (float)*(uint8*)ptr;
+ };
+ formatWrite = [](void* ptr, float v)
+ {
+ *(uint8*)ptr = (uint8)v;
+ };
+ }
+ GPUTextureDescription textureDesc = GPUTextureDescription::New3D(resolution.X, resolution.Y, resolution.Z, format, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess, mipCount);
+ if (outputSDF)
+ {
+ *outputSDF = sdf;
+ if (!outputSDF->Texture)
+ outputSDF->Texture = GPUTexture::New();
+ if (outputSDF->Texture->Init(textureDesc))
+ {
+ SAFE_DELETE_GPU_RESOURCE(outputSDF->Texture);
+ return true;
+ }
+ }
+
+ // TODO: support GPU to generate model SDF on-the-fly (if called during rendering)
+
+ // Setup acceleration structure for fast ray tracing the mesh triangles
+ MeshAccelerationStructure scene;
+ if (inputModel)
+ scene.Add(inputModel, lodIndex);
+ else if (modelData)
+ scene.Add(modelData, lodIndex);
+ scene.BuildBVH();
+
+ // Allocate memory for the distant field
+ const int32 voxelsSize = resolution.X * resolution.Y * resolution.Z * formatStride;
+ void* voxels = Allocator::Allocate(voxelsSize);
+ Vector3 xyzToLocalMul = uvwToLocalMul / Vector3(resolution);
+ Vector3 xyzToLocalAdd = uvwToLocalAdd;
+ const Vector2 encodeMAD(0.5f / sdf.MaxDistance * formatMaxValue, 0.5f * formatMaxValue);
+ const Vector2 decodeMAD(2.0f * sdf.MaxDistance / formatMaxValue, -sdf.MaxDistance);
+ int32 voxelSizeSum = voxelsSize;
+
+ // TODO: use optimized sparse storage for SDF data as hierarchical bricks as in papers below:
+ // https://graphics.pixar.com/library/IrradianceAtlas/paper.pdf
+ // http://maverick.inria.fr/Membres/Cyril.Crassin/thesis/CCrassinThesis_EN_Web.pdf
+ // http://ramakarl.com/pdfs/2016_Hoetzlein_GVDB.pdf
+ // https://www.cse.chalmers.se/~uffe/HighResolutionSparseVoxelDAGs.pdf
+
+ // Brute-force for each voxel to calculate distance to the closest triangle with point query and distance sign by raycasting around the voxel
+ const int32 sampleCount = 12;
+ Array sampleDirections;
+ sampleDirections.Resize(sampleCount);
+ {
+ RandomStream rand;
+ sampleDirections.Get()[0] = Vector3::Up;
+ sampleDirections.Get()[1] = Vector3::Down;
+ sampleDirections.Get()[2] = Vector3::Left;
+ sampleDirections.Get()[3] = Vector3::Right;
+ sampleDirections.Get()[4] = Vector3::Forward;
+ sampleDirections.Get()[5] = Vector3::Backward;
+ for (int32 i = 6; i < sampleCount; i++)
+ sampleDirections.Get()[i] = rand.GetUnitVector();
+ }
+ Function sdfJob = [&sdf, &resolution, &sampleDirections, &scene, &voxels, &xyzToLocalMul, &xyzToLocalAdd, &encodeMAD, &formatStride, &formatWrite](int32 z)
+ {
+ PROFILE_CPU_NAMED("Model SDF Job");
+ float hitDistance;
+ Vector3 hitNormal, hitPoint;
+ Triangle hitTriangle;
+ const int32 zAddress = resolution.Y * resolution.X * z;
+ for (int32 y = 0; y < resolution.Y; y++)
+ {
+ const int32 yAddress = resolution.X * y + zAddress;
+ for (int32 x = 0; x < resolution.X; x++)
+ {
+ float minDistance = sdf.MaxDistance;
+ Vector3 voxelPos = Vector3((float)x, (float)y, (float)z) * xyzToLocalMul + xyzToLocalAdd;
+
+ // Point query to find the distance to the closest surface
+ scene.PointQuery(voxelPos, minDistance, hitPoint, hitTriangle);
+
+ // Raycast samples around voxel to count triangle backfaces hit
+ int32 hitBackCount = 0, hitCount = 0;
+ for (int32 sample = 0; sample < sampleDirections.Count(); sample++)
+ {
+ Ray sampleRay(voxelPos, sampleDirections[sample]);
+ if (scene.RayCast(sampleRay, hitDistance, hitNormal, hitTriangle))
+ {
+ hitCount++;
+ const bool backHit = Vector3::Dot(sampleRay.Direction, hitTriangle.GetNormal()) > 0;
+ if (backHit)
+ hitBackCount++;
+ }
+ }
+
+ float distance = minDistance;
+ // TODO: surface thickness threshold? shift reduce distance for all voxels by something like 0.01 to enlarge thin geometry
+ //if ((float)hitBackCount > )hitCount * 0.3f && hitCount != 0)
+ if ((float)hitBackCount > (float)sampleDirections.Count() * 0.6f && hitCount != 0)
+ {
+ // Voxel is inside the geometry so turn it into negative distance to the surface
+ distance *= -1;
+ }
+ const int32 xAddress = x + yAddress;
+ formatWrite((byte*)voxels + xAddress * formatStride, distance * encodeMAD.X + encodeMAD.Y);
+ }
+ }
+ };
+ JobSystem::Execute(sdfJob, resolution.Z);
+
+ // Cache SDF data on a CPU
+ if (outputStream)
+ {
+ outputStream->WriteInt32(1); // Version
+ ModelSDFHeader data(sdf, textureDesc);
+ outputStream->Write(&data);
+ ModelSDFMip mipData(0, resolution.X * formatStride, voxelsSize);
+ outputStream->Write(&mipData);
+ outputStream->WriteBytes(voxels, voxelsSize);
+ }
+
+ // Upload data to the GPU
+ if (outputSDF)
+ {
+ BytesContainer data;
+ data.Link((byte*)voxels, voxelsSize);
+ auto task = outputSDF->Texture->UploadMipMapAsync(data, 0, resolution.X * formatStride, voxelsSize, true);
+ if (task)
+ task->Start();
+ }
+
+ // Generate mip maps
+ void* voxelsMip = nullptr;
+ for (int32 mipLevel = 1; mipLevel < mipCount; mipLevel++)
+ {
+ Int3 resolutionMip = Int3::Max(resolution / 2, Int3::One);
+ const int32 voxelsMipSize = resolutionMip.X * resolutionMip.Y * resolutionMip.Z * formatStride;
+ if (voxelsMip == nullptr)
+ voxelsMip = Allocator::Allocate(voxelsMipSize);
+
+ // Downscale mip
+ Function mipJob = [&voxelsMip, &voxels, &resolution, &resolutionMip, &encodeMAD, &decodeMAD, &formatStride, &formatRead, &formatWrite](int32 z)
+ {
+ PROFILE_CPU_NAMED("Model SDF Mip Job");
+ const int32 zAddress = resolutionMip.Y * resolutionMip.X * z;
+ for (int32 y = 0; y < resolutionMip.Y; y++)
+ {
+ const int32 yAddress = resolutionMip.X * y + zAddress;
+ for (int32 x = 0; x < resolutionMip.X; x++)
+ {
+ // Linear box filter around the voxel
+ // TODO: use min distance for nearby texels (texel distance + distance to texel)
+ float distance = 0;
+ for (int32 dz = 0; dz < 2; dz++)
+ {
+ const int32 dzAddress = (z * 2 + dz) * (resolution.Y * resolution.X);
+ for (int32 dy = 0; dy < 2; dy++)
+ {
+ const int32 dyAddress = (y * 2 + dy) * (resolution.X) + dzAddress;
+ for (int32 dx = 0; dx < 2; dx++)
+ {
+ const int32 dxAddress = (x * 2 + dx) + dyAddress;
+ const float d = formatRead((byte*)voxels + dxAddress * formatStride) * decodeMAD.X + decodeMAD.Y;
+ distance += d;
+ }
+ }
+ }
+ distance *= 1.0f / 8.0f;
+
+ const int32 xAddress = x + yAddress;
+ formatWrite((byte*)voxelsMip + xAddress * formatStride, distance * encodeMAD.X + encodeMAD.Y);
+ }
+ }
+ };
+ JobSystem::Execute(mipJob, resolutionMip.Z);
+
+ // Cache SDF data on a CPU
+ if (outputStream)
+ {
+ ModelSDFMip mipData(mipLevel, resolutionMip.X * formatStride, voxelsMipSize);
+ outputStream->Write(&mipData);
+ outputStream->WriteBytes(voxelsMip, voxelsMipSize);
+ }
+
+ // Upload to the GPU
+ if (outputSDF)
+ {
+ BytesContainer data;
+ data.Link((byte*)voxelsMip, voxelsMipSize);
+ auto task = outputSDF->Texture->UploadMipMapAsync(data, mipLevel, resolutionMip.X * formatStride, voxelsMipSize, true);
+ if (task)
+ task->Start();
+ }
+
+ // Go down
+ voxelSizeSum += voxelsSize;
+ Swap(voxelsMip, voxels);
+ resolution = resolutionMip;
+ }
+
+ Allocator::Free(voxelsMip);
+ Allocator::Free(voxels);
+
+#if !BUILD_RELEASE
+ auto endTime = Platform::GetTimeSeconds();
+ LOG(Info, "Generated SDF {}x{}x{} ({} kB) in {}ms for {}", resolution.X, resolution.Y, resolution.Z, voxelSizeSum / 1024, (int32)((endTime - startTime) * 1000.0), assetName);
+#endif
+ return false;
+}
+
+#if USE_EDITOR
void RemoveNamespace(String& name)
{
@@ -1308,3 +1611,5 @@ bool ModelTool::FindTexture(const String& sourcePath, const String& file, String
}
#endif
+
+#endif
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h
index 13351db29..8173df383 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.h
+++ b/Source/Engine/Tools/ModelTool/ModelTool.h
@@ -2,14 +2,17 @@
#pragma once
-#if COMPILE_WITH_MODEL_TOOL && USE_EDITOR
+#if COMPILE_WITH_MODEL_TOOL
#include "Engine/Core/Config.h"
+#include "Engine/Content/Assets/ModelBase.h"
+#if USE_EDITOR
#include "Engine/Serialization/ISerializable.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Graphics/Models/SkeletonData.h"
#include "Engine/Animations/AnimationData.h"
+class MemoryWriteStream;
class JsonWriter;
///
@@ -141,13 +144,52 @@ public:
}
};
+#endif
+
+struct ModelSDFHeader
+{
+ Vector3 LocalToUVWMul;
+ float WorldUnitsPerVoxel;
+ Vector3 LocalToUVWAdd;
+ float MaxDistance;
+ Vector3 LocalBoundsMin;
+ int32 MipLevels;
+ Vector3 LocalBoundsMax;
+ int32 Width;
+ int32 Height;
+ int32 Depth;
+ PixelFormat Format;
+ float ResolutionScale;
+ int32 LOD;
+
+ ModelSDFHeader() = default;
+ ModelSDFHeader(const ModelBase::SDFData& sdf, const struct GPUTextureDescription& desc);
+};
+
+struct ModelSDFMip
+{
+ int32 MipIndex;
+ uint32 RowPitch;
+ uint32 SlicePitch;
+
+ ModelSDFMip() = default;
+ ModelSDFMip(int32 mipIndex, uint32 rowPitch, uint32 slicePitch);
+ ModelSDFMip(int32 mipIndex, const TextureMipData& mip);
+};
+
///
-/// Import models and animations helper.
+/// Models data importing and processing utility.
///
class FLAXENGINE_API ModelTool
{
public:
+ // Optional: inputModel or modelData
+ // Optional: outputSDF or null, outputStream or null
+ static bool GenerateModelSDF(Model* inputModel, ModelData* modelData, float resolutionScale, int32 lodIndex, ModelBase::SDFData* outputSDF, MemoryWriteStream* outputStream, const StringView& assetName);
+
+#if USE_EDITOR
+public:
///
/// Declares the imported data type.
///
@@ -206,6 +248,10 @@ public:
bool ImportTextures = true;
bool RestoreMaterialsOnReimport = true;
+ // SDF
+ bool GenerateSDF = false;
+ float SDFResolution = 1.0f;
+
// Splitting
bool SplitObjects = false;
int32 ObjectIndex = -1;
@@ -283,6 +329,7 @@ private:
#if USE_OPEN_FBX
static bool ImportDataOpenFBX(const char* path, ImportedModelData& data, Options& options, String& errorMsg);
#endif
+#endif
};
#endif
diff --git a/Source/Engine/Tools/ModelTool/SpatialSort.cpp b/Source/Engine/Tools/ModelTool/SpatialSort.cpp
index 773455a7a..de13084dc 100644
--- a/Source/Engine/Tools/ModelTool/SpatialSort.cpp
+++ b/Source/Engine/Tools/ModelTool/SpatialSort.cpp
@@ -44,7 +44,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/** @file Implementation of the helper class to quickly find vertices close to a given position */
#include "SpatialSort.h"
-#if COMPILE_WITH_MODEL_TOOL
+#if COMPILE_WITH_MODEL_TOOL && USE_EDITOR
#include
using namespace Assimp;
diff --git a/Source/Engine/Tools/ModelTool/SpatialSort.h b/Source/Engine/Tools/ModelTool/SpatialSort.h
index 6a6371de7..34d716b70 100644
--- a/Source/Engine/Tools/ModelTool/SpatialSort.h
+++ b/Source/Engine/Tools/ModelTool/SpatialSort.h
@@ -43,7 +43,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
/** Small helper classes to optimise finding vertizes close to a given location */
-#ifndef AI_SPATIALSORT_H_INC
+#if !defined(AI_SPATIALSORT_H_INC) && COMPILE_WITH_MODEL_TOOL && USE_EDITOR
#define AI_SPATIALSORT_H_INC
#include
diff --git a/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.cpp b/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.cpp
index 040d862d6..4dd820853 100644
--- a/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.cpp
+++ b/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.cpp
@@ -1,6 +1,6 @@
// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
-#if COMPILE_WITH_MODEL_TOOL
+#if COMPILE_WITH_MODEL_TOOL && USE_EDITOR
#include "VertexTriangleAdjacency.h"
#include "Engine/Core/Math/Math.h"
diff --git a/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.h b/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.h
index 56e9cfd9c..c37d767ba 100644
--- a/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.h
+++ b/Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.h
@@ -2,7 +2,7 @@
#pragma once
-#if COMPILE_WITH_MODEL_TOOL
+#if COMPILE_WITH_MODEL_TOOL && USE_EDITOR
#include "Engine/Core/Config.h"
#include "Engine/Core/Types/BaseTypes.h"