diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs index bbb384562..484efb1d9 100644 --- a/Source/Editor/Content/Import/ModelImportEntry.cs +++ b/Source/Editor/Content/Import/ModelImportEntry.cs @@ -12,13 +12,14 @@ namespace FlaxEngine.Tools { partial struct Options { - private bool ShowGeometry => Type == ModelTool.ModelType.Model || Type == ModelTool.ModelType.SkinnedModel; - private bool ShowModel => Type == ModelTool.ModelType.Model; - private bool ShowSkinnedModel => Type == ModelTool.ModelType.SkinnedModel; - private bool ShowAnimation => Type == ModelTool.ModelType.Animation; + private bool ShowGeometry => Type == ModelType.Model || Type == ModelType.SkinnedModel || Type == ModelType.Prefab; + private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab; + private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab; + private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab; private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals; private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents; - private bool ShowFramesRange => ShowAnimation && Duration == ModelTool.AnimationDuration.Custom; + private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom; + private bool ShowSplitting => Type != ModelType.Prefab; } } } diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index 0cf16850d..c3ac3b247 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -72,7 +72,10 @@ namespace FlaxEditor.Content { if (_preview == null) { - _preview = new ModelPreview(false); + _preview = new ModelPreview(false) + { + ScaleToFit = false, + }; InitAssetPreview(_preview); } @@ -91,6 +94,7 @@ namespace FlaxEditor.Content _preview.Model = (Model)request.Asset; _preview.Parent = guiRoot; _preview.SyncBackbufferSize(); + _preview.ViewportCamera.SetArcBallView(_preview.Model.GetBox()); _preview.Task.OnDraw(); } diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index 0c0fc6a51..e2cd1e771 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -24,6 +24,16 @@ namespace FlaxEditor.Content /// protected ContentFolder _folder; + /// + /// Whether this node can be deleted. + /// + public virtual bool CanDelete => true; + + /// + /// Whether this node can be duplicated. + /// + public virtual bool CanDuplicate => true; + /// /// Gets the content folder item. /// @@ -301,7 +311,7 @@ namespace FlaxEditor.Content StartRenaming(); return true; case KeyboardKeys.Delete: - if (Folder.Exists) + if (Folder.Exists && CanDelete) Editor.Instance.Windows.ContentWin.Delete(Folder); return true; } @@ -310,7 +320,7 @@ namespace FlaxEditor.Content switch (key) { case KeyboardKeys.D: - if (Folder.Exists) + if (Folder.Exists && CanDuplicate) Editor.Instance.Windows.ContentWin.Duplicate(Folder); return true; } diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index def873622..a4f24f4ab 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -12,6 +12,12 @@ namespace FlaxEditor.Content { private FileSystemWatcher _watcher; + /// + public override bool CanDelete => false; + + /// + public override bool CanDuplicate => false; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp index 4d4ea5e07..8fd87bcfd 100644 --- a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp +++ b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp @@ -1270,7 +1270,7 @@ bool CookAssetsStep::Perform(CookingData& data) { Array assetTypes; data.Stats.AssetStats.GetValues(assetTypes); - Sorting::QuickSort(assetTypes.Get(), assetTypes.Count()); + Sorting::QuickSort(assetTypes); LOG(Info, ""); LOG(Info, "Top assets types stats:"); diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index 845651bed..517a86df5 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -119,7 +119,7 @@ bool DeployDataStep::Perform(CookingData& data) if (!version.StartsWith(TEXT("8."))) // Check for major part of 8.0 version.Clear(); } - Sorting::QuickSort(versions.Get(), versions.Count()); + Sorting::QuickSort(versions); const String version = versions.Last(); FileSystem::NormalizePath(srcDotnet); LOG(Info, "Using .Net Runtime {} at {}", version, srcDotnet); diff --git a/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs new file mode 100644 index 000000000..0a42b952e --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.IO; +using FlaxEditor.Content; +using FlaxEditor.CustomEditors.Editors; +using FlaxEngine; +using FlaxEngine.GUI; +using FlaxEngine.Tools; + +namespace FlaxEditor.CustomEditors.Dedicated; + +/// +/// The missing script editor. +/// +[CustomEditor(typeof(ModelPrefab)), DefaultEditor] +public class ModelPrefabEditor : GenericEditor +{ + private Guid _prefabId; + private Button _reimportButton; + private string _importPath; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + var modelPrefab = Values[0] as ModelPrefab; + if (modelPrefab == null) + return; + _prefabId = modelPrefab.PrefabID; + while (true) + { + var prefab = FlaxEngine.Content.Load(_prefabId); + if (prefab) + { + var prefabObjectId = modelPrefab.PrefabObjectID; + var prefabObject = prefab.GetDefaultInstance(ref prefabObjectId); + if (prefabObject.PrefabID == _prefabId) + break; + _prefabId = prefabObject.PrefabID; + } + } + + var button = layout.Button("Reimport", "Reimports the source asset as prefab."); + _reimportButton = button.Button; + _reimportButton.Clicked += OnReimport; + } + + private void OnReimport() + { + var prefab = FlaxEngine.Content.Load(_prefabId); + var modelPrefab = (ModelPrefab)Values[0]; + var importPath = modelPrefab.ImportPath; + var editor = Editor.Instance; + if (editor.ContentImporting.GetReimportPath("Model Prefab", ref importPath)) + return; + var folder = editor.ContentDatabase.Find(Path.GetDirectoryName(prefab.Path)) as ContentFolder; + if (folder == null) + return; + var importOptions = modelPrefab.ImportOptions; + importOptions.Type = ModelTool.ModelType.Prefab; + _importPath = importPath; + _reimportButton.Enabled = false; + editor.ContentImporting.ImportFileEnd += OnImportFileEnd; + editor.ContentImporting.Import(importPath, folder, true, importOptions); + } + + private void OnImportFileEnd(IFileEntryAction entry, bool failed) + { + if (entry.SourceUrl == _importPath) + { + // Restore button + _importPath = null; + _reimportButton.Enabled = true; + Editor.Instance.ContentImporting.ImportFileEnd -= OnImportFileEnd; + } + } +} diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index d7bfbbad7..f783822a5 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -246,6 +246,7 @@ namespace FlaxEditor.CustomEditors.Dedicated var multiAction = new MultiUndoAction(actions); multiAction.Do(); var presenter = ScriptsEditor.Presenter; + ScriptsEditor.ParentEditor?.RebuildLayout(); if (presenter != null) { presenter.Undo.AddAction(multiAction); diff --git a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs index 9a09c4cc2..23b5832e5 100644 --- a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs +++ b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs @@ -73,7 +73,6 @@ namespace FlaxEditor.CustomEditors { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); - for (int i = 0; i < Count; i++) { var v = instanceValues[i]; diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 4a3fa39ba..741b89d2d 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -201,7 +201,21 @@ namespace FlaxEditor.Gizmo ActorNode prefabRoot = GetPrefabRootInParent(actorNode); if (prefabRoot != null && actorNode != prefabRoot) { - hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot); + bool isPrefabInSelection = false; + foreach (var e in sceneEditing.Selection) + { + if (e is ActorNode ae && GetPrefabRootInParent(ae) == prefabRoot) + { + isPrefabInSelection = true; + break; + } + } + + // Skip selecting prefab root if we already had object from that prefab selected + if (!isPrefabInSelection) + { + hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot); + } } } diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index 969608676..8855ba58a 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -769,7 +769,7 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String // Get options from model FileSystem::NormalizePath(assetPath); - return ImportModelFile::TryGetImportOptions(assetPath, options); + return ImportModel::TryGetImportOptions(assetPath, options); } bool ManagedEditor::Import(const String& inputPath, const String& outputPath, const AudioTool::Options& options) diff --git a/Source/Editor/Modules/ContentImportingModule.cs b/Source/Editor/Modules/ContentImportingModule.cs index 85dd50d35..587d3a6c6 100644 --- a/Source/Editor/Modules/ContentImportingModule.cs +++ b/Source/Editor/Modules/ContentImportingModule.cs @@ -126,29 +126,35 @@ namespace FlaxEditor.Modules { if (item != null && !item.GetImportPath(out string importPath)) { - // Check if input file is missing - if (!System.IO.File.Exists(importPath)) - { - Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", item.Path, importPath)); - if (skipSettingsDialog) - return; - - // Ask user to select new file location - var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, item.ShortName); - if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files)) - return; - if (files != null && files.Length > 0) - importPath = files[0]; - - // Validate file path again - if (!System.IO.File.Exists(importPath)) - return; - } - + if (GetReimportPath(item.ShortName, ref importPath, skipSettingsDialog)) + return; Import(importPath, item.Path, true, skipSettingsDialog, settings); } } + internal bool GetReimportPath(string contextName, ref string importPath, bool skipSettingsDialog = false) + { + // Check if input file is missing + if (!System.IO.File.Exists(importPath)) + { + Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", contextName, importPath)); + if (skipSettingsDialog) + return true; + + // Ask user to select new file location + var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, contextName); + if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files)) + return true; + if (files != null && files.Length > 0) + importPath = files[0]; + + // Validate file path again + if (!System.IO.File.Exists(importPath)) + return true; + } + return false; + } + /// /// Imports the specified files. /// diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index ed6fc08d6..97cc505b4 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -31,13 +31,26 @@ namespace FlaxEditor.SceneGraph.Actors // Rotate to match the space (GUI uses upper left corner as a root) Actor.LocalOrientation = Quaternion.Euler(0, -180, -180); - var uiControl = new UIControl + bool canSpawn = true; + foreach (var uiControl in Actor.GetChildren()) { - Name = "Canvas Scalar", - Transform = Actor.Transform, - Control = new CanvasScaler() - }; - Root.Spawn(uiControl, Actor); + if (uiControl.Get() == null) + continue; + canSpawn = false; + break; + } + + if (canSpawn) + { + var uiControl = new UIControl + { + Name = "Canvas Scalar", + Transform = Actor.Transform, + Control = new CanvasScaler() + }; + Root.Spawn(uiControl, Actor); + } + _treeNode.Expand(); } /// diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index f64e46385..14f11ceef 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -85,12 +85,20 @@ namespace FlaxEditor.SceneGraph.GUI { if (Parent is ActorTreeNode parent) { - for (int i = 0; i < parent.ChildrenCount; i++) + var anyChanged = false; + var children = parent.Children; + for (int i = 0; i < children.Count; i++) { - if (parent.Children[i] is ActorTreeNode child && child.Actor) - child._orderInParent = child.Actor.OrderInParent; + if (children[i] is ActorTreeNode child && child.Actor) + { + var orderInParent = child.Actor.OrderInParent; + anyChanged |= child._orderInParent != orderInParent; + if (anyChanged) + child._orderInParent = orderInParent; + } } - parent.SortChildren(); + if (anyChanged) + parent.SortChildren(); } else if (Actor) { diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index 621c7c25e..9d4367303 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -1335,6 +1335,7 @@ namespace FlaxEditor.Surface.Archetypes Surface?.AddBatchedUndoAction(action); action.Do(); Surface?.OnNodesConnected(this, other); + Surface?.MarkAsEdited(); } } @@ -1911,6 +1912,7 @@ namespace FlaxEditor.Surface.Archetypes { var action = new StateMachineStateBase.AddRemoveTransitionAction(this); SourceState.Surface?.AddBatchedUndoAction(action); + SourceState.Surface?.MarkAsEdited(); action.Do(); } diff --git a/Source/Editor/Surface/Elements/FloatValue.cs b/Source/Editor/Surface/Elements/FloatValue.cs index 7674c68b9..56539e3e3 100644 --- a/Source/Editor/Surface/Elements/FloatValue.cs +++ b/Source/Editor/Surface/Elements/FloatValue.cs @@ -33,6 +33,10 @@ namespace FlaxEditor.Surface.Elements Archetype = archetype; ParentNode.ValuesChanged += OnNodeValuesChanged; + + // Disable slider if surface doesn't allow it + if (!ParentNode.Surface.CanLivePreviewValueChanges) + _slideSpeed = 0.0f; } private void OnNodeValuesChanged() diff --git a/Source/Editor/Surface/MaterialSurface.cs b/Source/Editor/Surface/MaterialSurface.cs index 08e1161d9..ba7f2e01e 100644 --- a/Source/Editor/Surface/MaterialSurface.cs +++ b/Source/Editor/Surface/MaterialSurface.cs @@ -22,6 +22,9 @@ namespace FlaxEditor.Surface { } + /// + public override bool CanLivePreviewValueChanges => false; + /// public override string GetTypeName(ScriptType type) { diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index ec878a407..419c2d468 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -533,6 +533,7 @@ namespace FlaxEditor.Surface UpdateSelectionRectangle(); } } + bool showPrimaryMenu = false; if (_rightMouseDown && button == MouseButton.Right) { _rightMouseDown = false; @@ -546,8 +547,7 @@ namespace FlaxEditor.Surface _cmStartPos = location; if (controlUnderMouse == null) { - // Show primary context menu - ShowPrimaryMenu(_cmStartPos); + showPrimaryMenu = true; } } _mouseMoveAmount = 0; @@ -573,8 +573,13 @@ namespace FlaxEditor.Surface return true; } + // If none of the child controls handled this show the primary context menu + if (showPrimaryMenu) + { + ShowPrimaryMenu(_cmStartPos); + } // Letting go of a connection or right clicking while creating a connection - if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened) + else if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened) { _cmStartPos = location; Cursor = CursorType.Default; diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 0185e364a..3ac702549 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -534,6 +534,11 @@ namespace FlaxEditor.Surface /// public virtual bool CanSetParameters => false; + /// + /// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight. + /// + public virtual bool CanLivePreviewValueChanges => true; + /// /// Determines whether the specified node archetype can be used in the surface. /// diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index c49392d01..9ac9b5571 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -1374,8 +1374,8 @@ namespace FlaxEditor.Viewport ivp.Invert(); // Create near and far points - var nearPoint = new Vector3(mousePosition, 0.0f); - var farPoint = new Vector3(mousePosition, 1.0f); + var nearPoint = new Vector3(mousePosition, _nearPlane); + var farPoint = new Vector3(mousePosition, _farPlane); viewport.Unproject(ref nearPoint, ref ivp, out nearPoint); viewport.Unproject(ref farPoint, ref ivp, out farPoint); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index a8d9ae1be..be8f09f3a 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -428,11 +428,9 @@ namespace FlaxEditor.Windows.Assets private void Update(ActorNode actorNode) { - if (actorNode.Actor) - { - actorNode.TreeNode.UpdateText(); - actorNode.TreeNode.OnOrderInParentChanged(); - } + actorNode.TreeNode.UpdateText(); + if (actorNode.TreeNode.IsCollapsed) + return; for (int i = 0; i < actorNode.ChildNodes.Count; i++) { diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index f50a832a1..025760b1e 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -440,6 +440,7 @@ namespace FlaxEditor.Windows.Assets { try { + FlaxEngine.Profiler.BeginEvent("PrefabWindow.Update"); if (Graph.Main != null) { // Due to fact that actors in prefab editor are only created but not added to gameplay @@ -468,6 +469,10 @@ namespace FlaxEditor.Windows.Assets Graph.Root.TreeNode.ExpandAll(true); } } + finally + { + FlaxEngine.Profiler.EndEvent(); + } // Auto fit if (_focusCamera && _viewport.Task.FrameCount > 1) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 168067977..dda812f84 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -114,18 +114,32 @@ namespace FlaxEditor.Windows } } - cm.AddButton("Delete", () => Delete(item)); + if (isFolder && folder.Node is MainContentTreeNode) + { + cm.AddSeparator(); + } + else + { + cm.AddButton("Delete", () => Delete(item)); - cm.AddSeparator(); + cm.AddSeparator(); - cm.AddButton("Duplicate", _view.Duplicate); + cm.AddButton("Duplicate", _view.Duplicate); - cm.AddButton("Copy", _view.Copy); + cm.AddButton("Copy", _view.Copy); + } b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); - cm.AddButton("Rename", () => Rename(item)); + if (isFolder && folder.Node is MainContentTreeNode) + { + // Do nothing + } + else + { + cm.AddButton("Rename", () => Rename(item)); + } // Custom options ContextMenuShow?.Invoke(cm, item); diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs index 372505795..f43fb0342 100644 --- a/Source/Editor/Windows/PluginsWindow.cs +++ b/Source/Editor/Windows/PluginsWindow.cs @@ -391,7 +391,7 @@ namespace FlaxEditor.Windows } Editor.Log("Plugin project has been cloned."); - + try { // Start git submodule clone @@ -412,24 +412,28 @@ namespace FlaxEditor.Windows } // Find project config file. Could be different then what the user named the folder. - var files = Directory.GetFiles(clonePath); string pluginProjectName = ""; - foreach (var file in files) + foreach (var file in Directory.GetFiles(clonePath)) { if (file.Contains(".flaxproj", StringComparison.OrdinalIgnoreCase)) { pluginProjectName = Path.GetFileNameWithoutExtension(file); - Debug.Log(pluginProjectName); + break; } } - if (string.IsNullOrEmpty(pluginProjectName)) - Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually."); - else { - await AddReferenceToProject(pluginName, pluginProjectName); - MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); + Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually."); + return; } + + await AddModuleReferencesInGameModule(clonePath); + await AddReferenceToProject(pluginName, pluginProjectName); + + if (Editor.Options.Options.SourceCode.AutoGenerateScriptsProjectFiles) + Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); + + MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } private void OnAddButtonClicked() @@ -749,6 +753,37 @@ namespace FlaxEditor.Windows MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } + private async Task AddModuleReferencesInGameModule(string pluginFolderPath) + { + // Common game build script location + var gameScript = Path.Combine(Globals.ProjectFolder, "Source/Game/Game.Build.cs"); + if (File.Exists(gameScript)) + { + var gameScriptContents = await File.ReadAllTextAsync(gameScript); + var insertLocation = gameScriptContents.IndexOf("base.Setup(options);", StringComparison.Ordinal); + if (insertLocation != -1) + { + insertLocation += 20; + var modifiedAny = false; + + // Find all code modules in a plugin to auto-reference them in game build script + foreach (var subDir in Directory.GetDirectories(Path.Combine(pluginFolderPath, "Source"))) + { + var pluginModuleName = Path.GetFileName(subDir); + var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs"); + if (File.Exists(pluginModuleScriptPath)) + { + gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");"); + modifiedAny = true; + } + } + + if (modifiedAny) + await File.WriteAllTextAsync(gameScript, gameScriptContents, Encoding.UTF8); + } + } + } + private async Task AddReferenceToProject(string pluginFolderName, string pluginName) { // Project flax config file diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index fb49a007e..42b12d911 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -624,8 +624,10 @@ void BehaviorTreeLoopDecorator::PostUpdate(const BehaviorUpdateContext& context, if (result == BehaviorUpdateResult::Success) { auto state = GetState(context.Memory); - state->Loops--; - if (state->Loops > 0) + if (!InfiniteLoop) + state->Loops--; + + if (state->Loops > 0 || InfiniteLoop) { // Keep running in a loop but reset node's state (preserve self state) result = BehaviorUpdateResult::Running; diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h index d72bdee04..b92f0f0d9 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.h +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -305,12 +305,16 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeLoopDecorator : public Behavi DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeLoopDecorator, BehaviorTreeDecorator); API_AUTO_SERIALIZATION(); - // Amount of times to execute the node. Unused if LoopCountSelector is used. - API_FIELD(Attributes="EditorOrder(10), Limit(0)") + // Is the loop infinite (until failed)? + API_FIELD(Attributes = "EditorOrder(10)") + bool InfiniteLoop = false; + + // Amount of times to execute the node. Unused if LoopCountSelector is used or if InfiniteLoop is used. + API_FIELD(Attributes="EditorOrder(20), Limit(0), VisibleIf(nameof(InfiniteLoop), true)") int32 LoopCount = 3; - // Amount of times to execute the node from behavior's knowledge (blackboard, goal or sensor). If set, overrides LoopCount. - API_FIELD(Attributes="EditorOrder(20)") + // Amount of times to execute the node from behavior's knowledge (blackboard, goal or sensor). If set, overrides LoopCount. Unused if InfiniteLoop is used. + API_FIELD(Attributes="EditorOrder(30), VisibleIf(nameof(InfiniteLoop), true)") BehaviorKnowledgeSelector LoopCountSelector; public: diff --git a/Source/Engine/Animations/AnimEvent.h b/Source/Engine/Animations/AnimEvent.h index 4ed7eab48..6cf54459f 100644 --- a/Source/Engine/Animations/AnimEvent.h +++ b/Source/Engine/Animations/AnimEvent.h @@ -17,6 +17,11 @@ API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public SerializableScriptin { DECLARE_SCRIPTING_TYPE(AnimEvent); + /// + /// Indicates whether the event can be executed in async from a thread that updates the animated model. Otherwise, event execution will be delayed until the sync point of the animated model and called from the main thread. Async events need to precisely handle data access, especially when it comes to editing scene objects with multi-threading. + /// + API_FIELD(Attributes="HideInEditor, NoSerialize") bool Async = false; + #if USE_EDITOR /// /// Event display color in the Editor. diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h index e382ef6e3..c69de8afa 100644 --- a/Source/Engine/Animations/AnimationData.h +++ b/Source/Engine/Animations/AnimationData.h @@ -98,7 +98,6 @@ public: /// struct AnimationData { -public: /// /// The duration of the animation (in frames). /// @@ -114,6 +113,11 @@ public: /// bool EnableRootMotion = false; + /// + /// The animation name. + /// + String Name; + /// /// The custom node name to be used as a root motion source. If not specified the actual root node will be used. /// @@ -131,14 +135,14 @@ public: FORCE_INLINE float GetLength() const { #if BUILD_DEBUG - ASSERT(FramesPerSecond != 0); + ASSERT(FramesPerSecond > ZeroTolerance); #endif return static_cast(Duration / FramesPerSecond); } uint64 GetMemoryUsage() const { - uint64 result = RootNodeName.Length() * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData); + uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData); for (const auto& e : Channels) result += e.GetMemoryUsage(); return result; @@ -151,9 +155,7 @@ public: { int32 result = 0; for (int32 i = 0; i < Channels.Count(); i++) - { result += Channels[i].GetKeyframesCount(); - } return result; } @@ -166,6 +168,7 @@ public: ::Swap(Duration, other.Duration); ::Swap(FramesPerSecond, other.FramesPerSecond); ::Swap(EnableRootMotion, other.EnableRootMotion); + ::Swap(Name, other.Name); ::Swap(RootNodeName, other.RootNodeName); Channels.Swap(other.Channels); } @@ -175,6 +178,7 @@ public: /// void Dispose() { + Name.Clear(); Duration = 0.0; FramesPerSecond = 0.0; RootNodeName.Clear(); diff --git a/Source/Engine/Animations/Animations.cpp b/Source/Engine/Animations/Animations.cpp index 6599bebac..e571af162 100644 --- a/Source/Engine/Animations/Animations.cpp +++ b/Source/Engine/Animations/Animations.cpp @@ -146,6 +146,7 @@ void AnimationsSystem::PostExecute(TaskGraph* graph) auto animatedModel = AnimationManagerInstance.UpdateList[index]; if (CanUpdateModel(animatedModel)) { + animatedModel->GraphInstance.InvokeAnimEvents(); animatedModel->OnAnimationUpdated_Sync(); } } diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 71c9534e0..5aa720d31 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -38,22 +38,16 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32 void AnimGraphInstanceData::Clear() { - Version = 0; - LastUpdateTime = -1; - CurrentFrame = 0; - RootTransform = Transform::Identity; - RootMotion = Transform::Identity; + ClearState(); Parameters.Resize(0); - State.Resize(0); - NodesPose.Resize(0); - Slots.Resize(0); - for (const auto& e : Events) - ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f); - Events.Resize(0); } void AnimGraphInstanceData::ClearState() { + for (const auto& e : ActiveEvents) + OutgoingEvents.Add(e.End((AnimatedModel*)Object)); + ActiveEvents.Clear(); + InvokeAnimEvents(); Version = 0; LastUpdateTime = -1; CurrentFrame = 0; @@ -62,9 +56,6 @@ void AnimGraphInstanceData::ClearState() State.Resize(0); NodesPose.Resize(0); Slots.Clear(); - for (const auto& e : Events) - ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f); - Events.Clear(); } void AnimGraphInstanceData::Invalidate() @@ -73,6 +64,43 @@ void AnimGraphInstanceData::Invalidate() CurrentFrame = 0; } +void AnimGraphInstanceData::InvokeAnimEvents() +{ + const bool all = IsInMainThread(); + for (int32 i = 0; i < OutgoingEvents.Count(); i++) + { + const OutgoingEvent e = OutgoingEvents[i]; + if (all || e.Instance->Async) + { + OutgoingEvents.RemoveAtKeepOrder(i); + switch (e.Type) + { + case OutgoingEvent::OnEvent: + e.Instance->OnEvent(e.Actor, e.Anim, e.Time, e.DeltaTime); + break; + case OutgoingEvent::OnBegin: + ((AnimContinuousEvent*)e.Instance)->OnBegin(e.Actor, e.Anim, e.Time, e.DeltaTime); + break; + case OutgoingEvent::OnEnd: + ((AnimContinuousEvent*)e.Instance)->OnEnd(e.Actor, e.Anim, e.Time, e.DeltaTime); + break; + } + } + } +} + +AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(AnimatedModel* actor) const +{ + OutgoingEvent out; + out.Instance = Instance; + out.Actor = actor; + out.Anim = Anim; + out.Time = 0.0f; + out.DeltaTime = 0.0f; + out.Type = OutgoingEvent::OnEnd; + return out; +} + AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor) { auto& context = AnimGraphExecutor::Context.Get(); @@ -208,7 +236,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) // Initialize buckets ResetBuckets(context, &_graph); } - for (auto& e : data.Events) + for (auto& e : data.ActiveEvents) e.Hit = false; // Init empty nodes data @@ -240,16 +268,17 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) if (animResult == nullptr) animResult = GetEmptyNodes(); } - if (data.Events.Count() != 0) + if (data.ActiveEvents.Count() != 0) { ANIM_GRAPH_PROFILE_EVENT("Events"); - for (int32 i = data.Events.Count() - 1; i >= 0; i--) + for (int32 i = data.ActiveEvents.Count() - 1; i >= 0; i--) { - const auto& e = data.Events[i]; + const auto& e = data.ActiveEvents[i]; if (!e.Hit) { - ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)context.Data->Object, e.Anim, 0.0f, 0.0f); - data.Events.RemoveAt(i); + // Remove active event that was not hit during this frame (eg. animation using it was not used in blending) + data.OutgoingEvents.Add(e.End((AnimatedModel*)context.Data->Object)); + data.ActiveEvents.RemoveAt(i); } } } @@ -284,7 +313,6 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, i); targetNodes[i] = node; } - } } @@ -319,6 +347,9 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) data.RootMotion = animResult->RootMotion; } + // Invoke any async anim events + context.Data->InvokeAnimEvents(); + // Cleanup context.Data = nullptr; } diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index 41edb72f3..afaa4441b 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -23,6 +23,9 @@ class AnimSubGraph; class AnimGraphBase; class AnimGraphNode; class AnimGraphExecutor; +class AnimatedModel; +class AnimEvent; +class AnimContinuousEvent; class SkinnedModel; class SkeletonData; @@ -349,16 +352,40 @@ public: /// void Invalidate(); + /// + /// Invokes any outgoing AnimEvent and AnimContinuousEvent collected during the last animation update. When called from non-main thread only Async events will be invoked. + /// + void InvokeAnimEvents(); + private: - struct Event + struct OutgoingEvent { + enum Types + { + OnEvent, + OnBegin, + OnEnd, + }; + AnimEvent* Instance; + AnimatedModel* Actor; + Animation* Anim; + float Time, DeltaTime; + Types Type; + }; + + struct ActiveEvent + { + AnimContinuousEvent* Instance; Animation* Anim; AnimGraphNode* Node; bool Hit; + + OutgoingEvent End(AnimatedModel* actor) const; }; - Array> Events; + Array> ActiveEvents; + Array> OutgoingEvents; }; /// @@ -441,7 +468,7 @@ public: /// The invalid transition valid used in Transitions to indicate invalid transition linkage. /// const static uint16 InvalidTransitionIndex = MAX_uint16; - + /// /// The outgoing transitions from this state to the other states. Each array item contains index of the transition data from the state node graph transitions cache. Value InvalidTransitionIndex is used for last transition to indicate the transitions amount. /// diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 6f0d7661b..d514a928c 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -100,48 +100,50 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float if (!k.Value.Instance) continue; const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f; +#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type }) if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration) { int32 stateIndex = -1; if (duration > 1) { // Begin for continuous event - for (stateIndex = 0; stateIndex < context.Data->Events.Count(); stateIndex++) + for (stateIndex = 0; stateIndex < context.Data->ActiveEvents.Count(); stateIndex++) { - const auto& e = context.Data->Events[stateIndex]; + const auto& e = context.Data->ActiveEvents[stateIndex]; if (e.Instance == k.Value.Instance && e.Node == node) break; } - if (stateIndex == context.Data->Events.Count()) + if (stateIndex == context.Data->ActiveEvents.Count()) { - auto& e = context.Data->Events.AddOne(); - e.Instance = k.Value.Instance; + ASSERT(k.Value.Instance->Is()); + auto& e = context.Data->ActiveEvents.AddOne(); + e.Instance = (AnimContinuousEvent*)k.Value.Instance; e.Anim = anim; e.Node = node; - ASSERT(k.Value.Instance->Is()); - ((AnimContinuousEvent*)k.Value.Instance)->OnBegin((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime); + ADD_OUTGOING_EVENT(OnBegin); } } // Event - k.Value.Instance->OnEvent((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime); + ADD_OUTGOING_EVENT(OnEvent); if (stateIndex != -1) - context.Data->Events[stateIndex].Hit = true; + context.Data->ActiveEvents[stateIndex].Hit = true; } else if (duration > 1) { // End for continuous event - for (int32 i = 0; i < context.Data->Events.Count(); i++) + for (int32 i = 0; i < context.Data->ActiveEvents.Count(); i++) { - const auto& e = context.Data->Events[i]; + const auto& e = context.Data->ActiveEvents[i]; if (e.Instance == k.Value.Instance && e.Node == node) { - ((AnimContinuousEvent*)k.Value.Instance)->OnEnd((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime); - context.Data->Events.RemoveAt(i); + ADD_OUTGOING_EVENT(OnEnd); + context.Data->ActiveEvents.RemoveAt(i); break; } } } +#undef ADD_OUTGOING_EVENT } } } @@ -1070,15 +1072,33 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu const auto nodes = node->GetNodes(this); const auto nodesA = static_cast(valueA.AsPointer); const auto nodesB = static_cast(valueB.AsPointer); + const auto& baseNodes = _graph.BaseModel.Get()->GetNodes(); Transform t, tA, tB; for (int32 i = 0; i < nodes->Nodes.Count(); i++) { tA = nodesA->Nodes[i]; tB = nodesB->Nodes[i]; - t.Translation = tA.Translation + tB.Translation; - t.Orientation = tA.Orientation * tB.Orientation; - t.Scale = tA.Scale * tB.Scale; + const auto& baseNode = baseNodes[i]; + + t.Translation = tA.Translation + (tB.Translation - baseNode.LocalTransform.Translation) * alpha; + + //auto baseOrientation = tA.Orientation; + //Quaternion additiveOrientation = alpha * (tB.Orientation - baseNode.LocalTransform.Orientation); + //t.Orientation = baseOrientation + additiveOrientation; + auto m1 = Matrix::RotationQuaternion(tA.Orientation); + auto m2 = Matrix::RotationQuaternion(alpha * tB.Orientation); + auto m3 = Matrix::RotationQuaternion(alpha * baseNode.LocalTransform.Orientation); + Matrix m4; + Matrix::Subtract(m2, m3, m4); + Matrix m5; + Matrix::Add(m1, m4, m5); + + t.SetRotation(m5); t.Orientation.Normalize(); + + t.Scale = tA.Scale * tB.Scale; + + //nodes->Nodes[i] = t; Transform::Lerp(tA, t, alpha, nodes->Nodes[i]); } Transform::Lerp(nodesA->RootMotion, nodesA->RootMotion + nodesB->RootMotion, alpha, nodes->RootMotion); diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index 691b00a50..4eee62126 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -34,7 +34,6 @@ #define CHECK_INVALID_BUFFER(model, buffer) \ if (buffer->IsValidFor(model) == false) \ { \ - LOG(Warning, "Invalid Model Instance Buffer size {0} for Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \ buffer->Setup(model); \ } diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index b823db5a3..5e94eac49 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -23,7 +23,6 @@ #define CHECK_INVALID_BUFFER(model, buffer) \ if (buffer->IsValidFor(model) == false) \ { \ - LOG(Warning, "Invalid Skinned Model Instance Buffer size {0} for Skinned Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \ buffer->Setup(model); \ } diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index a47e0bd0e..b632fc6d3 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -562,6 +562,7 @@ bool FlaxStorage::Reload() { if (!IsLoaded()) return false; + PROFILE_CPU(); OnReloading(this); @@ -728,7 +729,11 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId) } // Close file - CloseFileHandles(); + if (CloseFileHandles()) + { + LOG(Error, "Cannot close file access for '{}'", _path); + return true; + } // Change ID // TODO: here we could extend it and load assets from the storage and call asset ID change event to change references @@ -776,6 +781,8 @@ FlaxChunk* FlaxStorage::AllocateChunk() bool FlaxStorage::Create(const StringView& path, const AssetInitData* data, int32 dataCount, bool silentMode, const CustomData* customData) { + PROFILE_CPU(); + ZoneText(*path, path.Length()); LOG(Info, "Creating package at \'{0}\'. Silent Mode: {1}", path, silentMode); // Prepare to have access to the file @@ -1296,7 +1303,7 @@ FileReadStream* FlaxStorage::OpenFile() return stream; } -void FlaxStorage::CloseFileHandles() +bool FlaxStorage::CloseFileHandles() { // Note: this is usually called by the content manager when this file is not used or on exit // In those situations all the async tasks using this storage should be cancelled externally @@ -1323,10 +1330,12 @@ void FlaxStorage::CloseFileHandles() waitTime = 100; while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) Platform::Sleep(1); - ASSERT(_chunksLock == 0); + if (Platform::AtomicRead(&_chunksLock) != 0) + return true; // Failed, someone is still accessing the file // Close file handles (from all threads) _file.DeleteAll(); + return false; } void FlaxStorage::Dispose() @@ -1335,7 +1344,10 @@ void FlaxStorage::Dispose() return; // Close file - CloseFileHandles(); + if (CloseFileHandles()) + { + LOG(Error, "Cannot close file access for '{}'", _path); + } // Release data _chunks.ClearDelete(); diff --git a/Source/Engine/Content/Storage/FlaxStorage.h b/Source/Engine/Content/Storage/FlaxStorage.h index 85c9db072..77c912c5a 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.h +++ b/Source/Engine/Content/Storage/FlaxStorage.h @@ -405,7 +405,7 @@ public: /// /// Closes the file handles (it can be modified from the outside). /// - void CloseFileHandles(); + bool CloseFileHandles(); /// /// Releases storage resources and closes handle to the file. diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index cb366ffb3..a0b28f4f8 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -15,7 +15,7 @@ #include "Engine/Platform/Platform.h" #include "Engine/Engine/Globals.h" #include "ImportTexture.h" -#include "ImportModelFile.h" +#include "ImportModel.h" #include "ImportAudio.h" #include "ImportShader.h" #include "ImportFont.h" @@ -165,20 +165,7 @@ bool CreateAssetContext::AllocateChunk(int32 index) void CreateAssetContext::AddMeta(JsonWriter& writer) const { writer.JKEY("ImportPath"); - if (AssetsImportingManager::UseImportPathRelative && !FileSystem::IsRelative(InputPath) -#if PLATFORM_WINDOWS - // Import path from other drive should be stored as absolute on Windows to prevent issues - && InputPath.Length() > 2 && Globals::ProjectFolder.Length() > 2 && InputPath[0] == Globals::ProjectFolder[0] -#endif - ) - { - const String relativePath = FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, InputPath); - writer.String(relativePath); - } - else - { - writer.String(InputPath); - } + writer.String(AssetsImportingManager::GetImportPath(InputPath)); writer.JKEY("ImportUsername"); writer.String(Platform::GetUserName()); } @@ -189,7 +176,12 @@ void CreateAssetContext::ApplyChanges() auto storage = ContentStorageManager::TryGetStorage(TargetAssetPath); if (storage && storage->IsLoaded()) { - storage->CloseFileHandles(); + if (storage->CloseFileHandles()) + { + LOG(Error, "Cannot close file access for '{}'", TargetAssetPath); + _applyChangesResult = CreateAssetResult::CannotSaveFile; + return; + } } // Move file @@ -304,8 +296,24 @@ bool AssetsImportingManager::ImportIfEdited(const StringView& inputPath, const S return false; } +String AssetsImportingManager::GetImportPath(const String& path) +{ + if (UseImportPathRelative && !FileSystem::IsRelative(path) +#if PLATFORM_WINDOWS + // Import path from other drive should be stored as absolute on Windows to prevent issues + && path.Length() > 2 && Globals::ProjectFolder.Length() > 2 && path[0] == Globals::ProjectFolder[0] +#endif + ) + { + return FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, path); + } + return path; +} + bool AssetsImportingManager::Create(const Function& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg) { + PROFILE_CPU(); + ZoneText(*outputPath, outputPath.Length()); const auto startTime = Platform::GetTimeSeconds(); // Pick ID if not specified @@ -369,7 +377,7 @@ bool AssetsImportingManager::Create(const FunctionRegisterAsset(context.Data.Header, outputPath); + Content::GetRegistry()->RegisterAsset(context.Data.Header, context.TargetAssetPath); // Done const auto endTime = Platform::GetTimeSeconds(); @@ -380,7 +388,7 @@ bool AssetsImportingManager::Create(const FunctionGetEntriesCount() == 1 + && ( + (tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4) + || + (tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) + || + (tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) + )) + { + // Check import meta + rapidjson_flax::Document metadata; + metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length()); + if (metadata.HasParseError() == false) + { + options.Deserialize(metadata, nullptr); + return true; + } + } + } + return false; +} + +struct PrefabObject +{ + int32 NodeIndex; + String Name; + String AssetPath; +}; + +void RepackMeshLightmapUVs(ModelData& data) +{ + // Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart + int32 lodIndex = 0; + auto& lod = data.LODs[lodIndex]; + + // Build list of meshes with their area + struct LightmapUVsPack : RectPack + { + LightmapUVsPack(float x, float y, float width, float height) + : RectPack(x, y, width, height) + { + } + + void OnInsert() + { + } + }; + struct MeshEntry + { + MeshData* Mesh; + float Area; + float Size; + LightmapUVsPack* Slot; + }; + Array entries; + entries.Resize(lod.Meshes.Count()); + float areaSum = 0; + for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++) + { + auto& entry = entries[meshIndex]; + entry.Mesh = lod.Meshes[meshIndex]; + entry.Area = entry.Mesh->CalculateTrianglesArea(); + entry.Size = Math::Sqrt(entry.Area); + areaSum += entry.Area; + } + + if (areaSum > ZeroTolerance) + { + // Pack all surfaces into atlas + float atlasSize = Math::Sqrt(areaSum) * 1.02f; + int32 triesLeft = 10; + while (triesLeft--) + { + bool failed = false; + const float chartsPadding = (4.0f / 256.0f) * atlasSize; + LightmapUVsPack root(chartsPadding, chartsPadding, atlasSize - chartsPadding, atlasSize - chartsPadding); + for (auto& entry : entries) + { + entry.Slot = root.Insert(entry.Size, entry.Size, chartsPadding); + if (entry.Slot == nullptr) + { + // Failed to insert surface, increase atlas size and try again + atlasSize *= 1.5f; + failed = true; + break; + } + } + + if (!failed) + { + // Transform meshes lightmap UVs into the slots in the whole atlas + const float atlasSizeInv = 1.0f / atlasSize; + for (const auto& entry : entries) + { + Float2 uvOffset(entry.Slot->X * atlasSizeInv, entry.Slot->Y * atlasSizeInv); + Float2 uvScale((entry.Slot->Width - chartsPadding) * atlasSizeInv, (entry.Slot->Height - chartsPadding) * atlasSizeInv); + // TODO: SIMD + for (auto& uv : entry.Mesh->LightmapUVs) + { + uv = uv * uvScale + uvOffset; + } + } + break; + } + } + } +} + +void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData) +{ + // Skip if file is missing + if (!FileSystem::FileExists(context.TargetAssetPath)) + return; + + // Try to load asset that gets reimported + AssetReference asset = Content::LoadAsync(context.TargetAssetPath); + if (asset == nullptr) + return; + if (asset->WaitForLoaded()) + return; + + // Get model object + ModelBase* model = nullptr; + if (asset.Get()->GetTypeName() == Model::TypeName) + { + model = ((Model*)asset.Get()); + } + else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName) + { + model = ((SkinnedModel*)asset.Get()); + } + if (!model) + return; + + // Peek materials + for (int32 i = 0; i < modelData.Materials.Count(); i++) + { + auto& dstSlot = modelData.Materials[i]; + + if (model->MaterialSlots.Count() > i) + { + auto& srcSlot = model->MaterialSlots[i]; + + dstSlot.Name = srcSlot.Name; + dstSlot.ShadowsMode = srcSlot.ShadowsMode; + dstSlot.AssetID = srcSlot.Material.GetID(); + } + } +} + +void SetupMaterialSlots(ModelData& data, const Array& materials) +{ + Array materialSlotsTable; + materialSlotsTable.Resize(materials.Count()); + materialSlotsTable.SetAll(-1); + for (auto& lod : data.LODs) + { + for (MeshData* mesh : lod.Meshes) + { + int32 newSlotIndex = materialSlotsTable[mesh->MaterialSlotIndex]; + if (newSlotIndex == -1) + { + newSlotIndex = data.Materials.Count(); + data.Materials.AddOne() = materials[mesh->MaterialSlotIndex]; + } + mesh->MaterialSlotIndex = newSlotIndex; + } + } +} + +bool SortMeshGroups(IGrouping const& i1, IGrouping const& i2) +{ + return i1.GetKey().Compare(i2.GetKey()) < 0; +} + +CreateAssetResult ImportModel::Import(CreateAssetContext& context) +{ + // Get import options + Options options; + if (context.CustomArg != nullptr) + { + // Copy import options from argument + options = *static_cast(context.CustomArg); + } + else + { + // Restore the previous settings or use default ones + if (!TryGetImportOptions(context.TargetAssetPath, options)) + { + LOG(Warning, "Missing model import options. Using default values."); + } + } + + // Import model file + ModelData* data = options.Cached ? options.Cached->Data : nullptr; + ModelData dataThis; + Array>* meshesByNamePtr = options.Cached ? (Array>*)options.Cached->MeshesByName : nullptr; + Array> meshesByNameThis; + String autoImportOutput; + if (!data) + { + String errorMsg; + autoImportOutput = StringUtils::GetDirectoryName(context.TargetAssetPath); + autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); + if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput)) + { + LOG(Error, "Cannot import model file. {0}", errorMsg); + return CreateAssetResult::Error; + } + data = &dataThis; + + // Group meshes by the name (the same mesh name can be used by multiple meshes that use different materials) + if (data->LODs.Count() != 0) + { + const Function f = [](MeshData* const& x) -> StringView + { + return x->Name; + }; + ArrayExtensions::GroupBy(data->LODs[0].Meshes, f, meshesByNameThis); + Sorting::QuickSort(meshesByNameThis.Get(), meshesByNameThis.Count(), &SortMeshGroups); + } + meshesByNamePtr = &meshesByNameThis; + } + Array>& meshesByName = *meshesByNamePtr; + + // Import objects from file separately + ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr }; + Array prefabObjects; + if (options.Type == ModelTool::ModelType::Prefab) + { + // Normalize options + options.SplitObjects = false; + options.ObjectIndex = -1; + + // Import all of the objects recursive but use current model data to skip loading file again + options.Cached = &cached; + Function splitImport = [&context, &autoImportOutput](Options& splitOptions, const StringView& objectName, String& outputPath) + { + // Recursive importing of the split object + String postFix = objectName; + const int32 splitPos = postFix.FindLast(TEXT('|')); + if (splitPos != -1) + postFix = postFix.Substring(splitPos + 1); + // TODO: check for name collisions with material/texture assets + outputPath = autoImportOutput / String(StringUtils::GetFileNameWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); + splitOptions.SubAssetFolder = TEXT(" "); // Use the same folder as asset as they all are imported to the subdir for the prefab (see SubAssetFolder usage above) + return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); + }; + auto splitOptions = options; + LOG(Info, "Splitting imported {0} meshes", meshesByName.Count()); + PrefabObject prefabObject; + for (int32 groupIndex = 0; groupIndex < meshesByName.Count(); groupIndex++) + { + auto& group = meshesByName[groupIndex]; + + // Cache object options (nested sub-object import removes the meshes) + prefabObject.NodeIndex = group.First()->NodeIndex; + prefabObject.Name = group.First()->Name; + + splitOptions.Type = ModelTool::ModelType::Model; + splitOptions.ObjectIndex = groupIndex; + if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath)) + { + prefabObjects.Add(prefabObject); + } + } + LOG(Info, "Splitting imported {0} animations", data->Animations.Count()); + for (int32 i = 0; i < data->Animations.Count(); i++) + { + auto& animation = data->Animations[i]; + splitOptions.Type = ModelTool::ModelType::Animation; + splitOptions.ObjectIndex = i; + splitImport(splitOptions, animation.Name, prefabObject.AssetPath); + } + } + else if (options.SplitObjects) + { + // Import the first object within this call + options.SplitObjects = false; + options.ObjectIndex = 0; + + // Import rest of the objects recursive but use current model data to skip loading file again + options.Cached = &cached; + Function splitImport; + splitImport.Bind([&context](Options& splitOptions, const StringView& objectName) + { + // Recursive importing of the split object + String postFix = objectName; + const int32 splitPos = postFix.FindLast(TEXT('|')); + if (splitPos != -1) + postFix = postFix.Substring(splitPos + 1); + const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); + return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); + }); + auto splitOptions = options; + switch (options.Type) + { + case ModelTool::ModelType::Model: + case ModelTool::ModelType::SkinnedModel: + LOG(Info, "Splitting imported {0} meshes", meshesByName.Count()); + for (int32 groupIndex = 1; groupIndex < meshesByName.Count(); groupIndex++) + { + auto& group = meshesByName[groupIndex]; + splitOptions.ObjectIndex = groupIndex; + splitImport(splitOptions, group.GetKey()); + } + break; + case ModelTool::ModelType::Animation: + LOG(Info, "Splitting imported {0} animations", data->Animations.Count()); + for (int32 i = 1; i < data->Animations.Count(); i++) + { + auto& animation = data->Animations[i]; + splitOptions.ObjectIndex = i; + splitImport(splitOptions, animation.Name); + } + break; + } + } + + // When importing a single object as model asset then select a specific mesh group + Array meshesToDelete; + if (options.ObjectIndex >= 0 && + options.ObjectIndex < meshesByName.Count() && + (options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel)) + { + auto& group = meshesByName[options.ObjectIndex]; + if (&dataThis == data) + { + // Use meshes only from the the grouping (others will be removed manually) + { + auto& lod = dataThis.LODs[0]; + meshesToDelete.Add(lod.Meshes); + lod.Meshes.Clear(); + for (MeshData* mesh : group) + { + lod.Meshes.Add(mesh); + meshesToDelete.Remove(mesh); + } + } + for (int32 lodIndex = 1; lodIndex < dataThis.LODs.Count(); lodIndex++) + { + auto& lod = dataThis.LODs[lodIndex]; + Array lodMeshes = lod.Meshes; + lod.Meshes.Clear(); + for (MeshData* lodMesh : lodMeshes) + { + if (lodMesh->Name == group.GetKey()) + lod.Meshes.Add(lodMesh); + else + meshesToDelete.Add(lodMesh); + } + } + + // Use only materials references by meshes from the first grouping + { + auto materials = dataThis.Materials; + dataThis.Materials.Clear(); + SetupMaterialSlots(dataThis, materials); + } + } + else + { + // Copy data from others data + dataThis.Skeleton = data->Skeleton; + dataThis.Nodes = data->Nodes; + + // Move meshes from this group (including any LODs of them) + { + auto& lod = dataThis.LODs.AddOne(); + lod.ScreenSize = data->LODs[0].ScreenSize; + lod.Meshes.Add(group); + for (MeshData* mesh : group) + data->LODs[0].Meshes.Remove(mesh); + } + for (int32 lodIndex = 1; lodIndex < data->LODs.Count(); lodIndex++) + { + Array lodMeshes = data->LODs[lodIndex].Meshes; + for (int32 i = lodMeshes.Count() - 1; i >= 0; i--) + { + MeshData* lodMesh = lodMeshes[i]; + if (lodMesh->Name == group.GetKey()) + data->LODs[lodIndex].Meshes.Remove(lodMesh); + else + lodMeshes.RemoveAtKeepOrder(i); + } + if (lodMeshes.Count() == 0) + break; // No meshes of that name in this LOD so skip further ones + auto& lod = dataThis.LODs.AddOne(); + lod.ScreenSize = data->LODs[lodIndex].ScreenSize; + lod.Meshes.Add(lodMeshes); + } + + // Copy materials used by the meshes + SetupMaterialSlots(dataThis, data->Materials); + } + data = &dataThis; + } + + // Check if restore materials on model reimport + if (options.RestoreMaterialsOnReimport && data->Materials.HasItems()) + { + TryRestoreMaterials(context, *data); + } + + // When using generated lightmap UVs those coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space + // Model importer generates UVs in [0-1] space for each mesh so now we need to pack them inside the whole model (only when using multiple meshes) + if (options.Type == ModelTool::ModelType::Model && options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data->LODs.HasItems() && data->LODs[0].Meshes.Count() > 1) + { + RepackMeshLightmapUVs(*data); + } + + // Create destination asset type + CreateAssetResult result = CreateAssetResult::InvalidTypeID; + switch (options.Type) + { + case ModelTool::ModelType::Model: + result = CreateModel(context, *data, &options); + break; + case ModelTool::ModelType::SkinnedModel: + result = CreateSkinnedModel(context, *data, &options); + break; + case ModelTool::ModelType::Animation: + result = CreateAnimation(context, *data, &options); + break; + case ModelTool::ModelType::Prefab: + result = CreatePrefab(context, *data, options, prefabObjects); + break; + } + for (auto mesh : meshesToDelete) + Delete(mesh); + if (result != CreateAssetResult::Ok) + return result; + + // Create json with import context + rapidjson_flax::StringBuffer importOptionsMetaBuffer; + importOptionsMetaBuffer.Reserve(256); + CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer); + JsonWriter& importOptionsMeta = importOptionsMetaObj; + importOptionsMeta.StartObject(); + { + context.AddMeta(importOptionsMeta); + options.Serialize(importOptionsMeta, nullptr); + } + importOptionsMeta.EndObject(); + context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize()); + + return CreateAssetResult::Ok; +} + +CreateAssetResult ImportModel::Create(CreateAssetContext& context) +{ + ASSERT(context.CustomArg != nullptr); + auto& modelData = *(ModelData*)context.CustomArg; + + // Ensure model has any meshes + if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty())) + { + LOG(Warning, "Models has no valid meshes"); + return CreateAssetResult::Error; + } + + // Auto calculate LODs transition settings + modelData.CalculateLODsScreenSizes(); + + return CreateModel(context, modelData); +} + +CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options) +{ + PROFILE_CPU(); + IMPORT_SETUP(Model, Model::SerializedVersion); + + // Save model header + MemoryWriteStream stream(4096); + if (modelData.Pack2ModelHeader(&stream)) + return CreateAssetResult::Error; + if (context.AllocateChunk(0)) + return CreateAssetResult::CannotAllocateChunk; + context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); + + // Pack model LODs data + const auto lodCount = modelData.GetLODsCount(); + for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) + { + stream.SetPosition(0); + + // Pack meshes + auto& meshes = modelData.LODs[lodIndex].Meshes; + for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) + { + if (meshes[meshIndex]->Pack2Model(&stream)) + { + LOG(Warning, "Cannot pack mesh."); + return CreateAssetResult::Error; + } + } + + const int32 chunkIndex = lodIndex + 1; + if (context.AllocateChunk(chunkIndex)) + return CreateAssetResult::CannotAllocateChunk; + 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 ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options) +{ + PROFILE_CPU(); + IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion); + + // Save skinned model header + MemoryWriteStream stream(4096); + if (modelData.Pack2SkinnedModelHeader(&stream)) + return CreateAssetResult::Error; + if (context.AllocateChunk(0)) + return CreateAssetResult::CannotAllocateChunk; + context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); + + // Pack model LODs data + const auto lodCount = modelData.GetLODsCount(); + for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) + { + stream.SetPosition(0); + + // Mesh Data Version + stream.WriteByte(1); + + // Pack meshes + auto& meshes = modelData.LODs[lodIndex].Meshes; + for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) + { + if (meshes[meshIndex]->Pack2SkinnedModel(&stream)) + { + LOG(Warning, "Cannot pack mesh."); + return CreateAssetResult::Error; + } + } + + const int32 chunkIndex = lodIndex + 1; + if (context.AllocateChunk(chunkIndex)) + return CreateAssetResult::CannotAllocateChunk; + context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition()); + } + + return CreateAssetResult::Ok; +} + +CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options) +{ + PROFILE_CPU(); + IMPORT_SETUP(Animation, Animation::SerializedVersion); + + // Save animation data + MemoryWriteStream stream(8182); + const int32 animIndex = options && options->ObjectIndex != -1 ? options->ObjectIndex : 0; // Single animation per asset + if (modelData.Pack2AnimationHeader(&stream, animIndex)) + return CreateAssetResult::Error; + if (context.AllocateChunk(0)) + return CreateAssetResult::CannotAllocateChunk; + context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); + + return CreateAssetResult::Ok; +} + +CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects) +{ + PROFILE_CPU(); + if (data.Nodes.Count() == 0) + return CreateAssetResult::Error; + + // If that prefab already exists then we need to use it as base to preserve object IDs and local changes applied by user + const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + DEFAULT_PREFAB_EXTENSION_DOT; + auto* prefab = FileSystem::FileExists(outputPath) ? Content::Load(outputPath) : nullptr; + if (prefab) + { + // Ensure that prefab has Default Instance so ObjectsCache is valid (used below) + prefab->GetDefaultInstance(); + } + + // Create prefab structure + Dictionary nodeToActor; + Array nodeActors; + Actor* rootActor = nullptr; + for (int32 nodeIndex = 0; nodeIndex < data.Nodes.Count(); nodeIndex++) + { + const auto& node = data.Nodes[nodeIndex]; + + // Create actor(s) for this node + nodeActors.Clear(); + for (const PrefabObject& e : prefabObjects) + { + if (e.NodeIndex == nodeIndex) + { + auto* actor = New(); + actor->SetName(e.Name); + if (auto* model = Content::LoadAsync(e.AssetPath)) + { + actor->Model = model; + } + nodeActors.Add(actor); + } + } + Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New(); + if (nodeActors.Count() > 1) + { + for (Actor* e : nodeActors) + { + e->SetParent(nodeActor); + } + } + if (nodeActors.Count() != 1) + { + // Include default actor to iterate over it properly in code below + nodeActors.Add(nodeActor); + } + + // Setup node in hierarchy + nodeToActor.Add(nodeIndex, nodeActor); + nodeActor->SetName(node.Name); + nodeActor->SetLocalTransform(node.LocalTransform); + if (nodeIndex == 0) + { + // Special case for root actor to link any unlinked nodes + nodeToActor.Add(-1, nodeActor); + rootActor = nodeActor; + } + else + { + Actor* parentActor; + if (nodeToActor.TryGet(node.ParentIndex, parentActor)) + nodeActor->SetParent(parentActor); + } + + // Link with object from prefab (if reimporting) + if (prefab) + { + for (Actor* a : nodeActors) + { + for (const auto& i : prefab->ObjectsCache) + { + if (i.Value->GetTypeHandle() != a->GetTypeHandle()) // Type match + continue; + auto* o = (Actor*)i.Value; + if (o->GetName() != a->GetName()) // Name match + continue; + + // Mark as this object already exists in prefab so will be preserved when updating it + a->LinkPrefab(o->GetPrefabID(), o->GetPrefabObjectID()); + break; + } + } + } + } + ASSERT_LOW_LAYER(rootActor); + { + // Add script with import options + auto* modelPrefabScript = New(); + modelPrefabScript->SetParent(rootActor); + modelPrefabScript->ImportPath = AssetsImportingManager::GetImportPath(context.InputPath); + modelPrefabScript->ImportOptions = options; + + // Link with existing prefab instance + if (prefab) + { + for (const auto& i : prefab->ObjectsCache) + { + if (i.Value->GetTypeHandle() == modelPrefabScript->GetTypeHandle()) + { + modelPrefabScript->LinkPrefab(i.Value->GetPrefabID(), i.Value->GetPrefabObjectID()); + break; + } + } + } + } + + // Create prefab instead of native asset + bool failed; + if (prefab) + { + failed = prefab->ApplyAll(rootActor); + } + else + { + failed = PrefabManager::CreatePrefab(rootActor, outputPath, false); + } + + // Cleanup objects from memory + rootActor->DeleteObjectNow(); + + if (failed) + return CreateAssetResult::Error; + return CreateAssetResult::Skip; +} + +#endif diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h index 02e6bfc8d..710971fca 100644 --- a/Source/Engine/ContentImporters/ImportModel.h +++ b/Source/Engine/ContentImporters/ImportModel.h @@ -8,15 +8,10 @@ #include "Engine/Tools/ModelTool/ModelTool.h" -/// -/// Enable/disable caching model import options -/// -#define IMPORT_MODEL_CACHE_OPTIONS 1 - /// /// Importing models utility /// -class ImportModelFile +class ImportModel { public: typedef ModelTool::Options Options; @@ -45,9 +40,10 @@ public: static CreateAssetResult Create(CreateAssetContext& context); private: - 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); + static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreateSkinnedModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects); }; #endif diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp deleted file mode 100644 index b34bbfaaf..000000000 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#include "ImportModel.h" - -#if COMPILE_WITH_ASSETS_IMPORTER - -#include "Engine/Core/Log.h" -#include "Engine/Serialization/MemoryWriteStream.h" -#include "Engine/Serialization/JsonWriters.h" -#include "Engine/Graphics/Models/ModelData.h" -#include "Engine/Content/Assets/Model.h" -#include "Engine/Content/Assets/SkinnedModel.h" -#include "Engine/Content/Storage/ContentStorageManager.h" -#include "Engine/Content/Assets/Animation.h" -#include "Engine/Content/Content.h" -#include "Engine/Platform/FileSystem.h" -#include "AssetsImportingManager.h" - -bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& options) -{ -#if IMPORT_MODEL_CACHE_OPTIONS - if (FileSystem::FileExists(path)) - { - // Try to load asset file and asset info - auto tmpFile = ContentStorageManager::GetStorage(path); - AssetInitData data; - if (tmpFile - && tmpFile->GetEntriesCount() == 1 - && ( - (tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4) - || - (tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) - || - (tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) - )) - { - // Check import meta - rapidjson_flax::Document metadata; - metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length()); - if (metadata.HasParseError() == false) - { - options.Deserialize(metadata, nullptr); - return true; - } - } - } -#endif - return false; -} - -void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData) -{ - // Skip if file is missing - if (!FileSystem::FileExists(context.TargetAssetPath)) - return; - - // Try to load asset that gets reimported - AssetReference asset = Content::LoadAsync(context.TargetAssetPath); - if (asset == nullptr) - return; - if (asset->WaitForLoaded()) - return; - - // Get model object - ModelBase* model = nullptr; - if (asset.Get()->GetTypeName() == Model::TypeName) - { - model = ((Model*)asset.Get()); - } - else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName) - { - model = ((SkinnedModel*)asset.Get()); - } - if (!model) - return; - - // Peek materials - for (int32 i = 0; i < modelData.Materials.Count(); i++) - { - auto& dstSlot = modelData.Materials[i]; - - if (model->MaterialSlots.Count() > i) - { - auto& srcSlot = model->MaterialSlots[i]; - - dstSlot.Name = srcSlot.Name; - dstSlot.ShadowsMode = srcSlot.ShadowsMode; - dstSlot.AssetID = srcSlot.Material.GetID(); - } - } -} - -CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) -{ - // Get import options - Options options; - if (context.CustomArg != nullptr) - { - // Copy import options from argument - options = *static_cast(context.CustomArg); - } - else - { - // Restore the previous settings or use default ones - if (!TryGetImportOptions(context.TargetAssetPath, options)) - { - LOG(Warning, "Missing model import options. Using default values."); - } - } - if (options.SplitObjects) - { - options.OnSplitImport.Bind([&context](Options& splitOptions, const String& objectName) - { - // Recursive importing of the split object - String postFix = objectName; - const int32 splitPos = postFix.FindLast(TEXT('|')); - if (splitPos != -1) - postFix = postFix.Substring(splitPos + 1); - const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); - return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); - }); - } - - // Import model file - ModelData modelData; - String errorMsg; - String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); - autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); - if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput)) - { - LOG(Error, "Cannot import model file. {0}", errorMsg); - return CreateAssetResult::Error; - } - - // Check if restore materials on model reimport - if (options.RestoreMaterialsOnReimport && modelData.Materials.HasItems()) - { - TryRestoreMaterials(context, modelData); - } - - // Auto calculate LODs transition settings - modelData.CalculateLODsScreenSizes(); - - // Create destination asset type - CreateAssetResult result = CreateAssetResult::InvalidTypeID; - switch (options.Type) - { - case ModelTool::ModelType::Model: - result = ImportModel(context, modelData, &options); - break; - case ModelTool::ModelType::SkinnedModel: - result = ImportSkinnedModel(context, modelData, &options); - break; - case ModelTool::ModelType::Animation: - result = ImportAnimation(context, modelData, &options); - break; - } - if (result != CreateAssetResult::Ok) - return result; - -#if IMPORT_MODEL_CACHE_OPTIONS - // Create json with import context - rapidjson_flax::StringBuffer importOptionsMetaBuffer; - importOptionsMetaBuffer.Reserve(256); - CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer); - JsonWriter& importOptionsMeta = importOptionsMetaObj; - importOptionsMeta.StartObject(); - { - context.AddMeta(importOptionsMeta); - options.Serialize(importOptionsMeta, nullptr); - } - importOptionsMeta.EndObject(); - context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize()); -#endif - - return CreateAssetResult::Ok; -} - -CreateAssetResult ImportModelFile::Create(CreateAssetContext& context) -{ - ASSERT(context.CustomArg != nullptr); - auto& modelData = *(ModelData*)context.CustomArg; - - // Ensure model has any meshes - if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty())) - { - LOG(Warning, "Models has no valid meshes"); - return CreateAssetResult::Error; - } - - // Auto calculate LODs transition settings - modelData.CalculateLODsScreenSizes(); - - // Import - return ImportModel(context, modelData); -} - -CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options) -{ - // Base - IMPORT_SETUP(Model, Model::SerializedVersion); - - // Save model header - MemoryWriteStream stream(4096); - if (modelData.Pack2ModelHeader(&stream)) - return CreateAssetResult::Error; - if (context.AllocateChunk(0)) - return CreateAssetResult::CannotAllocateChunk; - context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); - - // Pack model LODs data - const auto lodCount = modelData.GetLODsCount(); - for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) - { - stream.SetPosition(0); - - // Pack meshes - auto& meshes = modelData.LODs[lodIndex].Meshes; - for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) - { - if (meshes[meshIndex]->Pack2Model(&stream)) - { - LOG(Warning, "Cannot pack mesh."); - return CreateAssetResult::Error; - } - } - - const int32 chunkIndex = lodIndex + 1; - if (context.AllocateChunk(chunkIndex)) - return CreateAssetResult::CannotAllocateChunk; - 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, const Options* options) -{ - // Base - IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion); - - // Save skinned model header - MemoryWriteStream stream(4096); - if (modelData.Pack2SkinnedModelHeader(&stream)) - return CreateAssetResult::Error; - if (context.AllocateChunk(0)) - return CreateAssetResult::CannotAllocateChunk; - context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); - - // Pack model LODs data - const auto lodCount = modelData.GetLODsCount(); - for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) - { - stream.SetPosition(0); - - // Mesh Data Version - stream.WriteByte(1); - - // Pack meshes - auto& meshes = modelData.LODs[lodIndex].Meshes; - for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) - { - if (meshes[meshIndex]->Pack2SkinnedModel(&stream)) - { - LOG(Warning, "Cannot pack mesh."); - return CreateAssetResult::Error; - } - } - - const int32 chunkIndex = lodIndex + 1; - if (context.AllocateChunk(chunkIndex)) - return CreateAssetResult::CannotAllocateChunk; - context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition()); - } - - return CreateAssetResult::Ok; -} - -CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options) -{ - // Base - IMPORT_SETUP(Animation, Animation::SerializedVersion); - - // Save animation data - MemoryWriteStream stream(8182); - if (modelData.Pack2AnimationHeader(&stream)) - return CreateAssetResult::Error; - if (context.AllocateChunk(0)) - return CreateAssetResult::CannotAllocateChunk; - context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); - - return CreateAssetResult::Ok; -} - -#endif diff --git a/Source/Engine/ContentImporters/ImportModelFile.h b/Source/Engine/ContentImporters/ImportModelFile.h deleted file mode 100644 index a40109601..000000000 --- a/Source/Engine/ContentImporters/ImportModelFile.h +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#pragma once - -#include "Types.h" - -#if COMPILE_WITH_ASSETS_IMPORTER - -#include "Engine/Content/Assets/Model.h" -#include "Engine/Tools/ModelTool/ModelTool.h" - -/// -/// Enable/disable caching model import options -/// -#define IMPORT_MODEL_CACHE_OPTIONS 1 - -/// -/// Importing models utility -/// -class ImportModelFile -{ -public: - typedef ModelTool::Options Options; - -public: - /// - /// Tries the get model import options from the target location asset. - /// - /// The asset path. - /// The options. - /// True if success, otherwise false. - static bool TryGetImportOptions(String path, Options& options); - - /// - /// Imports the model file. - /// - /// The importing context. - /// Result. - static CreateAssetResult Import(CreateAssetContext& context); - - /// - /// Creates the model asset from the ModelData storage (input argument should be pointer to ModelData). - /// - /// The importing context. - /// Result. - static CreateAssetResult Create(CreateAssetContext& context); - -private: - static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData); - static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData); - static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData); -}; - -#endif diff --git a/Source/Engine/ContentImporters/Types.h b/Source/Engine/ContentImporters/Types.h index 4ebf55583..388c533a8 100644 --- a/Source/Engine/ContentImporters/Types.h +++ b/Source/Engine/ContentImporters/Types.h @@ -18,7 +18,7 @@ class CreateAssetContext; /// /// Create/Import new asset callback result /// -DECLARE_ENUM_7(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID); +DECLARE_ENUM_8(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID, Skip); /// /// Create/Import new asset callback function diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h index 58117cf0a..cf45ed060 100644 --- a/Source/Engine/Core/Collections/Array.h +++ b/Source/Engine/Core/Collections/Array.h @@ -25,6 +25,19 @@ private: int32 _capacity; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromCount, int32 fromCapacity) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromCapacity); + Memory::MoveItems(to.Get(), from.Get(), fromCount); + Memory::DestructItems(from.Get(), fromCount); + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -134,7 +147,7 @@ public: _capacity = other._capacity; other._count = 0; other._capacity = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _count, _capacity); } /// @@ -191,7 +204,7 @@ public: _capacity = other._capacity; other._count = 0; other._capacity = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _count, _capacity); } return *this; } @@ -713,9 +726,18 @@ public: /// The other collection. void Swap(Array& other) { - ::Swap(_count, other._count); - ::Swap(_capacity, other._capacity); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + _allocation.Swap(other._allocation); + ::Swap(_count, other._count); + ::Swap(_capacity, other._capacity); + } + else + { + Array tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } /// @@ -726,9 +748,7 @@ public: T* data = _allocation.Get(); const int32 count = _count / 2; for (int32 i = 0; i < count; i++) - { ::Swap(data[i], data[_count - i - 1]); - } } public: diff --git a/Source/Engine/Core/Collections/ArrayExtensions.h b/Source/Engine/Core/Collections/ArrayExtensions.h index 2ae6da9c8..99d454906 100644 --- a/Source/Engine/Core/Collections/ArrayExtensions.h +++ b/Source/Engine/Core/Collections/ArrayExtensions.h @@ -55,9 +55,7 @@ public: for (int32 i = 0; i < obj.Count(); i++) { if (predicate(obj[i])) - { return i; - } } return INVALID_INDEX; } @@ -74,9 +72,7 @@ public: for (int32 i = 0; i < obj.Count(); i++) { if (predicate(obj[i])) - { return true; - } } return false; } @@ -93,13 +89,101 @@ public: for (int32 i = 0; i < obj.Count(); i++) { if (!predicate(obj[i])) - { return false; - } } return true; } + /// + /// Filters a sequence of values based on a predicate. + /// + /// The target collection. + /// The prediction function. Return true for elements that should be included in result list. + /// The result list with items that passed the predicate. + template + static void Where(const Array& obj, const Function& predicate, Array& result) + { + for (const T& i : obj) + { + if (predicate(i)) + result.Add(i); + } + } + + /// + /// Filters a sequence of values based on a predicate. + /// + /// The target collection. + /// The prediction function. Return true for elements that should be included in result list. + /// The result list with items that passed the predicate. + template + static Array Where(const Array& obj, const Function& predicate) + { + Array result; + Where(obj, predicate, result); + return result; + } + + /// + /// Projects each element of a sequence into a new form. + /// + /// The target collection. + /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element. + /// The result list whose elements are the result of invoking the transform function on each element of source. + template + static void Select(const Array& obj, const Function& selector, Array& result) + { + for (const TSource& i : obj) + result.Add(MoveTemp(selector(i))); + } + + /// + /// Projects each element of a sequence into a new form. + /// + /// The target collection. + /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element. + /// The result list whose elements are the result of invoking the transform function on each element of source. + template + static Array Select(const Array& obj, const Function& selector) + { + Array result; + Select(obj, selector, result); + return result; + } + + /// + /// Removes all the elements that match the conditions defined by the specified predicate. + /// + /// The target collection to modify. + /// A transform function that defines the conditions of the elements to remove. + template + static void RemoveAll(Array& obj, const Function& predicate) + { + for (int32 i = obj.Count() - 1; i >= 0; i--) + { + if (predicate(obj[i])) + obj.RemoveAtKeepOrder(i); + } + } + + /// + /// Removes all the elements that match the conditions defined by the specified predicate. + /// + /// The target collection to process. + /// A transform function that defines the conditions of the elements to remove. + /// The result list whose elements are the result of invoking the transform function on each element of source. + template + static Array RemoveAll(const Array& obj, const Function& predicate) + { + Array result; + for (const T& i : obj) + { + if (!predicate(i)) + result.Ass(i); + } + return result; + } + /// /// Groups the elements of a sequence according to a specified key selector function. /// @@ -109,7 +193,7 @@ public: template static void GroupBy(const Array& obj, const Function& keySelector, Array, AllocationType>& result) { - Dictionary> data(static_cast(obj.Count() * 3.0f)); + Dictionary> data; for (int32 i = 0; i < obj.Count(); i++) { const TKey key = keySelector(obj[i]); diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 4d65a4123..dd73be390 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -110,6 +110,33 @@ private: int32 _size = 0; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromSize); + Bucket* toData = to.Get(); + Bucket* fromData = from.Get(); + for (int32 i = 0; i < fromSize; i++) + { + Bucket& fromBucket = fromData[i]; + if (fromBucket.IsOccupied()) + { + Bucket& toBucket = toData[i]; + Memory::MoveItems(&toBucket.Key, &fromBucket.Key, 1); + Memory::MoveItems(&toBucket.Value, &fromBucket.Value, 1); + toBucket._state = Bucket::Occupied; + Memory::DestructItem(&fromBucket.Key); + Memory::DestructItem(&fromBucket.Value); + fromBucket._state = Bucket::Empty; + } + } + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -139,7 +166,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } /// @@ -180,7 +207,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } return *this; } @@ -510,7 +537,7 @@ public: return; ASSERT(capacity >= 0); AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; @@ -580,10 +607,19 @@ public: /// The other collection. void Swap(Dictionary& other) { - ::Swap(_elementsCount, other._elementsCount); - ::Swap(_deletedCount, other._deletedCount); - ::Swap(_size, other._size); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + else + { + Dictionary tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } public: @@ -930,7 +966,7 @@ private: { // Rebuild entire table completely AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); _allocation.Allocate(_size); Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index 4a4f3e924..a683edf15 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -93,6 +93,31 @@ private: int32 _size = 0; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromSize); + Bucket* toData = to.Get(); + Bucket* fromData = from.Get(); + for (int32 i = 0; i < fromSize; i++) + { + Bucket& fromBucket = fromData[i]; + if (fromBucket.IsOccupied()) + { + Bucket& toBucket = toData[i]; + Memory::MoveItems(&toBucket.Item, &fromBucket.Item, 1); + toBucket._state = Bucket::Occupied; + Memory::DestructItem(&fromBucket.Item); + fromBucket._state = Bucket::Empty; + } + } + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -122,7 +147,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } /// @@ -163,7 +188,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } return *this; } @@ -389,7 +414,7 @@ public: return; ASSERT(capacity >= 0); AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; @@ -458,10 +483,19 @@ public: /// The other collection. void Swap(HashSet& other) { - ::Swap(_elementsCount, other._elementsCount); - ::Swap(_deletedCount, other._deletedCount); - ::Swap(_size, other._size); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + else + { + HashSet tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } public: @@ -726,7 +760,7 @@ private: { // Rebuild entire table completely AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); _allocation.Allocate(_size); Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) diff --git a/Source/Engine/Core/Collections/Sorting.h b/Source/Engine/Core/Collections/Sorting.h index 2210f6f1c..67b639c80 100644 --- a/Source/Engine/Core/Collections/Sorting.h +++ b/Source/Engine/Core/Collections/Sorting.h @@ -45,6 +45,16 @@ public: }; public: + /// + /// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection). + /// + /// The data container. + template + FORCE_INLINE static void QuickSort(Array& data) + { + QuickSort(data.Get(), data.Count()); + } + /// /// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection). /// diff --git a/Source/Engine/Core/Compiler.h b/Source/Engine/Core/Compiler.h index 9a33b8758..4ea246077 100644 --- a/Source/Engine/Core/Compiler.h +++ b/Source/Engine/Core/Compiler.h @@ -93,3 +93,10 @@ #endif #define PACK_STRUCT(__Declaration__) PACK_BEGIN() __Declaration__ PACK_END() + +// C++ 17 +#if __cplusplus >= 201703L +#define IF_CONSTEXPR constexpr +#else +#define IF_CONSTEXPR +#endif diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp index bb8d42aba..b6b4fa3e2 100644 --- a/Source/Engine/Core/Log.cpp +++ b/Source/Engine/Core/Log.cpp @@ -58,7 +58,7 @@ bool Log::Logger::Init() int32 remaining = oldLogs.Count() - maxLogFiles + 1; if (remaining > 0) { - Sorting::QuickSort(oldLogs.Get(), oldLogs.Count()); + Sorting::QuickSort(oldLogs); // Delete the oldest logs int32 i = 0; diff --git a/Source/Engine/Core/Math/Vector2.cpp b/Source/Engine/Core/Math/Vector2.cpp index ec168de33..63e6fc4c5 100644 --- a/Source/Engine/Core/Math/Vector2.cpp +++ b/Source/Engine/Core/Math/Vector2.cpp @@ -15,6 +15,8 @@ const Float2 Float2::Zero(0.0f); template<> const Float2 Float2::One(1.0f); template<> +const Float2 Float2::Half(0.5f); +template<> const Float2 Float2::UnitX(1.0f, 0.0f); template<> const Float2 Float2::UnitY(0.0f, 1.0f); diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index cea03ec10..cce77788a 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -44,6 +44,9 @@ public: // Vector with all components equal 1 static FLAXENGINE_API const Vector2Base One; + // Vector with all components equal 0.5 + static FLAXENGINE_API const Vector2Base Half; + // Vector X=1, Y=0 static FLAXENGINE_API const Vector2Base UnitX; diff --git a/Source/Engine/Core/Math/Vector4.cpp b/Source/Engine/Core/Math/Vector4.cpp index b5fa7a81d..372fde6db 100644 --- a/Source/Engine/Core/Math/Vector4.cpp +++ b/Source/Engine/Core/Math/Vector4.cpp @@ -17,6 +17,8 @@ const Float4 Float4::Zero(0.0f); template<> const Float4 Float4::One(1.0f); template<> +const Float4 Float4::Half(0.5f); +template<> const Float4 Float4::UnitX(1.0f, 0.0f, 0.0f, 0.0f); template<> const Float4 Float4::UnitY(0.0f, 1.0f, 0.0f, 0.0f); diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 1cc6d4db8..7edb97ce5 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -54,6 +54,9 @@ public: // Vector with all components equal 1 static FLAXENGINE_API const Vector4Base One; + // Vector with all components equal 0.5 + static FLAXENGINE_API const Vector4Base Half; + // Vector X=1, Y=0, Z=0, W=0 static FLAXENGINE_API const Vector4Base UnitX; diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h index 8d7188d91..fb6b4555b 100644 --- a/Source/Engine/Core/Memory/Allocation.h +++ b/Source/Engine/Core/Memory/Allocation.h @@ -12,6 +12,8 @@ template class FixedAllocation { public: + enum { HasSwap = false }; + template class Data { @@ -61,12 +63,9 @@ public: { } - FORCE_INLINE void Swap(Data& other) + void Swap(Data& other) { - byte tmp[Capacity * sizeof(T)]; - Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T)); - Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T)); - Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T)); + // Not supported } }; }; @@ -77,6 +76,8 @@ public: class HeapAllocation { public: + enum { HasSwap = true }; + template class Data { @@ -179,6 +180,8 @@ template class InlinedAllocation { public: + enum { HasSwap = false }; + template class Data { @@ -267,14 +270,9 @@ public: } } - FORCE_INLINE void Swap(Data& other) + void Swap(Data& other) { - byte tmp[Capacity * sizeof(T)]; - Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T)); - Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T)); - Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T)); - ::Swap(_useOther, other._useOther); - _other.Swap(other._other); + // Not supported } }; }; diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index c0cfad34c..259649062 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -522,7 +522,13 @@ void EngineImpl::InitLog() LOG(Info, "Compiled for Dev Environment"); #endif LOG(Info, "Version " FLAXENGINE_VERSION_TEXT); - LOG(Info, "Compiled: {0} {1}", TEXT(__DATE__), TEXT(__TIME__)); + const Char* cpp = TEXT("?"); + if (__cplusplus == 202101L) cpp = TEXT("C++23"); + else if (__cplusplus == 202002L) cpp = TEXT("C++20"); + else if (__cplusplus == 201703L) cpp = TEXT("C++17"); + else if (__cplusplus == 201402L) cpp = TEXT("C++14"); + else if (__cplusplus == 201103L) cpp = TEXT("C++11"); + LOG(Info, "Compiled: {0} {1} {2}", TEXT(__DATE__), TEXT(__TIME__), cpp); #ifdef _MSC_VER const String mcsVer = StringUtils::ToString(_MSC_FULL_VER); LOG(Info, "Compiled with Visual C++ {0}.{1}.{2}.{3:0^2d}", mcsVer.Substring(0, 2), mcsVer.Substring(2, 2), mcsVer.Substring(4, 5), _MSC_BUILD); diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 574b74580..e99276a4e 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1271,6 +1271,9 @@ namespace FlaxEngine.Interop case Type _ when type == typeof(IntPtr): monoType = MTypes.Ptr; break; + case Type _ when type.IsPointer: + monoType = MTypes.Ptr; + break; case Type _ when type.IsEnum: monoType = MTypes.Enum; break; diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 3f19f5951..026f54fec 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1135,7 +1135,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, fieldPtr, out int fieldSize); fieldPtr += fieldSize; } - Assert.IsTrue((fieldPtr - nativePtr) <= Unsafe.SizeOf()); + //Assert.IsTrue((fieldPtr - nativePtr) <= GetTypeSize(typeof(T))); } internal static void ToManaged(ref T managedValue, IntPtr nativePtr, bool byRef) @@ -1182,7 +1182,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, nativePtr, out int fieldSize); nativePtr += fieldSize; } - Assert.IsTrue((nativePtr - fieldPtr) <= Unsafe.SizeOf()); + //Assert.IsTrue((nativePtr - fieldPtr) <= GetTypeSize(typeof(T))); } internal static void ToNative(ref T managedValue, IntPtr nativePtr) @@ -1331,39 +1331,50 @@ namespace FlaxEngine.Interop // Skip using in-built delegate for value types (eg. Transform) to properly handle instance value passing to method if (invokeDelegate == null && !method.DeclaringType.IsValueType) { - List methodTypes = new List(); - if (!method.IsStatic) - methodTypes.Add(method.DeclaringType); - if (returnType != typeof(void)) - methodTypes.Add(returnType); - methodTypes.AddRange(parameterTypes); - - List genericParamTypes = new List(); - foreach (var type in methodTypes) + // Thread-safe creation + lock (typeCache) { - if (type.IsByRef) - genericParamTypes.Add(type.GetElementType()); - else if (type.IsPointer) - genericParamTypes.Add(typeof(IntPtr)); - else - genericParamTypes.Add(type); - } - - string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(returnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}"; - Type invokerType = Type.GetType(invokerTypeName); - if (invokerType != null) - { - if (genericParamTypes.Count != 0) - invokerType = invokerType.MakeGenericType(genericParamTypes.ToArray()); - invokeDelegate = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.MarshalAndInvoke), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate(); - delegInvoke = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.CreateDelegate), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { method }); + if (invokeDelegate == null) + { + TryCreateDelegate(); + } } } - outDeleg = invokeDelegate; outDelegInvoke = delegInvoke; return outDeleg != null; } + + private void TryCreateDelegate() + { + var methodTypes = new List(); + if (!method.IsStatic) + methodTypes.Add(method.DeclaringType); + if (returnType != typeof(void)) + methodTypes.Add(returnType); + methodTypes.AddRange(parameterTypes); + + var genericParamTypes = new List(); + foreach (var type in methodTypes) + { + if (type.IsByRef) + genericParamTypes.Add(type.GetElementType()); + else if (type.IsPointer) + genericParamTypes.Add(typeof(IntPtr)); + else + genericParamTypes.Add(type); + } + + string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(returnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}"; + Type invokerType = Type.GetType(invokerTypeName); + if (invokerType != null) + { + if (genericParamTypes.Count != 0) + invokerType = invokerType.MakeGenericType(genericParamTypes.ToArray()); + delegInvoke = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.CreateDelegate), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { method }); + invokeDelegate = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.MarshalAndInvoke), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate(); + } + } #endif } @@ -1580,7 +1591,7 @@ namespace FlaxEngine.Interop private static IntPtr PinValue(T value) where T : struct { // Store the converted value in unmanaged memory so it will not be relocated by the garbage collector. - int size = Unsafe.SizeOf(); + int size = GetTypeSize(typeof(T)); uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length; ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index]; if (alloc.size < size) diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index 727fe7754..91ee2bcd7 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -10,6 +10,7 @@ #include "Engine/Core/Collections/BitArray.h" #include "Engine/Tools/ModelTool/ModelTool.h" #include "Engine/Tools/ModelTool/VertexTriangleAdjacency.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Platform/Platform.h" #define USE_MIKKTSPACE 1 #include "ThirdParty/MikkTSpace/mikktspace.h" @@ -78,6 +79,7 @@ void RemapArrayHelper(Array& target, const std::vector& remap) bool MeshData::GenerateLightmapUVs() { + PROFILE_CPU(); #if PLATFORM_WINDOWS // Prepare HRESULT hr; @@ -235,6 +237,7 @@ void RemapBuffer(Array& src, Array& dst, const Array& mapping, int3 void MeshData::BuildIndexBuffer() { + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); const int32 vertexCount = Positions.Count(); @@ -341,6 +344,7 @@ bool MeshData::GenerateNormals(float smoothingAngle) LOG(Warning, "Missing vertex or index data to generate normals."); return true; } + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); @@ -520,6 +524,7 @@ bool MeshData::GenerateTangents(float smoothingAngle) LOG(Warning, "Missing normals or texcoors data to generate tangents."); return true; } + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); const int32 vertexCount = Positions.Count(); @@ -706,6 +711,7 @@ void MeshData::ImproveCacheLocality() if (Positions.IsEmpty() || Indices.IsEmpty() || Positions.Count() <= VertexCacheSize) return; + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); @@ -886,6 +892,7 @@ void MeshData::ImproveCacheLocality() float MeshData::CalculateTrianglesArea() const { + PROFILE_CPU(); float sum = 0; // TODO: use SIMD for (int32 i = 0; i + 2 < Indices.Count(); i += 3) diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp index 1d67f4737..6e6ae04b5 100644 --- a/Source/Engine/Graphics/Models/ModelData.cpp +++ b/Source/Engine/Graphics/Models/ModelData.cpp @@ -625,6 +625,11 @@ bool MaterialSlotEntry::UsesProperties() const Normals.TextureIndex != -1; } +ModelLodData::~ModelLodData() +{ + Meshes.ClearDelete(); +} + BoundingBox ModelLodData::GetBox() const { if (Meshes.IsEmpty()) @@ -644,11 +649,9 @@ void ModelData::CalculateLODsScreenSizes() { const float autoComputeLodPowerBase = 0.5f; const int32 lodCount = LODs.Count(); - for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) { auto& lod = LODs[lodIndex]; - if (lodIndex == 0) { lod.ScreenSize = 1.0f; @@ -675,6 +678,8 @@ void ModelData::TransformBuffer(const Matrix& matrix) } } +#if USE_EDITOR + bool ModelData::Pack2ModelHeader(WriteStream* stream) const { // Validate input @@ -724,7 +729,12 @@ bool ModelData::Pack2ModelHeader(WriteStream* stream) const // Amount of meshes const int32 meshes = lod.Meshes.Count(); - if (meshes == 0 || meshes > MODEL_MAX_MESHES) + if (meshes == 0) + { + LOG(Warning, "Empty LOD."); + return true; + } + if (meshes > MODEL_MAX_MESHES) { LOG(Warning, "Too many meshes per LOD."); return true; @@ -880,20 +890,21 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const return false; } -bool ModelData::Pack2AnimationHeader(WriteStream* stream) const +bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const { // Validate input - if (stream == nullptr) + if (stream == nullptr || animIndex < 0 || animIndex >= Animations.Count()) { Log::ArgumentNullException(); return true; } - if (Animation.Duration <= ZeroTolerance || Animation.FramesPerSecond <= ZeroTolerance) + auto& anim = Animations.Get()[animIndex]; + if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance) { Log::InvalidOperationException(TEXT("Invalid animation duration.")); return true; } - if (Animation.Channels.IsEmpty()) + if (anim.Channels.IsEmpty()) { Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty.")); return true; @@ -901,22 +912,23 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream) const // Info stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change) - stream->WriteDouble(Animation.Duration); - stream->WriteDouble(Animation.FramesPerSecond); - stream->WriteBool(Animation.EnableRootMotion); - stream->WriteString(Animation.RootNodeName, 13); + stream->WriteDouble(anim.Duration); + stream->WriteDouble(anim.FramesPerSecond); + stream->WriteBool(anim.EnableRootMotion); + stream->WriteString(anim.RootNodeName, 13); // Animation channels - stream->WriteInt32(Animation.Channels.Count()); - for (int32 i = 0; i < Animation.Channels.Count(); i++) + stream->WriteInt32(anim.Channels.Count()); + for (int32 i = 0; i < anim.Channels.Count(); i++) { - auto& anim = Animation.Channels[i]; - - stream->WriteString(anim.NodeName, 172); - Serialization::Serialize(*stream, anim.Position); - Serialization::Serialize(*stream, anim.Rotation); - Serialization::Serialize(*stream, anim.Scale); + auto& channel = anim.Channels[i]; + stream->WriteString(channel.NodeName, 172); + Serialization::Serialize(*stream, channel.Position); + Serialization::Serialize(*stream, channel.Rotation); + Serialization::Serialize(*stream, channel.Scale); } return false; } + +#endif diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index d7359d905..65bedf876 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -366,12 +366,32 @@ struct FLAXENGINE_API MaterialSlotEntry bool UsesProperties() const; }; +/// +/// Data container for model hierarchy node. +/// +struct FLAXENGINE_API ModelDataNode +{ + /// + /// The parent node index. The root node uses value -1. + /// + int32 ParentIndex; + + /// + /// The local transformation of the node, relative to the parent node. + /// + Transform LocalTransform; + + /// + /// The name of this node. + /// + String Name; +}; + /// /// Data container for LOD metadata and sub meshes. /// -class FLAXENGINE_API ModelLodData +struct FLAXENGINE_API ModelLodData { -public: /// /// The screen size to switch LODs. Bottom limit of the model screen size to render this LOD. /// @@ -382,21 +402,10 @@ public: /// Array Meshes; -public: - /// - /// Initializes a new instance of the class. - /// - ModelLodData() - { - } - /// /// Finalizes an instance of the class. /// - ~ModelLodData() - { - Meshes.ClearDelete(); - } + ~ModelLodData(); /// /// Gets the bounding box combined for all meshes in this model LOD. @@ -426,7 +435,7 @@ public: Array Materials; /// - /// Array with all LODs. The first element is the top most LOD0 followed by the LOD1, LOD2, etc. + /// Array with all Level Of Details that contain meshes. The first element is the top most LOD0 followed by the LOD1, LOD2, etc. /// Array LODs; @@ -435,24 +444,20 @@ public: /// SkeletonData Skeleton; + /// + /// The scene nodes (in hierarchy). + /// + Array Nodes; + /// /// The node animations. /// - AnimationData Animation; - -public: - /// - /// Initializes a new instance of the class. - /// - ModelData() - { - } + Array Animations; public: /// /// Gets the valid level of details count. /// - /// The LOD count. FORCE_INLINE int32 GetLODsCount() const { return LODs.Count(); @@ -461,7 +466,6 @@ public: /// /// Determines whether this instance has valid skeleton structure. /// - /// True if has skeleton, otherwise false. FORCE_INLINE bool HasSkeleton() const { return Skeleton.Bones.HasItems(); @@ -479,6 +483,7 @@ public: /// The matrix to use for the transformation. void TransformBuffer(const Matrix& matrix); +#if USE_EDITOR public: /// /// Pack mesh data to the header stream @@ -498,6 +503,8 @@ public: /// Pack animation data to the header stream /// /// Output stream + /// Index of animation. /// True if cannot save data, otherwise false - bool Pack2AnimationHeader(WriteStream* stream) const; + bool Pack2AnimationHeader(WriteStream* stream, int32 animIndex = 0) const; +#endif }; diff --git a/Source/Engine/Graphics/Models/SkeletonMapping.h b/Source/Engine/Graphics/Models/SkeletonMapping.h index e3ec0c793..29fd307fc 100644 --- a/Source/Engine/Graphics/Models/SkeletonMapping.h +++ b/Source/Engine/Graphics/Models/SkeletonMapping.h @@ -36,7 +36,7 @@ public: /// /// The source model skeleton. /// The target skeleton. May be null to disable nodes mapping. - SkeletonMapping(Items& sourceSkeleton, Items* targetSkeleton) + SkeletonMapping(const Items& sourceSkeleton, const Items* targetSkeleton) { Size = sourceSkeleton.Count(); SourceToTarget.Resize(Size); // model => skeleton mapping diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 173b0ef85..015386fff 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -63,7 +63,6 @@ StreamingTexture::~StreamingTexture() { UnloadTexture(); SAFE_DELETE(_texture); - ASSERT(_streamingTasks.Count() == 0); } Float2 StreamingTexture::Size() const @@ -134,11 +133,9 @@ bool StreamingTexture::Create(const TextureHeader& header) void StreamingTexture::UnloadTexture() { ScopeLock lock(_owner->GetOwnerLocker()); - - // Release + CancelStreamingTasks(); _texture->ReleaseGPU(); _header.MipLevels = 0; - ASSERT(_streamingTasks.Count() == 0); } diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp index 0b857d394..a69187f6f 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp @@ -247,7 +247,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array& outInst if (foundUniqueLayers.HasItems()) { LOG(Info, "Found instance layers:"); - Sorting::QuickSort(foundUniqueLayers.Get(), foundUniqueLayers.Count()); + Sorting::QuickSort(foundUniqueLayers); for (const StringAnsi& name : foundUniqueLayers) { LOG(Info, "- {0}", String(name)); @@ -257,7 +257,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array& outInst if (foundUniqueExtensions.HasItems()) { LOG(Info, "Found instance extensions:"); - Sorting::QuickSort(foundUniqueExtensions.Get(), foundUniqueExtensions.Count()); + Sorting::QuickSort(foundUniqueExtensions); for (const StringAnsi& name : foundUniqueExtensions) { LOG(Info, "- {0}", String(name)); @@ -455,7 +455,7 @@ void GPUDeviceVulkan::GetDeviceExtensionsAndLayers(VkPhysicalDevice gpu, Array()) { @@ -265,6 +259,13 @@ void Actor::SetParent(Actor* value, bool worldPositionsStays, bool canBreakPrefa // Detect it actor is not in a game but new parent is already in a game (we should spawn it) const bool isBeingSpawned = !IsDuringPlay() && newScene && value->IsDuringPlay(); + // Actors system doesn't support editing scene hierarchy from multiple threads + if (!IsInMainThread() && (IsDuringPlay() || isBeingSpawned)) + { + LOG(Error, "Editing scene hierarchy is only allowed on a main thread."); + return; + } + // Handle changing scene (unregister from it) const bool isSceneChanging = prevScene != newScene; if (prevScene && isSceneChanging && wasActiveInTree) diff --git a/Source/Engine/Level/Actors/SkyLight.cpp b/Source/Engine/Level/Actors/SkyLight.cpp index 93e2a8276..7894ef1c3 100644 --- a/Source/Engine/Level/Actors/SkyLight.cpp +++ b/Source/Engine/Level/Actors/SkyLight.cpp @@ -99,6 +99,8 @@ void SkyLight::UpdateBounds() { _sphere = BoundingSphere(GetPosition(), GetScaledRadius()); BoundingBox::FromSphere(_sphere, _box); + if (_sceneRenderingKey != -1) + GetSceneRendering()->UpdateActor(this, _sceneRenderingKey); } void SkyLight::Draw(RenderContext& renderContext) diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index c10ea532d..569a7ce9b 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -22,6 +22,7 @@ #include "Engine/ContentImporters/CreateJson.h" #include "Engine/Debug/Exceptions/ArgumentNullException.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Threading/MainThreadTask.h" #include "Editor/Editor.h" // Apply flow: @@ -174,6 +175,12 @@ public: /// Collection with ids of the objects (actors and scripts) from the prefab after changes apply. Used to find new objects or old objects and use this information during changes sync (eg. generate ids for the new objects to prevent ids collisions). /// True if failed, otherwise false. static bool SynchronizePrefabInstances(PrefabInstancesData& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds); + + static void DeletePrefabObject(SceneObject* obj) + { + obj->SetParent(nullptr); + obj->DeleteObject(); + } }; void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor) @@ -302,14 +309,10 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI { // Remove object LOG(Info, "Removing object {0} from instance {1} (prefab: {2})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), prefabId); - - obj->DeleteObject(); - obj->SetParent(nullptr); - + DeletePrefabObject(obj); sceneObjects.Value->RemoveAtKeepOrder(i); existingObjectsCount--; i--; - continue; } @@ -358,10 +361,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI { // Remove object removed from the prefab LOG(Info, "Removing prefab instance object {0} from instance {1} (prefab object: {2}, prefab: {3})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), obj->GetPrefabObjectID(), prefabId); - - obj->DeleteObject(); - obj->SetParent(nullptr); - + DeletePrefabObject(obj); sceneObjects.Value->RemoveAtKeepOrder(i); deserializeSceneObjectIndex--; existingObjectsCount--; @@ -633,6 +633,19 @@ bool Prefab::ApplyAll(Actor* targetActor) } } } + if (!IsInMainThread()) + { + // Prefabs cannot be updated on async thread so sync it with a Main Thread + bool result = true; + Function action = [&] + { + result = ApplyAll(targetActor); + }; + const auto task = Task::StartNew(New(action)); + if (task->Wait(TimeSpan::FromSeconds(10))) + result = true; + return result; + } // Prevent cyclic references { @@ -921,9 +934,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr { // Remove object removed from the prefab LOG(Info, "Removing object {0} from prefab default instance", obj->GetSceneObjectId()); - - obj->DeleteObject(); - obj->SetParent(nullptr); + PrefabInstanceData::DeletePrefabObject(obj); sceneObjects->At(i) = nullptr; } } diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index b72f16633..65da270c0 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -90,11 +90,11 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId) if (objectId.IsValid()) { - const void* object; + SceneObject* object; if (ObjectsCache.TryGet(objectId, object)) { // Actor or Script - return (SceneObject*)object; + return object; } } diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h index 5e0075edf..9fa2b4fd0 100644 --- a/Source/Engine/Level/Prefabs/Prefab.h +++ b/Source/Engine/Level/Prefabs/Prefab.h @@ -44,7 +44,7 @@ public: /// /// The objects cache maps the id of the object contained in the prefab asset (actor or script) to the default instance deserialized from prefab data. Valid only if asset is loaded and GetDefaultInstance was called. /// - Dictionary ObjectsCache; + Dictionary ObjectsCache; public: /// diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index c7b42b7d2..bb4710bc7 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -76,12 +76,12 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent) return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr); } -Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization) +Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization) { return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, objectsCache, withSynchronization); } -Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization) +Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization) { PROFILE_CPU_NAMED("Prefab.Spawn"); if (prefab == nullptr) diff --git a/Source/Engine/Level/Prefabs/PrefabManager.h b/Source/Engine/Level/Prefabs/PrefabManager.h index 16d4a29cf..e5600bac4 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.h +++ b/Source/Engine/Level/Prefabs/PrefabManager.h @@ -89,7 +89,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); /// /// Spawns the instance of the prefab objects. If parent actor is specified then created actors are fully initialized (OnLoad event and BeginPlay is called if parent actor is already during gameplay). @@ -100,7 +100,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); #if USE_EDITOR diff --git a/Source/Engine/Level/SceneInfo.cpp b/Source/Engine/Level/SceneInfo.cpp index 65e4aa462..9c504bad7 100644 --- a/Source/Engine/Level/SceneInfo.cpp +++ b/Source/Engine/Level/SceneInfo.cpp @@ -11,29 +11,6 @@ String SceneInfo::ToString() const return TEXT("SceneInfo"); } -const int32 lightmapAtlasSizes[] = -{ - 32, - 64, - 128, - 256, - 512, - 1024, - 2048, - 4096 -}; -DECLARE_ENUM_8(LightmapAtlasSize, _32, _64, _128, _256, _512, _1024, _2048, _4096); - -LightmapAtlasSize getLightmapAtlasSize(int32 size) -{ - for (int32 i = 0; i < LightmapAtlasSize_Count; i++) - { - if (lightmapAtlasSizes[i] == size) - return (LightmapAtlasSize)i; - } - return LightmapAtlasSize::_1024; -} - void SceneInfo::Serialize(SerializeStream& stream, const void* otherObj) { SERIALIZE_GET_OTHER_OBJ(SceneInfo); diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 985da6c29..df30257f9 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -16,8 +16,9 @@ #if !BUILD_RELEASE || USE_EDITOR #include "Engine/Level/Level.h" #include "Engine/Threading/Threading.h" -#include "Engine/Level/Components/MissingScript.h" +#include "Engine/Level/Scripts/MissingScript.h" #endif +#include "Engine/Level/Scripts/ModelPrefab.h" #if USE_EDITOR @@ -46,6 +47,11 @@ void MissingScript::SetReferenceScript(const ScriptingObjectReference