diff --git a/Flax.flaxproj b/Flax.flaxproj index e5d367168..ebcdc2830 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -3,7 +3,7 @@ "Version": { "Major": 1, "Minor": 7, - "Build": 6400 + "Build": 6401 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", diff --git a/Source/Editor/Content/GUI/ContentView.cs b/Source/Editor/Content/GUI/ContentView.cs index 5e054e5c6..e1a3acdf6 100644 --- a/Source/Editor/Content/GUI/ContentView.cs +++ b/Source/Editor/Content/GUI/ContentView.cs @@ -520,8 +520,8 @@ namespace FlaxEditor.Content.GUI { int min = _selection.Min(x => x.IndexInParent); int max = _selection.Max(x => x.IndexInParent); - min = Mathf.Min(min, item.IndexInParent); - max = Mathf.Max(max, item.IndexInParent); + min = Mathf.Max(Mathf.Min(min, item.IndexInParent), 0); + max = Mathf.Min(Mathf.Max(max, item.IndexInParent), _children.Count - 1); var selection = new List(_selection); for (int i = min; i <= max; i++) { diff --git a/Source/Editor/Content/Import/AudioImportSettings.cs b/Source/Editor/Content/Import/AudioImportSettings.cs index 2fc0a1795..e0bfb6e20 100644 --- a/Source/Editor/Content/Import/AudioImportSettings.cs +++ b/Source/Editor/Content/Import/AudioImportSettings.cs @@ -1,144 +1,52 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. -using System.ComponentModel; -using System.Reflection; -using System.Runtime.InteropServices; +using System.Collections.Generic; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.Scripting; using FlaxEngine; -using FlaxEngine.Interop; +using FlaxEngine.Tools; + +namespace FlaxEngine.Tools +{ + partial class AudioTool + { + partial struct Options + { + private bool ShowBtiDepth => Format != AudioFormat.Vorbis; + } + } +} + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + [CustomEditor(typeof(FlaxEngine.Tools.AudioTool.Options)), DefaultEditor] + public class AudioToolOptionsEditor : GenericEditor + { + /// + protected override List GetItemsForType(ScriptType type) + { + // Show both fields and properties + return GetItemsForType(type, true, true); + } + } +} namespace FlaxEditor.Content.Import { /// /// Proxy object to present audio import settings in . /// + [HideInEditor] public class AudioImportSettings { /// - /// A custom set of bit depth audio import sizes. + /// The settings data. /// - public enum CustomBitDepth - { - /// - /// The 8. - /// - _8 = 8, - - /// - /// The 16. - /// - _16 = 16, - - /// - /// The 24. - /// - _24 = 24, - - /// - /// The 32. - /// - _32 = 32, - } - - /// - /// Converts the bit depth to enum. - /// - /// The bit depth. - /// The converted enum. - public static CustomBitDepth ConvertBitDepth(int f) - { - FieldInfo[] fields = typeof(CustomBitDepth).GetFields(); - for (int i = 0; i < fields.Length; i++) - { - var field = fields[i]; - if (field.Name.Equals("value__")) - continue; - - if (f == (int)field.GetRawConstantValue()) - return (CustomBitDepth)f; - } - - return CustomBitDepth._16; - } - - /// - /// The audio data format to import the audio clip as. - /// - [EditorOrder(10), DefaultValue(AudioFormat.Vorbis), Tooltip("The audio data format to import the audio clip as.")] - public AudioFormat Format { get; set; } = AudioFormat.Vorbis; - - /// - /// The audio data compression quality. Used only if target format is using compression. Value 0 means the smallest size, value 1 means the best quality. - /// - [EditorOrder(15), DefaultValue(0.4f), Limit(0, 1, 0.01f), Tooltip("The audio data compression quality. Used only if target format is using compression. Value 0 means the smallest size, value 1 means the best quality.")] - public float CompressionQuality { get; set; } = 0.4f; - - /// - /// Disables dynamic audio streaming. The whole clip will be loaded into the memory. Useful for small clips (eg. gunfire sounds). - /// - [EditorOrder(20), DefaultValue(false), Tooltip("Disables dynamic audio streaming. The whole clip will be loaded into the memory. Useful for small clips (eg. gunfire sounds).")] - public bool DisableStreaming { get; set; } = false; - - /// - /// Checks should the clip be played as spatial (3D) audio or as normal audio. 3D audio is stored in Mono format. - /// - [EditorOrder(30), DefaultValue(false), EditorDisplay(null, "Is 3D"), Tooltip("Checks should the clip be played as spatial (3D) audio or as normal audio. 3D audio is stored in Mono format.")] - public bool Is3D { get; set; } = false; - - /// - /// The size of a single sample in bits. The clip will be converted to this bit depth on import. - /// - [EditorOrder(40), DefaultValue(CustomBitDepth._16), Tooltip("The size of a single sample in bits. The clip will be converted to this bit depth on import.")] - public CustomBitDepth BitDepth { get; set; } = CustomBitDepth._16; - - [StructLayout(LayoutKind.Sequential)] - internal struct InternalOptions - { - [MarshalAs(UnmanagedType.I1)] - public AudioFormat Format; - public byte DisableStreaming; - public byte Is3D; - public int BitDepth; - public float Quality; - } - - internal void ToInternal(out InternalOptions options) - { - options = new InternalOptions - { - Format = Format, - DisableStreaming = (byte)(DisableStreaming ? 1 : 0), - Is3D = (byte)(Is3D ? 1 : 0), - Quality = CompressionQuality, - BitDepth = (int)BitDepth, - }; - } - - internal void FromInternal(ref InternalOptions options) - { - Format = options.Format; - DisableStreaming = options.DisableStreaming != 0; - Is3D = options.Is3D != 0; - CompressionQuality = options.Quality; - BitDepth = ConvertBitDepth(options.BitDepth); - } - - /// - /// Tries the restore the asset import options from the target resource file. - /// - /// The options. - /// The asset path. - /// True settings has been restored, otherwise false. - public static bool TryRestore(ref AudioImportSettings options, string assetPath) - { - if (AudioImportEntry.Internal_GetAudioImportOptions(assetPath, out var internalOptions)) - { - // Restore settings - options.FromInternal(ref internalOptions); - return true; - } - - return false; - } + [EditorDisplay(null, EditorDisplayAttribute.InlineStyle)] + public AudioTool.Options Settings = AudioTool.Options.Default; } /// @@ -147,7 +55,7 @@ namespace FlaxEditor.Content.Import /// public partial class AudioImportEntry : AssetImportEntry { - private AudioImportSettings _settings = new AudioImportSettings(); + private AudioImportSettings _settings = new(); /// /// Initializes a new instance of the class. @@ -157,7 +65,7 @@ namespace FlaxEditor.Content.Import : base(ref request) { // Try to restore target asset Audio import options (useful for fast reimport) - AudioImportSettings.TryRestore(ref _settings, ResultUrl); + Editor.TryRestoreImportOptions(ref _settings.Settings, ResultUrl); } /// @@ -166,27 +74,23 @@ namespace FlaxEditor.Content.Import /// public override bool TryOverrideSettings(object settings) { - if (settings is AudioImportSettings o) + if (settings is AudioImportSettings s) { - _settings = o; + _settings.Settings = s.Settings; + return true; + } + if (settings is AudioTool.Options o) + { + _settings.Settings = o; return true; } - return false; } /// public override bool Import() { - return Editor.Import(SourceUrl, ResultUrl, _settings); + return Editor.Import(SourceUrl, ResultUrl, _settings.Settings); } - - #region Internal Calls - - [LibraryImport("FlaxEngine", EntryPoint = "AudioImportEntryInternal_GetAudioImportOptions", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool Internal_GetAudioImportOptions(string path, out AudioImportSettings.InternalOptions result); - - #endregion } } diff --git a/Source/Editor/Content/Proxy/CubeTextureProxy.cs b/Source/Editor/Content/Proxy/CubeTextureProxy.cs index 691e4ac53..0477ad18e 100644 --- a/Source/Editor/Content/Proxy/CubeTextureProxy.cs +++ b/Source/Editor/Content/Proxy/CubeTextureProxy.cs @@ -54,12 +54,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - if (!_preview.HasLoadedAssets) - return false; - - // Check if all mip maps are streamed - var asset = (CubeTexture)request.Asset; - return asset.ResidentMipLevels >= Mathf.Max(1, (int)(asset.MipLevels * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Texture)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs index dcc3e3ec4..331ff81c3 100644 --- a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs @@ -62,7 +62,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - return _preview.HasLoadedAssets; + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((MaterialInstance)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/MaterialProxy.cs b/Source/Editor/Content/Proxy/MaterialProxy.cs index cf7c8b77e..a7fcfecc8 100644 --- a/Source/Editor/Content/Proxy/MaterialProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialProxy.cs @@ -106,7 +106,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - return _preview.HasLoadedAssets; + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Material)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index 845cbc80b..0cf16850d 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -82,12 +82,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - if (!_preview.HasLoadedAssets) - return false; - - // Check if asset is streamed enough - var asset = (Model)request.Asset; - return asset.LoadedLODs >= Mathf.Max(1, (int)(asset.LODs.Length * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Model)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs index f0193d0d0..6e228aa86 100644 --- a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs +++ b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs @@ -54,15 +54,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - if (!_preview.HasLoadedAssets) - return false; - - // Check if asset is streamed enough - var asset = (SkinnedModel)request.Asset; - var lods = asset.LODs.Length; - if (asset.IsLoaded && lods == 0) - return true; // Skeleton-only model - return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((SkinnedModel)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/TextureProxy.cs b/Source/Editor/Content/Proxy/TextureProxy.cs index 48bfcc9e9..709ca4dbf 100644 --- a/Source/Editor/Content/Proxy/TextureProxy.cs +++ b/Source/Editor/Content/Proxy/TextureProxy.cs @@ -57,11 +57,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - // Check if asset is streamed enough - var asset = (Texture)request.Asset; - var mipLevels = asset.MipLevels; - var minMipLevels = Mathf.Min(mipLevels, 7); - return asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return ThumbnailsModule.HasMinimumQuality((Texture)request.Asset); } /// diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index fa289bb3d..160c783ed 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -125,6 +125,74 @@ namespace FlaxEditor.Content.Thumbnails } } + internal static bool HasMinimumQuality(TextureBase asset) + { + var mipLevels = asset.MipLevels; + var minMipLevels = Mathf.Min(mipLevels, 7); + return asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality)); + } + + internal static bool HasMinimumQuality(Model asset) + { + if (!asset.IsLoaded) + return false; + var lods = asset.LODs.Length; + var slots = asset.MaterialSlots; + foreach (var slot in slots) + { + if (slot.Material && !HasMinimumQuality(slot.Material)) + return false; + } + return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality)); + } + + internal static bool HasMinimumQuality(SkinnedModel asset) + { + var lods = asset.LODs.Length; + if (asset.IsLoaded && lods == 0) + return true; // Skeleton-only model + var slots = asset.MaterialSlots; + foreach (var slot in slots) + { + if (slot.Material && !HasMinimumQuality(slot.Material)) + return false; + } + return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality)); + } + + internal static bool HasMinimumQuality(MaterialBase asset) + { + if (asset is MaterialInstance asInstance) + return HasMinimumQuality(asInstance); + return HasMinimumQualityInternal(asset); + } + + internal static bool HasMinimumQuality(Material asset) + { + return HasMinimumQualityInternal(asset); + } + + internal static bool HasMinimumQuality(MaterialInstance asset) + { + if (!HasMinimumQualityInternal(asset)) + return false; + var baseMaterial = asset.BaseMaterial; + return baseMaterial == null || HasMinimumQualityInternal(baseMaterial); + } + + private static bool HasMinimumQualityInternal(MaterialBase asset) + { + if (!asset.IsLoaded) + return false; + var parameters = asset.Parameters; + foreach (var parameter in parameters) + { + if (parameter.Value is TextureBase asTexture && !HasMinimumQuality(asTexture)) + return false; + } + return true; + } + #region IContentItemOwner /// diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 24fbfc965..1b46222ea 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using FlaxEditor.Content; -using FlaxEditor.Content.Import; using FlaxEditor.Content.Settings; using FlaxEditor.Content.Thumbnails; using FlaxEditor.Modules; @@ -154,12 +153,12 @@ namespace FlaxEditor public ContentFindingModule ContentFinding; /// - /// The scripts editing + /// The scripts editing. /// public CodeEditingModule CodeEditing; /// - /// The scripts documentation + /// The scripts documentation. /// public CodeDocsModule CodeDocs; @@ -179,7 +178,7 @@ namespace FlaxEditor public ProjectCacheModule ProjectCache; /// - /// The undo/redo + /// The undo/redo. /// public EditorUndo Undo; @@ -939,21 +938,6 @@ namespace FlaxEditor BehaviorTree = 12, } - /// - /// Imports the audio asset file to the target location. - /// - /// The source file path. - /// The result asset file path. - /// The settings. - /// True if importing failed, otherwise false. - public static bool Import(string inputPath, string outputPath, AudioImportSettings settings) - { - if (settings == null) - throw new ArgumentNullException(); - settings.ToInternal(out var internalOptions); - return Internal_ImportAudio(inputPath, outputPath, ref internalOptions); - } - /// /// Serializes the given object to json asset. /// @@ -1671,10 +1655,6 @@ namespace FlaxEditor [return: MarshalAs(UnmanagedType.U1)] internal static partial bool Internal_CloneAssetFile(string dstPath, string srcPath, ref Guid dstId); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_ImportAudio", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool Internal_ImportAudio(string inputPath, string outputPath, ref AudioImportSettings.InternalOptions options); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetAudioClipMetadata", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] internal static partial void Internal_GetAudioClipMetadata(IntPtr obj, out int originalSize, out int importedSize); diff --git a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs index f6a542faa..5ca9ff7ce 100644 --- a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs @@ -125,6 +125,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks fov = cam.FieldOfView; customAspectRatio = cam.CustomAspectRatio; view.RenderLayersMask = cam.RenderLayersMask; + view.Flags = cam.RenderFlags; + view.Mode = cam.RenderMode; } // Try to evaluate camera properties based on the animated tracks diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index e2c14f0dc..e01fd04bb 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -50,43 +50,6 @@ Guid ManagedEditor::ObjectID(0x91970b4e, 0x99634f61, 0x84723632, 0x54c776af); -// Disable warning C4800: 'const byte': forcing value to bool 'true' or 'false' (performance warning) -#if defined(_MSC_VER) -#pragma warning( push ) -#pragma warning( disable : 4800) -#endif - -struct InternalAudioOptions -{ - AudioFormat Format; - byte DisableStreaming; - byte Is3D; - int32 BitDepth; - float Quality; - - static void Convert(InternalAudioOptions* from, ImportAudio::Options* to) - { - to->Format = from->Format; - to->DisableStreaming = from->DisableStreaming; - to->Is3D = from->Is3D; - to->BitDepth = from->BitDepth; - to->Quality = from->Quality; - } - - static void Convert(ImportAudio::Options* from, InternalAudioOptions* to) - { - to->Format = from->Format; - to->DisableStreaming = from->DisableStreaming; - to->Is3D = from->Is3D; - to->BitDepth = from->BitDepth; - to->Quality = from->Quality; - } -}; - -#if defined(_MSC_VER) -#pragma warning( pop ) -#endif - // Pack log messages into a single scratch buffer to reduce dynamic memory allocations CriticalSection CachedLogDataLocker; Array CachedLogData; @@ -299,16 +262,6 @@ DEFINE_INTERNAL_CALL(MString*) EditorInternal_CanImport(MString* extensionObj) return importer ? MUtils::ToString(importer->ResultExtension) : nullptr; } -DEFINE_INTERNAL_CALL(bool) EditorInternal_ImportAudio(MString* inputPathObj, MString* outputPathObj, InternalAudioOptions* optionsObj) -{ - ImportAudio::Options options; - InternalAudioOptions::Convert(optionsObj, &options); - String inputPath, outputPath; - MUtils::ToString(inputPathObj, inputPath); - MUtils::ToString(outputPathObj, outputPath); - return ManagedEditor::Import(inputPath, outputPath, &options); -} - DEFINE_INTERNAL_CALL(void) EditorInternal_GetAudioClipMetadata(AudioClip* clip, int32* originalSize, int32* importedSize) { INTERNAL_CALL_CHECK(clip); @@ -769,24 +722,6 @@ DEFINE_INTERNAL_CALL(MTypeObject*) CustomEditorsUtilInternal_GetCustomEditor(MTy return CustomEditorsUtil::GetCustomEditor(targetType); } -DEFINE_INTERNAL_CALL(bool) AudioImportEntryInternal_GetAudioImportOptions(MString* pathObj, InternalAudioOptions* result) -{ - String path; - MUtils::ToString(pathObj, path); - FileSystem::NormalizePath(path); - - ImportAudio::Options options; - if (ImportAudio::TryGetImportOptions(path, options)) - { - // Convert into managed storage - InternalAudioOptions::Convert(&options, result); - - return true; - } - - return false; -} - DEFINE_INTERNAL_CALL(MArray*) LayersAndTagsSettingsInternal_GetCurrentLayers(int* layersCount) { *layersCount = Math::Max(1, Level::GetNonEmptyLayerNamesCount()); @@ -834,3 +769,15 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String FileSystem::NormalizePath(assetPath); return ImportModelFile::TryGetImportOptions(assetPath, options); } + +bool ManagedEditor::Import(const String& inputPath, const String& outputPath, const AudioTool::Options& options) +{ + return Import(inputPath, outputPath, (void*)&options); +} + +bool ManagedEditor::TryRestoreImportOptions(AudioTool::Options& options, String assetPath) +{ + // Get options from model + FileSystem::NormalizePath(assetPath); + return ImportAudio::TryGetImportOptions(assetPath, options); +} diff --git a/Source/Editor/Managed/ManagedEditor.h b/Source/Editor/Managed/ManagedEditor.h index ddd94b47e..93d1723a3 100644 --- a/Source/Editor/Managed/ManagedEditor.h +++ b/Source/Editor/Managed/ManagedEditor.h @@ -7,6 +7,7 @@ #include "Engine/ShadowsOfMordor/Types.h" #include "Engine/Tools/TextureTool/TextureTool.h" #include "Engine/Tools/ModelTool/ModelTool.h" +#include "Engine/Tools/AudioTool/AudioTool.h" namespace CSG { @@ -190,6 +191,25 @@ public: API_FUNCTION() static bool TryRestoreImportOptions(API_PARAM(Ref) ModelTool::Options& options, String assetPath); #endif +#if COMPILE_WITH_AUDIO_TOOL + /// + /// Imports the audio asset file to the target location. + /// + /// The source file path. + /// The result asset file path. + /// The import settings. + /// True if importing failed, otherwise false. + API_FUNCTION() static bool Import(const String& inputPath, const String& outputPath, const AudioTool::Options& options); + + /// + /// Tries the restore the asset import options from the target resource file. + /// + /// The options. + /// The asset path. + /// True settings has been restored, otherwise false. + API_FUNCTION() static bool TryRestoreImportOptions(API_PARAM(Ref) AudioTool::Options& options, String assetPath); +#endif + private: void OnEditorAssemblyLoaded(MAssembly* assembly); diff --git a/Source/Editor/Modules/PrefabsModule.cs b/Source/Editor/Modules/PrefabsModule.cs index 7b3ffebb3..bb76e125b 100644 --- a/Source/Editor/Modules/PrefabsModule.cs +++ b/Source/Editor/Modules/PrefabsModule.cs @@ -37,11 +37,6 @@ namespace FlaxEditor.Modules /// public event Action PrefabApplied; - /// - /// Locally cached actor for prefab creation. - /// - private Actor _prefabCreationActor; - internal PrefabsModule(Editor editor) : base(editor) { @@ -65,13 +60,14 @@ namespace FlaxEditor.Modules /// To create prefab manually (from code) use method. /// /// The scene selection to use. - public void CreatePrefab(List selection) + /// The prefab window that creates it. + public void CreatePrefab(List selection, Windows.Assets.PrefabWindow prefabWindow = null) { if (selection == null) selection = Editor.SceneEditing.Selection; if (selection.Count == 1 && selection[0] is ActorNode actorNode && actorNode.CanCreatePrefab) { - CreatePrefab(actorNode.Actor); + CreatePrefab(actorNode.Actor, true, prefabWindow); } } @@ -92,7 +88,8 @@ namespace FlaxEditor.Modules /// /// The root prefab actor. /// Allow renaming or not - public void CreatePrefab(Actor actor, bool rename) + /// The prefab window that creates it. + public void CreatePrefab(Actor actor, bool rename, Windows.Assets.PrefabWindow prefabWindow = null) { // Skip in invalid states if (!Editor.StateMachine.CurrentState.CanEditContent) @@ -105,42 +102,47 @@ namespace FlaxEditor.Modules PrefabCreating?.Invoke(actor); var proxy = Editor.ContentDatabase.GetProxy(); - _prefabCreationActor = actor; - Editor.Windows.ContentWin.NewItem(proxy, actor, OnPrefabCreated, actor.Name, rename); + Editor.Windows.ContentWin.NewItem(proxy, actor, contentItem => OnPrefabCreated(contentItem, actor, prefabWindow), actor.Name, rename); } - private void OnPrefabCreated(ContentItem contentItem) + private void OnPrefabCreated(ContentItem contentItem, Actor actor, Windows.Assets.PrefabWindow prefabWindow) { if (contentItem is PrefabItem prefabItem) { PrefabCreated?.Invoke(prefabItem); } - // Skip in invalid states - if (!Editor.StateMachine.CurrentState.CanEditScene) - return; + Undo undo = null; + if (prefabWindow != null) + { + prefabWindow.MarkAsEdited(); + undo = prefabWindow.Undo; + } + else + { + // Skip in invalid states + if (!Editor.StateMachine.CurrentState.CanEditScene) + return; + undo = Editor.Undo; + } // Record undo for prefab creating (backend links the target instance with the prefab) - if (Editor.Undo.Enabled) + if (undo.Enabled) { - if (!_prefabCreationActor) + if (!actor) return; - var actorsList = new List(); - Utilities.Utils.GetActorsTree(actorsList, _prefabCreationActor); + Utilities.Utils.GetActorsTree(actorsList, actor); var actions = new IUndoAction[actorsList.Count]; for (int i = 0; i < actorsList.Count; i++) - { - var action = BreakPrefabLinkAction.Linked(actorsList[i]); - actions[i] = action; - } - Undo.AddAction(new MultiUndoAction(actions)); - - _prefabCreationActor = null; + actions[i] = BreakPrefabLinkAction.Linked(actorsList[i]); + undo.AddAction(new MultiUndoAction(actions)); } - Editor.Instance.Windows.PropertiesWin.Presenter.BuildLayout(); + Editor.Windows.PropertiesWin.Presenter.BuildLayout(); + if (prefabWindow != null) + prefabWindow.Presenter.BuildLayout(); } /// diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index d3c2f6bca..4089f6d79 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; +using System.Text; using FlaxEditor.Options; using FlaxEditor.Scripting; using FlaxEngine; @@ -330,6 +332,78 @@ namespace FlaxEditor.Modules.SourceCodeEditing Editor.Instance.CodeEditing.SelectedEditor = editor; } + /// + /// Starts creating a new module + /// + internal void CreateModule(string path, string moduleName, bool editorModule, bool cpp) + { + if (string.IsNullOrEmpty(moduleName) || string.IsNullOrEmpty(path)) + { + Editor.LogWarning("Failed to create module due to no name"); + return; + } + + // Create folder + var moduleFolderPath = Path.Combine(path, moduleName); + Directory.CreateDirectory(moduleFolderPath); + + // Create module + var moduleText = "using Flax.Build;\n" + + "using Flax.Build.NativeCpp;\n" + + $"\npublic class {moduleName} : Game{(editorModule ? "Editor" : "")}Module\n" + + "{\n " + + "/// \n" + + " public override void Init()\n" + + " {\n" + + " base.Init();\n" + + "\n" + + " // C#-only scripting if false\n" + + $" BuildNativeCode = {(cpp ? "true" : "false")};\n" + + " }\n" + + "\n" + + " /// \n" + + " public override void Setup(BuildOptions options)\n" + + " {" + + "\n" + + " base.Setup(options);\n" + + "\n" + + " options.ScriptingAPI.IgnoreMissingDocumentationWarnings = true;\n" + + "\n" + + " // Here you can modify the build options for your game module\n" + + " // To reference another module use: options.PublicDependencies.Add(\"Audio\");\n" + + " // To add C++ define use: options.PublicDefinitions.Add(\"COMPILE_WITH_FLAX\");\n" + + " // To learn more see scripting documentation.\n" + + " }\n" + + "}"; + moduleText = Encoding.UTF8.GetString(Encoding.Default.GetBytes(moduleText)); + var modulePath = Path.Combine(moduleFolderPath, $"{moduleName}.Build.cs"); + File.WriteAllText(modulePath, moduleText); + Editor.Log($"Module created at {modulePath}"); + + // Get editor target and target files and add module + var files = Directory.GetFiles(path); + var targetModuleText = $"Modules.Add(\"{moduleName}\");\n "; + foreach (var file in files) + { + if (!file.Contains(".Build.cs", StringComparison.OrdinalIgnoreCase)) + continue; + var targetText = File.ReadAllText(file); + + // Skip game project if it is suppose to be an editor module + if (editorModule && targetText.Contains("GameProjectTarget", StringComparison.Ordinal)) + continue; + + // TODO: Handle edge case when there are no modules in a target + var index = targetText.IndexOf("Modules.Add"); + if (index != -1) + { + var newText = targetText.Insert(index, targetModuleText); + File.WriteAllText(file, newText); + Editor.Log($"Module added to Target: {file}"); + } + } + } + /// public override void OnUpdate() { diff --git a/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs b/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs index 604907ae4..0bf7f6afc 100644 --- a/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Windows; using FlaxEngine; namespace FlaxEditor.SceneGraph.Actors @@ -22,9 +23,9 @@ namespace FlaxEditor.SceneGraph.Actors } /// - public override void OnContextMenu(ContextMenu contextMenu) + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { - base.OnContextMenu(contextMenu); + base.OnContextMenu(contextMenu, window); var actor = (AnimatedModel)Actor; if (actor && actor.SkinnedModel) diff --git a/Source/Editor/SceneGraph/Actors/CameraNode.cs b/Source/Editor/SceneGraph/Actors/CameraNode.cs index 400a2e06f..1e01b5c50 100644 --- a/Source/Editor/SceneGraph/Actors/CameraNode.cs +++ b/Source/Editor/SceneGraph/Actors/CameraNode.cs @@ -6,6 +6,8 @@ using Real = System.Double; using Real = System.Single; #endif +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Windows; using FlaxEngine; namespace FlaxEditor.SceneGraph.Actors @@ -23,6 +25,25 @@ namespace FlaxEditor.SceneGraph.Actors { } + /// + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) + { + base.OnContextMenu(contextMenu, window); + if (window is not SceneTreeWindow win) + return; + var button = new ContextMenuButton(contextMenu, "Move Camera to View"); + button.Parent = contextMenu.ItemsContainer; + contextMenu.ItemsContainer.Children.Remove(button); + contextMenu.ItemsContainer.Children.Insert(4, button); + button.Clicked += () => + { + var c = Actor as Camera; + var viewport = Editor.Instance.Windows.EditWin.Viewport; + c.Position = viewport.ViewPosition; + c.Orientation = viewport.ViewOrientation; + }; + } + /// public override bool RayCastSelf(ref RayCastData ray, out Real distance, out Vector3 normal) { diff --git a/Source/Editor/SceneGraph/Actors/SceneNode.cs b/Source/Editor/SceneGraph/Actors/SceneNode.cs index 0405e4fdf..c2ee3645a 100644 --- a/Source/Editor/SceneGraph/Actors/SceneNode.cs +++ b/Source/Editor/SceneGraph/Actors/SceneNode.cs @@ -3,6 +3,7 @@ using System.IO; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.SceneGraph.GUI; +using FlaxEditor.Windows; using FlaxEngine; namespace FlaxEditor.SceneGraph.Actors @@ -65,7 +66,7 @@ namespace FlaxEditor.SceneGraph.Actors public override SceneNode ParentScene => this; /// - public override void OnContextMenu(ContextMenu contextMenu) + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { contextMenu.AddSeparator(); var path = Scene.Path; @@ -80,7 +81,7 @@ namespace FlaxEditor.SceneGraph.Actors if (Level.ScenesCount > 1) contextMenu.AddButton("Unload all but this scene", OnUnloadAllButSelectedScene).LinkTooltip("Unloads all of the active scenes except for the selected scene.").Enabled = Editor.Instance.StateMachine.CurrentState.CanChangeScene; - base.OnContextMenu(contextMenu); + base.OnContextMenu(contextMenu, window); } private void OnSelect() diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs index 1b35cdc35..3b9e8651d 100644 --- a/Source/Editor/SceneGraph/Actors/SplineNode.cs +++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs @@ -9,6 +9,7 @@ using Real = System.Single; using System; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Modules; +using FlaxEditor.Windows; using FlaxEngine; using FlaxEngine.Json; using Object = FlaxEngine.Object; @@ -203,9 +204,9 @@ namespace FlaxEditor.SceneGraph.Actors } } - public override void OnContextMenu(ContextMenu contextMenu) + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { - ParentNode.OnContextMenu(contextMenu); + ParentNode.OnContextMenu(contextMenu, window); } public static SceneGraphNode Create(StateData state) @@ -272,9 +273,9 @@ namespace FlaxEditor.SceneGraph.Actors DebugDraw.DrawSphere(new BoundingSphere(pos, tangentSize), Color.YellowGreen, 0, false); } - public override void OnContextMenu(ContextMenu contextMenu) + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { - ParentNode.OnContextMenu(contextMenu); + ParentNode.OnContextMenu(contextMenu, window); } public override void OnDispose() @@ -354,9 +355,9 @@ namespace FlaxEditor.SceneGraph.Actors } /// - public override void OnContextMenu(ContextMenu contextMenu) + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { - base.OnContextMenu(contextMenu); + base.OnContextMenu(contextMenu, window); contextMenu.AddButton("Add spline model", OnAddSplineModel); contextMenu.AddButton("Add spline collider", OnAddSplineCollider); diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index 52176ab8b..8de1a7f22 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -3,6 +3,7 @@ using System; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Windows; using FlaxEngine; namespace FlaxEditor.SceneGraph.Actors @@ -21,9 +22,9 @@ namespace FlaxEditor.SceneGraph.Actors } /// - public override void OnContextMenu(ContextMenu contextMenu) + public override void OnContextMenu(ContextMenu contextMenu, EditorWindow window) { - base.OnContextMenu(contextMenu); + base.OnContextMenu(contextMenu, window); contextMenu.AddButton("Add collider", OnAddMeshCollider).Enabled = ((StaticModel)Actor).Model != null; } diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index c4edda65e..d392e8309 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -599,7 +599,7 @@ namespace FlaxEditor.SceneGraph.GUI // Drag scripts else if (_dragScripts != null && _dragScripts.HasValidDrag) { - foreach(var script in _dragScripts.Objects) + foreach (var script in _dragScripts.Objects) { var customAction = script.HasPrefabLink ? new ReparentAction(script) : null; using (new UndoBlock(ActorNode.Root.Undo, script, "Change script parent", customAction)) @@ -616,7 +616,7 @@ namespace FlaxEditor.SceneGraph.GUI var spawnParent = myActor; if (DragOverMode == DragItemPositioning.Above || DragOverMode == DragItemPositioning.Below) spawnParent = newParent; - + for (int i = 0; i < _dragAssets.Objects.Count; i++) { var item = _dragAssets.Objects[i]; @@ -720,7 +720,7 @@ namespace FlaxEditor.SceneGraph.GUI for (var i = 0; i < tree.Selection.Count; i++) { var e = tree.Selection[i]; - + // Skip if parent is already selected to keep correct parenting if (tree.Selection.Contains(e.Parent)) continue; diff --git a/Source/Editor/SceneGraph/SceneGraphNode.cs b/Source/Editor/SceneGraph/SceneGraphNode.cs index a9c350bb2..672a239dc 100644 --- a/Source/Editor/SceneGraph/SceneGraphNode.cs +++ b/Source/Editor/SceneGraph/SceneGraphNode.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using FlaxEditor.Modules; using FlaxEditor.SceneGraph.Actors; +using FlaxEditor.Windows; using FlaxEngine; namespace FlaxEditor.SceneGraph @@ -339,7 +340,7 @@ namespace FlaxEditor.SceneGraph /// /// Called when scene tree window wants to show the context menu. Allows to add custom options. /// - public virtual void OnContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu contextMenu) + public virtual void OnContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu contextMenu, EditorWindow window) { } diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index de02e9336..7d12a0625 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -745,12 +745,12 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { return inputType.IsVoid; } @@ -1162,7 +1162,7 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { if (nodeArch.Tag is not ScriptMemberInfo memberInfo) return false; @@ -1188,7 +1188,7 @@ namespace FlaxEditor.Surface.Archetypes return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { if (nodeArch.Tag is not ScriptMemberInfo memberInfo) return false; @@ -1836,12 +1836,12 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { return inputType.IsVoid; } @@ -1982,7 +1982,7 @@ namespace FlaxEditor.Surface.Archetypes UpdateSignature(); } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); if (scriptType == ScriptType.Null) @@ -2011,7 +2011,7 @@ namespace FlaxEditor.Surface.Archetypes return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); if (scriptType == ScriptType.Null) @@ -2093,7 +2093,7 @@ namespace FlaxEditor.Surface.Archetypes UpdateSignature(); } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { if (outputType.IsVoid) return true; @@ -2130,7 +2130,7 @@ namespace FlaxEditor.Surface.Archetypes return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { return inputType.IsVoid; } @@ -2353,7 +2353,7 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { // Event based nodes always have a pulse input, so it's always compatible with void if (outputType.IsVoid) @@ -2373,7 +2373,7 @@ namespace FlaxEditor.Surface.Archetypes return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { // Event based nodes always have a pulse output, so it's always compatible with void if (inputType.IsVoid) diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs index 16f20809c..789fd35c2 100644 --- a/Source/Editor/Surface/Archetypes/Packing.cs +++ b/Source/Editor/Surface/Archetypes/Packing.cs @@ -217,7 +217,7 @@ namespace FlaxEditor.Surface.Archetypes { } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { var typeName = (string)nodeArch.DefaultValues[0]; var type = TypeUtils.GetType(typeName); @@ -234,7 +234,7 @@ namespace FlaxEditor.Surface.Archetypes return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { var typeName = (string)nodeArch.DefaultValues[0]; var type = TypeUtils.GetType(typeName); @@ -255,7 +255,7 @@ namespace FlaxEditor.Surface.Archetypes { } - internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) { var typeName = (string)nodeArch.DefaultValues[0]; var type = TypeUtils.GetType(typeName); @@ -267,7 +267,7 @@ namespace FlaxEditor.Surface.Archetypes return false; } - internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) { var typeName = (string)nodeArch.DefaultValues[0]; var type = TypeUtils.GetType(typeName); diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 59987f0aa..3fe727840 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -490,6 +490,56 @@ namespace FlaxEditor.Surface.Archetypes _combobox.Width = Width - 30; } } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + if (inputType == ScriptType.Object) + return true; + + SurfaceParameter parameter = context.GetParameter((Guid)nodeArch.DefaultValues[0]); + ScriptType type = parameter?.Type ?? ScriptType.Null; + + if (parameter == null || type == ScriptType.Null || parameter.Type.Type == null) + return false; + if (DefaultPrototypes == null || !DefaultPrototypes.TryGetValue(parameter.Type.Type, out var elements)) + { + return VisjectSurface.FullCastCheck(inputType, type, hint); + } + if (elements == null) + return false; + + for (var i = 0; i < elements.Length; i++) + { + if(elements[i].Type != NodeElementType.Output) + continue; + + if (VisjectSurface.FullCastCheck(elements[i].ConnectionsType, inputType, hint)) + return true; + } + return false; + } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + SurfaceParameter parameter = context.GetParameter((Guid)nodeArch.DefaultValues[0]); + ScriptType type = parameter?.Type ?? ScriptType.Null; + + if (parameter == null || type == ScriptType.Null) + return false; + if (parameter.Type.Type == null || DefaultPrototypes == null || !DefaultPrototypes.TryGetValue(parameter.Type.Type, out var elements)) + return false; + if (elements == null) + return false; + + for (var i = 0; i < elements.Length; i++) + { + if(elements[i].Type != NodeElementType.Input) + continue; + if (VisjectSurface.FullCastCheck(elements[i].ConnectionsType, outputType, hint)) + return true; + } + return false; + } } /// @@ -675,6 +725,53 @@ namespace FlaxEditor.Surface.Archetypes /// protected override bool UseNormalMaps => false; + + internal new static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + if (inputType == ScriptType.Object) + return true; + + SurfaceParameter parameter = context.GetParameter((Guid)nodeArch.DefaultValues[0]); + ScriptType type = parameter?.Type ?? ScriptType.Null; + + if (parameter == null || type == ScriptType.Null) + return false; + if (parameter.Type.Type == null || DefaultPrototypesParticleEmitter == null || !DefaultPrototypesParticleEmitter.TryGetValue(parameter.Type.Type, out var elements)) + return false; + if (elements == null) + return false; + + for (var i = 0; i < elements.Length; i++) + { + if(elements[i].Type != NodeElementType.Output) + continue; + if (VisjectSurface.FullCastCheck(elements[i].ConnectionsType, inputType, hint)) + return true; + } + return false; + } + + internal new static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + SurfaceParameter parameter = context.GetParameter((Guid)nodeArch.DefaultValues[0]); + ScriptType type = parameter?.Type ?? ScriptType.Null; + + if (parameter == null || type == ScriptType.Null) + return false; + if (parameter.Type.Type == null || DefaultPrototypesParticleEmitter == null || !DefaultPrototypesParticleEmitter.TryGetValue(parameter.Type.Type, out var elements)) + return false; + if (elements == null) + return false; + + for (var i = 0; i < elements.Length; i++) + { + if(elements[i].Type != NodeElementType.Input) + continue; + if (VisjectSurface.FullCastCheck(elements[i].ConnectionsType, outputType, hint)) + return true; + } + return false; + } } /// @@ -692,6 +789,22 @@ namespace FlaxEditor.Surface.Archetypes /// protected override bool UseNormalMaps => false; + + internal new static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + if (inputType == ScriptType.Object) + return true; + + SurfaceParameter parameter = context.GetParameter((Guid)nodeArch.DefaultValues[0]); + ScriptType type = parameter?.Type ?? ScriptType.Null; + + return VisjectSurface.FullCastCheck(inputType, type, hint); + } + + internal new static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + return false; + } } /// @@ -874,6 +987,22 @@ namespace FlaxEditor.Surface.Archetypes _combobox.Width = Width - 50; } } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + return inputType == ScriptType.Void; + } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint, VisjectSurfaceContext context) + { + if (outputType == ScriptType.Void) + return true; + + SurfaceParameter parameter = context.GetParameter((Guid)nodeArch.DefaultValues[0]); + ScriptType type = parameter?.Type ?? ScriptType.Null; + + return VisjectSurface.FullCastCheck(outputType, type, hint); + } } /// @@ -885,6 +1014,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Create = (id, context, arch, groupArch) => new SurfaceNodeParamsGet(id, context, arch, groupArch), + IsInputCompatible = SurfaceNodeParamsGet.IsInputCompatible, + IsOutputCompatible = SurfaceNodeParamsGet.IsOutputCompatible, Title = "Get Parameter", Description = "Parameter value getter", Flags = NodeFlags.MaterialGraph | NodeFlags.AnimGraph, @@ -902,6 +1033,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Create = (id, context, arch, groupArch) => new SurfaceNodeParamsGetParticleEmitter(id, context, arch, groupArch), + IsInputCompatible = SurfaceNodeParamsGetParticleEmitter.IsInputCompatible, + IsOutputCompatible = SurfaceNodeParamsGetParticleEmitter.IsOutputCompatible, Title = "Get Parameter", Description = "Parameter value getter", Flags = NodeFlags.ParticleEmitterGraph, @@ -919,6 +1052,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Create = (id, context, arch, groupArch) => new SurfaceNodeParamsGetVisualScript(id, context, arch, groupArch), + IsInputCompatible = SurfaceNodeParamsGetVisualScript.IsInputCompatible, + IsOutputCompatible = SurfaceNodeParamsGetVisualScript.IsOutputCompatible, Title = "Get Parameter", Description = "Parameter value getter", Flags = NodeFlags.VisualScriptGraph, @@ -936,6 +1071,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Create = (id, context, arch, groupArch) => new SurfaceNodeParamsSet(id, context, arch, groupArch), + IsInputCompatible = SurfaceNodeParamsSet.IsInputCompatible, + IsOutputCompatible = SurfaceNodeParamsSet.IsOutputCompatible, Title = "Set Parameter", Description = "Parameter value setter", Flags = NodeFlags.VisualScriptGraph, diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index ac7341cd6..6d2d31b8c 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -787,7 +787,7 @@ namespace FlaxEditor.Surface.Archetypes } } - private class AsNode : SurfaceNode + internal class AsNode : SurfaceNode { private TypePickerControl _picker; @@ -838,6 +838,15 @@ namespace FlaxEditor.Surface.Archetypes box.CurrentType = type ? type : ScriptType.FlaxObject; } + /// + /// Sets the type of the picker and the type of the output box + /// + /// Target Type + public void SetPickerValue(ScriptType type) + { + _picker.Value = type; + } + /// public override void OnDestroy() { @@ -999,6 +1008,15 @@ namespace FlaxEditor.Surface.Archetypes GetBox(4).CurrentType = type ? type : _picker.Type; } + /// + /// Sets the type of the picker and the type of the output box + /// + /// Target Type + public void SetPickerValue(ScriptType type) + { + _picker.Value = type; + } + /// public override void OnDestroy() { diff --git a/Source/Editor/Surface/ContextMenu/VisjectCM.cs b/Source/Editor/Surface/ContextMenu/VisjectCM.cs index 8369d9c4d..930741807 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCM.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCM.cs @@ -623,7 +623,7 @@ namespace FlaxEditor.Surface.ContextMenu Archetypes = archetypes }; - var group = CreateGroup(groupArchetype); + var group = CreateGroup(groupArchetype, false); group.ArrowImageOpened = new SpriteBrush(Style.Current.ArrowDown); group.ArrowImageClosed = new SpriteBrush(Style.Current.ArrowRight); group.Close(false); @@ -726,9 +726,7 @@ namespace FlaxEditor.Surface.ContextMenu SelectedItem = previousSelectedItem; // Scroll into view (without smoothing) - _panel1.VScrollBar.SmoothingScale = 0; - _panel1.ScrollViewTo(SelectedItem); - _panel1.VScrollBar.SmoothingScale = 1; + _panel1.ScrollViewTo(SelectedItem, true); } return true; } diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index 7fd583202..37da7d74d 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -110,11 +110,11 @@ namespace FlaxEditor.Surface.ContextMenu bool isCompatible = false; if (startBox.IsOutput && _archetype.IsInputCompatible != null) { - isCompatible |= _archetype.IsInputCompatible.Invoke(_archetype, startBox.CurrentType, _archetype.ConnectionsHints); + isCompatible |= _archetype.IsInputCompatible.Invoke(_archetype, startBox.CurrentType, _archetype.ConnectionsHints, startBox.ParentNode.Context); } else if (!startBox.IsOutput && _archetype.IsOutputCompatible != null) { - isCompatible |= _archetype.IsOutputCompatible.Invoke(_archetype, startBox.CurrentType, startBox.ParentNode.Archetype.ConnectionsHints); + isCompatible |= _archetype.IsOutputCompatible.Invoke(_archetype, startBox.CurrentType, startBox.ParentNode.Archetype.ConnectionsHints, startBox.ParentNode.Context); } else if (_archetype.Elements != null) { diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index 53bba2d68..53f3e3db2 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -792,8 +792,62 @@ namespace FlaxEditor.Surface.Elements if (useCaster) { // Connect via Caster - //AddCaster(oB, iB); - throw new NotImplementedException("AddCaster(..) function"); + const float casterXOffset = 250; + if (Surface.Undo != null && Surface.Undo.Enabled) + { + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + SurfaceNode node = Surface.Context.SpawnNode(7, 22, Float2.Zero); // 22 AsNode, 25 CastNode + Surface.Undo.Enabled = undoEnabled; + if (node is not Archetypes.Tools.AsNode castNode) + throw new Exception("Node is not a casting node!"); + + // Set the type of the casting node + undoEnabled = castNode.Surface.Undo.Enabled; + castNode.Surface.Undo.Enabled = false; + castNode.SetPickerValue(iB.CurrentType); + castNode.Surface.Undo.Enabled = undoEnabled; + if (node.GetBox(0) is not OutputBox castOutputBox || node.GetBox(1) is not InputBox castInputBox) + throw new NullReferenceException("Casting failed. Cast node is invalid!"); + + // We set the position of the cast node here to set it relative to the target nodes input box + undoEnabled = castNode.Surface.Undo.Enabled; + castNode.Surface.Undo.Enabled = false; + var wantedOffset = iB.ParentNode.Location - new Float2(casterXOffset, -(iB.LocalY - castOutputBox.LocalY)); + castNode.Location = Surface.Root.PointFromParent(ref wantedOffset); + castNode.Surface.Undo.Enabled = undoEnabled; + + var spawnNodeAction = new AddRemoveNodeAction(castNode, true); + + var connectToCastNodeAction = new ConnectBoxesAction(castInputBox, oB, true); + castInputBox.CreateConnection(oB); + connectToCastNodeAction.End(); + + var connectCastToTargetNodeAction = new ConnectBoxesAction(iB, castOutputBox, true); + iB.CreateConnection(castOutputBox); + connectCastToTargetNodeAction.End(); + + Surface.AddBatchedUndoAction(new MultiUndoAction(spawnNodeAction, connectToCastNodeAction, connectCastToTargetNodeAction)); + } + else + { + SurfaceNode node = Surface.Context.SpawnNode(7, 22, Float2.Zero); // 22 AsNode, 25 CastNode + if (node is not Archetypes.Tools.AsNode castNode) + throw new Exception("Node is not a casting node!"); + + // Set the type of the casting node + castNode.SetPickerValue(iB.CurrentType); + if (node.GetBox(0) is not OutputBox castOutputBox || node.GetBox(1) is not InputBox castInputBox) + throw new NullReferenceException("Casting failed. Cast node is invalid!"); + + // We set the position of the cast node here to set it relative to the target nodes input box + var wantedOffset = iB.ParentNode.Location - new Float2(casterXOffset, -(iB.LocalY - castOutputBox.LocalY)); + castNode.Location = Surface.Root.PointFromParent(ref wantedOffset); + + castInputBox.CreateConnection(oB); + iB.CreateConnection(castOutputBox); + } + Surface.MarkAsEdited(); } else { diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index fe54268e2..0284fc76d 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -92,7 +92,7 @@ namespace FlaxEditor.Surface /// /// Checks if the given type is compatible with the given node archetype. Used for custom nodes /// - public delegate bool IsCompatible(NodeArchetype nodeArch, ScriptType portType, ConnectionsHint hint); + public delegate bool IsCompatible(NodeArchetype nodeArch, ScriptType portType, ConnectionsHint hint, VisjectSurfaceContext context); /// /// Unique node type ID within a single group. diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index edfa15214..f60f6ff3a 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using FlaxEditor.Surface.Elements; using FlaxEngine; namespace FlaxEditor.Surface @@ -135,8 +136,25 @@ namespace FlaxEditor.Surface endPos = _lastInstigatorUnderMouse.ConnectionOrigin; } + Float2 actualStartPos = startPos; + Float2 actualEndPos = endPos; + + if (_connectionInstigator is Archetypes.Tools.RerouteNode) + { + if (endPos.X < startPos.X && _lastInstigatorUnderMouse is null or Box { IsOutput: true}) + { + actualStartPos = endPos; + actualEndPos = startPos; + } + } + else if (_connectionInstigator is Box { IsOutput: false }) + { + actualStartPos = endPos; + actualEndPos = startPos; + } + // Draw connection - _connectionInstigator.DrawConnectingLine(ref startPos, ref endPos, ref lineColor); + _connectionInstigator.DrawConnectingLine(ref actualStartPos, ref actualEndPos, ref lineColor); } /// diff --git a/Source/Editor/Tools/Terrain/TerrainTools.cpp b/Source/Editor/Tools/Terrain/TerrainTools.cpp index 545e60d54..89d0aa17d 100644 --- a/Source/Editor/Tools/Terrain/TerrainTools.cpp +++ b/Source/Editor/Tools/Terrain/TerrainTools.cpp @@ -285,6 +285,7 @@ bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches StringAnsi TerrainTools::SerializePatch(Terrain* terrain, const Int2& patchCoord) { + CHECK_RETURN(terrain, StringAnsi::Empty); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, StringAnsi::Empty); @@ -300,6 +301,7 @@ StringAnsi TerrainTools::SerializePatch(Terrain* terrain, const Int2& patchCoord void TerrainTools::DeserializePatch(Terrain* terrain, const Int2& patchCoord, const StringAnsiView& value) { + CHECK(terrain); auto patch = terrain->GetPatch(patchCoord); CHECK(patch); @@ -317,27 +319,31 @@ void TerrainTools::DeserializePatch(Terrain* terrain, const Int2& patchCoord, co bool TerrainTools::InitializePatch(Terrain* terrain, const Int2& patchCoord) { + CHECK_RETURN(terrain, true); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, true); return patch->InitializeHeightMap(); } -bool TerrainTools::ModifyHeightMap(Terrain* terrain, const Int2& patchCoord, float* samples, const Int2& offset, const Int2& size) +bool TerrainTools::ModifyHeightMap(Terrain* terrain, const Int2& patchCoord, const float* samples, const Int2& offset, const Int2& size) { + CHECK_RETURN(terrain, true); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, true); return patch->ModifyHeightMap(samples, offset, size); } -bool TerrainTools::ModifyHolesMask(Terrain* terrain, const Int2& patchCoord, byte* samples, const Int2& offset, const Int2& size) +bool TerrainTools::ModifyHolesMask(Terrain* terrain, const Int2& patchCoord, const byte* samples, const Int2& offset, const Int2& size) { + CHECK_RETURN(terrain, true); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, true); return patch->ModifyHolesMask(samples, offset, size); } -bool TerrainTools::ModifySplatMap(Terrain* terrain, const Int2& patchCoord, int32 index, Color32* samples, const Int2& offset, const Int2& size) +bool TerrainTools::ModifySplatMap(Terrain* terrain, const Int2& patchCoord, int32 index, const Color32* samples, const Int2& offset, const Int2& size) { + CHECK_RETURN(terrain, true); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, true); CHECK_RETURN(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT, true); @@ -346,6 +352,7 @@ bool TerrainTools::ModifySplatMap(Terrain* terrain, const Int2& patchCoord, int3 float* TerrainTools::GetHeightmapData(Terrain* terrain, const Int2& patchCoord) { + CHECK_RETURN(terrain, nullptr); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, nullptr); return patch->GetHeightmapData(); @@ -353,6 +360,7 @@ float* TerrainTools::GetHeightmapData(Terrain* terrain, const Int2& patchCoord) byte* TerrainTools::GetHolesMaskData(Terrain* terrain, const Int2& patchCoord) { + CHECK_RETURN(terrain, nullptr); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, nullptr); return patch->GetHolesMaskData(); @@ -360,6 +368,7 @@ byte* TerrainTools::GetHolesMaskData(Terrain* terrain, const Int2& patchCoord) Color32* TerrainTools::GetSplatMapData(Terrain* terrain, const Int2& patchCoord, int32 index) { + CHECK_RETURN(terrain, nullptr); auto patch = terrain->GetPatch(patchCoord); CHECK_RETURN(patch, nullptr); return patch->GetSplatMapData(index); diff --git a/Source/Editor/Tools/Terrain/TerrainTools.h b/Source/Editor/Tools/Terrain/TerrainTools.h index 6f4682bf6..bb35c45c9 100644 --- a/Source/Editor/Tools/Terrain/TerrainTools.h +++ b/Source/Editor/Tools/Terrain/TerrainTools.h @@ -68,7 +68,7 @@ DECLARE_SCRIPTING_TYPE_NO_SPAWN(TerrainTools); /// The offset from the first row and column of the heightmap data (offset destination x and z start position). /// The size of the heightmap to modify (x and z). Amount of samples in each direction. /// True if failed, otherwise false. - API_FUNCTION() static bool ModifyHeightMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, float* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size); + API_FUNCTION() static bool ModifyHeightMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, const float* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size); /// /// Modifies the terrain patch holes mask with the given samples. @@ -79,7 +79,7 @@ DECLARE_SCRIPTING_TYPE_NO_SPAWN(TerrainTools); /// The offset from the first row and column of the mask data (offset destination x and z start position). /// The size of the mask to modify (x and z). Amount of samples in each direction. /// True if failed, otherwise false. - API_FUNCTION() static bool ModifyHolesMask(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, byte* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size); + API_FUNCTION() static bool ModifyHolesMask(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, const byte* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size); /// /// Modifies the terrain patch splat map (layers mask) with the given samples. @@ -91,7 +91,7 @@ DECLARE_SCRIPTING_TYPE_NO_SPAWN(TerrainTools); /// The offset from the first row and column of the splatmap data (offset destination x and z start position). /// The size of the splatmap to modify (x and z). Amount of samples in each direction. /// True if failed, otherwise false. - API_FUNCTION() static bool ModifySplatMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, int32 index, Color32* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size); + API_FUNCTION() static bool ModifySplatMap(Terrain* terrain, API_PARAM(Ref) const Int2& patchCoord, int32 index, const Color32* samples, API_PARAM(Ref) const Int2& offset, API_PARAM(Ref) const Int2& size); /// /// Gets the raw pointer to the heightmap data (cached internally by the c++ core in editor). diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 8c71a8b02..13c06157a 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using FlaxEditor.Content.Settings; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEditor.Options; @@ -9,6 +10,8 @@ using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Widgets; using FlaxEngine; using FlaxEngine.GUI; +using Newtonsoft.Json; +using JsonSerializer = FlaxEngine.Json.JsonSerializer; namespace FlaxEditor.Viewport { @@ -486,10 +489,63 @@ namespace FlaxEditor.Viewport } } + // View Layers + { + var viewLayers = ViewWidgetButtonMenu.AddChildMenu("View Layers").ContextMenu; + viewLayers.AddButton("Copy layers", () => Clipboard.Text = JsonSerializer.Serialize(Task.View.RenderLayersMask)); + viewLayers.AddButton("Paste layers", () => + { + try + { + Task.ViewLayersMask = JsonSerializer.Deserialize(Clipboard.Text); + } + catch + { + } + }); + viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = Editor.Instance.Icons.Rotate32; + viewLayers.AddButton("Disable layers", () => Task.ViewLayersMask = new LayersMask(0)).Icon = Editor.Instance.Icons.Rotate32; + viewLayers.AddSeparator(); + var layers = LayersAndTagsSettings.GetCurrentLayers(); + if (layers != null && layers.Length > 0) + { + for (int i = 0; i < layers.Length; i++) + { + var layer = layers[i]; + var button = viewLayers.AddButton(layer); + button.CloseMenuOnClick = false; + button.Tag = 1 << i; + } + } + viewLayers.ButtonClicked += button => + { + if (button.Tag != null) + { + int layerIndex = (int)button.Tag; + LayersMask mask = new LayersMask(layerIndex); + Task.ViewLayersMask ^= mask; + button.Icon = (Task.ViewLayersMask & mask) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + } + }; + viewLayers.VisibleChanged += WidgetViewLayersShowHide; + } + // View Flags { var viewFlags = ViewWidgetButtonMenu.AddChildMenu("View Flags").ContextMenu; + viewFlags.AddButton("Copy flags", () => Clipboard.Text = JsonSerializer.Serialize(Task.ViewFlags)); + viewFlags.AddButton("Paste flags", () => + { + try + { + Task.ViewFlags = JsonSerializer.Deserialize(Clipboard.Text); + } + catch + { + } + }); viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32; + viewFlags.AddButton("Disable flags", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Rotate32; viewFlags.AddSeparator(); for (int i = 0; i < EditorViewportViewFlagsValues.Length; i++) { @@ -504,7 +560,7 @@ namespace FlaxEditor.Viewport { var v = (ViewFlags)button.Tag; Task.ViewFlags ^= v; - button.Icon = (Task.View.Flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + button.Icon = (Task.ViewFlags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } }; viewFlags.VisibleChanged += WidgetViewFlagsShowHide; @@ -513,6 +569,18 @@ namespace FlaxEditor.Viewport // Debug View { var debugView = ViewWidgetButtonMenu.AddChildMenu("Debug View").ContextMenu; + debugView.AddButton("Copy view", () => Clipboard.Text = JsonSerializer.Serialize(Task.ViewMode)); + debugView.AddButton("Paste view", () => + { + try + { + Task.ViewMode = JsonSerializer.Deserialize(Clipboard.Text); + } + catch + { + } + }); + debugView.AddSeparator(); for (int i = 0; i < EditorViewportViewModeValues.Length; i++) { ref var v = ref EditorViewportViewModeValues[i]; @@ -954,7 +1022,7 @@ namespace FlaxEditor.Viewport // Center mouse position if it's too close to the edge var size = Size; - var center = size * 0.5f; + var center = Float2.Round(size * 0.5f); if (Mathf.Abs(_viewMousePos.X - center.X) > center.X * 0.8f || Mathf.Abs(_viewMousePos.Y - center.Y) > center.Y * 0.8f) { _viewMousePos = center; @@ -1084,9 +1152,9 @@ namespace FlaxEditor.Viewport _isVirtualMouseRightDown = false; // Cancel when mouse right or escape is pressed if (_wasVirtualMouseRightDown) wasControllingMouse = true; - if (_isVirtualMouseRightDown) + if (_isVirtualMouseRightDown) _isControllingMouse = _isVirtualMouseRightDown; - + if (wasControllingMouse != _isControllingMouse) { if (_isControllingMouse) @@ -1591,6 +1659,24 @@ namespace FlaxEditor.Viewport } } + private void WidgetViewLayersShowHide(Control cm) + { + if (cm.Visible == false) + return; + + var ccm = (ContextMenu)cm; + var layersMask = Task.ViewLayersMask; + foreach (var e in ccm.Items) + { + if (e is ContextMenuButton b && b != null && b.Tag != null) + { + int layerIndex = (int)b.Tag; + LayersMask mask = new LayersMask(layerIndex); + b.Icon = (layersMask & mask) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + } + } + } + private float GetGamepadAxis(GamepadAxis axis) { var value = FlaxEngine.Input.GetGamepadAxis(InputGamepadIndex.All, axis); diff --git a/Source/Editor/Windows/Assets/AudioClipWindow.cs b/Source/Editor/Windows/Assets/AudioClipWindow.cs index f5f244c58..8cf31f416 100644 --- a/Source/Editor/Windows/Assets/AudioClipWindow.cs +++ b/Source/Editor/Windows/Assets/AudioClipWindow.cs @@ -116,7 +116,7 @@ namespace FlaxEditor.Windows.Assets _window = window; // Try to restore target asset AudioClip import options (useful for fast reimport) - AudioImportSettings.TryRestore(ref ImportSettings, window.Item.Path); + Editor.TryRestoreImportOptions(ref ImportSettings.Settings, window.Item.Path); // Prepare restore data PeekState(); @@ -134,6 +134,11 @@ namespace FlaxEditor.Windows.Assets /// public void Reimport() { + if (_window?._previewSource != null) + { + _window._previewSource.Stop(); + _window.UpdateToolstrip(); + } Editor.Instance.ContentImporting.Reimport((BinaryAssetItem)_window.Item, ImportSettings, true); } diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 47ce09274..097d4992a 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -80,11 +80,8 @@ namespace FlaxEditor.Windows.Assets { if (!IsEdited) return; - if (_asset.WaitForLoaded()) - { return; - } if (Editor.SaveJsonAsset(_item.Path, _object)) { @@ -137,7 +134,7 @@ namespace FlaxEditor.Windows.Assets } } _presenter.Select(_object); - + if (_typeText != null) _typeText.Dispose(); var typeText = new ClickableLabel @@ -152,7 +149,7 @@ namespace FlaxEditor.Windows.Assets typeText.LocalY += (_toolstrip.Height - typeText.Height) * 0.5f; typeText.RightClick = () => Clipboard.Text = Asset.DataTypeName; _typeText = typeText; - + _undo.Clear(); ClearEditedFlag(); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index 5a532e362..70aa1dca3 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -233,7 +233,7 @@ namespace FlaxEditor.Windows.Assets contextMenu.AddSeparator(); - b = contextMenu.AddButton("Create Prefab", () => Editor.Prefabs.CreatePrefab(Selection)); + b = contextMenu.AddButton("Create Prefab", () => Editor.Prefabs.CreatePrefab(Selection, this)); b.Enabled = isSingleActorSelected && (Selection[0] as ActorNode).CanCreatePrefab && Editor.Windows.ContentWin.CurrentViewFolder.CanHaveAssets; @@ -313,7 +313,7 @@ namespace FlaxEditor.Windows.Assets } if (showCustomNodeOptions) { - Selection[0].OnContextMenu(contextMenu); + Selection[0].OnContextMenu(contextMenu, this); } ContextMenuShow?.Invoke(contextMenu); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 755bf4b1d..4deaf5d7d 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -53,6 +53,11 @@ namespace FlaxEditor.Windows.Assets /// public PrefabWindowViewport Viewport => _viewport; + /// + /// Gets the prefab objects properties editor. + /// + public CustomEditorPresenter Presenter => _propertiesEditor; + /// /// Gets the undo system used by this window for changes tracking. /// @@ -146,7 +151,7 @@ namespace FlaxEditor.Windows.Assets Graph = new LocalSceneGraph(new CustomRootNode(this)); _tree = new PrefabTree { - Margin = new Margin(0.0f, 0.0f, -16.0f, 0.0f), // Hide root node + Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node IsScrollable = true, }; _tree.AddChild(Graph.Root.TreeNode); diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 1dd26ecf5..03873df57 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -1,11 +1,13 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.Assertions; +using FlaxEngine.GUI; using FlaxEngine.Json; namespace FlaxEditor.Windows @@ -145,9 +147,18 @@ namespace FlaxEditor.Windows cm.AddButton("Refresh all thumbnails", RefreshViewItemsThumbnails); } + cm.AddSeparator(); + + // Check if is source folder to add new module + if (folder?.ParentFolder?.Node is ProjectTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) + { + var button = cm.AddButton("New module"); + button.CloseMenuOnClick = false; + button.Clicked += () => NewModule(button, parentFolderNode.Source.Path); + } + if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode)) { - cm.AddSeparator(); cm.AddButton("New folder", NewFolder); } @@ -311,5 +322,138 @@ namespace FlaxEditor.Windows return; } } + + private void NewModule(ContextMenuButton button, string path) + { + var popup = new ContextMenuBase + { + Size = new Float2(230, 125), + ClipChildren = false, + CullChildren = false, + }; + popup.Show(button, new Float2(button.Width, 0)); + + var nameLabel = new Label + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Name", + HorizontalAlignment = TextAlignment.Near, + }; + nameLabel.LocalX += 10; + nameLabel.LocalY += 10; + + var nameTextBox = new TextBox + { + Parent = popup, + WatermarkText = "Module Name", + AnchorPreset = AnchorPresets.TopLeft, + IsMultiline = false, + }; + nameTextBox.LocalX += 100; + nameTextBox.LocalY += 10; + var defaultTextBoxBorderColor = nameTextBox.BorderColor; + var defaultTextBoxBorderSelectedColor = nameTextBox.BorderSelectedColor; + nameTextBox.TextChanged += () => + { + if (string.IsNullOrEmpty(nameTextBox.Text)) + { + nameTextBox.BorderColor = defaultTextBoxBorderColor; + nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor; + return; + } + + var pluginPath = Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text); + if (Directory.Exists(pluginPath)) + { + nameTextBox.BorderColor = Color.Red; + nameTextBox.BorderSelectedColor = Color.Red; + } + else + { + nameTextBox.BorderColor = defaultTextBoxBorderColor; + nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor; + } + }; + + var editorLabel = new Label + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Editor", + HorizontalAlignment = TextAlignment.Near, + }; + editorLabel.LocalX += 10; + editorLabel.LocalY += 35; + + var editorCheckBox = new CheckBox + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + }; + editorCheckBox.LocalY += 35; + editorCheckBox.LocalX += 100; + + var cppLabel = new Label + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "C++", + HorizontalAlignment = TextAlignment.Near, + }; + cppLabel.LocalX += 10; + cppLabel.LocalY += 60; + + var cppCheckBox = new CheckBox + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + }; + cppCheckBox.LocalY += 60; + cppCheckBox.LocalX += 100; + + var submitButton = new Button + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Create", + Width = 70, + }; + submitButton.LocalX += 40; + submitButton.LocalY += 90; + submitButton.Clicked += () => + { + // TODO: Check all modules in project including plugins + if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text))) + { + Editor.LogWarning("Cannot create module due to name conflict."); + return; + } + Editor.CodeEditing.CreateModule(path, nameTextBox.Text, editorCheckBox.Checked, cppCheckBox.Checked); + nameTextBox.Clear(); + editorCheckBox.Checked = false; + cppCheckBox.Checked = false; + popup.Hide(); + button.ParentContextMenu.Hide(); + }; + + var cancelButton = new Button + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Cancel", + Width = 70, + }; + cancelButton.LocalX += 120; + cancelButton.LocalY += 90; + cancelButton.Clicked += () => + { + nameTextBox.Clear(); + editorCheckBox.Checked = false; + cppCheckBox.Checked = false; + popup.Hide(); + button.ParentContextMenu.Hide(); + }; + } } } diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index b1183753e..a06ec839d 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -329,7 +329,7 @@ namespace FlaxEditor.Windows b.Checked = ShowPluginsFiles; b.CloseMenuOnClick = false; b.AutoCheck = true; - + b = show.ContextMenu.AddButton("Generated files", () => ShowGeneratedFiles = !ShowGeneratedFiles); b.TooltipText = "Shows generated files"; b.Checked = ShowGeneratedFiles; @@ -755,7 +755,7 @@ namespace FlaxEditor.Windows } /// - /// Stars creating the folder. + /// Starts creating the folder. /// public void NewFolder() { diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index 63d110368..a99c3ac7d 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -215,7 +215,7 @@ namespace FlaxEditor.Windows } if (showCustomNodeOptions) { - Editor.SceneEditing.Selection[0].OnContextMenu(contextMenu); + Editor.SceneEditing.Selection[0].OnContextMenu(contextMenu, this); } ContextMenuShow?.Invoke(contextMenu); diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index fba052269..cbaa27371 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -74,7 +74,7 @@ namespace FlaxEditor.Windows root.TreeNode.Expand(); _tree = new Tree(true) { - Margin = new Margin(0.0f, 0.0f, -16.0f, 0.0f), // Hide root node + Margin = new Margin(0.0f, 0.0f, -16.0f, _sceneTreePanel.ScrollBarsSize), // Hide root node IsScrollable = true, }; _tree.AddChild(root.TreeNode); diff --git a/Source/Editor/Windows/Search/ContentFinder.cs b/Source/Editor/Windows/Search/ContentFinder.cs index 05e1d6792..e19d212b8 100644 --- a/Source/Editor/Windows/Search/ContentFinder.cs +++ b/Source/Editor/Windows/Search/ContentFinder.cs @@ -54,9 +54,7 @@ namespace FlaxEditor.Windows.Search _selectedItem.BackgroundColor = Style.Current.BackgroundSelected; if (_matchedItems.Count > VisibleItemCount) { - _resultPanel.VScrollBar.SmoothingScale = 0; - _resultPanel.ScrollViewTo(_selectedItem); - _resultPanel.VScrollBar.SmoothingScale = 1; + _resultPanel.ScrollViewTo(_selectedItem, true); } } } diff --git a/Source/Editor/Windows/SplashScreen.cpp b/Source/Editor/Windows/SplashScreen.cpp index d2371a540..92894b537 100644 --- a/Source/Editor/Windows/SplashScreen.cpp +++ b/Source/Editor/Windows/SplashScreen.cpp @@ -127,6 +127,14 @@ const Char* SplashScreenQuotes[] = TEXT("Who is signing all these integers?!"), TEXT("Flax fact: Flax was called Celelej once."), TEXT("Changing text overflow setti-"), + TEXT("Testing tests"), + TEXT("Free hugs"), + TEXT("Think outside the box"), + TEXT("Let's make something fantastic"), + TEXT("Be brave"), + TEXT("Drum roll please"), + TEXT("Good Luck Have Fun"), + TEXT("GG Well Played"), }; SplashScreen::~SplashScreen() diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index 8750bed03..d04da7274 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -348,7 +348,7 @@ Asset::LoadResult AudioClip::load() #if !BUILD_RELEASE // Validate buffer start times - if (Math::NotNearEqual(_buffersStartTimes[_totalChunks], GetLength())) + if (!Math::NearEqual(_buffersStartTimes[_totalChunks], GetLength(), 1.0f / 60.0f)) { LOG(Warning, "Invalid audio buffers data size. Expected length: {0}s", GetLength()); for (int32 i = 0; i < _totalChunks + 1; i++) @@ -454,14 +454,12 @@ bool AudioClip::WriteBuffer(int32 chunkIndex) } break; case AudioFormat::Raw: - { data = Span(chunk->Get(), chunk->Size()); - } - break; + break; default: return true; } - info.NumSamples = data.Length() / bytesPerSample; + info.NumSamples = Math::AlignDown(data.Length() / bytesPerSample, info.NumChannels * bytesPerSample); // Convert to Mono if used as 3D source and backend doesn't support it if (Is3D() && info.NumChannels > 1 && EnumHasNoneFlags(AudioBackend::Features(), AudioBackend::FeatureFlags::SpatialMultiChannel)) diff --git a/Source/Engine/Audio/AudioSource.cpp b/Source/Engine/Audio/AudioSource.cpp index 2cee52a8a..afe5c4b49 100644 --- a/Source/Engine/Audio/AudioSource.cpp +++ b/Source/Engine/Audio/AudioSource.cpp @@ -183,7 +183,7 @@ void AudioSource::Stop() float AudioSource::GetTime() const { - if (_state == States::Stopped || SourceIDs.IsEmpty()) + if (_state == States::Stopped || SourceIDs.IsEmpty() || !Clip->IsLoaded()) return 0.0f; float time = AudioBackend::Source::GetCurrentBufferTime(this); @@ -265,6 +265,7 @@ void AudioSource::Cleanup() void AudioSource::OnClipChanged() { Stop(); + _clipChanged = true; } void AudioSource::OnClipLoaded() @@ -318,6 +319,12 @@ void AudioSource::SetNonStreamingBuffer() void AudioSource::PlayInternal() { + if (_clipChanged && SourceIDs.HasItems()) + { + // If clip was changed between source setup (OnEnable) and actual playback start then ensure to flush any runtime properties with the audio backend + _clipChanged = false; + AudioBackend::Source::SpatialSetupChanged(this); + } AudioBackend::Source::Play(this); _isActuallyPlayingSth = true; @@ -482,6 +489,7 @@ void AudioSource::OnEnable() { _prevPos = GetPosition(); _velocity = Vector3::Zero; + _clipChanged = false; Audio::OnAddSource(this); GetScene()->Ticking.Update.AddTick(this); diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h index 3b6f32153..70cdb4180 100644 --- a/Source/Engine/Audio/AudioSource.h +++ b/Source/Engine/Audio/AudioSource.h @@ -53,6 +53,7 @@ private: bool _loop; bool _playOnStart; bool _allowSpatialization; + bool _clipChanged = false; bool _isActuallyPlayingSth = false; bool _needToUpdateStreamingBuffers = false; diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index e25b83447..2f65a4b6c 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -228,11 +228,9 @@ namespace ALC ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth) { // TODO: cache enum values in Init()?? - switch (bitDepth) { case 8: - { switch (numChannels) { case 1: @@ -247,13 +245,8 @@ ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth) return alGetEnumValue("AL_FORMAT_61CHN8"); case 8: return alGetEnumValue("AL_FORMAT_71CHN8"); - default: - CRASH; - return 0; } - } case 16: - { switch (numChannels) { case 1: @@ -268,19 +261,22 @@ ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth) return alGetEnumValue("AL_FORMAT_61CHN16"); case 8: return alGetEnumValue("AL_FORMAT_71CHN16"); - default: - CRASH; - return 0; } - } case 32: - { switch (numChannels) { case 1: +#ifdef AL_FORMAT_MONO_FLOAT32 + return AL_FORMAT_MONO_FLOAT32; +#else return alGetEnumValue("AL_FORMAT_MONO_FLOAT32"); +#endif case 2: +#ifdef AL_FORMAT_STEREO_FLOAT32 + return AL_FORMAT_STEREO_FLOAT32; +#else return alGetEnumValue("AL_FORMAT_STEREO_FLOAT32"); +#endif case 4: return alGetEnumValue("AL_FORMAT_QUAD32"); case 6: @@ -289,15 +285,9 @@ ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth) return alGetEnumValue("AL_FORMAT_61CHN32"); case 8: return alGetEnumValue("AL_FORMAT_71CHN32"); - default: - CRASH; - return 0; } } - default: - CRASH; - return 0; - } + return 0; } void AudioBackendOAL::Listener_OnAdd(AudioListener* listener) @@ -607,7 +597,8 @@ void AudioBackendOAL::Buffer_Write(uint32 bufferId, byte* samples, const AudioDa { PROFILE_CPU(); - // TODO: maybe use temporary buffers per thread to reduce dynamic allocations when uploading data to OpenAL? + // Pick the format for the audio data (it might not be supported natively) + ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth); // Mono or stereo if (info.NumChannels <= 2) @@ -618,28 +609,23 @@ void AudioBackendOAL::Buffer_Write(uint32 bufferId, byte* samples, const AudioDa { const uint32 bufferSize = info.NumSamples * sizeof(float); float* sampleBufferFloat = (float*)Allocator::Allocate(bufferSize); - AudioTool::ConvertToFloat(samples, info.BitDepth, sampleBufferFloat, info.NumSamples); - const ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth); + format = GetOpenALBufferFormat(info.NumChannels, 32); alBufferData(bufferId, format, sampleBufferFloat, bufferSize, info.SampleRate); ALC_CHECK_ERROR(alBufferData); - Allocator::Free(sampleBufferFloat); } else { LOG(Warning, "OpenAL doesn't support bit depth larger than 16. Your audio data will be truncated."); - const uint32 bufferSize = info.NumSamples * 2; byte* sampleBuffer16 = (byte*)Allocator::Allocate(bufferSize); - AudioTool::ConvertBitDepth(samples, info.BitDepth, sampleBuffer16, 16, info.NumSamples); - const ALenum format = GetOpenALBufferFormat(info.NumChannels, 16); + format = GetOpenALBufferFormat(info.NumChannels, 16); alBufferData(bufferId, format, sampleBuffer16, bufferSize, info.SampleRate); ALC_CHECK_ERROR(alBufferData); - Allocator::Free(sampleBuffer16); } } @@ -648,19 +634,15 @@ void AudioBackendOAL::Buffer_Write(uint32 bufferId, byte* samples, const AudioDa // OpenAL expects unsigned 8-bit data, but engine stores it as signed, so convert const uint32 bufferSize = info.NumSamples * (info.BitDepth / 8); byte* sampleBuffer = (byte*)Allocator::Allocate(bufferSize); - for (uint32 i = 0; i < info.NumSamples; i++) sampleBuffer[i] = ((int8*)samples)[i] + 128; - const ALenum format = GetOpenALBufferFormat(info.NumChannels, 16); alBufferData(bufferId, format, sampleBuffer, bufferSize, info.SampleRate); ALC_CHECK_ERROR(alBufferData); - Allocator::Free(sampleBuffer); } - else + else if (format) { - const ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth); alBufferData(bufferId, format, samples, info.NumSamples * (info.BitDepth / 8), info.SampleRate); ALC_CHECK_ERROR(alBufferData); } @@ -675,10 +657,9 @@ void AudioBackendOAL::Buffer_Write(uint32 bufferId, byte* samples, const AudioDa { const uint32 bufferSize = info.NumChannels * sizeof(int32); byte* sampleBuffer32 = (byte*)Allocator::Allocate(bufferSize); - AudioTool::ConvertBitDepth(samples, info.BitDepth, sampleBuffer32, 32, info.NumSamples); - const ALenum format = GetOpenALBufferFormat(info.NumChannels, 32); + format = GetOpenALBufferFormat(info.NumChannels, 32); alBufferData(bufferId, format, sampleBuffer32, bufferSize, info.SampleRate); ALC_CHECK_ERROR(alBufferData); @@ -693,19 +674,23 @@ void AudioBackendOAL::Buffer_Write(uint32 bufferId, byte* samples, const AudioDa for (uint32 i = 0; i < info.NumSamples; i++) sampleBuffer[i] = ((int8*)samples)[i] + 128; - const ALenum format = GetOpenALBufferFormat(info.NumChannels, 16); + format = GetOpenALBufferFormat(info.NumChannels, 16); alBufferData(bufferId, format, sampleBuffer, bufferSize, info.SampleRate); ALC_CHECK_ERROR(alBufferData); Allocator::Free(sampleBuffer); } - else + else if (format) { - const ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth); alBufferData(bufferId, format, samples, info.NumSamples * (info.BitDepth / 8), info.SampleRate); ALC_CHECK_ERROR(alBufferData); } } + + if (!format) + { + LOG(Error, "Not suppported audio data format for OpenAL device: BitDepth={}, NumChannels={}", info.BitDepth, info.NumChannels); + } } const Char* AudioBackendOAL::Base_Name() diff --git a/Source/Engine/Content/Cache/AssetsCache.cpp b/Source/Engine/Content/Cache/AssetsCache.cpp index ddb7159f9..c9a10f721 100644 --- a/Source/Engine/Content/Cache/AssetsCache.cpp +++ b/Source/Engine/Content/Cache/AssetsCache.cpp @@ -15,15 +15,8 @@ #include "Engine/Engine/Globals.h" #include "FlaxEngine.Gen.h" -AssetsCache::AssetsCache() - : _isDirty(false) - , _registry(4096) -{ -} - void AssetsCache::Init() { - // Cache data Entry e; int32 count; const DateTime loadStartTime = DateTime::Now(); @@ -32,13 +25,11 @@ void AssetsCache::Init() #else _path = Globals::ProjectContentFolder / TEXT("AssetsCache.dat"); #endif - LOG(Info, "Loading Asset Cache {0}...", _path); // Check if assets registry exists if (!FileSystem::FileExists(_path)) { - // Back _isDirty = true; LOG(Warning, "Cannot find assets cache file"); return; @@ -49,7 +40,6 @@ void AssetsCache::Init() DeleteMe deleteStream(stream); // Load version - // Note: every Engine build is using different assets cache int32 version; stream->ReadInt32(&version); if (version != FLAXENGINE_VERSION_BUILD) @@ -58,24 +48,28 @@ void AssetsCache::Init() return; } - // Load Engine workspace path - String workspacePath; - stream->ReadString(&workspacePath, -410); + // Load paths + String enginePath, projectPath; + stream->ReadString(&enginePath, -410); + stream->ReadString(&projectPath, -410); // Flags AssetsCacheFlags flags; stream->ReadInt32((int32*)&flags); - // Check if other engine instance used this cache (cache depends on engine build and install location) - // Skip it for relative paths mode - if (!(flags & AssetsCacheFlags::RelativePaths) && workspacePath != Globals::StartupFolder) + // Check if other workspace instance used this cache + if (EnumHasNoneFlags(flags, AssetsCacheFlags::RelativePaths) && enginePath != Globals::StartupFolder) { - LOG(Warning, "Assets cache generated by the different engine installation in \'{0}\'", workspacePath); + LOG(Warning, "Assets cache generated by the different {1} installation in \'{0}\'", enginePath, TEXT("engine")); + return; + } + if (EnumHasNoneFlags(flags, AssetsCacheFlags::RelativePaths) && projectPath != Globals::ProjectFolder) + { + LOG(Warning, "Assets cache generated by the different {1} installation in \'{0}\'", projectPath, TEXT("project")); return; } ScopeLock lock(_locker); - _isDirty = false; // Load elements count @@ -103,15 +97,11 @@ void AssetsCache::Init() e.Info.Path = Globals::StartupFolder / e.Info.Path; } - // Validate entry - if (!IsEntryValid(e)) - { - // Reject + // Use only valid entries + if (IsEntryValid(e)) + _registry.Add(e.Info.ID, e); + else rejectedCount++; - continue; - } - - _registry.Add(e.Info.ID, e); } // Paths mapping @@ -148,7 +138,6 @@ void AssetsCache::Init() } } - // End const int32 loadTimeInMs = static_cast((DateTime::Now() - loadStartTime).GetTotalMilliseconds()); LOG(Info, "Asset Cache loaded {0} entries in {1} ms ({2} rejected)", _registry.Count(), loadTimeInMs, rejectedCount); } @@ -188,8 +177,9 @@ bool AssetsCache::Save(const StringView& path, const Registry& entries, const Pa // Version stream->WriteInt32(FLAXENGINE_VERSION_BUILD); - // Engine workspace path + // Paths stream->WriteString(Globals::StartupFolder, -410); + stream->WriteString(Globals::ProjectFolder, -410); // Flags stream->WriteInt32((int32)flags); @@ -202,7 +192,6 @@ bool AssetsCache::Save(const StringView& path, const Registry& entries, const Pa for (auto i = entries.Begin(); i.IsNotEnd(); ++i) { auto& e = i->Value; - stream->Write(e.Info.ID); stream->WriteString(e.Info.TypeName, index - 13); stream->WriteString(e.Info.Path, index); @@ -211,7 +200,6 @@ bool AssetsCache::Save(const StringView& path, const Registry& entries, const Pa #else stream->WriteInt64(0); #endif - index++; } @@ -222,7 +210,6 @@ bool AssetsCache::Save(const StringView& path, const Registry& entries, const Pa { stream->Write(i->Value); stream->WriteString(i->Key, index + 73); - index++; } diff --git a/Source/Engine/Content/Cache/AssetsCache.h b/Source/Engine/Content/Cache/AssetsCache.h index 4f12c0d22..6dd9471e3 100644 --- a/Source/Engine/Content/Cache/AssetsCache.h +++ b/Source/Engine/Content/Cache/AssetsCache.h @@ -74,7 +74,7 @@ public: typedef Dictionary PathsMapping; private: - bool _isDirty; + bool _isDirty = false; CriticalSection _locker; Registry _registry; PathsMapping _pathsMapping; @@ -82,15 +82,8 @@ private: public: /// - /// Initializes a new instance of the class. + /// Gets amount of registered assets. /// - AssetsCache(); - -public: - /// - /// Gets amount of registered assets - /// - /// Registry size int32 Size() const { _locker.Lock(); diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 6aecd1778..ea627f2c7 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -125,16 +125,12 @@ void ContentService::LateUpdate() if (timeNow - LastUnloadCheckTime < Content::AssetsUpdateInterval) return; LastUnloadCheckTime = timeNow; - - Asset* asset; - ScopeLock lock(AssetsLocker); - - // TODO: maybe it would be better to link for asset remove ref event and cache only assets with no references - test it with millions of assets? + AssetsLocker.Lock(); // Verify all assets for (auto i = Assets.Begin(); i.IsNotEnd(); ++i) { - asset = i->Value; + Asset* asset = i->Value; // Check if has no references and is not during unloading if (asset->GetReferencesCount() <= 0 && !UnloadQueue.ContainsKey(asset)) @@ -158,7 +154,7 @@ void ContentService::LateUpdate() // Unload marked assets for (int32 i = 0; i < ToUnload.Count(); i++) { - asset = ToUnload[i]; + Asset* asset = ToUnload[i]; // Check if has no references if (asset->GetReferencesCount() <= 0) @@ -170,6 +166,8 @@ void ContentService::LateUpdate() UnloadQueue.Remove(asset); } + AssetsLocker.Unlock(); + // Update cache (for longer sessions it will help to reduce cache misses) Cache.Save(); } @@ -212,7 +210,6 @@ bool FindAssets(const ProjectInfo* project, HashSet& project { if (projects.Contains(project)) return false; - projects.Add(project); bool found = findAsset(id, project->ProjectFolderPath / TEXT("Content"), tmpCache, info); for (const auto& reference : project->References) @@ -220,7 +217,6 @@ bool FindAssets(const ProjectInfo* project, HashSet& project if (reference.Project) found |= FindAssets(reference.Project, projects, id, tmpCache, info); } - return found; } @@ -232,7 +228,6 @@ bool Content::GetAssetInfo(const Guid& id, AssetInfo& info) return false; #if ENABLE_ASSETS_DISCOVERY - // Find asset in registry if (Cache.FindAsset(id, info)) return true; @@ -270,19 +265,15 @@ bool Content::GetAssetInfo(const Guid& id, AssetInfo& info) //LOG(Warning, "Cannot find {0}.", id); return false; - #else - // Find asset in registry return Cache.FindAsset(id, info); - #endif } bool Content::GetAssetInfo(const StringView& path, AssetInfo& info) { #if ENABLE_ASSETS_DISCOVERY - // Find asset in registry if (Cache.FindAsset(path, info)) return true; @@ -326,12 +317,9 @@ bool Content::GetAssetInfo(const StringView& path, AssetInfo& info) } return false; - #else - // Find asset in registry return Cache.FindAsset(path, info); - #endif } diff --git a/Source/Engine/ContentImporters/ImportAudio.cpp b/Source/Engine/ContentImporters/ImportAudio.cpp index 10915b2d1..803bf58a8 100644 --- a/Source/Engine/ContentImporters/ImportAudio.cpp +++ b/Source/Engine/ContentImporters/ImportAudio.cpp @@ -18,32 +18,6 @@ #include "Engine/Tools/AudioTool/OggVorbisDecoder.h" #include "Engine/Tools/AudioTool/OggVorbisEncoder.h" #include "Engine/Serialization/JsonWriters.h" -#include "Engine/Scripting/Enums.h" - -String ImportAudio::Options::ToString() const -{ - return String::Format(TEXT("Format:{}, DisableStreaming:{}, Is3D:{}, Quality:{}, BitDepth:{}"), ScriptingEnum::ToString(Format), DisableStreaming, Is3D, Quality, BitDepth); -} - -void ImportAudio::Options::Serialize(SerializeStream& stream, const void* otherObj) -{ - SERIALIZE_GET_OTHER_OBJ(ImportAudio::Options); - - SERIALIZE(Format); - SERIALIZE(DisableStreaming); - SERIALIZE(Is3D); - SERIALIZE(Quality); - SERIALIZE(BitDepth); -} - -void ImportAudio::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - DESERIALIZE(Format); - DESERIALIZE(DisableStreaming); - DESERIALIZE(Is3D); - DESERIALIZE(Quality); - DESERIALIZE(BitDepth); -} bool ImportAudio::TryGetImportOptions(const StringView& path, Options& options) { @@ -90,6 +64,10 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& } } + // Vorbis uses fixed 16-bit depth + if (options.Format == AudioFormat::Vorbis) + options.BitDepth = AudioTool::BitDepth::_16; + LOG_STR(Info, options.ToString()); // Open the file @@ -112,16 +90,14 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& sampleBuffer.Link(audioData.Get()); // Convert bit depth if need to - if (options.BitDepth != static_cast(info.BitDepth)) + uint32 outputBitDepth = (uint32)options.BitDepth; + if (outputBitDepth != info.BitDepth) { - const uint32 outBufferSize = info.NumSamples * (options.BitDepth / 8); + const uint32 outBufferSize = info.NumSamples * (outputBitDepth / 8); sampleBuffer.Allocate(outBufferSize); - - AudioTool::ConvertBitDepth(audioData.Get(), info.BitDepth, sampleBuffer.Get(), options.BitDepth, info.NumSamples); - - info.BitDepth = options.BitDepth; + AudioTool::ConvertBitDepth(audioData.Get(), info.BitDepth, sampleBuffer.Get(), outputBitDepth, info.NumSamples); + info.BitDepth = outputBitDepth; bytesPerSample = info.BitDepth / 8; - bufferSize = outBufferSize; } @@ -149,7 +125,7 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& context.Data.Header.Chunks[chunkIndex]->Data.Copy(dataPtr, dataSize); #define WRITE_DATA(chunkIndex, dataPtr, dataSize) \ - samplesPerChunk[chunkIndex] = (dataSize) / (options.BitDepth / 8); \ + samplesPerChunk[chunkIndex] = (dataSize) / (outputBitDepth / 8); \ switch (options.Format) \ { \ case AudioFormat::Raw: \ @@ -183,8 +159,9 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& else { // Split audio data into a several chunks (uniform data spread) - const int32 MinChunkSize = 1 * 1024 * 1024; // 1 MB - const int32 chunkSize = Math::Max(MinChunkSize, (int32)Math::AlignUp(bufferSize / ASSET_FILE_DATA_CHUNKS, 256)); + const int32 minChunkSize = 1 * 1024 * 1024; // 1 MB + const int32 dataAlignment = info.NumChannels * bytesPerSample; // Ensure to never split samples in-between (eg. 24-bit that uses 3 bytes) + const int32 chunkSize = Math::Max(minChunkSize, (int32)Math::AlignUp(bufferSize / ASSET_FILE_DATA_CHUNKS, dataAlignment)); const int32 chunksCount = Math::CeilToInt((float)bufferSize / chunkSize); ASSERT(chunksCount > 0 && chunksCount <= ASSET_FILE_DATA_CHUNKS); diff --git a/Source/Engine/ContentImporters/ImportAudio.h b/Source/Engine/ContentImporters/ImportAudio.h index fb4ee64e3..3f3eda5ed 100644 --- a/Source/Engine/ContentImporters/ImportAudio.h +++ b/Source/Engine/ContentImporters/ImportAudio.h @@ -3,12 +3,12 @@ #pragma once #include "Types.h" -#include "Engine/Tools/AudioTool/AudioDecoder.h" -#include "Engine/Core/ISerializable.h" -#include "Engine/Audio/Config.h" #if COMPILE_WITH_ASSETS_IMPORTER +#include "Engine/Tools/AudioTool/AudioTool.h" +#include "Engine/Tools/AudioTool/AudioDecoder.h" + /// /// Enable/disable caching audio import options /// @@ -20,23 +20,7 @@ class ImportAudio { public: - /// - /// Importing audio options - /// - struct Options : public ISerializable - { - AudioFormat Format = AudioFormat::Vorbis; - bool DisableStreaming = false; - bool Is3D = false; - int32 BitDepth = 16; - float Quality = 0.4f; - - String ToString() const; - - // [ISerializable] - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; - }; + typedef AudioTool::Options Options; public: /// diff --git a/Source/Engine/Core/Collections/ChunkedArray.h b/Source/Engine/Core/Collections/ChunkedArray.h index d01711e38..38bf92fb8 100644 --- a/Source/Engine/Core/Collections/ChunkedArray.h +++ b/Source/Engine/Core/Collections/ChunkedArray.h @@ -136,13 +136,11 @@ public: public: bool IsEnd() const { - ASSERT(_collection); return Index() == _collection->Count(); } bool IsNotEnd() const { - ASSERT(_collection); return Index() != _collection->Count(); } @@ -171,8 +169,6 @@ public: public: Iterator& operator++() { - ASSERT(_collection); - // Check if it is not at end const int32 end = _collection->Count(); if (Index() != end) @@ -188,38 +184,18 @@ public: _index = 0; } } - return *this; } Iterator operator++(int) { - ASSERT(_collection); Iterator temp = *this; - - // Check if it is not at end - const int32 end = _collection->Count(); - if (Index() != end) - { - // Move forward within chunk - _index++; - - // Check if need to change chunk - if (_index == ChunkSize && _chunkIndex < _collection->_chunks.Count() - 1) - { - // Move to next chunk - _chunkIndex++; - _index = 0; - } - } - + ++temp; return temp; } Iterator& operator--() { - ASSERT(_collection); - // Check if it's not at beginning if (_index != 0 || _chunkIndex != 0) { @@ -236,32 +212,13 @@ public: _index--; } } - return *this; } Iterator operator--(int) { - ASSERT(_collection); Iterator temp = *this; - - // Check if it's not at beginning - if (_index != 0 || _chunkIndex != 0) - { - // Check if need to change chunk - if (_index == 0) - { - // Move to previous chunk - _chunkIndex--; - _index = ChunkSize - 1; - } - else - { - // Move backward within chunk - _index--; - } - } - + --temp; return temp; } }; diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index c454f02d1..575863dc9 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -309,7 +309,7 @@ public: FORCE_INLINE bool operator==(const Iterator& v) const { - return _index == v._index && &_collection == &v._collection; + return _index == v._index && _collection == v._collection; } FORCE_INLINE bool operator!=(const Iterator& v) const diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index 35458595e..bb3fd8bf1 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -73,28 +73,22 @@ void ObjectsRemovalService::Flush(float dt, float gameDelta) PoolLocker.Lock(); - int32 itemsLeft; - do + // Update timeouts and delete objects that timed out + for (auto i = Pool.Begin(); i.IsNotEnd(); ++i) { - // Update timeouts and delete objects that timed out - itemsLeft = Pool.Count(); - for (auto i = Pool.Begin(); i.IsNotEnd(); ++i) + auto& bucket = *i; + Object* obj = bucket.Key; + const float ttl = bucket.Value - ((obj->Flags & ObjectFlags::UseGameTimeForDelete) != ObjectFlags::None ? gameDelta : dt); + if (ttl <= 0.0f) { - Object* obj = i->Key; - const float ttl = i->Value - ((obj->Flags & ObjectFlags::UseGameTimeForDelete) != ObjectFlags::None ? gameDelta : dt); - if (ttl <= 0.0f) - { - Pool.Remove(i); - obj->OnDeleteObject(); - itemsLeft--; - } - else - { - i->Value = ttl; - } + Pool.Remove(i); + obj->OnDeleteObject(); + } + else + { + bucket.Value = ttl; } } - while (itemsLeft != Pool.Count()); // Continue removing if any new item was added during removing (eg. sub-object delete with 0 timeout) PoolLocker.Unlock(); } diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index 89a227eb7..d061d0745 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -171,6 +171,8 @@ int32 FindVertex(const MeshData& mesh, int32 vertexIndex, int32 startIndex, int3 const Float3 vNormal = mesh.Normals.HasItems() ? mesh.Normals[vertexIndex] : Float3::Zero; const Float3 vTangent = mesh.Tangents.HasItems() ? mesh.Tangents[vertexIndex] : Float3::Zero; const Float2 vLightmapUV = mesh.LightmapUVs.HasItems() ? mesh.LightmapUVs[vertexIndex] : Float2::Zero; + const Color vColor = mesh.Colors.HasItems() ? mesh.Colors[vertexIndex] : Color::Black; // Assuming Color::Black as a default color + const int32 end = startIndex + searchRange; for (size_t i = 0; i < sparialSortCache.size(); i++) @@ -184,6 +186,8 @@ int32 FindVertex(const MeshData& mesh, int32 vertexIndex, int32 startIndex, int3 const Float3 vNormal = mesh.Normals.HasItems() ? mesh.Normals[vertexIndex] : Float3::Zero; const Float3 vTangent = mesh.Tangents.HasItems() ? mesh.Tangents[vertexIndex] : Float3::Zero; const Float2 vLightmapUV = mesh.LightmapUVs.HasItems() ? mesh.LightmapUVs[vertexIndex] : Float2::Zero; + const Color vColor = mesh.Colors.HasItems() ? mesh.Colors[vertexIndex] : Color::Black; // Assuming Color::Black as a default color + const int32 end = startIndex + searchRange; for (int32 v = startIndex; v < end; v++) @@ -201,6 +205,8 @@ int32 FindVertex(const MeshData& mesh, int32 vertexIndex, int32 startIndex, int3 continue; if (mesh.LightmapUVs.HasItems() && (vLightmapUV - mesh.LightmapUVs[v]).LengthSquared() > uvEpsSqr) continue; + if (mesh.Colors.HasItems() && vColor != mesh.Colors[v]) + continue; // TODO: check more components? return v; diff --git a/Source/Engine/Graphics/RenderTask.cs b/Source/Engine/Graphics/RenderTask.cs index a1f90a9c5..fd42c754c 100644 --- a/Source/Engine/Graphics/RenderTask.cs +++ b/Source/Engine/Graphics/RenderTask.cs @@ -39,5 +39,19 @@ namespace FlaxEngine View = view; } } + + /// + /// The rendering mask for layers. Used to exclude objects from rendering (via property). + /// + public LayersMask ViewLayersMask + { + get => View.RenderLayersMask; + set + { + var view = View; + view.RenderLayersMask = value; + View = view; + } + } } } diff --git a/Source/Engine/Graphics/RenderView.cpp b/Source/Engine/Graphics/RenderView.cpp index 46766c34d..efcbb1513 100644 --- a/Source/Engine/Graphics/RenderView.cpp +++ b/Source/Engine/Graphics/RenderView.cpp @@ -176,7 +176,7 @@ void RenderView::SetProjector(float nearPlane, float farPlane, const Float3& pos CullingFrustum = Frustum; } -void RenderView::CopyFrom(Camera* camera, Viewport* viewport) +void RenderView::CopyFrom(const Camera* camera, const Viewport* viewport) { const Vector3 cameraPos = camera->GetPosition(); LargeWorlds::UpdateOrigin(Origin, cameraPos); @@ -192,6 +192,8 @@ void RenderView::CopyFrom(Camera* camera, Viewport* viewport) Frustum.GetInvMatrix(IVP); CullingFrustum = Frustum; RenderLayersMask = camera->RenderLayersMask; + Flags = camera->RenderFlags; + Mode = camera->RenderMode; } void RenderView::GetWorldMatrix(const Transform& transform, Matrix& world) const diff --git a/Source/Engine/Graphics/RenderView.cs b/Source/Engine/Graphics/RenderView.cs index 091661514..b770037a3 100644 --- a/Source/Engine/Graphics/RenderView.cs +++ b/Source/Engine/Graphics/RenderView.cs @@ -102,6 +102,8 @@ namespace FlaxEngine NonJitteredProjection = Projection; TemporalAAJitter = Float4.Zero; RenderLayersMask = camera.RenderLayersMask; + Flags = camera.RenderFlags; + Mode = camera.RenderMode; UpdateCachedData(); } diff --git a/Source/Engine/Graphics/RenderView.h b/Source/Engine/Graphics/RenderView.h index 5cfe06a8d..e72ebaa45 100644 --- a/Source/Engine/Graphics/RenderView.h +++ b/Source/Engine/Graphics/RenderView.h @@ -295,10 +295,12 @@ public: /// Camera's FOV angle (in degrees) void SetProjector(float nearPlane, float farPlane, const Float3& position, const Float3& direction, const Float3& up, float angle); - // Copy view data from camera - // @param camera Camera to copy its data - // @param camera The custom viewport to use for view/projection matrices override. - void CopyFrom(Camera* camera, Viewport* viewport = nullptr); + /// + /// Copies view data from camera to the view. + /// + /// The camera to copy its data. + /// The custom viewport to use for view/projection matrices override. + void CopyFrom(const Camera* camera, const Viewport* viewport = nullptr); public: FORCE_INLINE DrawPass GetShadowsDrawPassMask(ShadowsCastingMode shadowsMode) const diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp index 28957d9f3..d758ec999 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp @@ -649,22 +649,6 @@ void GPUContextVulkan::UpdateDescriptorSets(ComputePipelineStateVulkan* pipeline } } -void GPUContextVulkan::BindPipeline() -{ - if (_psDirtyFlag && _currentState && (_rtDepth || _rtCount)) - { - // Clear flag - _psDirtyFlag = false; - - // Change state - const auto cmdBuffer = _cmdBufferManager->GetCmdBuffer(); - const auto pipeline = _currentState->GetState(_renderPass); - vkCmdBindPipeline(cmdBuffer->GetHandle(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - - RENDER_STAT_PS_STATE_CHANGE(); - } -} - void GPUContextVulkan::OnDrawCall() { GPUPipelineStateVulkan* pipelineState = _currentState; @@ -704,7 +688,15 @@ void GPUContextVulkan::OnDrawCall() BeginRenderPass(); } - BindPipeline(); + // Bind pipeline + if (_psDirtyFlag && _currentState && (_rtDepth || _rtCount)) + { + _psDirtyFlag = false; + const auto cmdBuffer = _cmdBufferManager->GetCmdBuffer(); + const auto pipeline = _currentState->GetState(_renderPass); + vkCmdBindPipeline(cmdBuffer->GetHandle(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + RENDER_STAT_PS_STATE_CHANGE(); + } //UpdateDynamicStates(); @@ -1115,9 +1107,7 @@ void GPUContextVulkan::Dispatch(GPUShaderProgramCS* shader, uint32 threadGroupCo EndRenderPass(); auto pipelineState = shaderVulkan->GetOrCreateState(); - UpdateDescriptorSets(pipelineState); - FlushBarriers(); // Bind pipeline @@ -1152,10 +1142,8 @@ void GPUContextVulkan::DispatchIndirect(GPUShaderProgramCS* shader, GPUBuffer* b EndRenderPass(); auto pipelineState = shaderVulkan->GetOrCreateState(); - UpdateDescriptorSets(pipelineState); AddBufferBarrier(bufferForArgsVulkan, VK_ACCESS_INDIRECT_COMMAND_READ_BIT); - FlushBarriers(); // Bind pipeline diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.h index 08b5c1d6a..125bba8cf 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.h @@ -161,7 +161,6 @@ private: void UpdateDescriptorSets(const struct SpirvShaderDescriptorInfo& descriptorInfo, class DescriptorSetWriterVulkan& dsWriter, bool& needsWrite); void UpdateDescriptorSets(GPUPipelineStateVulkan* pipelineState); void UpdateDescriptorSets(ComputePipelineStateVulkan* pipelineState); - void BindPipeline(); void OnDrawCall(); public: diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 77ce515a7..75e3c12d4 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -886,6 +886,14 @@ void Actor::EndPlay() OnDisable(); } + for (auto* script : Scripts) + { + CHECK_EXECUTE_IN_EDITOR + { + script->OnDestroy(); + } + } + OnEndPlay(); // Clear flag @@ -899,15 +907,6 @@ void Actor::EndPlay() e->EndPlay(); } - // Fire event for scripting - for (auto* script : Scripts) - { - CHECK_EXECUTE_IN_EDITOR - { - script->OnDestroy(); - } - } - // Inform attached scripts for (int32 i = 0; i < Scripts.Count(); i++) { diff --git a/Source/Engine/Level/Actors/Camera.cpp b/Source/Engine/Level/Actors/Camera.cpp index 72c7f4c54..c3559c87f 100644 --- a/Source/Engine/Level/Actors/Camera.cpp +++ b/Source/Engine/Level/Actors/Camera.cpp @@ -416,6 +416,8 @@ void Camera::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(Far, _far); SERIALIZE_MEMBER(OrthoScale, _orthoScale); SERIALIZE(RenderLayersMask); + SERIALIZE(RenderFlags); + SERIALIZE(RenderMode); } void Camera::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -430,6 +432,8 @@ void Camera::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier DESERIALIZE_MEMBER(Far, _far); DESERIALIZE_MEMBER(OrthoScale, _orthoScale); DESERIALIZE(RenderLayersMask); + DESERIALIZE(RenderFlags); + DESERIALIZE(RenderMode); } void Camera::OnEnable() diff --git a/Source/Engine/Level/Actors/Camera.h b/Source/Engine/Level/Actors/Camera.h index 5a0bece33..22d950dd4 100644 --- a/Source/Engine/Level/Actors/Camera.h +++ b/Source/Engine/Level/Actors/Camera.h @@ -7,6 +7,7 @@ #include "Engine/Core/Math/Viewport.h" #include "Engine/Core/Math/Ray.h" #include "Engine/Core/Types/LayersMask.h" +#include "Engine/Graphics/Enums.h" #include "Engine/Scripting/ScriptingObjectReference.h" #if USE_EDITOR #include "Engine/Content/AssetReference.h" @@ -134,6 +135,18 @@ public: API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Camera\")") LayersMask RenderLayersMask; + /// + /// Frame rendering flags used to switch between graphics features for this camera. + /// + API_FIELD(Attributes = "EditorOrder(110), EditorDisplay(\"Camera\")") + ViewFlags RenderFlags = ViewFlags::DefaultGame; + + /// + /// Describes frame rendering modes for this camera. + /// + API_FIELD(Attributes = "EditorOrder(120), EditorDisplay(\"Camera\")") + ViewMode RenderMode = ViewMode::Default; + public: /// /// Projects the point from 3D world-space to game window coordinates (in screen pixels for default viewport calculated from ). diff --git a/Source/Engine/Level/Actors/PointLight.h b/Source/Engine/Level/Actors/PointLight.h index 50df19232..f40d4a0b0 100644 --- a/Source/Engine/Level/Actors/PointLight.h +++ b/Source/Engine/Level/Actors/PointLight.h @@ -30,18 +30,18 @@ public: API_FIELD(Attributes="EditorOrder(3), DefaultValue(0.0f), EditorDisplay(\"Light\"), Limit(0, 1000, 0.01f)") float SourceLength = 0.0f; - /// - /// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled. - /// - API_FIELD(Attributes="EditorOrder(13), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f), VisibleIf(nameof(UseInverseSquaredFalloff), true)") - float FallOffExponent = 8.0f; - /// /// Whether to use physically based inverse squared distance falloff, where Radius is only clamping the light's contribution. /// - API_FIELD(Attributes="EditorOrder(14), DefaultValue(false), EditorDisplay(\"Light\")") + API_FIELD(Attributes = "EditorOrder(13), DefaultValue(false), EditorDisplay(\"Light\")") bool UseInverseSquaredFalloff = false; + /// + /// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled. + /// + API_FIELD(Attributes="EditorOrder(14), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f), VisibleIf(nameof(UseInverseSquaredFalloff), true)") + float FallOffExponent = 8.0f; + /// /// IES texture (light profiles from real world measured data) /// diff --git a/Source/Engine/Level/Actors/SpotLight.h b/Source/Engine/Level/Actors/SpotLight.h index e6179a522..37735692a 100644 --- a/Source/Engine/Level/Actors/SpotLight.h +++ b/Source/Engine/Level/Actors/SpotLight.h @@ -29,18 +29,18 @@ public: API_FIELD(Attributes="EditorOrder(2), DefaultValue(0.0f), EditorDisplay(\"Light\"), Limit(0, 1000, 0.01f)") float SourceRadius = 0.0f; - /// - /// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled. - /// - API_FIELD(Attributes="EditorOrder(13), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f), VisibleIf(nameof(UseInverseSquaredFalloff), true)") - float FallOffExponent = 8.0f; - /// /// Whether to use physically based inverse squared distance falloff, where Radius is only clamping the light's contribution. /// - API_FIELD(Attributes="EditorOrder(14), DefaultValue(false), EditorDisplay(\"Light\")") + API_FIELD(Attributes = "EditorOrder(13), DefaultValue(false), EditorDisplay(\"Light\")") bool UseInverseSquaredFalloff = false; + /// + /// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled. + /// + API_FIELD(Attributes="EditorOrder(14), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f), VisibleIf(nameof(UseInverseSquaredFalloff), true)") + float FallOffExponent = 8.0f; + /// /// IES texture (light profiles from real world measured data) /// diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index 6ea1be5fd..123013064 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -332,6 +332,8 @@ bool PrefabManager::CreatePrefab(Actor* targetActor, const StringView& outputPat // Serialize to json data ASSERT(!IsCreatingPrefab); IsCreatingPrefab = true; + const Guid targetPrefabId = targetActor->GetPrefabID(); + const bool hasTargetPrefabId = targetPrefabId.IsValid(); rapidjson_flax::StringBuffer actorsDataBuffer; { CompactJsonWriter writerObj(actorsDataBuffer); @@ -340,7 +342,27 @@ bool PrefabManager::CreatePrefab(Actor* targetActor, const StringView& outputPat for (int32 i = 0; i < sceneObjects->Count(); i++) { SceneObject* obj = sceneObjects->At(i); + + // Detect when creating prefab from object that is already part of prefab then serialize it as unlinked + const Guid prefabId = obj->GetPrefabID(); + const Guid prefabObjectId = obj->GetPrefabObjectID(); + bool isObjectFromPrefab = targetPrefabId == prefabId && prefabId.IsValid(); // Allow to use other nested prefabs properly (ignore only root object's prefab link) + if (isObjectFromPrefab) + { + //obj->BreakPrefabLink(); + obj->_prefabID = Guid::Empty; + obj->_prefabObjectID = Guid::Empty; + } + writer.SceneObject(obj); + + // Restore broken link + if (hasTargetPrefabId) + { + //obj->LinkPrefab(prefabId, prefabObjectId); + obj->_prefabID = prefabId; + obj->_prefabObjectID = prefabObjectId; + } } writer.EndArray(); } @@ -406,7 +428,6 @@ bool PrefabManager::CreatePrefab(Actor* targetActor, const StringView& outputPat { SceneObject* obj = sceneObjects->At(i); Guid prefabObjectId; - if (objectInstanceIdToPrefabObjectId.TryGet(obj->GetSceneObjectId(), prefabObjectId)) { obj->LinkPrefab(assetInfo.ID, prefabObjectId); diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index ab314ab72..4787b4549 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -945,7 +945,7 @@ const char* ButtonCodeToKeyName(KeyboardKeys code) // Row #6 case KeyboardKeys::Control: return "LCTL"; // Left Control case KeyboardKeys::LeftWindows: return "LWIN"; - case KeyboardKeys::LeftMenu: return "LALT"; + case KeyboardKeys::Alt: return "LALT"; case KeyboardKeys::Spacebar: return "SPCE"; case KeyboardKeys::RightMenu: return "RALT"; case KeyboardKeys::RightWindows: return "RWIN"; @@ -2700,6 +2700,8 @@ Float2 LinuxPlatform::GetDesktopSize() if (screenIdx >= count) return Float2::Zero; + // this function is used as a fallback to place a window at the center of + // a screen so we report only one screen instead of the real desktop Float2 size((float)xsi[screenIdx].width, (float)xsi[screenIdx].height); X11::XFree(xsi); return size; @@ -2707,14 +2709,72 @@ Float2 LinuxPlatform::GetDesktopSize() Rectangle LinuxPlatform::GetMonitorBounds(const Float2& screenPos) { - // TODO: do it in a proper way - return Rectangle(Float2::Zero, GetDesktopSize()); + if (!xDisplay) + return Rectangle::Empty; + + int event, err; + const bool ok = X11::XineramaQueryExtension(xDisplay, &event, &err); + if (!ok) + return Rectangle::Empty; + + int count; + int screenIdx = 0; + X11::XineramaScreenInfo* xsi = X11::XineramaQueryScreens(xDisplay, &count); + if (screenIdx >= count) + return Rectangle::Empty; + // find the screen for this screenPos + for (int i = 0; i < count; i++) + { + if (screenPos.X >= xsi[i].x_org && screenPos.X < xsi[i].x_org+xsi[i].width + && screenPos.Y >= xsi[i].y_org && screenPos.Y < xsi[i].y_org+xsi[i].height) + { + screenIdx = i; + break; + } + } + + Float2 org((float)xsi[screenIdx].x_org, (float)xsi[screenIdx].y_org); + Float2 size((float)xsi[screenIdx].width, (float)xsi[screenIdx].height); + X11::XFree(xsi); + return Rectangle(org, size); } Rectangle LinuxPlatform::GetVirtualDesktopBounds() { - // TODO: do it in a proper way - return Rectangle(Float2::Zero, GetDesktopSize()); + if (!xDisplay) + return Rectangle::Empty; + + int event, err; + const bool ok = X11::XineramaQueryExtension(xDisplay, &event, &err); + if (!ok) + return Rectangle::Empty; + + int count; + X11::XineramaScreenInfo* xsi = X11::XineramaQueryScreens(xDisplay, &count); + if (count <= 0) + return Rectangle::Empty; + // get all screen dimensions and assume the monitors form a rectangle + // as you can arrange monitors to your liking this is not necessarily the case + int minX = INT32_MAX, minY = INT32_MAX; + int maxX = 0, maxY = 0; + for (int i = 0; i < count; i++) + { + int maxScreenX = xsi[i].x_org + xsi[i].width; + int maxScreenY = xsi[i].y_org + xsi[i].height; + if (maxScreenX > maxX) + maxX = maxScreenX; + if (maxScreenY > maxY) + maxY = maxScreenY; + if (minX > xsi[i].x_org) + minX = xsi[i].x_org; + if (minY > xsi[i].y_org) + minY = xsi[i].y_org; + } + + Float2 org(static_cast(minX), static_cast(minY)); + Float2 size(static_cast(maxX - minX), static_cast(maxY - minY)); + X11::XFree(xsi); + return Rectangle(org, size); } String LinuxPlatform::GetMainDirectory() diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp index 6894a5121..a232dea55 100644 --- a/Source/Engine/Platform/Linux/LinuxWindow.cpp +++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp @@ -41,8 +41,10 @@ extern Dictionary KeyNameMap; extern Array KeyCodeMap; extern X11::Cursor Cursors[(int32)CursorType::MAX]; -static const uint32 MouseDoubleClickTime = 500; +static constexpr uint32 MouseDoubleClickTime = 500; +static constexpr uint32 MaxDoubleClickDistanceSquared = 10; static X11::Time MouseLastButtonPressTime = 0; +static Float2 OldMouseClickPosition; LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) : WindowBase(settings) @@ -153,7 +155,11 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) hints.max_height = (int)settings.MaximumSize.Y; hints.flags |= USSize; } - X11::XSetNormalHints(display, window, &hints); + // honor the WM placement except for manual (overriding) placements + if (settings.StartPosition == WindowStartPosition::Manual) + { + X11::XSetNormalHints(display, window, &hints); + } // Ensures the child window is always on top of the parent window if (settings.Parent) @@ -595,15 +601,19 @@ void LinuxWindow::OnButtonPress(void* event) // Handle double-click if (buttonEvent->button == Button1) { - if (buttonEvent->time < (MouseLastButtonPressTime + MouseDoubleClickTime)) + if ( + buttonEvent->time < (MouseLastButtonPressTime + MouseDoubleClickTime) && + Float2::DistanceSquared(mousePos, OldMouseClickPosition) < MaxDoubleClickDistanceSquared) { Input::Mouse->OnMouseDoubleClick(ClientToScreen(mousePos), mouseButton, this); MouseLastButtonPressTime = 0; + OldMouseClickPosition = mousePos; return; } else { MouseLastButtonPressTime = buttonEvent->time; + OldMouseClickPosition = mousePos; } } diff --git a/Source/Engine/Serialization/JsonTools.cpp b/Source/Engine/Serialization/JsonTools.cpp index c395df4f1..3cbe8f65c 100644 --- a/Source/Engine/Serialization/JsonTools.cpp +++ b/Source/Engine/Serialization/JsonTools.cpp @@ -81,7 +81,6 @@ void JsonTools::ChangeIds(Document& doc, const Dictionary& mapping) ::ChangeIds(doc, doc, mapping); } - Float2 JsonTools::GetFloat2(const Value& value) { Float2 result; diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp index 0d16e4f7c..ce4116134 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp @@ -301,7 +301,7 @@ void ShadersCompilation::RegisterForShaderReloads(Asset* asset, const String& in { // Create a directory watcher to track the included file changes const String directory = StringUtils::GetDirectoryName(includedPath); - if (!ShaderIncludesWatcher.ContainsKey(directory)) + if (FileSystem::DirectoryExists(directory) && !ShaderIncludesWatcher.ContainsKey(directory)) { auto watcher = New(directory, false); watcher->OnEvent.Bind(); diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index 92a8c09d3..8e05062c8 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -1126,12 +1126,9 @@ bool TerrainPatch::InitializeHeightMap() float* TerrainPatch::GetHeightmapData() { PROFILE_CPU_NAMED("Terrain.GetHeightmapData"); - if (_cachedHeightMap.HasItems()) return _cachedHeightMap.Get(); - CacheHeightData(); - return _cachedHeightMap.Get(); } @@ -1144,12 +1141,9 @@ void TerrainPatch::ClearHeightmapCache() byte* TerrainPatch::GetHolesMaskData() { PROFILE_CPU_NAMED("Terrain.GetHolesMaskData"); - if (_cachedHolesMask.HasItems()) return _cachedHolesMask.Get(); - CacheHeightData(); - return _cachedHolesMask.Get(); } @@ -1161,15 +1155,11 @@ void TerrainPatch::ClearHolesMaskCache() Color32* TerrainPatch::GetSplatMapData(int32 index) { - ASSERT(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT); - + CHECK_RETURN(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT, nullptr); PROFILE_CPU_NAMED("Terrain.GetSplatMapData"); - if (_cachedSplatMap[index].HasItems()) return _cachedSplatMap[index].Get(); - CacheSplatData(); - return _cachedSplatMap[index].Get(); } diff --git a/Source/Engine/Tools/AudioTool/AudioTool.cpp b/Source/Engine/Tools/AudioTool/AudioTool.cpp index a5a5d4663..3136a5a0d 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.cpp +++ b/Source/Engine/Tools/AudioTool/AudioTool.cpp @@ -1,14 +1,49 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +#if COMPILE_WITH_AUDIO_TOOL + #include "AudioTool.h" #include "Engine/Core/Core.h" #include "Engine/Core/Memory/Allocation.h" +#if USE_EDITOR +#include "Engine/Serialization/Serialization.h" +#include "Engine/Scripting/Enums.h" +#endif #define CONVERT_TO_MONO_AVG 1 #if !CONVERT_TO_MONO_AVG #include "Engine/Core/Math/Math.h" #endif +#if USE_EDITOR + +String AudioTool::Options::ToString() const +{ + return String::Format(TEXT("Format:{}, DisableStreaming:{}, Is3D:{}, Quality:{}, BitDepth:{}"), ScriptingEnum::ToString(Format), DisableStreaming, Is3D, Quality, (int32)BitDepth); +} + +void AudioTool::Options::Serialize(SerializeStream& stream, const void* otherObj) +{ + SERIALIZE_GET_OTHER_OBJ(AudioTool::Options); + + SERIALIZE(Format); + SERIALIZE(DisableStreaming); + SERIALIZE(Is3D); + SERIALIZE(Quality); + SERIALIZE(BitDepth); +} + +void AudioTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ + DESERIALIZE(Format); + DESERIALIZE(DisableStreaming); + DESERIALIZE(Is3D); + DESERIALIZE(Quality); + DESERIALIZE(BitDepth); +} + +#endif + void ConvertToMono8(const int8* input, uint8* output, uint32 numSamples, uint32 numChannels) { for (uint32 i = 0; i < numSamples; i++) @@ -231,8 +266,7 @@ void AudioTool::ConvertToFloat(const byte* input, uint32 inBitDepth, float* outp for (uint32 i = 0; i < numSamples; i++) { const int8 sample = *(int8*)input; - output[i] = sample / 127.0f; - + output[i] = sample * (1.0f / 127.0f); input++; } } @@ -241,8 +275,7 @@ void AudioTool::ConvertToFloat(const byte* input, uint32 inBitDepth, float* outp for (uint32 i = 0; i < numSamples; i++) { const int16 sample = *(int16*)input; - output[i] = sample / 32767.0f; - + output[i] = sample * (1.0f / 32767.0f); input += 2; } } @@ -251,8 +284,7 @@ void AudioTool::ConvertToFloat(const byte* input, uint32 inBitDepth, float* outp for (uint32 i = 0; i < numSamples; i++) { const int32 sample = Convert24To32Bits(input); - output[i] = sample / 2147483647.0f; - + output[i] = sample * (1.0f / 2147483647.0f); input += 3; } } @@ -261,8 +293,7 @@ void AudioTool::ConvertToFloat(const byte* input, uint32 inBitDepth, float* outp for (uint32 i = 0; i < numSamples; i++) { const int32 sample = *(int32*)input; - output[i] = sample / 2147483647.0f; - + output[i] = sample * (1.0f / 2147483647.0f); input += 4; } } @@ -278,7 +309,8 @@ void AudioTool::ConvertFromFloat(const float* input, int32* output, uint32 numSa { const float sample = *(float*)input; output[i] = static_cast(sample * 2147483647.0f); - input++; } } + +#endif diff --git a/Source/Engine/Tools/AudioTool/AudioTool.h b/Source/Engine/Tools/AudioTool/AudioTool.h index b9525ffa7..d7e28df57 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.h +++ b/Source/Engine/Tools/AudioTool/AudioTool.h @@ -2,16 +2,86 @@ #pragma once +#if COMPILE_WITH_AUDIO_TOOL + #include "Engine/Core/Config.h" #include "Engine/Core/Types/BaseTypes.h" +#if USE_EDITOR +#include "Engine/Core/ISerializable.h" +#endif +#include "Engine/Audio/Types.h" /// /// Audio data importing and processing utilities. /// -class FLAXENGINE_API AudioTool +API_CLASS(Namespace="FlaxEngine.Tools", Static) class FLAXENGINE_API AudioTool { -public: + DECLARE_SCRIPTING_TYPE_MINIMAL(AudioTool); +#if USE_EDITOR + +public: + /// + /// Declares the imported audio clip bit depth. + /// + API_ENUM(Attributes="HideInEditor") enum class BitDepth : int32 + { + // 8-bits per sample. + _8 = 8, + // 16-bits per sample. + _16 = 16, + // 24-bits per sample. + _24 = 24, + // 32-bits per sample. + _32 = 32, + }; + + /// + /// Audio import options. + /// + API_STRUCT(Attributes="HideInEditor") struct FLAXENGINE_API Options : public ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(Options); + + /// + /// The audio data format to import the audio clip as. + /// + API_FIELD(Attributes="EditorOrder(10)") + AudioFormat Format = AudioFormat::Vorbis; + + /// + /// The audio data compression quality. Used only if target format is using compression. Value 0 means the smallest size, value 1 means the best quality. + /// + API_FIELD(Attributes="EditorOrder(20), EditorDisplay(null, \"Compression Quality\"), Limit(0, 1, 0.01f)") + float Quality = 0.4f; + + /// + /// Disables dynamic audio streaming. The whole clip will be loaded into the memory. Useful for small clips (eg. gunfire sounds). + /// + API_FIELD(Attributes="EditorOrder(30)") + bool DisableStreaming = false; + + /// + /// Checks should the clip be played as spatial (3D) audio or as normal audio. 3D audio is stored in Mono format. + /// + API_FIELD(Attributes="EditorOrder(40), EditorDisplay(null, \"Is 3D\")") + bool Is3D = false; + + /// + /// The size of a single sample in bits. The clip will be converted to this bit depth on import. + /// + API_FIELD(Attributes="EditorOrder(50), VisibleIf(nameof(ShowBtiDepth))") + BitDepth BitDepth = BitDepth::_16; + + String ToString() const; + + // [ISerializable] + void Serialize(SerializeStream& stream, const void* otherObj) override; + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; + }; +#endif + +public: /// /// Converts a set of audio samples using multiple channels into a set of mono samples. /// @@ -59,3 +129,5 @@ public: return (input[2] << 24) | (input[1] << 16) | (input[0] << 8); } }; + +#endif diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index b50f3dd46..02337411b 100644 --- a/Source/Engine/UI/GUI/Common/Button.cs +++ b/Source/Engine/UI/GUI/Common/Button.cs @@ -314,6 +314,28 @@ namespace FlaxEngine.GUI return false; } + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if (base.OnMouseDoubleClick(location, button)) + return true; + + if (button == MouseButton.Left && _isPressed) + { + OnPressEnd(); + OnClick(); + return true; + } + + if (button == MouseButton.Left && !_isPressed) + { + OnPressBegin(); + return true; + } + + return false; + } + /// public override bool OnTouchDown(Float2 location, int pointerId) { diff --git a/Source/Engine/UI/GUI/Common/CheckBox.cs b/Source/Engine/UI/GUI/Common/CheckBox.cs index 2f1ff42a9..2128c0311 100644 --- a/Source/Engine/UI/GUI/Common/CheckBox.cs +++ b/Source/Engine/UI/GUI/Common/CheckBox.cs @@ -277,6 +277,27 @@ namespace FlaxEngine.GUI return base.OnMouseDown(location, button); } + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && !_isPressed) + { + OnPressBegin(); + return true; + } + + if (button == MouseButton.Left && _isPressed) + { + OnPressEnd(); + if (_box.Contains(ref location)) + { + OnClick(); + return true; + } + } + return base.OnMouseDoubleClick(location, button); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 6ad962d6d..aea4e173a 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -666,6 +666,30 @@ namespace FlaxEngine.GUI return false; } + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if (base.OnMouseDoubleClick(location, button)) + return true; + + if (_touchDown && button == MouseButton.Left) + { + _touchDown = false; + ShowPopup(); + return true; + } + + if (button == MouseButton.Left) + { + _touchDown = true; + if (!IsPopupOpened) + Focus(); + return true; + } + + return false; + } + /// public override bool OnTouchDown(Float2 location, int pointerId) { diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 05183d57a..c08b59f48 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -769,13 +769,13 @@ namespace FlaxEngine.GUI { SetSelection(SelectionLeft); } - else if (SelectionLeft > 0) + else if (SelectionLeft >= 0) { int position; if (ctrl) position = FindPrevWordBegin(); else - position = _selectionEnd - 1; + position = Mathf.Max(_selectionEnd - 1, 0); if (shift) { diff --git a/Source/Engine/UI/GUI/Panels/DropPanel.cs b/Source/Engine/UI/GUI/Panels/DropPanel.cs index d650d6205..66e7413eb 100644 --- a/Source/Engine/UI/GUI/Panels/DropPanel.cs +++ b/Source/Engine/UI/GUI/Panels/DropPanel.cs @@ -501,6 +501,29 @@ namespace FlaxEngine.GUI return false; } + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if (base.OnMouseDoubleClick(location, button)) + return true; + + _mouseOverHeader = HeaderRectangle.Contains(location); + if (button == MouseButton.Left && _mouseOverHeader) + { + _mouseButtonLeftDown = true; + return true; + } + + if (button == MouseButton.Left && _mouseButtonLeftDown) + { + _mouseButtonLeftDown = false; + if (_mouseOverHeader) + Toggle(); + return true; + } + return false; + } + /// public override void OnMouseLeave() { diff --git a/Source/Engine/UI/GUI/Panels/ScrollBar.cs b/Source/Engine/UI/GUI/Panels/ScrollBar.cs index a51a1df40..abde37b37 100644 --- a/Source/Engine/UI/GUI/Panels/ScrollBar.cs +++ b/Source/Engine/UI/GUI/Panels/ScrollBar.cs @@ -23,9 +23,9 @@ namespace FlaxEngine.GUI // Scrolling - private float _clickChange = 20, _scrollChange = 100; + private float _clickChange = 20, _scrollChange = 50; private float _minimum, _maximum = 100; - private float _value, _targetValue; + private float _startValue, _value, _targetValue; private readonly Orientation _orientation; private RootControl.UpdateDelegate _update; @@ -42,6 +42,7 @@ namespace FlaxEngine.GUI // Smoothing private float _thumbOpacity = DefaultMinimumOpacity; + private float _scrollAnimationProgress = 0f; /// /// Gets the orientation. @@ -59,14 +60,19 @@ namespace FlaxEngine.GUI public float TrackThickness { get; set; } = 2.0f; /// - /// Gets or sets the value smoothing scale (0 to not use it). + /// The maximum time it takes to animate from current to target scroll position /// - public float SmoothingScale { get; set; } = 0.6f; + public float ScrollAnimationDuration { get; set; } = 0.18f; /// /// Gets a value indicating whether use scroll value smoothing. /// - public bool UseSmoothing => !Mathf.IsZero(SmoothingScale); + public bool UseSmoothing => EnableSmoothing && !Mathf.IsZero(ScrollAnimationDuration); + + /// + /// Enables scroll smoothing + /// + public bool EnableSmoothing { get; set; } = true; /// /// Gets or sets the minimum value. @@ -112,11 +118,15 @@ namespace FlaxEngine.GUI if (!Mathf.NearEqual(value, _targetValue)) { _targetValue = value; + _startValue = _value; + _scrollAnimationProgress = 0f; // Check if skip smoothing if (!UseSmoothing) { _value = value; + _startValue = value; + _scrollAnimationProgress = 1f; OnValueChanged(); } else @@ -208,7 +218,8 @@ namespace FlaxEngine.GUI { if (!Mathf.NearEqual(_value, _targetValue)) { - _value = _targetValue; + _value = _targetValue = _startValue; + _scrollAnimationProgress = 0f; SetUpdate(ref _update, null); OnValueChanged(); } @@ -274,7 +285,8 @@ namespace FlaxEngine.GUI internal void Reset() { - _value = _targetValue = 0; + _value = _targetValue = _startValue = 0; + _scrollAnimationProgress = 0f; } /// @@ -296,22 +308,39 @@ namespace FlaxEngine.GUI _thumbOpacity = isDeltaSlow ? targetOpacity : Mathf.Lerp(_thumbOpacity, targetOpacity, deltaTime * 10.0f); bool needUpdate = Mathf.Abs(_thumbOpacity - targetOpacity) > 0.001f; - // Ensure scroll bar is visible - if (Visible) + // Ensure scroll bar is visible and smoothing is required + if (Visible && Mathf.Abs(_targetValue - _value) > 0.01f) { - // Value smoothing - if (Mathf.Abs(_targetValue - _value) > 0.01f) + // Interpolate or not if running slow + float value; + if (!isDeltaSlow && UseSmoothing) { - // Interpolate or not if running slow - float value; - if (!isDeltaSlow && UseSmoothing) - value = Mathf.Lerp(_value, _targetValue, deltaTime * 20.0f * SmoothingScale); - else - value = _targetValue; - _value = value; - OnValueChanged(); - needUpdate = true; + // percentage of scroll from 0 to _scrollChange, ex. 0.5 at _scrollChange / 2 + var minScrollChangeRatio = Mathf.Clamp(Mathf.Abs(_targetValue - _startValue) / _scrollChange, 0, 1); + + // shorten the duration if we scrolled less than _scrollChange + var actualDuration = ScrollAnimationDuration * minScrollChangeRatio; + var step = deltaTime / actualDuration; + + var progress = _scrollAnimationProgress; + progress = Mathf.Clamp(progress + step, 0, 1); + + // https://easings.net/#easeOutSine + var easedProgress = Mathf.Sin((progress * Mathf.Pi) / 2); + value = Mathf.Lerp(_startValue, _targetValue, easedProgress); + + _scrollAnimationProgress = progress; } + else + { + value = _targetValue; + _startValue = _targetValue; + _scrollAnimationProgress = 0f; + } + + _value = value; + OnValueChanged(); + needUpdate = true; } // End updating if all animations are done @@ -371,7 +400,7 @@ namespace FlaxEngine.GUI float mousePosition = _orientation == Orientation.Vertical ? slidePosition.Y : slidePosition.X; float percentage = (mousePosition - _mouseOffset - _thumbSize / 2) / (TrackSize - _thumbSize); - Value = _minimum + percentage * (_maximum - _minimum); + TargetValue = _minimum + percentage * (_maximum - _minimum); } } @@ -381,7 +410,7 @@ namespace FlaxEngine.GUI if (ThumbEnabled) { // Scroll - Value = _value - delta * _scrollChange; + Value = _targetValue - delta * _scrollChange; } return true; } diff --git a/Source/Shaders/IESProfile.hlsl b/Source/Shaders/IESProfile.hlsl index 710ae8484..2b101f176 100644 --- a/Source/Shaders/IESProfile.hlsl +++ b/Source/Shaders/IESProfile.hlsl @@ -7,7 +7,7 @@ float ComputeLightProfileMultiplier(Texture2D tex, float3 worldPosition, float3 lightPosition, float3 lightDirection) { float3 l = normalize(worldPosition - lightPosition); - float d = dot(lightPosition, lightDirection); + float d = dot(l, lightDirection); float angle = asin(d) / PI + 0.5f; return tex.SampleLevel(SamplerLinearClamp, float2(angle, 0), 0).r; } diff --git a/Source/Tools/Flax.Build/Build/Assembler.cs b/Source/Tools/Flax.Build/Build/Assembler.cs index be4990087..cbe92dd8d 100644 --- a/Source/Tools/Flax.Build/Build/Assembler.cs +++ b/Source/Tools/Flax.Build/Build/Assembler.cs @@ -154,7 +154,7 @@ namespace Flax.Build // Run the compilation using var memoryStream = new MemoryStream(); - CSharpParseOptions parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9).WithPreprocessorSymbols(PreprocessorSymbols); + CSharpParseOptions parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest).WithPreprocessorSymbols(PreprocessorSymbols); var syntaxTrees = new List(); foreach (var sourceFile in SourceFiles) { diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index a344eea50..1ba8e6709 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -641,6 +641,44 @@ namespace Flax.Build.Projects.VisualStudio File.WriteAllText(e.Key, profile.ToString(), Encoding.UTF8); } } + + // Generate Rider-specific configuration files + { + StringBuilder dotSettingsFileContent = new StringBuilder(); + string dotSettingsUserFilePath = solution.Path + ".DotSettings.user"; + + // Solution settings (user layer) + bool useResharperBuild = false; // This needs to be disabled for custom build steps to run properly + + if (File.Exists(dotSettingsUserFilePath)) + { + foreach (var line in File.ReadAllLines(dotSettingsUserFilePath)) + { + if (line.Contains(@"/UseMsbuildSolutionBuilder/@EntryValue")) + { + if (!useResharperBuild) + { + dotSettingsFileContent.Append("\t").Append(@"No"); + if (line.Contains("")) + dotSettingsFileContent.Append("\n"); + else + dotSettingsFileContent.Append("\n"); + } + continue; + } + dotSettingsFileContent.Append(line).Append("\n"); + } + } + else + { + dotSettingsFileContent.Append(@"").Append("\n"); + if (!useResharperBuild) + dotSettingsFileContent.Append("\t").Append(@"No"); + dotSettingsFileContent.Append("\n"); + } + + Utilities.WriteFileIfChanged(dotSettingsUserFilePath, dotSettingsFileContent.ToString()); + } } ///