diff --git a/Content/Shaders/SSR.flax b/Content/Shaders/SSR.flax index a422ce5c0..d7cfdca1a 100644 --- a/Content/Shaders/SSR.flax +++ b/Content/Shaders/SSR.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0317fb2ca888fee6d32c7754d6f83e2f1e58924725a8221c5131e974501168ac -size 10912 +oid sha256:d3c3ceba0aa0b79f5619147a0ec8ac30d87c142322bd256dc235b7b4f890fc94 +size 11142 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/PreviewsCache.cpp b/Source/Editor/Content/PreviewsCache.cpp index 2d62e8170..b0f3619a5 100644 --- a/Source/Editor/Content/PreviewsCache.cpp +++ b/Source/Editor/Content/PreviewsCache.cpp @@ -5,6 +5,7 @@ #include "Engine/Graphics/GPUContext.h" #include "Engine/Threading/Threading.h" #include "Engine/Graphics/RenderTools.h" +#include "Engine/Content/Content.h" #include "Engine/Content/Factories/BinaryAssetFactory.h" #include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/Content/Upgraders/TextureAssetUpgrader.h" @@ -92,15 +93,12 @@ SpriteHandle PreviewsCache::FindSlot(const Guid& id) { if (WaitForLoaded()) return SpriteHandle::Invalid; - - // Find entry int32 index; if (_assets.Find(id, index)) { const String spriteName = StringUtils::ToString(index); return FindSprite(spriteName); } - return SpriteHandle::Invalid; } @@ -114,6 +112,17 @@ Asset::LoadResult PreviewsCache::load() return LoadResult::Failed; _assets.Set(previewsMetaChunk->Get(), ASSETS_ICONS_PER_ATLAS); + // Verify if cached assets still exist (don't store thumbnails for removed files) + AssetInfo assetInfo; + for (Guid& id : _assets) + { + if (id.IsValid() && Content::GetAsset(id) == nullptr && !Content::GetAssetInfo(id, assetInfo)) + { + // Free slot (no matter the texture contents) + id = Guid::Empty; + } + } + // Setup atlas sprites array Sprite sprite; sprite.Area.Size = static_cast(ASSET_ICON_SIZE) / ASSETS_ICONS_ATLAS_SIZE; @@ -162,7 +171,7 @@ SpriteHandle PreviewsCache::OccupySlot(GPUTexture* source, const Guid& id) if (WaitForLoaded()) return SpriteHandle::Invalid; - // Find free slot and for that asset + // Find this asset slot or use the first empty int32 index = _assets.Find(id); if (index == INVALID_INDEX) index = _assets.Find(Guid::Empty); @@ -201,14 +210,12 @@ bool PreviewsCache::ReleaseSlot(const Guid& id) { bool result = false; ScopeLock lock(Locker); - int32 index = _assets.Find(id); if (index != INVALID_INDEX) { _assets[index] = Guid::Empty; result = true; } - return result; } 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/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 004c2aed7..d959b524a 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Windows; using FlaxEngine; @@ -68,5 +69,15 @@ namespace FlaxEditor.Content { return new SceneItem(path, id); } + + /// + public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) + { + var id = ((SceneItem)item).ID; + if (Level.FindScene(id) == null) + { + menu.AddButton("Open (additive)", () => { Editor.Instance.Scene.OpenScene(id, true); }); + } + } } } diff --git a/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs b/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs index 6e60602ff..40ad17720 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs @@ -35,6 +35,11 @@ namespace FlaxEditor.Content.Thumbnails /// The finalized state. /// Disposed, + + /// + /// The request has failed (eg. asset cannot be loaded). + /// + Failed, }; /// @@ -78,6 +83,14 @@ namespace FlaxEditor.Content.Thumbnails Proxy = proxy; } + internal void Update() + { + if (State == States.Prepared && (!Asset || Asset.LastLoadFailed)) + { + State = States.Failed; + } + } + /// /// Prepares this request. /// @@ -85,11 +98,8 @@ namespace FlaxEditor.Content.Thumbnails { if (State != States.Created) throw new InvalidOperationException(); - - // Prepare Asset = FlaxEngine.Content.LoadAsync(Item.Path); Proxy.OnThumbnailDrawPrepare(this); - State = States.Prepared; } @@ -101,9 +111,7 @@ namespace FlaxEditor.Content.Thumbnails { if (State != States.Prepared) throw new InvalidOperationException(); - Item.Thumbnail = icon; - State = States.Rendered; } diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index 1282e4daa..b213cb798 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -21,15 +21,11 @@ namespace FlaxEditor.Content.Thumbnails /// public const float MinimumRequiredResourcesQuality = 0.8f; - // TODO: free atlas slots for deleted assets - private readonly List _cache = new List(4); private readonly string _cacheFolder; - - private DateTime _lastFlushTime; - private readonly List _requests = new List(128); private readonly PreviewRoot _guiRoot = new PreviewRoot(); + private DateTime _lastFlushTime; private RenderTask _task; private GPUTexture _output; @@ -88,7 +84,6 @@ namespace FlaxEditor.Content.Thumbnails } } - // Add request AddRequest(assetItem, proxy); } } @@ -118,15 +113,15 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < _cache.Count; i++) { if (_cache[i].ReleaseSlot(assetItem.ID)) - { break; - } } } } internal static bool HasMinimumQuality(TextureBase asset) { + if (asset.HasStreamingError) + return true; // Don't block thumbnails queue when texture fails to stream in (eg. unsupported format) var mipLevels = asset.MipLevels; var minMipLevels = Mathf.Min(mipLevels, 7); return asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality)); @@ -198,13 +193,7 @@ namespace FlaxEditor.Content.Thumbnails /// void IContentItemOwner.OnItemDeleted(ContentItem item) { - if (item is AssetItem assetItem) - { - lock (_requests) - { - RemoveRequest(assetItem); - } - } + DeletePreview(item); } /// @@ -494,10 +483,7 @@ namespace FlaxEditor.Content.Thumbnails { // Wait some frames before start generating previews (late init feature) if (Time.TimeSinceStartup < 1.0f || HasAllAtlasesLoaded() == false) - { - // Back return; - } lock (_requests) { @@ -515,6 +501,7 @@ namespace FlaxEditor.Content.Thumbnails var request = _requests[i]; try { + request.Update(); if (request.IsReady) { isAnyReady = true; @@ -523,6 +510,10 @@ namespace FlaxEditor.Content.Thumbnails { request.Prepare(); } + else if (request.State == ThumbnailRequest.States.Failed) + { + _requests.RemoveAt(i--); + } } catch (Exception ex) { diff --git a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp index 995f19fce..88f993f20 100644 --- a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp @@ -169,6 +169,30 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) permissions += String::Format(TEXT("\n "), e.Item); } + // Setup default Android screen orientation + auto defaultOrienation = platformSettings->DefaultOrientation; + String orientation = String("fullSensor"); + switch (defaultOrienation) + { + case AndroidPlatformSettings::ScreenOrientation::Portrait: + orientation = String("portrait"); + break; + case AndroidPlatformSettings::ScreenOrientation::PortraitReverse: + orientation = String("reversePortrait"); + break; + case AndroidPlatformSettings::ScreenOrientation::LandscapeRight: + orientation = String("landscape"); + break; + case AndroidPlatformSettings::ScreenOrientation::LandscapeLeft: + orientation = String("reverseLandscape"); + break; + case AndroidPlatformSettings::ScreenOrientation::AutoRotation: + orientation = String("fullSensor"); + break; + default: + break; + } + // Setup Android application attributes String attributes; if (data.Configuration != BuildConfiguration::Release) @@ -223,6 +247,7 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) EditorUtilities::ReplaceInFile(manifestPath, TEXT("${PackageName}"), packageName); EditorUtilities::ReplaceInFile(manifestPath, TEXT("${ProjectVersion}"), projectVersion); EditorUtilities::ReplaceInFile(manifestPath, TEXT("${AndroidPermissions}"), permissions); + EditorUtilities::ReplaceInFile(manifestPath, TEXT("${DefaultOrientation}"), orientation); EditorUtilities::ReplaceInFile(manifestPath, TEXT("${AndroidAttributes}"), attributes); const String stringsPath = data.OriginalOutputPath / TEXT("app/src/main/res/values/strings.xml"); EditorUtilities::ReplaceInFile(stringsPath, TEXT("${ProjectName}"), gameSettings->ProductName); 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 c1179c77a..59ff0d744 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -48,21 +48,21 @@ bool DeployDataStep::Perform(CookingData& data) } if (buildSettings.SkipDotnetPackaging && data.Tools->UseSystemDotnet()) { - // Use system-installed .Net Runtime + // Use system-installed .NET Runtime FileSystem::DeleteDirectory(dstDotnet); } else { - // Deploy .Net Runtime files + // Deploy .NET Runtime files FileSystem::CreateDirectory(dstDotnet); String srcDotnet = depsRoot / TEXT("Dotnet"); if (FileSystem::DirectoryExists(srcDotnet)) { - // Use prebuilt .Net installation for that platform - LOG(Info, "Using .Net Runtime {} at {}", data.Tools->GetName(), srcDotnet); + // Use prebuilt .NET installation for that platform + LOG(Info, "Using .NET Runtime {} at {}", data.Tools->GetName(), srcDotnet); if (EditorUtilities::CopyDirectoryIfNewer(dstDotnet, srcDotnet, true)) { - data.Error(TEXT("Failed to copy .Net runtime data files.")); + data.Error(TEXT("Failed to copy .NET runtime data files.")); return true; } } @@ -85,7 +85,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (canUseSystemDotnet && (aotMode == DotNetAOTModes::None || aotMode == DotNetAOTModes::ILC)) { - // Ask Flax.Build to provide .Net SDK location for the current platform + // Ask Flax.Build to provide .NET SDK location for the current platform String sdks; bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), GAME_BUILD_DOTNET_VER), data.CacheDirectory); failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks); @@ -101,7 +101,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed || !FileSystem::DirectoryExists(srcDotnet)) { - data.Error(TEXT("Failed to get .Net SDK location for a current platform.")); + data.Error(TEXT("Failed to get .NET SDK location for the current host platform.")); return true; } @@ -110,19 +110,25 @@ bool DeployDataStep::Perform(CookingData& data) FileSystem::GetChildDirectories(versions, srcDotnet / TEXT("host/fxr")); if (versions.Count() == 0) { - data.Error(TEXT("Failed to get .Net SDK location for a current platform.")); + data.Error(TEXT("Failed to find any .NET hostfxr versions for the current host platform.")); return true; } for (String& version : versions) { version = String(StringUtils::GetFileName(version)); - if (!version.StartsWith(TEXT("7."))) + if (!version.StartsWith(TEXT("7.")) && !version.StartsWith(TEXT("8."))) // .NET 7 or .NET 8 version.Clear(); } - Sorting::QuickSort(versions.Get(), versions.Count()); + Sorting::QuickSort(versions); const String version = versions.Last(); + if (version.IsEmpty()) + { + data.Error(TEXT("Failed to find supported .NET hostfxr version for the current host platform.")); + return true; + } + FileSystem::NormalizePath(srcDotnet); - LOG(Info, "Using .Net Runtime {} at {}", version, srcDotnet); + LOG(Info, "Using .NET Runtime {} at {}", version, srcDotnet); // Check if previously deployed files are valid (eg. system-installed .NET was updated from version 7.0.3 to 7.0.5) { @@ -158,13 +164,13 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed) { - data.Error(TEXT("Failed to copy .Net runtime data files.")); + data.Error(TEXT("Failed to copy .NET runtime data files.")); return true; } } else { - // Ask Flax.Build to provide .Net Host Runtime location for the target platform + // Ask Flax.Build to provide .NET Host Runtime location for the target platform String sdks; const Char *platformName, *archName; data.GetBuildPlatformName(platformName, archName); @@ -180,11 +186,11 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed || !FileSystem::DirectoryExists(srcDotnet)) { - data.Error(TEXT("Failed to get .Net SDK location for a current platform.")); + data.Error(TEXT("Failed to get .NET SDK location for the current host platform.")); return true; } FileSystem::NormalizePath(srcDotnet); - LOG(Info, "Using .Net Runtime {} at {}", TEXT("Host"), srcDotnet); + LOG(Info, "Using .NET Runtime {} at {}", TEXT("Host"), srcDotnet); // Deploy runtime files const Char* corlibPrivateName = TEXT("System.Private.CoreLib.dll"); @@ -249,7 +255,7 @@ bool DeployDataStep::Perform(CookingData& data) DEPLOY_NATIVE_FILE("libmonosgen-2.0.dylib"); DEPLOY_NATIVE_FILE("libSystem.IO.Compression.Native.dylib"); DEPLOY_NATIVE_FILE("libSystem.Native.dylib"); - DEPLOY_NATIVE_FILE("libSystem.Net.Security.Native.dylib"); + DEPLOY_NATIVE_FILE("libSystem.NET.Security.Native.dylib"); DEPLOY_NATIVE_FILE("libSystem.Security.Cryptography.Native.Apple.dylib"); break; #undef DEPLOY_NATIVE_FILE @@ -257,7 +263,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed) { - data.Error(TEXT("Failed to copy .Net runtime data files.")); + data.Error(TEXT("Failed to copy .NET runtime data files.")); return true; } } @@ -278,7 +284,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (ScriptsBuilder::RunBuildTool(args)) { - data.Error(TEXT("Failed to optimize .Net class library.")); + data.Error(TEXT("Failed to optimize .NET class library.")); return true; } } diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 9de330213..a519b1da1 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -7,9 +7,8 @@ using FlaxEditor.CustomEditors.GUI; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Json; using FlaxEngine.Utilities; -using Newtonsoft.Json; -using JsonSerializer = FlaxEngine.Json.JsonSerializer; namespace FlaxEditor.CustomEditors { @@ -386,22 +385,22 @@ namespace FlaxEditor.CustomEditors LinkedLabel = label; } - private void RevertDiffToDefault(CustomEditor editor) - { - if (editor.ChildrenEditors.Count == 0) - { - // Skip if no change detected - if (!editor.Values.IsDefaultValueModified) - return; + /// + /// If true, the value reverting to default/reference will be handled via iteration over children editors, instead of for a whole object at once. + /// + public virtual bool RevertValueWithChildren => ChildrenEditors.Count != 0; - editor.SetValueToDefault(); + private void RevertDiffToDefault() + { + if (RevertValueWithChildren) + { + foreach (var child in ChildrenEditors) + child.RevertDiffToDefault(); } else { - for (int i = 0; i < editor.ChildrenEditors.Count; i++) - { - RevertDiffToDefault(editor.ChildrenEditors[i]); - } + if (Values.IsDefaultValueModified) + SetValueToDefault(); } } @@ -414,11 +413,6 @@ namespace FlaxEditor.CustomEditors { if (!Values.IsDefaultValueModified) return false; - - // Skip array items (show diff only on a bottom level properties and fields) - if (ParentEditor is Editors.ArrayEditor) - return false; - return true; } } @@ -430,7 +424,7 @@ namespace FlaxEditor.CustomEditors { if (!Values.HasDefaultValue) return; - RevertDiffToDefault(this); + RevertDiffToDefault(); } /// @@ -468,22 +462,17 @@ namespace FlaxEditor.CustomEditors } } - private void RevertDiffToReference(CustomEditor editor) + private void RevertDiffToReference() { - if (editor.ChildrenEditors.Count == 0) + if (RevertValueWithChildren) { - // Skip if no change detected - if (!editor.Values.IsReferenceValueModified) - return; - - editor.SetValueToReference(); + foreach (var child in ChildrenEditors) + child.RevertDiffToReference(); } else { - for (int i = 0; i < editor.ChildrenEditors.Count; i++) - { - RevertDiffToReference(editor.ChildrenEditors[i]); - } + if (Values.IsReferenceValueModified) + SetValueToReference(); } } @@ -496,11 +485,6 @@ namespace FlaxEditor.CustomEditors { if (!Values.IsReferenceValueModified) return false; - - // Skip array items (show diff only on a bottom level properties and fields) - if (ParentEditor is Editors.ArrayEditor) - return false; - return true; } } @@ -512,7 +496,7 @@ namespace FlaxEditor.CustomEditors { if (!Values.HasReferenceValue) return; - RevertDiffToReference(this); + RevertDiffToReference(); } /// @@ -657,7 +641,7 @@ namespace FlaxEditor.CustomEditors // Default try { - obj = JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings); + obj = Newtonsoft.Json.JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings); } catch { @@ -762,7 +746,7 @@ namespace FlaxEditor.CustomEditors /// public void SetValueToDefault() { - SetValue(Values.DefaultValue); + SetValueCloned(Values.DefaultValue); } /// @@ -799,7 +783,19 @@ namespace FlaxEditor.CustomEditors return; } - SetValue(Values.ReferenceValue); + SetValueCloned(Values.ReferenceValue); + } + + private void SetValueCloned(object value) + { + // For objects (eg. arrays) we need to clone them to prevent editing default/reference value within editor + if (value != null && !value.GetType().IsValueType) + { + var json = JsonSerializer.Serialize(value); + value = JsonSerializer.Deserialize(json, value.GetType()); + } + + SetValue(value); } /// @@ -811,7 +807,6 @@ namespace FlaxEditor.CustomEditors { if (_isSetBlocked) return; - if (OnDirty(this, value, token)) { _hasValueDirty = true; diff --git a/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs new file mode 100644 index 000000000..1e41fcc32 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs @@ -0,0 +1,84 @@ +// 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) + { + if (_prefabId == Guid.Empty) + { + break; + } + + 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/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 6f623fb23..919da4301 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -87,6 +87,7 @@ namespace FlaxEditor.CustomEditors.Editors protected bool NotNullItems; private IntegerValueElement _size; + private PropertyNameLabel _sizeLabel; private Color _background; private int _elementsCount; private bool _readOnly; @@ -109,6 +110,9 @@ namespace FlaxEditor.CustomEditors.Editors } } + /// + public override bool RevertValueWithChildren => false; // Always revert value for a whole collection + /// public override void Initialize(LayoutElementsContainer layout) { @@ -174,7 +178,9 @@ namespace FlaxEditor.CustomEditors.Editors } else { - _size = dragArea.IntegerValue("Size"); + var sizeProperty = dragArea.AddPropertyItem("Size"); + _sizeLabel = sizeProperty.Labels.Last(); + _size = sizeProperty.IntegerValue(); _size.IntValue.MinValue = 0; _size.IntValue.MaxValue = ushort.MaxValue; _size.IntValue.Value = size; @@ -274,6 +280,15 @@ namespace FlaxEditor.CustomEditors.Editors } } + /// + protected override void Deinitialize() + { + _size = null; + _sizeLabel = null; + + base.Deinitialize(); + } + /// /// Rebuilds the parent layout if its collection. /// @@ -296,7 +311,6 @@ namespace FlaxEditor.CustomEditors.Editors { if (IsSetBlocked) return; - Resize(_size.IntValue.Value); } @@ -311,11 +325,9 @@ namespace FlaxEditor.CustomEditors.Editors return; var cloned = CloneValues(); - var tmp = cloned[dstIndex]; cloned[dstIndex] = cloned[srcIndex]; cloned[srcIndex] = tmp; - SetValue(cloned); } @@ -371,6 +383,17 @@ namespace FlaxEditor.CustomEditors.Editors if (HasDifferentValues || HasDifferentTypes) return; + // Update reference/default value indicator + if (_sizeLabel != null) + { + var color = Color.Transparent; + if (Values.HasReferenceValue && Values.ReferenceValue is IList referenceValue && referenceValue.Count != Count) + color = FlaxEngine.GUI.Style.Current.BackgroundSelected; + else if (Values.HasDefaultValue && Values.DefaultValue is IList defaultValue && defaultValue.Count != Count) + color = Color.Yellow * 0.8f; + _sizeLabel.HighlightStripColor = color; + } + // Check if collection has been resized (by UI or from external source) if (Count != _elementsCount) { 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/Editor.cpp b/Source/Editor/Editor.cpp index 527bfeb6d..2cf9b9b8f 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -404,13 +404,23 @@ int32 Editor::LoadProduct() // Create new project option if (CommandLine::Options.NewProject) + { + Array projectFiles; + FileSystem::DirectoryGetFiles(projectFiles, projectPath, TEXT("*.flaxproj"), DirectorySearchOption::TopDirectoryOnly); + if (projectFiles.Count() == 1) + { + // Skip creating new project if it already exists + LOG(Info, "Skip creatinng new project because it already exists"); + CommandLine::Options.NewProject.Reset(); + } + } + if (CommandLine::Options.NewProject) { if (projectPath.IsEmpty()) projectPath = Platform::GetWorkingDirectory(); else if (!FileSystem::DirectoryExists(projectPath)) FileSystem::CreateDirectory(projectPath); FileSystem::NormalizePath(projectPath); - String folderName = StringUtils::GetFileName(projectPath); String tmpName; for (int32 i = 0; i < folderName.Length(); i++) diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index b384b6515..393bf564e 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -1343,108 +1343,6 @@ namespace FlaxEditor public float AutoRebuildNavMeshTimeoutMs; } - [StructLayout(LayoutKind.Sequential)] - [NativeMarshalling(typeof(VisualScriptLocalMarshaller))] - internal struct VisualScriptLocal - { - public string Value; - public string ValueTypeName; - public uint NodeId; - public int BoxId; - } - - [CustomMarshaller(typeof(VisualScriptLocal), MarshalMode.Default, typeof(VisualScriptLocalMarshaller))] - internal static class VisualScriptLocalMarshaller - { - [StructLayout(LayoutKind.Sequential)] - internal struct VisualScriptLocalNative - { - public IntPtr Value; - public IntPtr ValueTypeName; - public uint NodeId; - public int BoxId; - } - - internal static VisualScriptLocal ConvertToManaged(VisualScriptLocalNative unmanaged) => ToManaged(unmanaged); - internal static VisualScriptLocalNative ConvertToUnmanaged(VisualScriptLocal managed) => ToNative(managed); - - internal static VisualScriptLocal ToManaged(VisualScriptLocalNative managed) - { - return new VisualScriptLocal() - { - Value = ManagedString.ToManaged(managed.Value), - ValueTypeName = ManagedString.ToManaged(managed.ValueTypeName), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static VisualScriptLocalNative ToNative(VisualScriptLocal managed) - { - return new VisualScriptLocalNative() - { - Value = ManagedString.ToNative(managed.Value), - ValueTypeName = ManagedString.ToNative(managed.ValueTypeName), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static void Free(VisualScriptLocalNative unmanaged) - { - ManagedString.Free(unmanaged.Value); - ManagedString.Free(unmanaged.ValueTypeName); - } - } - - [StructLayout(LayoutKind.Sequential)] - [NativeMarshalling(typeof(VisualScriptStackFrameMarshaller))] - internal struct VisualScriptStackFrame - { - public VisualScript Script; - public uint NodeId; - public int BoxId; - } - - [CustomMarshaller(typeof(VisualScriptStackFrame), MarshalMode.Default, typeof(VisualScriptStackFrameMarshaller))] - internal static class VisualScriptStackFrameMarshaller - { - [StructLayout(LayoutKind.Sequential)] - internal struct VisualScriptStackFrameNative - { - public IntPtr Script; - public uint NodeId; - public int BoxId; - } - - internal static VisualScriptStackFrame ConvertToManaged(VisualScriptStackFrameNative unmanaged) => ToManaged(unmanaged); - internal static VisualScriptStackFrameNative ConvertToUnmanaged(VisualScriptStackFrame managed) => ToNative(managed); - - internal static VisualScriptStackFrame ToManaged(VisualScriptStackFrameNative managed) - { - return new VisualScriptStackFrame() - { - Script = VisualScriptMarshaller.ConvertToManaged(managed.Script), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static VisualScriptStackFrameNative ToNative(VisualScriptStackFrame managed) - { - return new VisualScriptStackFrameNative() - { - Script = VisualScriptMarshaller.ConvertToUnmanaged(managed.Script), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static void Free(VisualScriptStackFrameNative unmanaged) - { - } - } - internal void BuildCommand(string arg) { if (TryBuildCommand(arg)) @@ -1723,21 +1621,6 @@ namespace FlaxEditor [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_RunVisualScriptBreakpointLoopTick", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] internal static partial void Internal_RunVisualScriptBreakpointLoopTick(float deltaTime); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptLocals", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = "localsCount")] - internal static partial VisualScriptLocal[] Internal_GetVisualScriptLocals(out int localsCount); - - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptStackFrames", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = "stackFrameCount")] - internal static partial VisualScriptStackFrame[] Internal_GetVisualScriptStackFrames(out int stackFrameCount); - - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptPreviousScopeFrame", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - internal static partial VisualScriptStackFrame Internal_GetVisualScriptPreviousScopeFrame(); - - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_EvaluateVisualScriptLocal", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool Internal_EvaluateVisualScriptLocal(IntPtr script, ref VisualScriptLocal local); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_DeserializeSceneObject", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] internal static partial void Internal_DeserializeSceneObject(IntPtr sceneObject, string json); diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 27878a763..66512236c 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -3,6 +3,8 @@ using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Json; +using System.Collections.Generic; namespace FlaxEditor.GUI.Dialogs { @@ -30,6 +32,8 @@ namespace FlaxEditor.GUI.Dialogs private const float HSVMargin = 0.0f; private const float ChannelsMargin = 4.0f; private const float ChannelTextWidth = 12.0f; + private const float SavedColorButtonWidth = 20.0f; + private const float SavedColorButtonHeight = 20.0f; private Color _initialValue; private Color _value; @@ -52,6 +56,9 @@ namespace FlaxEditor.GUI.Dialogs private Button _cOK; private Button _cEyedropper; + private List _savedColors = new List(); + private List 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..8a7751ecb 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 } } } @@ -1068,20 +1070,26 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu else { const auto nodes = node->GetNodes(this); - const auto nodesA = static_cast(valueA.AsPointer); - const auto nodesB = static_cast(valueB.AsPointer); - Transform t, tA, tB; + const auto basePoseNodes = static_cast(valueA.AsPointer); + const auto blendPoseNodes = static_cast(valueB.AsPointer); + const auto& refrenceNodes = _graph.BaseModel.Get()->GetNodes(); + Transform t, basePoseTransform, blendPoseTransform, refrenceTransform; 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; - t.Orientation.Normalize(); - Transform::Lerp(tA, t, alpha, nodes->Nodes[i]); + basePoseTransform = basePoseNodes->Nodes[i]; + blendPoseTransform = blendPoseNodes->Nodes[i]; + refrenceTransform = refrenceNodes[i].LocalTransform; + + // base + (blend - refrence) = transform + t.Translation = basePoseTransform.Translation + (blendPoseTransform.Translation - refrenceTransform.Translation); + auto diff = Quaternion::Invert(refrenceTransform.Orientation) * blendPoseTransform.Orientation; + t.Orientation = basePoseTransform.Orientation * diff; + t.Scale = basePoseTransform.Scale + (blendPoseTransform.Scale - refrenceTransform.Scale); + + //lerp base and transform + Transform::Lerp(basePoseTransform, t, alpha, nodes->Nodes[i]); } - Transform::Lerp(nodesA->RootMotion, nodesA->RootMotion + nodesB->RootMotion, alpha, nodes->RootMotion); + Transform::Lerp(basePoseNodes->RootMotion, basePoseNodes->RootMotion + blendPoseNodes->RootMotion, alpha, nodes->RootMotion); value = nodes; } } diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 93d904d5e..de710cb7d 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -310,6 +310,10 @@ void Asset::ChangeID(const Guid& newId) if (!IsVirtual()) CRASH; + // ID has to be unique + if (Content::GetAsset(newId) != nullptr) + CRASH; + const Guid oldId = _id; ManagedScriptingObject::ChangeID(newId); Content::onAssetChangeId(this, oldId, newId); @@ -438,12 +442,15 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const // Note: to reproduce this case just include material into material (use layering). // So during loading first material it will wait for child materials loaded calling this function + const double timeoutInSeconds = timeoutInMilliseconds * 0.001; + const double startTime = Platform::GetTimeSeconds(); Task* task = loadingTask; Array> localQueue; - while (!Engine::ShouldExit()) +#define CHECK_CONDITIONS() (!Engine::ShouldExit() && (timeoutInSeconds <= 0.0 || Platform::GetTimeSeconds() - startTime < timeoutInSeconds)) + do { // Try to execute content tasks - while (task->IsQueued() && !Engine::ShouldExit()) + while (task->IsQueued() && CHECK_CONDITIONS()) { // Dequeue task from the loading queue ContentLoadTask* tmp; @@ -494,7 +501,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const break; } } - } + } while (CHECK_CONDITIONS()); +#undef CHECK_CONDITIONS } else { 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/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index 9748ba60c..320116d60 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -892,6 +892,11 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& PrintStack(LogType::Error); break; } + if (boxBase->ID == 1) + { + value = instance; + break; + } // TODO: check if instance is of event type (including inheritance) // Add Visual Script method to the event bindings table diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 982ae599f..897817850 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -230,6 +230,7 @@ bool Content::GetAssetInfo(const Guid& id, AssetInfo& info) // Find asset in registry if (Cache.FindAsset(id, info)) return true; + PROFILE_CPU(); // Locking injects some stalls but we need to make it safe (only one thread can pass though it at once) ScopeLock lock(WorkspaceDiscoveryLocker); @@ -276,6 +277,7 @@ bool Content::GetAssetInfo(const StringView& path, AssetInfo& info) // Find asset in registry if (Cache.FindAsset(path, info)) return true; + PROFILE_CPU(); const auto extension = FileSystem::GetExtension(path).ToLower(); @@ -538,6 +540,8 @@ void Content::DeleteAsset(Asset* asset) void Content::DeleteAsset(const StringView& path) { + PROFILE_CPU(); + // Try to delete already loaded asset Asset* asset = GetAsset(path); if (asset != nullptr) @@ -566,12 +570,12 @@ void Content::DeleteAsset(const StringView& path) void Content::deleteFileSafety(const StringView& path, const Guid& id) { - // Check if given id is invalid if (!id.IsValid()) { LOG(Warning, "Cannot remove file \'{0}\'. Given ID is invalid.", path); return; } + PROFILE_CPU(); // Ensure that file has the same ID (prevent from deleting different assets) auto storage = ContentStorageManager::TryGetStorage(path); @@ -678,6 +682,7 @@ bool Content::FastTmpAssetClone(const StringView& path, String& resultPath) bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPath, const Guid& dstId) { + PROFILE_CPU(); ASSERT(FileSystem::AreFilePathsEqual(srcPath, dstPath) == false && dstId.IsValid()); LOG(Info, "Cloning asset \'{0}\' to \'{1}\'({2}).", srcPath, dstPath, dstId); @@ -796,6 +801,7 @@ Asset* Content::CreateVirtualAsset(MClass* type) Asset* Content::CreateVirtualAsset(const ScriptingTypeHandle& type) { + PROFILE_CPU(); auto& assetType = type.GetType(); // Init mock asset info diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index a47e0bd0e..9d4691be1 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,8 +1303,10 @@ FileReadStream* FlaxStorage::OpenFile() return stream; } -void FlaxStorage::CloseFileHandles() +bool FlaxStorage::CloseFileHandles() { + PROFILE_CPU(); + // 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 +1332,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 +1346,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/ImportTexture.cpp b/Source/Engine/ContentImporters/ImportTexture.cpp index 51b93420c..ebc7e514c 100644 --- a/Source/Engine/ContentImporters/ImportTexture.cpp +++ b/Source/Engine/ContentImporters/ImportTexture.cpp @@ -8,7 +8,6 @@ #include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Graphics/Textures/TextureData.h" -#include "Engine/Graphics/Textures/TextureUtils.h" #include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Content/Storage/ContentStorageManager.h" #include "Engine/ContentImporters/ImportIES.h" 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 cf45ed060..a8390f051 100644 --- a/Source/Engine/Core/Collections/Array.h +++ b/Source/Engine/Core/Collections/Array.h @@ -734,9 +734,7 @@ public: } else { - Array tmp = MoveTemp(other); - other = *this; - *this = MoveTemp(tmp); + ::Swap(other, *this); } } 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 dd73be390..d2a840fff 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -616,9 +616,7 @@ public: } else { - Dictionary tmp = MoveTemp(other); - other = *this; - *this = MoveTemp(tmp); + ::Swap(other, *this); } } 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/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/Templates.h b/Source/Engine/Core/Templates.h index 75674e2d0..876a4db83 100644 --- a/Source/Engine/Core/Templates.h +++ b/Source/Engine/Core/Templates.h @@ -304,7 +304,7 @@ template inline void Swap(T& a, T& b) noexcept { T tmp = MoveTemp(a); - a = b; + a = MoveTemp(b); b = MoveTemp(tmp); } diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index fc04d9668..026f54fec 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -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 } diff --git a/Source/Engine/Engine/Time.cpp b/Source/Engine/Engine/Time.cpp index a7c4bf80d..3adccdbd9 100644 --- a/Source/Engine/Engine/Time.cpp +++ b/Source/Engine/Engine/Time.cpp @@ -104,7 +104,7 @@ bool Time::TickData::OnTickBegin(float targetFps, float maxDeltaTime) if (targetFps > ZeroTolerance) { - int skip = (int)(1 + (time - NextBegin) / (1.0 / targetFps)); + int skip = (int)(1 + (time - NextBegin) * targetFps); NextBegin += (1.0 / targetFps) * skip; } } @@ -160,7 +160,7 @@ bool Time::FixedStepTickData::OnTickBegin(float targetFps, float maxDeltaTime) if (targetFps > ZeroTolerance) { - int skip = (int)(1 + (time - NextBegin) / (1.0 / targetFps)); + int skip = (int)(1 + (time - NextBegin) * targetFps); NextBegin += (1.0 / targetFps) * skip; } } 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/PixelFormatExtensions.cpp b/Source/Engine/Graphics/PixelFormatExtensions.cpp index 991844b22..f79530337 100644 --- a/Source/Engine/Graphics/PixelFormatExtensions.cpp +++ b/Source/Engine/Graphics/PixelFormatExtensions.cpp @@ -660,6 +660,37 @@ int PixelFormatExtensions::ComputeComponentsCount(const PixelFormat format) } } +int32 PixelFormatExtensions::ComputeBlockSize(PixelFormat format) +{ + switch (format) + { + case PixelFormat::BC1_Typeless: + case PixelFormat::BC1_UNorm: + case PixelFormat::BC1_UNorm_sRGB: + case PixelFormat::BC2_Typeless: + case PixelFormat::BC2_UNorm: + case PixelFormat::BC2_UNorm_sRGB: + case PixelFormat::BC3_Typeless: + case PixelFormat::BC3_UNorm: + case PixelFormat::BC3_UNorm_sRGB: + case PixelFormat::BC4_Typeless: + case PixelFormat::BC4_UNorm: + case PixelFormat::BC4_SNorm: + case PixelFormat::BC5_Typeless: + case PixelFormat::BC5_UNorm: + case PixelFormat::BC5_SNorm: + case PixelFormat::BC6H_Typeless: + case PixelFormat::BC6H_Uf16: + case PixelFormat::BC6H_Sf16: + case PixelFormat::BC7_Typeless: + case PixelFormat::BC7_UNorm: + case PixelFormat::BC7_UNorm_sRGB: + return 4; + default: + return 1; + } +} + PixelFormat PixelFormatExtensions::TosRGB(const PixelFormat format) { switch (format) diff --git a/Source/Engine/Graphics/PixelFormatExtensions.h b/Source/Engine/Graphics/PixelFormatExtensions.h index 604bcb2c3..362e5634f 100644 --- a/Source/Engine/Graphics/PixelFormatExtensions.h +++ b/Source/Engine/Graphics/PixelFormatExtensions.h @@ -173,6 +173,13 @@ public: /// The components count. API_FUNCTION() static int ComputeComponentsCount(PixelFormat format); + /// + /// Computes the amount of pixels per-axis stored in the a single block of the format (eg. 4 for BC-family). Returns 1 for uncompressed formats. + /// + /// The . + /// The block pixels count. + API_FUNCTION() static int32 ComputeBlockSize(PixelFormat format); + /// /// Finds the equivalent sRGB format to the provided format. /// diff --git a/Source/Engine/Graphics/PostProcessEffect.h b/Source/Engine/Graphics/PostProcessEffect.h index 2e444c0ea..1513ad64e 100644 --- a/Source/Engine/Graphics/PostProcessEffect.h +++ b/Source/Engine/Graphics/PostProcessEffect.h @@ -13,7 +13,7 @@ struct RenderContext; /// Custom PostFx which can modify final image by processing it with material based filters. The base class for all post process effects used by the graphics pipeline. Allows to extend frame rendering logic and apply custom effects such as outline, night vision, contrast etc. /// /// -/// Override this class and implement custom post fx logic. Use MainRenderTask.Instance.CustomPostFx.Add(myPostFx) to attach your script to rendering or add script to camera actor. +/// Override this class and implement custom post fx logic. Use MainRenderTask.Instance.AddCustomPostFx(myPostFx) to attach your script to rendering or add script to camera actor. /// API_CLASS(Abstract) class FLAXENGINE_API PostProcessEffect : public Script { diff --git a/Source/Engine/Graphics/RenderBuffers.cpp b/Source/Engine/Graphics/RenderBuffers.cpp index e0f4869ad..7ee900858 100644 --- a/Source/Engine/Graphics/RenderBuffers.cpp +++ b/Source/Engine/Graphics/RenderBuffers.cpp @@ -192,6 +192,9 @@ bool RenderBuffers::Init(int32 width, int32 height) _viewport = Viewport(0, 0, static_cast(width), static_cast(height)); LastEyeAdaptationTime = 0; + // Flush any pool render targets to prevent over-allocating GPU memory when resizing game viewport + RenderTargetPool::Flush(false, 4); + return result; } diff --git a/Source/Engine/Graphics/RenderTargetPool.cpp b/Source/Engine/Graphics/RenderTargetPool.cpp index fe22b4eaf..891610ab8 100644 --- a/Source/Engine/Graphics/RenderTargetPool.cpp +++ b/Source/Engine/Graphics/RenderTargetPool.cpp @@ -7,35 +7,31 @@ struct Entry { - bool IsOccupied; GPUTexture* RT; - uint64 LastFrameTaken; uint64 LastFrameReleased; uint32 DescriptionHash; + bool IsOccupied; }; namespace { - Array TemporaryRTs(64); + Array TemporaryRTs; } -void RenderTargetPool::Flush(bool force) +void RenderTargetPool::Flush(bool force, int32 framesOffset) { - const uint64 framesOffset = 3 * 60; + if (framesOffset < 0) + framesOffset = 3 * 60; // For how many frames RTs should be cached (by default) const uint64 maxReleaseFrame = Engine::FrameCount - framesOffset; force |= Engine::ShouldExit(); for (int32 i = 0; i < TemporaryRTs.Count(); i++) { - auto& tmp = TemporaryRTs[i]; - - if (!tmp.IsOccupied && (force || (tmp.LastFrameReleased < maxReleaseFrame))) + const auto& e = TemporaryRTs[i]; + if (!e.IsOccupied && (force || e.LastFrameReleased < maxReleaseFrame)) { - // Release - tmp.RT->DeleteObjectNow(); - TemporaryRTs.RemoveAt(i); - i--; - + e.RT->DeleteObjectNow(); + TemporaryRTs.RemoveAt(i--); if (TemporaryRTs.IsEmpty()) break; } @@ -48,19 +44,14 @@ GPUTexture* RenderTargetPool::Get(const GPUTextureDescription& desc) const uint32 descHash = GetHash(desc); for (int32 i = 0; i < TemporaryRTs.Count(); i++) { - auto& tmp = TemporaryRTs[i]; - - if (!tmp.IsOccupied && tmp.DescriptionHash == descHash) + auto& e = TemporaryRTs[i]; + if (!e.IsOccupied && e.DescriptionHash == descHash) { - ASSERT(tmp.RT); - // Mark as used - tmp.IsOccupied = true; - tmp.LastFrameTaken = Engine::FrameCount; - return tmp.RT; + e.IsOccupied = true; + return e.RT; } } - #if !BUILD_RELEASE if (TemporaryRTs.Count() > 2000) { @@ -71,24 +62,23 @@ GPUTexture* RenderTargetPool::Get(const GPUTextureDescription& desc) // Create new rt const String name = TEXT("TemporaryRT_") + StringUtils::ToString(TemporaryRTs.Count()); - auto newRenderTarget = GPUDevice::Instance->CreateTexture(name); - if (newRenderTarget->Init(desc)) + GPUTexture* rt = GPUDevice::Instance->CreateTexture(name); + if (rt->Init(desc)) { - Delete(newRenderTarget); + Delete(rt); LOG(Error, "Cannot create temporary render target. Description: {0}", desc.ToString()); return nullptr; } // Create temporary rt entry - Entry entry; - entry.IsOccupied = true; - entry.LastFrameReleased = 0; - entry.LastFrameTaken = Engine::FrameCount; - entry.RT = newRenderTarget; - entry.DescriptionHash = descHash; - TemporaryRTs.Add(entry); + Entry e; + e.IsOccupied = true; + e.LastFrameReleased = 0; + e.RT = rt; + e.DescriptionHash = descHash; + TemporaryRTs.Add(e); - return newRenderTarget; + return rt; } void RenderTargetPool::Release(GPUTexture* rt) @@ -98,14 +88,13 @@ void RenderTargetPool::Release(GPUTexture* rt) for (int32 i = 0; i < TemporaryRTs.Count(); i++) { - auto& tmp = TemporaryRTs[i]; - - if (tmp.RT == rt) + auto& e = TemporaryRTs[i]; + if (e.RT == rt) { // Mark as free - ASSERT(tmp.IsOccupied); - tmp.IsOccupied = false; - tmp.LastFrameReleased = Engine::FrameCount; + ASSERT(e.IsOccupied); + e.IsOccupied = false; + e.LastFrameReleased = Engine::FrameCount; return; } } diff --git a/Source/Engine/Graphics/RenderTargetPool.h b/Source/Engine/Graphics/RenderTargetPool.h index 345e5c81e..4ff69e2ea 100644 --- a/Source/Engine/Graphics/RenderTargetPool.h +++ b/Source/Engine/Graphics/RenderTargetPool.h @@ -15,7 +15,8 @@ public: /// Flushes the temporary render targets. /// /// True if release unused render targets by force, otherwise will use a few frames of delay. - static void Flush(bool force = false); + /// Amount of previous frames that should persist in the pool after flush. Resources used more than given value wil be freed. Use value of -1 to auto pick default duration. + static void Flush(bool force = false, int32 framesOffset = -1); /// /// Gets a temporary render target. diff --git a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp index caebcfc3c..033d50466 100644 --- a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp +++ b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp @@ -115,27 +115,20 @@ bool ShaderAssetBase::Save() bool IsValidShaderCache(DataContainer& shaderCache, Array& includes) { if (shaderCache.Length() == 0) - { return false; - } - MemoryReadStream stream(shaderCache.Get(), shaderCache.Length()); // Read cache format version int32 version; stream.ReadInt32(&version); if (version != GPU_SHADER_CACHE_VERSION) - { return false; - } // Read the location of additional data that contains list of included source files int32 additionalDataStart; stream.ReadInt32(&additionalDataStart); stream.SetPosition(additionalDataStart); - bool result = true; - // Read all includes int32 includesCount; stream.ReadInt32(&includesCount); @@ -144,28 +137,16 @@ bool IsValidShaderCache(DataContainer& shaderCache, Array& include { String& include = includes.AddOne(); stream.ReadString(&include, 11); + include = ShadersCompilation::ResolveShaderPath(include); DateTime lastEditTime; stream.Read(lastEditTime); // Check if included file exists locally and has been modified since last compilation if (FileSystem::FileExists(include) && FileSystem::GetFileLastEditTime(include) > lastEditTime) - { - result = false; - } + return false; } -#if 0 - // Check duplicates - for (int32 i = 0; i < includes.Count(); i++) - { - if (includes.FindLast(includes[i]) != i) - { - CRASH; - } - } -#endif - - return result; + return true; } #endif diff --git a/Source/Engine/Graphics/Textures/GPUTexture.cpp b/Source/Engine/Graphics/Textures/GPUTexture.cpp index ccdf4be95..403f545ba 100644 --- a/Source/Engine/Graphics/Textures/GPUTexture.cpp +++ b/Source/Engine/Graphics/Textures/GPUTexture.cpp @@ -480,6 +480,16 @@ bool GPUTexture::Init(const GPUTextureDescription& desc) break; } } + const bool isCompressed = PixelFormatExtensions::IsCompressed(desc.Format); + if (isCompressed) + { + const int32 blockSize = PixelFormatExtensions::ComputeBlockSize(desc.Format); + if (desc.Width < blockSize || desc.Height < blockSize) + { + LOG(Warning, "Cannot create texture. Invalid dimensions. Description: {0}", desc.ToString()); + return true; + } + } // Release previous data ReleaseGPU(); @@ -487,7 +497,7 @@ bool GPUTexture::Init(const GPUTextureDescription& desc) // Initialize _desc = desc; _sRGB = PixelFormatExtensions::IsSRGB(desc.Format); - _isBlockCompressed = PixelFormatExtensions::IsCompressed(desc.Format); + _isBlockCompressed = isCompressed; if (OnInit()) { ReleaseGPU(); diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 015386fff..ea70a9d9e 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -114,9 +114,9 @@ bool StreamingTexture::Create(const TextureHeader& header) { // Ensure that streaming doesn't go too low because the hardware expects the texture to be min in size of compressed texture block int32 lastMip = header.MipLevels - 1; - while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4) + while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4 && lastMip > 0) lastMip--; - _minMipCountBlockCompressed = header.MipLevels - lastMip + 1; + _minMipCountBlockCompressed = Math::Min(header.MipLevels - lastMip + 1, header.MipLevels); } // Request resource streaming @@ -296,6 +296,7 @@ Task* StreamingTexture::UpdateAllocation(int32 residency) // Setup texture if (texture->Init(desc)) { + Streaming.Error = true; LOG(Error, "Cannot allocate texture {0}.", ToString()); } if (allocatedResidency != 0) diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp index 181955fce..c3d8898a5 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.cpp +++ b/Source/Engine/Graphics/Textures/TextureBase.cpp @@ -223,6 +223,11 @@ void TextureBase::SetTextureGroup(int32 textureGroup) } } +bool TextureBase::HasStreamingError() const +{ + return _texture.Streaming.Error; +} + BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& slicePitch) { BytesContainer result; diff --git a/Source/Engine/Graphics/Textures/TextureBase.h b/Source/Engine/Graphics/Textures/TextureBase.h index e47bb548d..befe16069 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.h +++ b/Source/Engine/Graphics/Textures/TextureBase.h @@ -148,6 +148,11 @@ public: /// API_PROPERTY() void SetTextureGroup(int32 textureGroup); + /// + /// Returns true if texture streaming failed (eg. pixel format is unsupported or texture data cannot be uploaded to GPU due to memory limit). + /// + API_PROPERTY() bool HasStreamingError() const; + public: /// /// Gets the mip data. diff --git a/Source/Engine/Graphics/Textures/TextureUtils.h b/Source/Engine/Graphics/Textures/TextureUtils.h deleted file mode 100644 index b021d27fe..000000000 --- a/Source/Engine/Graphics/Textures/TextureUtils.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#pragma once - -#include "Engine/Core/Types/BaseTypes.h" -#include "Types.h" - -/// -/// Texture utilities class -/// -class TextureUtils -{ -public: - static PixelFormat ToPixelFormat(const TextureFormatType format, int32 width, int32 height, bool canCompress) - { - const bool canUseBlockCompression = width % 4 == 0 && height % 4 == 0; - if (canCompress && canUseBlockCompression) - { - switch (format) - { - case TextureFormatType::ColorRGB: - return PixelFormat::BC1_UNorm; - case TextureFormatType::ColorRGBA: - return PixelFormat::BC3_UNorm; - case TextureFormatType::NormalMap: - return PixelFormat::BC5_UNorm; - case TextureFormatType::GrayScale: - return PixelFormat::BC4_UNorm; - case TextureFormatType::HdrRGBA: - return PixelFormat::BC7_UNorm; - case TextureFormatType::HdrRGB: -#if PLATFORM_LINUX - // TODO: support BC6H compression for Linux Editor - return PixelFormat::BC7_UNorm; -#else - return PixelFormat::BC6H_Uf16; -#endif - default: - return PixelFormat::Unknown; - } - } - - switch (format) - { - case TextureFormatType::ColorRGB: - return PixelFormat::R8G8B8A8_UNorm; - case TextureFormatType::ColorRGBA: - return PixelFormat::R8G8B8A8_UNorm; - case TextureFormatType::NormalMap: - return PixelFormat::R16G16_UNorm; - case TextureFormatType::GrayScale: - return PixelFormat::R8_UNorm; - case TextureFormatType::HdrRGBA: - return PixelFormat::R16G16B16A16_Float; - case TextureFormatType::HdrRGB: - return PixelFormat::R11G11B10_Float; - default: - return PixelFormat::Unknown; - } - } -}; 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_parent == this); - e->_parent = nullptr; - e->DeleteObject(); + auto script = Scripts[i]; + ASSERT(script->_parent == this); + if (script->_wasAwakeCalled) + { + script->_wasAwakeCalled = false; + CHECK_EXECUTE_IN_EDITOR + { + script->OnDestroy(); + } + } + script->_parent = nullptr; + script->DeleteObject(); } #if BUILD_DEBUG ASSERT(callsCheck == Scripts.Count()); @@ -239,14 +247,8 @@ const Guid& Actor::GetSceneObjectId() const void Actor::SetParent(Actor* value, bool worldPositionsStays, bool canBreakPrefabLink) { - // Check if value won't change if (_parent == value) return; - if (IsDuringPlay() && !IsInMainThread()) - { - LOG(Error, "Editing scene hierarchy is only allowed on a main thread."); - return; - } #if USE_EDITOR || !BUILD_RELEASE if (Is()) { @@ -265,6 +267,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) @@ -888,9 +897,13 @@ void Actor::EndPlay() for (auto* script : Scripts) { - CHECK_EXECUTE_IN_EDITOR + if (script->_wasAwakeCalled) { - script->OnDestroy(); + script->_wasAwakeCalled = false; + CHECK_EXECUTE_IN_EDITOR + { + script->OnDestroy(); + } } } @@ -1033,7 +1046,10 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) } else if (!parent && parentId.IsValid()) { - LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); + if (_prefabObjectID.IsValid()) + LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID); + else + LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); } } } 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/Level.cpp b/Source/Engine/Level/Level.cpp index 5f1432d31..41033876d 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -439,7 +439,7 @@ public: bool Do() const override { - auto scene = Scripting::FindObject(TargetScene); + auto scene = Level::FindScene(TargetScene); if (!scene) return true; return unloadScene(scene); @@ -934,13 +934,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou // Loaded scene objects list CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); - const int32 objectsCount = (int32)data.Size(); - sceneObjects->Resize(objectsCount); + const int32 dataCount = (int32)data.Size(); + sceneObjects->Resize(dataCount); sceneObjects->At(0) = scene; // Spawn all scene objects SceneObjectsFactory::Context context(modifier.Value); - context.Async = JobSystem::GetThreadsCount() > 1 && objectsCount > 10; + context.Async = JobSystem::GetThreadsCount() > 1 && dataCount > 10; { PROFILE_CPU_NAMED("Spawn"); SceneObject** objects = sceneObjects->Get(); @@ -963,12 +963,12 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou } else SceneObjectsFactory::HandleObjectDeserializationError(stream); - }, objectsCount - 1); + }, dataCount - 1); ScenesLock.Lock(); } else { - for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene + for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene { auto& stream = data[i]; auto obj = SceneObjectsFactory::Spawn(context, stream); @@ -1012,13 +1012,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou SceneObjectsFactory::Deserialize(context, obj, data[i]); idMapping = nullptr; } - }, objectsCount - 1); + }, dataCount - 1); ScenesLock.Lock(); } else { Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); - for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene + for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene { auto& objData = data[i]; auto obj = objects[i]; @@ -1049,7 +1049,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou PROFILE_CPU_NAMED("Initialize"); SceneObject** objects = sceneObjects->Get(); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { SceneObject* obj = objects[i]; if (obj) diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index c10ea532d..35b02bf40 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; } } @@ -1219,14 +1230,14 @@ bool Prefab::SyncChangesInternal(PrefabInstancesData& prefabInstancesData) { ScopeLock lock(Locker); _isCreatingDefaultInstance = true; - _defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache, true); + _defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache, true); _isCreatingDefaultInstance = false; } // Instantiate prefab instance from prefab (default spawning logic) // Note: it will get any added or removed objects from the nested prefabs // TODO: try to optimize by using recreated default instance to ApplyAllInternal (will need special path there if apply is done with default instance to unlink it instead of destroying) - const auto targetActor = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, nullptr, true); + const auto targetActor = PrefabManager::SpawnPrefab(this, nullptr, nullptr, true); if (targetActor == nullptr) { LOG(Warning, "Failed to instantiate default prefab instance from changes synchronization."); diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index b72f16633..25a2bd086 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -76,7 +76,7 @@ Actor* Prefab::GetDefaultInstance() _isCreatingDefaultInstance = true; // Instantiate objects from prefab (default spawning logic) - _defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache); + _defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache); _isCreatingDefaultInstance = false; return _defaultInstance; @@ -87,17 +87,12 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId) const auto result = GetDefaultInstance(); if (!result) return nullptr; - if (objectId.IsValid()) { - const void* object; + SceneObject* object; if (ObjectsCache.TryGet(objectId, object)) - { - // Actor or Script - return (SceneObject*)object; - } + return object; } - return result; } 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..d99a89e39 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) @@ -94,8 +94,8 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac LOG(Warning, "Waiting for prefab asset be loaded failed. {0}", prefab->ToString()); return nullptr; } - const int32 objectsCount = prefab->ObjectsCount; - if (objectsCount == 0) + const int32 dataCount = prefab->ObjectsCount; + if (dataCount == 0) { LOG(Warning, "Prefab has no objects. {0}", prefab->ToString()); return nullptr; @@ -107,7 +107,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac // Prepare CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); - sceneObjects->Resize(objectsCount); + sceneObjects->Resize(dataCount); CollectionPoolCache::ScopeCache modifier = Cache::ISerializeModifier.Get(); modifier->EngineBuild = prefab->DataEngineBuild; modifier->IdsMapping.EnsureCapacity(prefab->ObjectsIds.Count() * 4); @@ -126,7 +126,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac // Deserialize prefab objects auto prevIdMapping = Scripting::ObjectsLookupIdMapping.Get(); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = SceneObjectsFactory::Spawn(context, stream); @@ -145,7 +145,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); } - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = sceneObjects->At(i); @@ -154,28 +154,6 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac } Scripting::ObjectsLookupIdMapping.Set(prevIdMapping); - // Pick prefab root object - if (sceneObjects->IsEmpty()) - { - LOG(Warning, "No valid objects in prefab."); - return nullptr; - } - Actor* root = nullptr; - const Guid prefabRootObjectId = prefab->GetRootObjectId(); - for (int32 i = 0; i < objectsCount; i++) - { - if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId) - { - root = dynamic_cast(sceneObjects->At(i)); - break; - } - } - if (!root) - { - LOG(Warning, "Missing prefab root object."); - return nullptr; - } - // Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it) if (withSynchronization) { @@ -183,6 +161,30 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData); } + // Pick prefab root object + Actor* root = nullptr; + const Guid prefabRootObjectId = prefab->GetRootObjectId(); + for (int32 i = 0; i < dataCount && !root; i++) + { + if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId) + root = dynamic_cast(sceneObjects->At(i)); + } + if (!root) + { + // Fallback to the first actor that has no parent + for (int32 i = 0; i < sceneObjects->Count() && !root; i++) + { + SceneObject* obj = sceneObjects->At(i); + if (obj && !obj->GetParent()) + root = dynamic_cast(obj); + } + } + if (!root) + { + LOG(Warning, "Missing prefab root object. {0}", prefab->ToString()); + return nullptr; + } + // Prepare parent linkage for prefab root actor if (root->_parent) root->_parent->Children.Remove(root); @@ -264,7 +266,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac } // Link objects to prefab (only deserialized from prefab data) - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = sceneObjects->At(i); diff --git a/Source/Engine/Level/Prefabs/PrefabManager.h b/Source/Engine/Level/Prefabs/PrefabManager.h index 16d4a29cf..ad990da9f 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 = true); /// /// 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 = true); #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 2ae947c85..d9c86d250 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