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"