diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs
index 2a150306d..663acf05f 100644
--- a/Content/Editor/Scripting/ScriptTemplate.cs
+++ b/Content/Editor/Scripting/ScriptTemplate.cs
@@ -2,35 +2,35 @@
using System.Collections.Generic;
using FlaxEngine;
-namespace %namespace%
+namespace %namespace%;
+
+///
+/// %class% Script.
+///
+public class %class% : Script
{
- ///
- /// %class% Script.
- ///
- public class %class% : Script
+ ///
+ public override void OnStart()
{
- ///
- public override void OnStart()
- {
- // Here you can add code that needs to be called when script is created, just before the first game update
- }
-
- ///
- public override void OnEnable()
- {
- // Here you can add code that needs to be called when script is enabled (eg. register for events)
- }
+ // Here you can add code that needs to be called when script is created, just before the first game update
+ }
+
+ ///
+ public override void OnEnable()
+ {
+ // Here you can add code that needs to be called when script is enabled (eg. register for events)
+ }
- ///
- public override void OnDisable()
- {
- // Here you can add code that needs to be called when script is disabled (eg. unregister from events)
- }
+ ///
+ public override void OnDisable()
+ {
+ // Here you can add code that needs to be called when script is disabled (eg. unregister from events)
+ }
- ///
- public override void OnUpdate()
- {
- // Here you can add code that needs to be called every frame
- }
+ ///
+ public override void OnUpdate()
+ {
+ // Here you can add code that needs to be called every frame
}
}
+
diff --git a/Source/Editor/Content/GUI/ContentView.DragDrop.cs b/Source/Editor/Content/GUI/ContentView.DragDrop.cs
index 348e2b443..ffce81e2d 100644
--- a/Source/Editor/Content/GUI/ContentView.DragDrop.cs
+++ b/Source/Editor/Content/GUI/ContentView.DragDrop.cs
@@ -68,7 +68,7 @@ namespace FlaxEditor.Content.GUI
_validDragOver = true;
result = DragDropEffect.Copy;
}
- else if (_dragActors.HasValidDrag)
+ else if (_dragActors != null && _dragActors.HasValidDrag)
{
_validDragOver = true;
result = DragDropEffect.Move;
@@ -94,7 +94,7 @@ namespace FlaxEditor.Content.GUI
result = DragDropEffect.Copy;
}
// Check if drop actor(s)
- else if (_dragActors.HasValidDrag)
+ else if (_dragActors != null && _dragActors.HasValidDrag)
{
// Import actors
var currentFolder = Editor.Instance.Windows.ContentWin.CurrentViewFolder;
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/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs
index 94db3a5b9..6bbdc0a51 100644
--- a/Source/Editor/Content/Items/ContentItem.cs
+++ b/Source/Editor/Content/Items/ContentItem.cs
@@ -486,7 +486,7 @@ namespace FlaxEditor.Content
else
Render2D.FillRectangle(rectangle, Color.Black);
}
-
+
///
/// Draws the item thumbnail.
///
@@ -684,7 +684,7 @@ namespace FlaxEditor.Content
var thumbnailSize = size.X;
thumbnailRect = new Rectangle(0, 0, thumbnailSize, thumbnailSize);
nameAlignment = TextAlignment.Center;
-
+
if (this is ContentFolder)
{
// Small shadow
@@ -692,7 +692,7 @@ namespace FlaxEditor.Content
var color = Color.Black.AlphaMultiplied(0.2f);
Render2D.FillRectangle(shadowRect, color);
Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f));
-
+
if (isSelected)
Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
else if (IsMouseOver)
@@ -706,14 +706,14 @@ namespace FlaxEditor.Content
var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1);
var color = Color.Black.AlphaMultiplied(0.2f);
Render2D.FillRectangle(shadowRect, color);
-
+
Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f));
Render2D.FillRectangle(TextRectangle, style.LightBackground);
-
+
var accentHeight = 2 * view.ViewScale;
var barRect = new Rectangle(0, thumbnailRect.Height - accentHeight, clientRect.Width, accentHeight);
Render2D.FillRectangle(barRect, Color.DimGray);
-
+
DrawThumbnail(ref thumbnailRect, false);
if (isSelected)
{
@@ -733,18 +733,18 @@ namespace FlaxEditor.Content
var thumbnailSize = size.Y - 2 * DefaultMarginSize;
thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize);
nameAlignment = TextAlignment.Near;
-
+
if (isSelected)
Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
else if (IsMouseOver)
Render2D.FillRectangle(clientRect, style.BackgroundHighlighted);
-
+
DrawThumbnail(ref thumbnailRect);
break;
}
default: throw new ArgumentOutOfRangeException();
}
-
+
// Draw short name
Render2D.PushClip(ref textRect);
Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 1f, 0.95f);
diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp
index b66a96a9b..0008228f7 100644
--- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp
+++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp
@@ -17,7 +17,9 @@
#include "Editor/ProjectInfo.h"
#include "Editor/Cooker/GameCooker.h"
#include "Editor/Utilities/EditorUtilities.h"
-#include
+
+#include "pugixml/pugixml_extra.hpp"
+
using namespace pugi;
IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform);
@@ -170,17 +172,17 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
const String plistPath = data.DataOutputPath / TEXT("Info.plist");
{
xml_document doc;
- xml_node plist = doc.child_or_append(PUGIXML_TEXT("plist"));
+ xml_node_extra plist = xml_node_extra(doc).child_or_append(PUGIXML_TEXT("plist"));
plist.append_attribute(PUGIXML_TEXT("version")).set_value(PUGIXML_TEXT("1.0"));
- xml_node dict = plist.child_or_append(PUGIXML_TEXT("dict"));
+ xml_node_extra dict = plist.child_or_append(PUGIXML_TEXT("dict"));
#define ADD_ENTRY(key, value) \
- dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \
- dict.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT(value))
+ dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \
+ dict.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT(value))
#define ADD_ENTRY_STR(key, value) \
- dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \
+ dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \
{ std::u16string valueStr(value.GetText()); \
- dict.append_child(PUGIXML_TEXT("string")).set_child_value(pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); }
+ dict.append_child_with_value(PUGIXML_TEXT("string"), pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); }
ADD_ENTRY("CFBundleDevelopmentRegion", "English");
ADD_ENTRY("CFBundlePackageType", "APPL");
@@ -194,22 +196,22 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
ADD_ENTRY_STR("CFBundleVersion", projectVersion);
ADD_ENTRY_STR("NSHumanReadableCopyright", gameSettings->CopyrightNotice);
- dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("CFBundleSupportedPlatforms"));
- xml_node CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array"));
- CFBundleSupportedPlatforms.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("MacOSX"));
+ dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("CFBundleSupportedPlatforms"));
+ xml_node_extra CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array"));
+ CFBundleSupportedPlatforms.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("MacOSX"));
- dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture"));
- xml_node LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict"));
+ dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture"));
+ xml_node_extra LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict"));
switch (_arch)
{
case ArchitectureType::x64:
- LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("x86_64"));
+ LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("x86_64"));
break;
case ArchitectureType::ARM64:
- LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("arm64"));
+ LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("arm64"));
break;
}
- LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("10.15"));
+ LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("10.15"));
#undef ADD_ENTRY
#undef ADD_ENTRY_STR
diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
index 03d5ddd55..a6c4e6623 100644
--- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
@@ -20,7 +20,7 @@ public class MissingScriptEditor : GenericEditor
}
dropPanel.HeaderTextColor = Color.OrangeRed;
-
+
base.Initialize(layout);
}
}
diff --git a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs
index 03d58ef33..7b9b65c5c 100644
--- a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs
@@ -348,7 +348,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
if (!CanEditTangent())
return;
-
+
var index = _lastPointSelected.Index;
var currentTangentInPosition = _selectedSpline.GetSplineLocalTangent(index, true).Translation;
var currentTangentOutPosition = _selectedSpline.GetSplineLocalTangent(index, false).Translation;
diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
index 4aa02ac78..4c153e759 100644
--- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
@@ -88,7 +88,7 @@ namespace FlaxEditor.CustomEditors.Editors
LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked;
// Add button with the link icon
-
+
_linkButton = new Button
{
BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32),
diff --git a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
index 4d0c0f662..2ef993e75 100644
--- a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
@@ -160,7 +160,6 @@ namespace FlaxEditor.CustomEditors.Editors
var option = _options[comboBox.SelectedIndex];
if (option.Type != null)
value = option.Creator(option.Type);
-
}
SetValue(value);
RebuildLayoutOnRefresh();
diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs
index 911397785..dbd5d124c 100644
--- a/Source/Editor/CustomEditors/Editors/TagEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs
@@ -634,7 +634,7 @@ namespace FlaxEditor.CustomEditors.Editors
var textSize = FlaxEngine.GUI.Style.Current.FontMedium.MeasureText(buttonText);
if (textSize.Y > button.Width)
button.Width = textSize.Y + 2;
-
+
button.SetAnchorPreset(AnchorPresets.MiddleRight, false, true);
button.Clicked += ShowPicker;
}
diff --git a/Source/Editor/CustomEditors/GUI/PropertiesList.cs b/Source/Editor/CustomEditors/GUI/PropertiesList.cs
index 2b2f0a3d3..93aacbd34 100644
--- a/Source/Editor/CustomEditors/GUI/PropertiesList.cs
+++ b/Source/Editor/CustomEditors/GUI/PropertiesList.cs
@@ -175,7 +175,7 @@ namespace FlaxEditor.CustomEditors.GUI
{
// Clear flag
_mouseOverSplitter = false;
-
+
if (_cursorChanged)
{
Cursor = CursorType.Default;
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 06a6f0979..e0c460d0c 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -935,21 +935,6 @@ namespace FlaxEditor
Animation = 11,
}
- ///
- /// 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.
///
@@ -1667,10 +1652,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/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
index 7ecd197eb..628af18e1 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
@@ -154,6 +154,7 @@ namespace FlaxEditor.GUI.ContextMenu
}
// Unlock and perform controls update
+ Location = Float2.Zero;
UnlockChildrenRecursive();
PerformLayout();
@@ -162,7 +163,6 @@ namespace FlaxEditor.GUI.ContextMenu
var dpiSize = Size * dpiScale;
var locationWS = parent.PointToWindow(location);
var locationSS = parentWin.PointToScreen(locationWS);
- Location = Float2.Zero;
var monitorBounds = Platform.GetMonitorBounds(locationSS);
var rightBottomLocationSS = locationSS + dpiSize;
bool isUp = false, isLeft = false;
diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp
index afcbb740a..d2d2f216a 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;
@@ -295,16 +258,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);
@@ -765,24 +718,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());
@@ -830,3 +765,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/UIModule.cs b/Source/Editor/Modules/UIModule.cs
index 299865dea..c882efb87 100644
--- a/Source/Editor/Modules/UIModule.cs
+++ b/Source/Editor/Modules/UIModule.cs
@@ -553,6 +553,13 @@ namespace FlaxEditor.Modules
cm.AddSeparator();
_menuEditSelectAll = cm.AddButton("Select all", inputOptions.SelectAll, Editor.SceneEditing.SelectAllScenes);
_menuEditFind = cm.AddButton("Find", inputOptions.Search, Editor.Windows.SceneWin.Search);
+ cm.AddSeparator();
+ cm.AddButton("Game Settings", () =>
+ {
+ var item = Editor.ContentDatabase.Find(GameSettings.GameSettingsAssetPath);
+ if(item != null)
+ Editor.ContentEditing.Open(item);
+ });
// Scene
MenuScene = MainMenu.AddButton("Scene");
diff --git a/Source/Editor/Options/GeneralOptions.cs b/Source/Editor/Options/GeneralOptions.cs
index 63822a7ca..33124bab0 100644
--- a/Source/Editor/Options/GeneralOptions.cs
+++ b/Source/Editor/Options/GeneralOptions.cs
@@ -106,7 +106,7 @@ namespace FlaxEditor.Options
[DefaultValue(60.0f), Limit(0, 666)]
[EditorDisplay("General", "Editor FPS"), EditorOrder(110), Tooltip("Limit for the editor draw/update frames per second rate (FPS). Use higher values if you need more responsive interface or lower values to use less device power. Value 0 disables any limits.")]
public float EditorFPS { get; set; } = 60.0f;
-
+
///
/// Gets or sets The FPS of the editor when the editor window is not focused. Usually set to lower then the editor FPS.
///
@@ -203,7 +203,7 @@ namespace FlaxEditor.Options
[DefaultValue(5), Limit(1)]
[EditorDisplay("Auto Save", "Auto Save Frequency"), EditorOrder(801), Tooltip("The interval between auto saves (in minutes)")]
public int AutoSaveFrequency { get; set; } = 5;
-
+
///
/// Gets or sets a value indicating the time before the auto save that the popup shows (in seconds).
///
diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs
index 5fd8c31c7..95a273f19 100644
--- a/Source/Editor/Options/InterfaceOptions.cs
+++ b/Source/Editor/Options/InterfaceOptions.cs
@@ -166,7 +166,19 @@ namespace FlaxEditor.Options
/// Gets or sets the output log text font.
///
[EditorDisplay("Output Log", "Text Font"), EditorOrder(320), Tooltip("The output log text font.")]
- public FontReference OutputLogTextFont { get; set; } = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10);
+ public FontReference OutputLogTextFont
+ {
+ get => _outputLogFont;
+ set
+ {
+ if (value == null)
+ _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10);
+ else if (!value.Font)
+ _outputLogFont.Font = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont);
+ else
+ _outputLogFont = value;
+ }
+ }
///
/// Gets or sets the output log text color.
@@ -225,29 +237,82 @@ namespace FlaxEditor.Options
public int NumberOfGameClientsToLaunch = 1;
private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.PrimaryFont);
+ private FontReference _titleFont = new FontReference(DefaultFont, 18);
+ private FontReference _largeFont = new FontReference(DefaultFont, 14);
+ private FontReference _mediumFont = new FontReference(DefaultFont, 9);
+ private FontReference _smallFont = new FontReference(DefaultFont, 9);
+ private FontReference _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10);
///
/// Gets or sets the title font for editor UI.
///
[EditorDisplay("Fonts"), EditorOrder(600), Tooltip("The title font for editor UI.")]
- public FontReference TitleFont { get; set; } = new FontReference(DefaultFont, 18);
+ public FontReference TitleFont
+ {
+ get => _titleFont;
+ set
+ {
+ if (value == null)
+ _titleFont = new FontReference(DefaultFont, 18);
+ else if (!value.Font)
+ _titleFont.Font = DefaultFont;
+ else
+ _titleFont = value;
+ }
+ }
///
/// Gets or sets the large font for editor UI.
///
[EditorDisplay("Fonts"), EditorOrder(610), Tooltip("The large font for editor UI.")]
- public FontReference LargeFont { get; set; } = new FontReference(DefaultFont, 14);
+ public FontReference LargeFont
+ {
+ get => _largeFont;
+ set
+ {
+ if (value == null)
+ _largeFont = new FontReference(DefaultFont, 14);
+ else if (!value.Font)
+ _largeFont.Font = DefaultFont;
+ else
+ _largeFont = value;
+ }
+ }
///
/// Gets or sets the medium font for editor UI.
///
[EditorDisplay("Fonts"), EditorOrder(620), Tooltip("The medium font for editor UI.")]
- public FontReference MediumFont { get; set; } = new FontReference(DefaultFont, 9);
+ public FontReference MediumFont
+ {
+ get => _mediumFont;
+ set
+ {
+ if (value == null)
+ _mediumFont = new FontReference(DefaultFont, 9);
+ else if (!value.Font)
+ _mediumFont.Font = DefaultFont;
+ else
+ _mediumFont = value;
+ }
+ }
///
/// Gets or sets the small font for editor UI.
///
[EditorDisplay("Fonts"), EditorOrder(630), Tooltip("The small font for editor UI.")]
- public FontReference SmallFont { get; set; } = new FontReference(DefaultFont, 9);
+ public FontReference SmallFont
+ {
+ get => _smallFont;
+ set
+ {
+ if (value == null)
+ _smallFont = new FontReference(DefaultFont, 9);
+ else if (!value.Font)
+ _smallFont.Font = DefaultFont;
+ else
+ _smallFont = value;
+ }
+ }
}
}
diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs
index 454c3a5d2..b0c3a36dd 100644
--- a/Source/Editor/Scripting/ScriptType.cs
+++ b/Source/Editor/Scripting/ScriptType.cs
@@ -1393,5 +1393,32 @@ namespace FlaxEditor.Scripting
return _custom.GetMembers(name, MemberTypes.Method, bindingAttr).FirstOrDefault();
return ScriptMemberInfo.Null;
}
+
+ ///
+ /// Basic check to see if a type could be casted to another type
+ ///
+ /// Source type
+ /// Target type
+ /// True if the type can be casted
+ public static bool CanCast(ScriptType from, ScriptType to)
+ {
+ if (from == to)
+ return true;
+ if (from == Null || to == Null)
+ return false;
+ return (from.Type != typeof(void) && from.Type != typeof(FlaxEngine.Object)) &&
+ (to.Type != typeof(void) && to.Type != typeof(FlaxEngine.Object)) &&
+ from.IsAssignableFrom(to);
+ }
+
+ ///
+ /// Basic check to see if this type could be casted to another type
+ ///
+ /// Target type
+ /// True if the type can be casted
+ public bool CanCastTo(ScriptType to)
+ {
+ return CanCast(this, to);
+ }
}
}
diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs
index cffb2ad6f..e9d73ae68 100644
--- a/Source/Editor/Surface/Archetypes/Function.cs
+++ b/Source/Editor/Surface/Archetypes/Function.cs
@@ -744,6 +744,16 @@ namespace FlaxEditor.Surface.Archetypes
base.OnDestroy();
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ return inputType.IsVoid;
+ }
}
private sealed class InvokeMethodNode : SurfaceNode
@@ -1151,6 +1161,54 @@ namespace FlaxEditor.Surface.Archetypes
base.OnDestroy();
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ if (nodeArch.Tag is not ScriptMemberInfo memberInfo)
+ return false;
+
+ if (!memberInfo.IsStatic)
+ {
+ if (VisjectSurface.FullCastCheck(memberInfo.DeclaringType, outputType, hint))
+ return true;
+ }
+
+ var parameters = memberInfo.GetParameters();
+ bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid);
+ if (outputType.IsVoid)
+ return !isPure;
+
+ foreach (var param in parameters)
+ {
+ if (param.IsOut)
+ continue;
+ if (VisjectSurface.FullCastCheck(param.Type, outputType, hint))
+ return true;
+ }
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ if (nodeArch.Tag is not ScriptMemberInfo memberInfo)
+ return false;
+ if (VisjectSurface.FullCastCheck(memberInfo.ValueType, inputType, hint))
+ return true;
+
+ var parameters = memberInfo.GetParameters();
+ bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid);
+ if (inputType.IsVoid)
+ return !isPure;
+
+ foreach (var param in memberInfo.GetParameters())
+ {
+ if (!param.IsOut)
+ continue;
+ if (VisjectSurface.FullCastCheck(param.Type, inputType, hint))
+ return true;
+ }
+ return false;
+ }
}
private sealed class ReturnNode : SurfaceNode
@@ -1777,6 +1835,16 @@ namespace FlaxEditor.Surface.Archetypes
base.OnDestroy();
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ return inputType.IsVoid;
+ }
}
private abstract class FieldNodeBase : SurfaceNode
@@ -1913,6 +1981,64 @@ namespace FlaxEditor.Surface.Archetypes
Title = "Get " + SurfaceUtils.GetMethodDisplayName((string)Values[1]);
UpdateSignature();
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
+ if (scriptType == ScriptType.Null)
+ return false;
+
+ var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
+ foreach (var member in members)
+ {
+ if (!SurfaceUtils.IsValidVisualScriptField(member))
+ continue;
+
+ if (member)
+ {
+ if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
+ return true;
+ }
+ else
+ {
+ var isStatic = (bool)nodeArch.DefaultValues[3];
+ if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
+ if (scriptType == ScriptType.Null)
+ return false;
+
+ var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
+ foreach (var member in members)
+ {
+ if (!SurfaceUtils.IsValidVisualScriptField(member))
+ continue;
+
+ if (member)
+ {
+ if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint))
+ return true;
+ }
+ else
+ {
+ var typeName = (string)nodeArch.DefaultValues[2];
+ if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), inputType, hint))
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
}
private sealed class SetFieldNode : FieldNodeBase
@@ -1966,6 +2092,48 @@ namespace FlaxEditor.Surface.Archetypes
Title = "Set " + SurfaceUtils.GetMethodDisplayName((string)Values[1]);
UpdateSignature();
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ if (outputType.IsVoid)
+ return true;
+
+ var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
+ if (scriptType == ScriptType.Null)
+ return false;
+
+ var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
+ foreach (var member in members)
+ {
+ if (!SurfaceUtils.IsValidVisualScriptField(member))
+ continue;
+
+ if (member)
+ {
+ if (VisjectSurface.FullCastCheck(member.ValueType, outputType, hint))
+ return true;
+ if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
+ return true;
+ }
+ else
+ {
+ var typeName = (string)nodeArch.DefaultValues[2];
+ if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), outputType, hint))
+ return true;
+ var isStatic = (bool)nodeArch.DefaultValues[3];
+ if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ return inputType.IsVoid;
+ }
}
private abstract class EventBaseNode : SurfaceNode, IFunctionsDependantNode
@@ -2184,6 +2352,43 @@ namespace FlaxEditor.Surface.Archetypes
base.OnDestroy();
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ // Event based nodes always have a pulse input, so it's always compatible with void
+ if (outputType.IsVoid)
+ return true;
+
+ var eventName = (string)nodeArch.DefaultValues[1];
+ var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
+ var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
+ if (member && SurfaceUtils.IsValidVisualScriptEvent(member))
+ {
+ if (!member.IsStatic)
+ {
+ if (VisjectSurface.FullCastCheck(eventType, outputType, hint))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ // Event based nodes always have a pulse output, so it's always compatible with void
+ if (inputType.IsVoid)
+ return true;
+
+ var eventName = (string)nodeArch.DefaultValues[1];
+ var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
+ var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
+ if (member && SurfaceUtils.IsValidVisualScriptEvent(member))
+ {
+ if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint))
+ return true;
+ }
+ return false;
+ }
}
private sealed class BindEventNode : EventBaseNode
@@ -2265,6 +2470,8 @@ namespace FlaxEditor.Surface.Archetypes
Title = string.Empty,
Description = "Overrides the base class method with custom implementation",
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste,
+ IsInputCompatible = MethodOverrideNode.IsInputCompatible,
+ IsOutputCompatible = MethodOverrideNode.IsOutputCompatible,
Size = new Float2(240, 60),
DefaultValues = new object[]
{
@@ -2277,6 +2484,8 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 4,
Create = (id, context, arch, groupArch) => new InvokeMethodNode(id, context, arch, groupArch),
+ IsInputCompatible = InvokeMethodNode.IsInputCompatible,
+ IsOutputCompatible = InvokeMethodNode.IsOutputCompatible,
Title = string.Empty,
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(240, 60),
@@ -2317,6 +2526,8 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 6,
Create = (id, context, arch, groupArch) => new VisualScriptFunctionNode(id, context, arch, groupArch),
+ IsInputCompatible = VisualScriptFunctionNode.IsInputCompatible,
+ IsOutputCompatible = VisualScriptFunctionNode.IsOutputCompatible,
Title = "New Function",
Description = "Adds a new function to the script",
Flags = NodeFlags.VisualScriptGraph,
@@ -2330,6 +2541,8 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 7,
Create = (id, context, arch, groupArch) => new GetFieldNode(id, context, arch, groupArch),
+ IsInputCompatible = GetFieldNode.IsInputCompatible,
+ IsOutputCompatible = GetFieldNode.IsOutputCompatible,
Title = string.Empty,
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(240, 60),
@@ -2345,6 +2558,8 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 8,
Create = (id, context, arch, groupArch) => new SetFieldNode(id, context, arch, groupArch),
+ IsInputCompatible = SetFieldNode.IsInputCompatible,
+ IsOutputCompatible = SetFieldNode.IsOutputCompatible,
Title = string.Empty,
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(240, 60),
@@ -2361,6 +2576,8 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 9,
Create = (id, context, arch, groupArch) => new BindEventNode(id, context, arch, groupArch),
+ IsInputCompatible = EventBaseNode.IsInputCompatible,
+ IsOutputCompatible = EventBaseNode.IsOutputCompatible,
Title = string.Empty,
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(260, 60),
@@ -2383,6 +2600,8 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 10,
Create = (id, context, arch, groupArch) => new UnbindEventNode(id, context, arch, groupArch),
+ IsInputCompatible = EventBaseNode.IsInputCompatible,
+ IsOutputCompatible = EventBaseNode.IsOutputCompatible,
Title = string.Empty,
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(260, 60),
diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs
index 5a5e9cd5b..6b9e72e7e 100644
--- a/Source/Editor/Surface/Archetypes/Packing.cs
+++ b/Source/Editor/Surface/Archetypes/Packing.cs
@@ -216,6 +216,35 @@ namespace FlaxEditor.Surface.Archetypes
: base(id, context, nodeArch, groupArch, false)
{
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ var typeName = (string)nodeArch.DefaultValues[0];
+ var type = TypeUtils.GetType(typeName);
+ if (type)
+ {
+ var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray();
+ var fieldsLength = fields.Length;
+ for (var i = 0; i < fieldsLength; i++)
+ {
+ if (VisjectSurface.FullCastCheck(fields[i].ValueType, outputType, hint))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ var typeName = (string)nodeArch.DefaultValues[0];
+ var type = TypeUtils.GetType(typeName);
+ if (type)
+ {
+ if (VisjectSurface.FullCastCheck(type, inputType, hint))
+ return true;
+ }
+ return false;
+ }
}
private sealed class UnpackStructureNode : StructureNode
@@ -225,6 +254,35 @@ namespace FlaxEditor.Surface.Archetypes
: base(id, context, nodeArch, groupArch, true)
{
}
+
+ internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
+ {
+ var typeName = (string)nodeArch.DefaultValues[0];
+ var type = TypeUtils.GetType(typeName);
+ if (type)
+ {
+ if (VisjectSurface.FullCastCheck(type, outputType, hint))
+ return true;
+ }
+ return false;
+ }
+
+ internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
+ {
+ var typeName = (string)nodeArch.DefaultValues[0];
+ var type = TypeUtils.GetType(typeName);
+ if (type)
+ {
+ var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray();
+ var fieldsLength = fields.Length;
+ for (var i = 0; i < fieldsLength; i++)
+ {
+ if (VisjectSurface.FullCastCheck(fields[i].ValueType, inputType, hint))
+ return true;
+ }
+ }
+ return false;
+ }
}
///
@@ -351,6 +409,8 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 26,
Title = "Pack Structure",
Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch),
+ IsInputCompatible = PackStructureNode.IsInputCompatible,
+ IsOutputCompatible = PackStructureNode.IsOutputCompatible,
Description = "Makes the structure data to from the components.",
Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(180, 20),
@@ -461,6 +521,8 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 36,
Title = "Unpack Structure",
Create = (id, context, arch, groupArch) => new UnpackStructureNode(id, context, arch, groupArch),
+ IsInputCompatible = UnpackStructureNode.IsInputCompatible,
+ IsOutputCompatible = UnpackStructureNode.IsOutputCompatible,
Description = "Breaks the structure data to allow extracting components from it.",
Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(180, 20),
diff --git a/Source/Editor/Surface/ContextMenu/VisjectCM.cs b/Source/Editor/Surface/ContextMenu/VisjectCM.cs
index bae62556e..342429fc8 100644
--- a/Source/Editor/Surface/ContextMenu/VisjectCM.cs
+++ b/Source/Editor/Surface/ContextMenu/VisjectCM.cs
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
-using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
@@ -40,6 +39,8 @@ namespace FlaxEditor.Surface.ContextMenu
public delegate List ParameterGetterDelegate();
private readonly List _groups = new List(16);
+ private CheckBox _contextSensitiveToggle;
+ private bool _contextSensitiveSearchEnabled = true;
private readonly TextBox _searchBox;
private bool _waitingForInput;
private VisjectCMGroup _surfaceParametersGroup;
@@ -127,7 +128,7 @@ namespace FlaxEditor.Surface.ContextMenu
_parameterSetNodeArchetype = info.ParameterSetNodeArchetype ?? Archetypes.Parameters.Nodes[3];
// Context menu dimensions
- Size = new Float2(320, 248);
+ Size = new Float2(300, 400);
var headerPanel = new Panel(ScrollBars.None)
{
@@ -139,17 +140,41 @@ namespace FlaxEditor.Surface.ContextMenu
};
// Title bar
+ var titleFontReference = new FontReference(Style.Current.FontLarge.Asset, 10);
var titleLabel = new Label
{
- Width = Width - 8,
+ Width = Width * 0.5f - 8f,
Height = 20,
X = 4,
Parent = headerPanel,
Text = "Select Node",
- HorizontalAlignment = TextAlignment.Center,
- Font = new FontReference(Style.Current.FontLarge.Asset, 10),
+ HorizontalAlignment = TextAlignment.Near,
+ Font = titleFontReference,
};
+ // Context sensitive toggle
+ var contextSensitiveLabel = new Label
+ {
+ Width = Width * 0.5f - 28,
+ Height = 20,
+ X = Width * 0.5f,
+ Parent = headerPanel,
+ Text = "Context Sensitive",
+ TooltipText = "Should the nodes be filtered to only show those that can be connected in the current context?",
+ HorizontalAlignment = TextAlignment.Far,
+ Font = titleFontReference,
+ };
+
+ _contextSensitiveToggle = new CheckBox
+ {
+ Width = 20,
+ Height = 20,
+ X = Width - 24,
+ Parent = headerPanel,
+ Checked = _contextSensitiveSearchEnabled,
+ };
+ _contextSensitiveToggle.StateChanged += OnContextSensitiveToggleStateChanged;
+
// Search box
_searchBox = new SearchBox(false, 2, 22)
{
@@ -288,6 +313,10 @@ namespace FlaxEditor.Surface.ContextMenu
OnSearchFilterChanged();
}
}
+ else if (_contextSensitiveSearchEnabled)
+ {
+ group.EvaluateVisibilityWithBox(_selectedBox);
+ }
Profiler.EndEvent();
}
@@ -321,6 +350,8 @@ namespace FlaxEditor.Surface.ContextMenu
Parent = group
};
}
+ if (_contextSensitiveSearchEnabled)
+ group.EvaluateVisibilityWithBox(_selectedBox);
group.SortChildren();
group.Parent = _groupsPanel;
_groups.Add(group);
@@ -418,8 +449,26 @@ namespace FlaxEditor.Surface.ContextMenu
return;
Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged");
-
- if (string.IsNullOrEmpty(_searchBox.Text))
+ UpdateFilters();
+ _searchBox.Focus();
+ Profiler.EndEvent();
+ }
+
+ private void OnContextSensitiveToggleStateChanged(CheckBox checkBox)
+ {
+ // Skip events during setup or init stuff
+ if (IsLayoutLocked)
+ return;
+
+ Profiler.BeginEvent("VisjectCM.OnContextSensitiveToggleStateChanged");
+ _contextSensitiveSearchEnabled = checkBox.Checked;
+ UpdateFilters();
+ Profiler.EndEvent();
+ }
+
+ private void UpdateFilters()
+ {
+ if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null)
{
ResetView();
Profiler.EndEvent();
@@ -430,7 +479,7 @@ namespace FlaxEditor.Surface.ContextMenu
LockChildrenRecursive();
for (int i = 0; i < _groups.Count; i++)
{
- _groups[i].UpdateFilter(_searchBox.Text);
+ _groups[i].UpdateFilter(_searchBox.Text, _contextSensitiveSearchEnabled ? _selectedBox : null);
_groups[i].UpdateItemSort(_selectedBox);
}
SortGroups();
@@ -443,9 +492,6 @@ namespace FlaxEditor.Surface.ContextMenu
PerformLayout();
if (SelectedItem != null)
_panel1.ScrollViewTo(SelectedItem);
- _searchBox.Focus();
- Profiler.EndEvent();
-
Profiler.EndEvent();
}
@@ -503,7 +549,11 @@ namespace FlaxEditor.Surface.ContextMenu
_searchBox.Clear();
SelectedItem = null;
for (int i = 0; i < _groups.Count; i++)
+ {
_groups[i].ResetView();
+ if (_contextSensitiveSearchEnabled)
+ _groups[i].EvaluateVisibilityWithBox(_selectedBox);
+ }
UnlockChildrenRecursive();
SortGroups();
@@ -759,5 +809,12 @@ namespace FlaxEditor.Surface.ContextMenu
{
return GetPreviousSiblings(item).OfType();
}
+
+ ///
+ public override void OnDestroy()
+ {
+ _contextSensitiveToggle.StateChanged -= OnContextSensitiveToggleStateChanged;
+ base.OnDestroy();
+ }
}
}
diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs b/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs
index 446840f2c..e52e10482 100644
--- a/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs
+++ b/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs
@@ -66,7 +66,7 @@ namespace FlaxEditor.Surface.ContextMenu
{
if (_children[i] is VisjectCMItem item)
{
- item.UpdateFilter(null);
+ item.UpdateFilter(null, null);
item.UpdateScore(null);
}
}
@@ -84,23 +84,42 @@ namespace FlaxEditor.Surface.ContextMenu
/// Updates the filter.
///
/// The filter text.
- public void UpdateFilter(string filterText)
+ /// The optionally selected box to show hints for it.
+ public void UpdateFilter(string filterText, Box selectedBox)
{
Profiler.BeginEvent("VisjectCMGroup.UpdateFilter");
// Update items
bool isAnyVisible = false;
+ bool groupHeaderMatches = QueryFilterHelper.Match(filterText, HeaderText);
for (int i = 0; i < _children.Count; i++)
{
if (_children[i] is VisjectCMItem item)
{
- item.UpdateFilter(filterText);
+ item.UpdateFilter(filterText, selectedBox, groupHeaderMatches);
isAnyVisible |= item.Visible;
}
}
- // Update header title
- if (QueryFilterHelper.Match(filterText, HeaderText))
+ // Update itself
+ if (isAnyVisible)
+ {
+ if (!string.IsNullOrEmpty(filterText))
+ Open(false);
+ Visible = true;
+ }
+ else
+ {
+ // Hide group if none of the items matched the filter
+ Visible = false;
+ }
+
+ Profiler.EndEvent();
+ }
+
+ internal void EvaluateVisibilityWithBox(Box selectedBox)
+ {
+ if (selectedBox == null)
{
for (int i = 0; i < _children.Count; i++)
{
@@ -109,14 +128,25 @@ namespace FlaxEditor.Surface.ContextMenu
item.Visible = true;
}
}
- isAnyVisible = true;
+ Visible = true;
+ return;
+ }
+
+ Profiler.BeginEvent("VisjectCMGroup.EvaluateVisibilityWithBox");
+
+ bool isAnyVisible = false;
+ for (int i = 0; i < _children.Count; i++)
+ {
+ if (_children[i] is VisjectCMItem item)
+ {
+ item.Visible = item.CanConnectTo(selectedBox);
+ isAnyVisible |= item.Visible;
+ }
}
// Update itself
if (isAnyVisible)
{
- if (!string.IsNullOrEmpty(filterText))
- Open(false);
Visible = true;
}
else
diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs
index b68d529a1..7fd583202 100644
--- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs
+++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using FlaxEditor.Scripting;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Utilities;
using FlaxEngine;
@@ -56,7 +57,7 @@ namespace FlaxEditor.Surface.ContextMenu
/// The group archetype.
/// The archetype.
public VisjectCMItem(VisjectCMGroup group, GroupArchetype groupArchetype, NodeArchetype archetype)
- : base(0, 0, 120, 12)
+ : base(0, 0, 120, 14)
{
Group = group;
_groupArchetype = groupArchetype;
@@ -77,7 +78,7 @@ namespace FlaxEditor.Surface.ContextMenu
if (!Visible)
return;
- if (selectedBox != null && CanConnectTo(selectedBox, NodeArchetype))
+ if (selectedBox != null && CanConnectTo(selectedBox))
SortScore += 1;
if (Data != null)
SortScore += 1;
@@ -92,35 +93,93 @@ namespace FlaxEditor.Surface.ContextMenu
textRect = new Rectangle(22, 0, Width - 24, Height);
}
- private bool CanConnectTo(Box startBox, NodeArchetype nodeArchetype)
+ ///
+ /// Checks if this context menu item can be connected to a given box, before a node is actually spawned.
+ ///
+ /// The connected box
+ /// True if the connected box is compatible with this item
+ public bool CanConnectTo(Box startBox)
{
+ // Is compatible if box is null for reset reasons
if (startBox == null)
- return false;
- if (!startBox.IsOutput)
- return false; // For now, I'm only handing the output box case
+ return true;
- if (nodeArchetype.Elements != null)
+ if (_archetype == null)
+ return false;
+
+ bool isCompatible = false;
+ if (startBox.IsOutput && _archetype.IsInputCompatible != null)
{
- for (int i = 0; i < nodeArchetype.Elements.Length; i++)
- {
- if (nodeArchetype.Elements[i].Type == NodeElementType.Input &&
- startBox.CanUseType(nodeArchetype.Elements[i].ConnectionsType))
- {
- return true;
- }
- }
+ isCompatible |= _archetype.IsInputCompatible.Invoke(_archetype, startBox.CurrentType, _archetype.ConnectionsHints);
}
- return false;
+ else if (!startBox.IsOutput && _archetype.IsOutputCompatible != null)
+ {
+ isCompatible |= _archetype.IsOutputCompatible.Invoke(_archetype, startBox.CurrentType, startBox.ParentNode.Archetype.ConnectionsHints);
+ }
+ else if (_archetype.Elements != null)
+ {
+ // Check compatibility based on the defined elements in the archetype. This handles all the default groups and items
+ isCompatible = CheckElementsCompatibility(startBox);
+ }
+
+ return isCompatible;
+ }
+
+ private bool CheckElementsCompatibility(Box startBox)
+ {
+ bool isCompatible = false;
+ foreach (NodeElementArchetype element in _archetype.Elements)
+ {
+ // Ignore all elements that aren't inputs or outputs (e.g. input fields)
+ if (element.Type != NodeElementType.Output && element.Type != NodeElementType.Input)
+ continue;
+
+ // Ignore elements with the same direction as the box
+ if ((startBox.IsOutput && element.Type == NodeElementType.Output) || (!startBox.IsOutput && element.Type == NodeElementType.Input))
+ continue;
+
+ ScriptType fromType;
+ ScriptType toType;
+ ConnectionsHint hint;
+ if (startBox.IsOutput)
+ {
+ fromType = element.ConnectionsType;
+ toType = startBox.CurrentType;
+ hint = _archetype.ConnectionsHints;
+ }
+ else
+ {
+ fromType = startBox.CurrentType;
+ toType = element.ConnectionsType;
+ hint = startBox.ParentNode.Archetype.ConnectionsHints;
+ }
+
+ isCompatible |= VisjectSurface.FullCastCheck(fromType, toType, hint);
+ }
+
+ return isCompatible;
}
///
/// Updates the filter.
///
/// The filter text.
- public void UpdateFilter(string filterText)
+ /// The optionally selected box to show hints for it.
+ /// True if item's group header got a filter match and item should stay visible.
+ public void UpdateFilter(string filterText, Box selectedBox, bool groupHeaderMatches = false)
{
+ if (selectedBox != null)
+ {
+ Visible = CanConnectTo(selectedBox);
+ if (!Visible)
+ {
+ _highlights?.Clear();
+ return;
+ }
+ }
+
_isStartsWithMatch = _isFullMatch = false;
- if (filterText == null)
+ if (string.IsNullOrEmpty(filterText))
{
// Clear filter
_highlights?.Clear();
@@ -184,7 +243,7 @@ namespace FlaxEditor.Surface.ContextMenu
Data = data;
}
- else
+ else if (!groupHeaderMatches)
{
// Hide
_highlights?.Clear();
diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs
index e771fa71c..a03c8f701 100644
--- a/Source/Editor/Surface/Elements/Box.cs
+++ b/Source/Editor/Surface/Elements/Box.cs
@@ -91,7 +91,7 @@ namespace FlaxEditor.Surface.Elements
_currentType = value;
// Check if will need to update box connections due to type change
- if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !CanCast(prev, _currentType))
+ if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !prev.CanCastTo(_currentType))
{
// Remove all invalid connections and update those which still can be valid
var connections = Connections.ToArray();
@@ -231,58 +231,8 @@ namespace FlaxEditor.Surface.Elements
}
// Check using connection hints
- var connectionsHints = ParentNode.Archetype.ConnectionsHints;
- if (Archetype.ConnectionsType == ScriptType.Null && connectionsHints != ConnectionsHint.None)
- {
- if ((connectionsHints & ConnectionsHint.Anything) == ConnectionsHint.Anything)
- return true;
- if ((connectionsHints & ConnectionsHint.Value) == ConnectionsHint.Value && type.Type != typeof(void))
- return true;
- if ((connectionsHints & ConnectionsHint.Enum) == ConnectionsHint.Enum && type.IsEnum)
- return true;
- if ((connectionsHints & ConnectionsHint.Array) == ConnectionsHint.Array && type.IsArray)
- return true;
- if ((connectionsHints & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && type.IsDictionary)
- return true;
- if ((connectionsHints & ConnectionsHint.Vector) == ConnectionsHint.Vector)
- {
- var t = type.Type;
- if (t == typeof(Vector2) ||
- t == typeof(Vector3) ||
- t == typeof(Vector4) ||
- t == typeof(Float2) ||
- t == typeof(Float3) ||
- t == typeof(Float4) ||
- t == typeof(Double2) ||
- t == typeof(Double3) ||
- t == typeof(Double4) ||
- t == typeof(Int2) ||
- t == typeof(Int3) ||
- t == typeof(Int4) ||
- t == typeof(Color))
- {
- return true;
- }
- }
- if ((connectionsHints & ConnectionsHint.Scalar) == ConnectionsHint.Scalar)
- {
- var t = type.Type;
- if (t == typeof(bool) ||
- t == typeof(char) ||
- t == typeof(byte) ||
- t == typeof(short) ||
- t == typeof(ushort) ||
- t == typeof(int) ||
- t == typeof(uint) ||
- t == typeof(long) ||
- t == typeof(ulong) ||
- t == typeof(float) ||
- t == typeof(double))
- {
- return true;
- }
- }
- }
+ if (VisjectSurface.IsTypeCompatible(Archetype.ConnectionsType, type, ParentNode.Archetype.ConnectionsHints))
+ return true;
// Check independent and if there is box with bigger potential because it may block current one from changing type
var parentArch = ParentNode.Archetype;
@@ -296,7 +246,7 @@ namespace FlaxEditor.Surface.Elements
var b = ParentNode.GetBox(boxes[i]);
// Check if its the same and tested type matches the default value type
- if (b == this && CanCast(parentArch.DefaultType, type))
+ if (b == this && parentArch.DefaultType.CanCastTo(type))
{
// Can
return true;
@@ -718,17 +668,6 @@ namespace FlaxEditor.Surface.Elements
}
}
- private static bool CanCast(ScriptType oB, ScriptType iB)
- {
- if (oB == iB)
- return true;
- if (oB == ScriptType.Null || iB == ScriptType.Null)
- return false;
- return (oB.Type != typeof(void) && oB.Type != typeof(FlaxEngine.Object)) &&
- (iB.Type != typeof(void) && iB.Type != typeof(FlaxEngine.Object)) &&
- oB.IsAssignableFrom(iB);
- }
-
///
public bool AreConnected(IConnectionInstigator other)
{
@@ -787,7 +726,7 @@ namespace FlaxEditor.Surface.Elements
{
if (!iB.CanUseType(oB.CurrentType))
{
- if (!CanCast(oB.CurrentType, iB.CurrentType))
+ if (!oB.CurrentType.CanCastTo(iB.CurrentType))
{
// Cannot
return false;
@@ -798,7 +737,7 @@ namespace FlaxEditor.Surface.Elements
{
if (!oB.CanUseType(iB.CurrentType))
{
- if (!CanCast(oB.CurrentType, iB.CurrentType))
+ if (!oB.CurrentType.CanCastTo(iB.CurrentType))
{
// Cannot
return false;
@@ -871,7 +810,7 @@ namespace FlaxEditor.Surface.Elements
bool useCaster = false;
if (!iB.CanUseType(oB.CurrentType))
{
- if (CanCast(oB.CurrentType, iB.CurrentType))
+ if (oB.CurrentType.CanCastTo(iB.CurrentType))
useCaster = true;
else
return;
diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs
index cf78b559c..8ef64ec77 100644
--- a/Source/Editor/Surface/Elements/OutputBox.cs
+++ b/Source/Editor/Surface/Elements/OutputBox.cs
@@ -35,7 +35,7 @@ namespace FlaxEditor.Surface.Elements
{
// Calculate control points
CalculateBezierControlPoints(start, end, out var control1, out var control2);
-
+
// Draw line
Render2D.DrawBezier(start, control1, control2, end, color, thickness);
@@ -54,16 +54,16 @@ namespace FlaxEditor.Surface.Elements
const float maxControlLength = 150f;
var dst = (end - start).Length;
var yDst = Mathf.Abs(start.Y - end.Y);
-
+
// Calculate control points
var minControlDst = dst * 0.5f;
var maxControlDst = Mathf.Max(Mathf.Min(maxControlLength, dst), minControlLength);
var controlDst = Mathf.Lerp(minControlDst, maxControlDst, Mathf.Clamp(yDst / minControlLength, 0f, 1f));
-
+
control1 = new Float2(start.X + controlDst, start.Y);
control2 = new Float2(end.X - controlDst, end.Y);
}
-
+
///
/// Checks if a point intersects a connection
///
@@ -86,11 +86,13 @@ namespace FlaxEditor.Surface.Elements
public static bool IntersectsConnection(ref Float2 start, ref Float2 end, ref Float2 point, float distance)
{
// Pretty much a point in rectangle check
- if ((point.X - start.X) * (end.X - point.X) < 0) return false;
+ if ((point.X - start.X) * (end.X - point.X) < 0)
+ return false;
float offset = Mathf.Sign(end.Y - start.Y) * distance;
- if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0) return false;
-
+ if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0)
+ return false;
+
float squaredDistance = distance;
CalculateBezierControlPoints(start, end, out var control1, out var control2);
diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs
index 6dc923ce2..fe54268e2 100644
--- a/Source/Editor/Surface/NodeArchetype.cs
+++ b/Source/Editor/Surface/NodeArchetype.cs
@@ -89,6 +89,11 @@ namespace FlaxEditor.Surface
/// The created node object.
public delegate SurfaceNode CreateCustomNodeFunc(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch);
+ ///
+ /// 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);
+
///
/// Unique node type ID within a single group.
///
@@ -99,6 +104,16 @@ namespace FlaxEditor.Surface
///
public CreateCustomNodeFunc Create;
+ ///
+ /// Function for asynchronously loaded nodes to check if input ports are compatible, for filtering.
+ ///
+ public IsCompatible IsInputCompatible;
+
+ ///
+ /// Function for asynchronously loaded nodes to check if output ports are compatible, for filtering.
+ ///
+ public IsCompatible IsOutputCompatible;
+
///
/// Default initial size of the node.
///
@@ -184,6 +199,8 @@ namespace FlaxEditor.Surface
{
TypeID = TypeID,
Create = Create,
+ IsInputCompatible = IsInputCompatible,
+ IsOutputCompatible = IsOutputCompatible,
Size = Size,
Flags = Flags,
Title = Title,
diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs
index 08b9c6d63..5f4c3ef07 100644
--- a/Source/Editor/Surface/SurfaceUtils.cs
+++ b/Source/Editor/Surface/SurfaceUtils.cs
@@ -406,8 +406,8 @@ namespace FlaxEditor.Surface
internal static bool IsValidVisualScriptType(ScriptType scriptType)
{
- if (!scriptType.IsPublic ||
- scriptType.HasAttribute(typeof(HideInEditorAttribute), true) ||
+ if (!scriptType.IsPublic ||
+ scriptType.HasAttribute(typeof(HideInEditorAttribute), true) ||
scriptType.HasAttribute(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false))
return false;
if (scriptType.IsGenericType)
diff --git a/Source/Editor/Surface/VisjectSurface.Connecting.cs b/Source/Editor/Surface/VisjectSurface.Connecting.cs
index 2fbff7cb8..dd34f9c7c 100644
--- a/Source/Editor/Surface/VisjectSurface.Connecting.cs
+++ b/Source/Editor/Surface/VisjectSurface.Connecting.cs
@@ -136,6 +136,88 @@ namespace FlaxEditor.Surface
return result;
}
+ ///
+ /// Checks if a type is compatible with another type and can be casted by using a connection hint
+ ///
+ /// Source type
+ /// Type to check compatibility with
+ /// Hint to check if casting is possible
+ /// True if the source type is compatible with the target type
+ public static bool IsTypeCompatible(ScriptType from, ScriptType to, ConnectionsHint hint)
+ {
+ if (from == ScriptType.Null && hint != ConnectionsHint.None)
+ {
+ if ((hint & ConnectionsHint.Anything) == ConnectionsHint.Anything)
+ return true;
+ if ((hint & ConnectionsHint.Value) == ConnectionsHint.Value && to.Type != typeof(void))
+ return true;
+ if ((hint & ConnectionsHint.Enum) == ConnectionsHint.Enum && to.IsEnum)
+ return true;
+ if ((hint & ConnectionsHint.Array) == ConnectionsHint.Array && to.IsArray)
+ return true;
+ if ((hint & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && to.IsDictionary)
+ return true;
+ if ((hint & ConnectionsHint.Vector) == ConnectionsHint.Vector)
+ {
+ var t = to.Type;
+ if (t == typeof(Vector2) ||
+ t == typeof(Vector3) ||
+ t == typeof(Vector4) ||
+ t == typeof(Float2) ||
+ t == typeof(Float3) ||
+ t == typeof(Float4) ||
+ t == typeof(Double2) ||
+ t == typeof(Double3) ||
+ t == typeof(Double4) ||
+ t == typeof(Int2) ||
+ t == typeof(Int3) ||
+ t == typeof(Int4) ||
+ t == typeof(Color))
+ {
+ return true;
+ }
+ }
+ if ((hint & ConnectionsHint.Scalar) == ConnectionsHint.Scalar)
+ {
+ var t = to.Type;
+ if (t == typeof(bool) ||
+ t == typeof(char) ||
+ t == typeof(byte) ||
+ t == typeof(short) ||
+ t == typeof(ushort) ||
+ t == typeof(int) ||
+ t == typeof(uint) ||
+ t == typeof(long) ||
+ t == typeof(ulong) ||
+ t == typeof(float) ||
+ t == typeof(double))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if a type is compatible with another type and can be casted by using a connection hint
+ ///
+ /// Source type
+ /// Target type
+ /// Connection hint
+ /// True if any method of casting or compatibility check succeeds
+ public static bool FullCastCheck(ScriptType from, ScriptType to, ConnectionsHint hint)
+ {
+ // Yes, from and to are switched on purpose
+ if (CanUseDirectCastStatic(to, from, false))
+ return true;
+ if (IsTypeCompatible(from, to, hint))
+ return true;
+ // Same here
+ return to.CanCastTo(from);
+ }
+
///
/// Checks if can use direct conversion from one type to another.
///
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/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
index 5a532e362..6448ebbb2 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;
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs
index 659bab249..50aafd46a 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.
///
diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
index d360e5570..d8790172b 100644
--- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
+++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
@@ -321,6 +321,7 @@ namespace FlaxEditor.Windows.Assets
var tree = group.Tree();
tree.TreeControl.RightClick += OnTreeNodeRightClick;
+ tree.TreeControl.SelectedChanged += OnTreeSelectedChanged;
for (int i = 0; i < nodes.Length; i++)
{
if (nodes[i].ParentIndex == -1)
@@ -367,6 +368,12 @@ namespace FlaxEditor.Windows.Assets
menu.Show(node, location);
}
+ private void OnTreeSelectedChanged(List before, List after)
+ {
+ if (after.Count != 0)
+ ((SkeletonPropertiesProxy)Values[0]).Window._preview.ShowDebugDraw = true;
+ }
+
private void OnTreeNodeCopyName(ContextMenuButton b)
{
Clipboard.Text = (string)b.Tag;
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index c25316e9a..b8e8a15ed 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -989,7 +989,7 @@ namespace FlaxEditor.Windows
}
_view.ShowItems(items, _sortType, false, true);
}
- else
+ else if (target != null)
{
// Show folder contents
var items = target.Folder.Children;
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index bbdf763fb..fdedbb3c2 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -463,6 +463,18 @@ namespace FlaxEditor.Windows
Cursor = CursorType.Default;
}
+ ///
+ public override void OnMouseLeave()
+ {
+ base.OnMouseLeave();
+
+ // Remove focus from game window when mouse moves out and the cursor is hidden during game
+ if ((IsFocused || ContainsFocus) && Parent != null && Editor.IsPlayMode && !Screen.CursorVisible)
+ {
+ Parent.Focus();
+ }
+ }
+
///
public override void OnShowContextMenu(ContextMenu menu)
{
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 84642ac1f..c905c7eef 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/Asset.h b/Source/Engine/Content/Asset.h
index 2d2ba4b3f..be188b2b9 100644
--- a/Source/Engine/Content/Asset.h
+++ b/Source/Engine/Content/Asset.h
@@ -227,7 +227,7 @@ protected:
bool onLoad(LoadAssetTask* task);
void onLoaded();
- void onLoaded_MainThread();
+ virtual void onLoaded_MainThread();
virtual void onUnload_MainThread();
#if USE_EDITOR
virtual void onRename(const StringView& newPath) = 0;
diff --git a/Source/Engine/Content/JsonAsset.cpp b/Source/Engine/Content/JsonAsset.cpp
index 1fbc2abed..9977a28e1 100644
--- a/Source/Engine/Content/JsonAsset.cpp
+++ b/Source/Engine/Content/JsonAsset.cpp
@@ -12,6 +12,7 @@
#include "FlaxEngine.Gen.h"
#include "Cache/AssetsCache.h"
#include "Engine/Core/Log.h"
+#include "Engine/Core/Config/Settings.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Content/Factories/JsonAssetFactory.h"
@@ -126,8 +127,7 @@ void FindIds(ISerializable::DeserializeStream& node, Array& output)
}
else if (node.IsString())
{
- const auto length = node.GetStringLength();
- if (length == 32)
+ if (node.GetStringLength() == 32)
{
// Try parse as Guid in format `N` (32 hex chars)
Guid id;
@@ -362,10 +362,25 @@ void JsonAsset::unload(bool isReloading)
#endif
Scripting::ScriptsUnload.Unbind(this);
DeleteInstance();
+ _isAfterReload |= isReloading;
JsonAssetBase::unload(isReloading);
}
+void JsonAsset::onLoaded_MainThread()
+{
+ JsonAssetBase::onLoaded_MainThread();
+
+ // Special case for Settings assets to flush them after edited and saved in Editor
+ const StringAsANSI<> dataTypeNameAnsi(DataTypeName.Get(), DataTypeName.Length());
+ const auto typeHandle = Scripting::FindScriptingType(StringAnsiView(dataTypeNameAnsi.Get(), DataTypeName.Length()));
+ if (Instance && typeHandle && typeHandle.IsSubclassOf(SettingsBase::TypeInitializer) && _isAfterReload)
+ {
+ _isAfterReload = false;
+ ((SettingsBase*)Instance)->Apply();
+ }
+}
+
bool JsonAsset::CreateInstance()
{
ScopeLock lock(Locker);
diff --git a/Source/Engine/Content/JsonAsset.h b/Source/Engine/Content/JsonAsset.h
index c53649d33..a8e89cec0 100644
--- a/Source/Engine/Content/JsonAsset.h
+++ b/Source/Engine/Content/JsonAsset.h
@@ -118,6 +118,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API JsonAsset : public JsonAssetBase
DECLARE_ASSET_HEADER(JsonAsset);
private:
ScriptingType::Dtor _dtor;
+ bool _isAfterReload = false;
public:
///
@@ -149,6 +150,7 @@ protected:
// [JsonAssetBase]
LoadResult loadAsset() override;
void unload(bool isReloading) override;
+ void onLoaded_MainThread() override;
private:
bool CreateInstance();
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/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs
index 500636290..f3bb57665 100644
--- a/Source/Engine/Engine/NativeInterop.cs
+++ b/Source/Engine/Engine/NativeInterop.cs
@@ -685,8 +685,10 @@ namespace FlaxEngine.Interop
T value = default;
if (nativePtr == IntPtr.Zero)
return value;
-
- MarshalHelper.ToManaged(ref value, nativePtr, false);
+ if (typeof(T).IsValueType)
+ value = (T)ManagedHandle.FromIntPtr(nativePtr).Target;
+ else
+ MarshalHelper.ToManaged(ref value, nativePtr, false);
return value;
}
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index dff527924..b5ccc291b 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -519,6 +519,7 @@ void AnimatedModel::UpdateBounds()
}
else if (SkinnedModel && SkinnedModel->IsLoaded())
{
+ const auto modelBox = SkinnedModel->GetBox(_transform.GetWorld());
if (GraphInstance.NodesPose.Count() != 0)
{
// Per-bone bounds estimated from positions
@@ -526,6 +527,7 @@ void AnimatedModel::UpdateBounds()
const int32 bonesCount = skeleton.Bones.Count();
#define GET_NODE_POS(i) _transform.LocalToWorld(GraphInstance.NodesPose[skeleton.Bones[i].NodeIndex].GetTranslation())
BoundingBox box(GET_NODE_POS(0));
+ box.Merge(modelBox);
for (int32 boneIndex = 1; boneIndex < bonesCount; boneIndex++)
box.Merge(GET_NODE_POS(boneIndex));
_box = box;
@@ -533,11 +535,12 @@ void AnimatedModel::UpdateBounds()
}
else
{
- _box = SkinnedModel->GetBox(_transform.GetWorld());
+ // No animation asset applied
+ _box = modelBox;
}
// Apply margin based on model dimensions
- const Vector3 modelBoxSize = SkinnedModel->GetBox().GetSize();
+ const Vector3 modelBoxSize = modelBox.GetSize();
const Vector3 center = _box.GetCenter();
const Vector3 sizeHalf = Vector3::Max(_box.GetSize() + modelBoxSize * 0.2f, modelBoxSize) * 0.5f;
_box = BoundingBox(center - sizeHalf, center + sizeHalf);
diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp
index a2b148f28..7d3933175 100644
--- a/Source/Engine/Level/Prefabs/PrefabManager.cpp
+++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp
@@ -321,6 +321,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);
@@ -329,7 +331,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();
}
@@ -395,7 +417,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/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp
index 52234fd7a..5850b6736 100644
--- a/Source/Engine/Particles/Particles.cpp
+++ b/Source/Engine/Particles/Particles.cpp
@@ -1283,8 +1283,8 @@ void ParticlesSystem::Job(int32 index)
updateBounds = true;
}
// TODO: if using fixed timestep quantize the dt and accumulate remaining part for the next update?
- if (dt <= 1.0f / 240.0f)
- return;
+ //if (dt <= 1.0f / 240.0f)
+ // return;
dt *= effect->SimulationSpeed;
instance.Time += dt;
const float fps = particleSystem->FramesPerSecond;
diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp
index ab314ab72..6195215e3 100644
--- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp
+++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp
@@ -2707,8 +2707,34 @@ 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()
diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp
index 7694836cb..e6fe63f9c 100644
--- a/Source/Engine/Platform/Linux/LinuxWindow.cpp
+++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp
@@ -153,7 +153,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)
diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp
index d712102fa..1bc50f207 100644
--- a/Source/Engine/Platform/Windows/WindowsWindow.cpp
+++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp
@@ -220,6 +220,12 @@ void WindowsWindow::Show()
if (!_settings.HasBorder)
{
SetWindowPos(_handle, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER);
+ if (!_settings.IsRegularWindow && _settings.ShowAfterFirstPaint && _settings.StartPosition == WindowStartPosition::Manual)
+ {
+ int32 x = Math::TruncToInt(_settings.Position.X);
+ int32 y = Math::TruncToInt(_settings.Position.Y);
+ SetWindowPos(_handle, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
+ }
}
#endif
@@ -711,8 +717,27 @@ void WindowsWindow::CheckForWindowResize()
// Cache client size
RECT rect;
GetClientRect(_handle, &rect);
- const int32 width = Math::Max(rect.right - rect.left, 0L);
- const int32 height = Math::Max(rect.bottom - rect.top, 0L);
+ int32 width = Math::Max(rect.right - rect.left, 0L);
+ int32 height = Math::Max(rect.bottom - rect.top, 0L);
+
+ // Check for windows maximized size and see if it needs to adjust position if needed
+ if (_maximized)
+ {
+ // Pick the current monitor data for sizing
+ const HMONITOR monitor = MonitorFromWindow(_handle, MONITOR_DEFAULTTONEAREST);
+ MONITORINFO monitorInfo;
+ monitorInfo.cbSize = sizeof(MONITORINFO);
+ GetMonitorInfoW(monitor, &monitorInfo);
+
+ auto cwidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
+ auto cheight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
+ if (width > cwidth && height > cheight)
+ {
+ width = cwidth;
+ height = cheight;
+ SetWindowPos(_handle, HWND_TOP, monitorInfo.rcWork.left, monitorInfo.rcWork.top, width, height, SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
+ }
+ }
_clientSize = Float2(static_cast(width), static_cast(height));
// Check if window size has been changed
diff --git a/Source/Engine/Serialization/JsonTools.cpp b/Source/Engine/Serialization/JsonTools.cpp
index 07a577033..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;
@@ -256,7 +255,7 @@ BoundingBox JsonTools::GetBoundingBox(const Value& value)
Guid JsonTools::GetGuid(const Value& value)
{
- if (value.IsNull())
+ if (!value.IsString())
return Guid::Empty;
CHECK_RETURN(value.GetStringLength() == 32, Guid::Empty);
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..cd61ee37a 100644
--- a/Source/Engine/Tools/AudioTool/AudioTool.h
+++ b/Source/Engine/Tools/AudioTool/AudioTool.h
@@ -2,16 +2,85 @@
#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;
+ };
+
+public:
///
/// Converts a set of audio samples using multiple channels into a set of mono samples.
///
@@ -58,4 +127,7 @@ public:
{
return (input[2] << 24) | (input[1] << 16) | (input[0] << 8);
}
+#endif
};
+
+#endif
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
index 8aa9199fe..000a2e77b 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
@@ -7,6 +7,7 @@
#include "Engine/Core/DeleteMe.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Platform/FileSystem.h"
+#include "Engine/Platform/File.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
// Import Assimp library
@@ -515,8 +516,47 @@ bool ImportTexture(ImportedModelData& result, AssimpImporterData& data, aiString
bool ImportMaterialTexture(ImportedModelData& result, AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type)
{
aiString aFilename;
- return aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS &&
- ImportTexture(result, data, aFilename, textureIndex, type);
+ if (aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS)
+ {
+ // Check for embedded textures
+ String filename = String(aFilename.C_Str()).TrimTrailing();
+ if (filename.StartsWith(TEXT(AI_EMBEDDED_TEXNAME_PREFIX)))
+ {
+ const aiTexture* aTex = data.Scene->GetEmbeddedTexture(aFilename.C_Str());
+ const StringView texIndexName(filename.Get() + (ARRAY_COUNT(AI_EMBEDDED_TEXNAME_PREFIX) - 1));
+ uint32 texIndex;
+ if (!aTex && !StringUtils::Parse(texIndexName.Get(), texIndexName.Length(), &texIndex) && texIndex >= 0 && texIndex < data.Scene->mNumTextures)
+ aTex = data.Scene->mTextures[texIndex];
+ if (aTex && aTex->mHeight == 0 && aTex->mWidth > 0)
+ {
+ // Export embedded texture to temporary file
+ filename = String::Format(TEXT("{0}_tex_{1}.{2}"), StringUtils::GetFileNameWithoutExtension(data.Path), texIndexName, String(aTex->achFormatHint));
+ File::WriteAllBytes(String(StringUtils::GetDirectoryName(data.Path)) / filename, (const byte*)aTex->pcData, (int32)aTex->mWidth);
+ }
+ }
+
+ // Find texture file path
+ String path;
+ if (ModelTool::FindTexture(data.Path, filename, path))
+ return true;
+
+ // Check if already used
+ textureIndex = 0;
+ while (textureIndex < result.Textures.Count())
+ {
+ if (result.Textures[textureIndex].FilePath == path)
+ return true;
+ textureIndex++;
+ }
+
+ // Import texture
+ auto& texture = result.Textures.AddOne();
+ texture.FilePath = path;
+ texture.Type = type;
+ texture.AssetID = Guid::Empty;
+ return true;
+ }
+ return false;
}
bool ImportMaterials(ImportedModelData& result, AssimpImporterData& data, String& errorMsg)
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
index 215eadaba..f1e89d6bc 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
@@ -12,6 +12,8 @@
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Platform/File.h"
+#define OPEN_FBX_CONVERT_SPACE 1
+
// Import OpenFBX library
// Source: https://github.com/nem0/OpenFBX
#include
@@ -85,12 +87,16 @@ struct OpenFbxImporterData
const ModelTool::Options& Options;
ofbx::GlobalSettings GlobalSettings;
+#if OPEN_FBX_CONVERT_SPACE
+ Quaternion RootConvertRotation = Quaternion::Identity;
Float3 Up;
Float3 Front;
Float3 Right;
bool ConvertRH;
+#else
+ static constexpr bool ConvertRH = false;
+#endif
float FrameRate;
- Quaternion RootConvertRotation = Quaternion::Identity;
Array Nodes;
Array Bones;
@@ -103,7 +109,9 @@ struct OpenFbxImporterData
, Path(path)
, Options(options)
, GlobalSettings(*scene->getGlobalSettings())
+#if OPEN_FBX_CONVERT_SPACE
, ConvertRH(GlobalSettings.CoordAxis == ofbx::CoordSystem_RightHanded)
+#endif
, Nodes(static_cast(scene->getMeshCount() * 4.0f))
{
float frameRate = scene->getSceneFrameRate();
@@ -114,6 +122,7 @@ struct OpenFbxImporterData
frameRate = 30.0f;
}
FrameRate = frameRate;
+#if OPEN_FBX_CONVERT_SPACE
const float coordAxisSign = GlobalSettings.CoordAxis == ofbx::CoordSystem_LeftHanded ? -1.0f : +1.0f;
switch (GlobalSettings.UpAxis)
{
@@ -170,6 +179,7 @@ struct OpenFbxImporterData
break;
default: ;
}
+#endif
}
bool ImportMaterialTexture(ImportedModelData& result, const ofbx::Material* mat, ofbx::Texture::TextureType textureType, int32& textureIndex, TextureEntry::TypeHint type) const
@@ -366,13 +376,12 @@ void ProcessNodes(OpenFbxImporterData& data, const ofbx::Object* aNode, int32 pa
{
node.LodIndex = data.Nodes[parentIndex].LodIndex;
if (node.LodIndex == 0)
- {
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
- }
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
}
auto transform = ToMatrix(aNode->evalLocal(aNode->getLocalTranslation(), aNode->getLocalRotation()));
+#if OPEN_FBX_CONVERT_SPACE
if (data.ConvertRH)
{
// Mirror all base vectors at the local Z axis
@@ -388,6 +397,7 @@ void ProcessNodes(OpenFbxImporterData& data, const ofbx::Object* aNode, int32 pa
transform.M33 = -transform.M33;
transform.M43 = -transform.M43;
}
+#endif
transform.Decompose(node.LocalTransform);
data.Nodes.Add(node);
@@ -416,8 +426,9 @@ Matrix GetOffsetMatrix(OpenFbxImporterData& data, const ofbx::Mesh* mesh, const
}
}
}
+ //return Matrix::Identity;
return ToMatrix(node->getGlobalTransform());
-#elif 1
+#else
Matrix t = Matrix::Identity;
const int32 boneIdx = data.FindBone(node);
int32 idx = data.Bones[boneIdx].NodeIndex;
@@ -427,17 +438,6 @@ Matrix GetOffsetMatrix(OpenFbxImporterData& data, const ofbx::Mesh* mesh, const
idx = data.Nodes[idx].ParentIndex;
} while (idx != -1);
return t;
-#else
- auto* skin = mesh->getGeometry()->getSkin();
- for (int i = 0, c = skin->getClusterCount(); i < c; i++)
- {
- const ofbx::Cluster* cluster = skin->getCluster(i);
- if (cluster->getLink() == node)
- {
- return ToMatrix(cluster->getTransformLinkMatrix());
- }
- }
- return Matrix::Identity;
#endif
}
@@ -455,17 +455,14 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
const auto aMesh = data.Scene->getMesh(i);
const auto aGeometry = aMesh->getGeometry();
const ofbx::Skin* skin = aGeometry->getSkin();
-
if (skin == nullptr || IsMeshInvalid(aMesh))
continue;
- for (int clusterIndex = 0, c = skin->getClusterCount(); clusterIndex < c; clusterIndex++)
+ for (int clusterIndex = 0, clusterCount = skin->getClusterCount(); clusterIndex < clusterCount; clusterIndex++)
{
const ofbx::Cluster* cluster = skin->getCluster(clusterIndex);
-
if (cluster->getIndicesCount() == 0)
continue;
-
const auto link = cluster->getLink();
ASSERT(link != nullptr);
@@ -487,7 +484,7 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
// Add bone
boneIndex = data.Bones.Count();
- data.Bones.EnsureCapacity(Math::Max(128, boneIndex + 16));
+ data.Bones.EnsureCapacity(256);
data.Bones.Resize(boneIndex + 1);
auto& bone = data.Bones[boneIndex];
@@ -495,7 +492,7 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
bone.NodeIndex = nodeIndex;
bone.ParentBoneIndex = -1;
bone.FbxObj = link;
- bone.OffsetMatrix = GetOffsetMatrix(data, aMesh, link);
+ bone.OffsetMatrix = GetOffsetMatrix(data, aMesh, link) * Matrix::Scaling(data.GlobalSettings.UnitScaleFactor);
bone.OffsetMatrix.Invert();
// Mirror offset matrices (RH to LH)
@@ -509,6 +506,15 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
m.M32 = -m.M32;
m.M34 = -m.M34;
}
+
+ // Convert bone matrix if scene uses root transform
+ if (!data.RootConvertRotation.IsIdentity())
+ {
+ Matrix m;
+ Matrix::RotationQuaternion(data.RootConvertRotation, m);
+ m.Invert();
+ bone.OffsetMatrix = m * bone.OffsetMatrix;
+ }
}
}
}
@@ -547,9 +553,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
// Vertex positions
mesh.Positions.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.Positions.Get()[i] = ToFloat3(vertices[i + firstVertexOffset]);
- }
// Indices (dummy index buffer)
if (vertexCount % 3 != 0)
@@ -559,24 +563,18 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
}
mesh.Indices.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.Indices.Get()[i] = i;
- }
// Texture coordinates
if (uvs)
{
mesh.UVs.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.UVs.Get()[i] = ToFloat2(uvs[i + firstVertexOffset]);
- }
if (data.ConvertRH)
{
for (int32 v = 0; v < vertexCount; v++)
- {
mesh.UVs[v].Y = 1.0f - mesh.UVs[v].Y;
- }
}
}
@@ -593,16 +591,12 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
{
mesh.Normals.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.Normals.Get()[i] = ToFloat3(normals[i + firstVertexOffset]);
- }
if (data.ConvertRH)
{
// Mirror normals along the Z axis
for (int32 i = 0; i < vertexCount; i++)
- {
mesh.Normals.Get()[i].Z *= -1.0f;
- }
}
}
@@ -615,16 +609,12 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
{
mesh.Tangents.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.Tangents.Get()[i] = ToFloat3(tangents[i + firstVertexOffset]);
- }
if (data.ConvertRH)
{
// Mirror tangents along the Z axis
for (int32 i = 0; i < vertexCount; i++)
- {
mesh.Tangents.Get()[i].Z *= -1.0f;
- }
}
}
@@ -670,15 +660,11 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
{
mesh.LightmapUVs.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.LightmapUVs.Get()[i] = ToFloat2(lightmapUVs[i + firstVertexOffset]);
- }
if (data.ConvertRH)
{
for (int32 v = 0; v < vertexCount; v++)
- {
mesh.LightmapUVs[v].Y = 1.0f - mesh.LightmapUVs[v].Y;
- }
}
}
else
@@ -692,9 +678,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
{
mesh.Colors.Resize(vertexCount, false);
for (int i = 0; i < vertexCount; i++)
- {
mesh.Colors.Get()[i] = ToColor(colors[i + firstVertexOffset]);
- }
}
// Blend Indices and Blend Weights
@@ -705,13 +689,11 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
mesh.BlendIndices.SetAll(Int4::Zero);
mesh.BlendWeights.SetAll(Float4::Zero);
- for (int clusterIndex = 0, c = skin->getClusterCount(); clusterIndex < c; clusterIndex++)
+ for (int clusterIndex = 0, clusterCount = skin->getClusterCount(); clusterIndex < clusterCount; clusterIndex++)
{
const ofbx::Cluster* cluster = skin->getCluster(clusterIndex);
-
if (cluster->getIndicesCount() == 0)
continue;
-
const auto link = cluster->getLink();
ASSERT(link != nullptr);
@@ -815,15 +797,11 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
{
// Mirror positions along the Z axis
for (int32 i = 0; i < vertexCount; i++)
- {
mesh.Positions[i].Z *= -1.0f;
- }
for (auto& blendShapeData : mesh.BlendShapes)
{
for (auto& v : blendShapeData.Vertices)
- {
v.PositionDelta.Z *= -1.0f;
- }
}
}
@@ -834,9 +812,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
{
// Invert the order
for (int32 i = 0; i < mesh.Indices.Count(); i += 3)
- {
Swap(mesh.Indices[i], mesh.Indices[i + 2]);
- }
}
if ((data.Options.CalculateTangents || !tangents) && mesh.UVs.HasItems())
@@ -888,9 +864,7 @@ bool ImportMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx
{
node.LodIndex = data.Nodes[0].LodIndex;
if (node.LodIndex == 0)
- {
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
- }
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
}
node.LocalTransform = Transform::Identity;
@@ -999,7 +973,6 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve& curv
{
if (curveNode == nullptr)
return;
-
const auto keyframes = curve.Resize(info.FramesCount);
const auto bone = curveNode->getBone();
Frame localFrame;
@@ -1143,6 +1116,14 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
}
fileData.Resize(0);
+ // Tweak scene if exported by Blender
+ auto& globalInfo = *scene->getGlobalInfo();
+ if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase))
+ {
+ auto ptr = const_cast(scene->getGlobalSettings());
+ ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1);
+ }
+
// Process imported scene
context = New(path, options, scene);
auto& globalSettings = context->GlobalSettings;
@@ -1153,10 +1134,13 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
// Log scene info
LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context->FrameRate, globalSettings.UnitScaleFactor);
+ LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor));
LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)"));
+#if OPEN_FBX_CONVERT_SPACE
LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context->Up, context->Front, context->Right);
+#endif
// Extract embedded textures
if (EnumHasAnyFlags(data.Types, ImportDataTypes::Textures))
@@ -1186,6 +1170,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
}
}
+#if OPEN_FBX_CONVERT_SPACE
// Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded))
if (context->Up == Float3(1, 0, 0) && context->Front == Float3(0, 0, 1) && context->Right == Float3(0, 1, 0))
{
@@ -1215,15 +1200,16 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
context->RootConvertRotation = Quaternion::Euler(90, 0, 0);*/
if (!context->RootConvertRotation.IsIdentity())
{
- for (int32 i = 0; i < context->Nodes.Count(); i++)
+ for (auto& node : context->Nodes)
{
- if (context->Nodes[i].ParentIndex == -1)
+ if (node.ParentIndex == -1)
{
- context->Nodes[i].LocalTransform.Orientation = context->RootConvertRotation * context->Nodes[i].LocalTransform.Orientation;
+ node.LocalTransform.Orientation = context->RootConvertRotation * node.LocalTransform.Orientation;
break;
}
}
}
+#endif
}
DeleteMe contextCleanup(options.SplitContext ? nullptr : context);
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index 9c9fb00a4..b703b47d1 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -31,6 +31,7 @@
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/ContentImporters/CreateMaterial.h"
+#include "Engine/ContentImporters/CreateMaterialInstance.h"
#include "Engine/ContentImporters/CreateCollisionData.h"
#include "Engine/Serialization/Serialization.h"
#include "Editor/Utilities/EditorUtilities.h"
@@ -387,6 +388,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(SloppyOptimization);
SERIALIZE(LODTargetError);
SERIALIZE(ImportMaterials);
+ SERIALIZE(ImportMaterialsAsInstances);
+ SERIALIZE(InstanceToImportAs);
SERIALIZE(ImportTextures);
SERIALIZE(RestoreMaterialsOnReimport);
SERIALIZE(GenerateSDF);
@@ -430,6 +433,8 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(SloppyOptimization);
DESERIALIZE(LODTargetError);
DESERIALIZE(ImportMaterials);
+ DESERIALIZE(ImportMaterialsAsInstances);
+ DESERIALIZE(InstanceToImportAs);
DESERIALIZE(ImportTextures);
DESERIALIZE(RestoreMaterialsOnReimport);
DESERIALIZE(GenerateSDF);
@@ -503,11 +508,11 @@ bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options&
if (ImportDataAssimp(importPath.Get(), data, options, errorMsg))
return true;
#elif USE_AUTODESK_FBX_SDK
- if (ImportDataAutodeskFbxSdk(importPath.Get(), data, options, errorMsg))
- return true;
+ if (ImportDataAutodeskFbxSdk(importPath.Get(), data, options, errorMsg))
+ return true;
#elif USE_OPEN_FBX
- if (ImportDataOpenFBX(importPath.Get(), data, options, errorMsg))
- return true;
+ if (ImportDataOpenFBX(importPath.Get(), data, options, errorMsg))
+ return true;
#else
LOG(Error, "Compiled without model importing backend.");
return true;
@@ -620,62 +625,62 @@ bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options&
bool SortDepths(const Pair& a, const Pair& b)
{
- return a.First < b.First;
+ return a.First < b.First;
}
void CreateLinearListFromTree(Array& nodes, Array& mapping)
{
// Customized breadth first tree algorithm (each node has no direct reference to the children so we build the cache for the nodes depth level)
- const int32 count = nodes.Count();
- Array> depths(count); // Pair.First = Depth, Pair.Second = Node Index
- depths.SetSize(count);
- depths.Set(-1);
- for (int32 i = 0; i < count; i++)
- {
- // Skip evaluated nodes
- if (depths[i].First != -1)
- continue;
+ const int32 count = nodes.Count();
+ Array> depths(count); // Pair.First = Depth, Pair.Second = Node Index
+ depths.Resize(count);
+ depths.SetAll(-1);
+ for (int32 i = 0; i < count; i++)
+ {
+ // Skip evaluated nodes
+ if (depths[i].First != -1)
+ continue;
- // Find the first node with calculated depth and get the distance to it
- int32 end = i;
- int32 lastDepth;
- int32 relativeDepth = 0;
- do
- {
- lastDepth = depths[end].First;
- end = nodes[end].ParentIndex;
- relativeDepth++;
- } while (end != -1 && lastDepth == -1);
+ // Find the first node with calculated depth and get the distance to it
+ int32 end = i;
+ int32 lastDepth;
+ int32 relativeDepth = 0;
+ do
+ {
+ lastDepth = depths[end].First;
+ end = nodes[end].ParentIndex;
+ relativeDepth++;
+ } while (end != -1 && lastDepth == -1);
- // Set the depth (second item is the node index)
- depths[i] = MakePair(lastDepth + relativeDepth, i);
- }
- for (int32 i = 0; i < count; i++)
- {
- // Strange divide by 2 but works
- depths[i].First = depths[i].First >> 1;
- }
+ // Set the depth (second item is the node index)
+ depths[i] = ToPair(lastDepth + relativeDepth, i);
+ }
+ for (int32 i = 0; i < count; i++)
+ {
+ // Strange divide by 2 but works
+ depths[i].First = depths[i].First >> 1;
+ }
- // Order nodes by depth O(n*log(n))
- depths.Sort(SortDepths);
+ // Order nodes by depth O(n*log(n))
+ depths.Sort(SortDepths);
- // Extract nodes mapping O(n^2)
- mapping.EnsureCapacity(count, false);
- mapping.SetSize(count);
- for (int32 i = 0; i < count; i++)
- {
- int32 newIndex = -1;
- for (int32 j = 0; j < count; j++)
- {
- if (depths[j].Second == i)
- {
- newIndex = j;
- break;
- }
- }
- ASSERT(newIndex != -1);
- mapping[i] = newIndex;
- }
+ // Extract nodes mapping O(n^2)
+ mapping.EnsureCapacity(count, false);
+ mapping.Resize(count);
+ for (int32 i = 0; i < count; i++)
+ {
+ int32 newIndex = -1;
+ for (int32 j = 0; j < count; j++)
+ {
+ if (depths[j].Second == i)
+ {
+ newIndex = j;
+ break;
+ }
+ }
+ ASSERT(newIndex != -1);
+ mapping[i] = newIndex;
+ }
}
#endif
@@ -841,19 +846,35 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
const auto mesh = data.LODs[0].Meshes[i];
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
{
- LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name);
-
auto indices = Int4::Zero;
auto weights = Float4::UnitX;
// Check if use a single bone for skinning
auto nodeIndex = data.Skeleton.FindNode(mesh->Name);
auto boneIndex = data.Skeleton.FindBone(nodeIndex);
- if (boneIndex != -1)
+ if (boneIndex == -1 && nodeIndex != -1 && data.Skeleton.Bones.Count() < MAX_BONES_PER_MODEL)
{
- LOG(Warning, "Using auto-detected bone {0} (index {1})", data.Skeleton.Nodes[nodeIndex].Name, boneIndex);
+ // Add missing bone to be used by skinned model from animated nodes pose
+ boneIndex = data.Skeleton.Bones.Count();
+ auto& bone = data.Skeleton.Bones.AddOne();
+ bone.ParentIndex = -1;
+ bone.NodeIndex = nodeIndex;
+ bone.LocalTransform = CombineTransformsFromNodeIndices(data.Nodes, -1, nodeIndex);
+ CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex);
+ LOG(Warning, "Using auto-created bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
indices.X = boneIndex;
}
+ else if (boneIndex != -1)
+ {
+ // Fallback to already added bone
+ LOG(Warning, "Using auto-detected bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
+ indices.X = boneIndex;
+ }
+ else
+ {
+ // No bone
+ LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name);
+ }
mesh->BlendIndices.Resize(mesh->Positions.Count());
mesh->BlendWeights.Resize(mesh->Positions.Count());
@@ -978,23 +999,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
importedFileNames.Add(filename);
#if COMPILE_WITH_ASSETS_IMPORTER
auto assetPath = autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT;
- CreateMaterial::Options materialOptions;
- materialOptions.Diffuse.Color = material.Diffuse.Color;
- if (material.Diffuse.TextureIndex != -1)
- materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID;
- materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask;
- materialOptions.Emissive.Color = material.Emissive.Color;
- if (material.Emissive.TextureIndex != -1)
- materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID;
- materialOptions.Opacity.Value = material.Opacity.Value;
- if (material.Opacity.TextureIndex != -1)
- materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID;
- if (material.Normals.TextureIndex != -1)
- materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID;
- if (material.TwoSided || material.Diffuse.HasAlphaMask)
- materialOptions.Info.CullMode = CullMode::TwoSided;
- if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1)
- materialOptions.Info.BlendMode = MaterialBlendMode::Transparent;
// When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1])
if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1)
@@ -1006,7 +1010,42 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
continue;
}
- AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions);
+ if (options.ImportMaterialsAsInstances)
+ {
+ // Create material instance
+ AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID);
+ if (MaterialInstance* materialInstance = Content::Load(assetPath))
+ {
+ materialInstance->SetBaseMaterial(options.InstanceToImportAs);
+ materialInstance->Save();
+ }
+ else
+ {
+ LOG(Error, "Failed to load material instance after creation. ({0})", assetPath);
+ }
+ }
+ else
+ {
+ // Create material
+ CreateMaterial::Options materialOptions;
+ materialOptions.Diffuse.Color = material.Diffuse.Color;
+ if (material.Diffuse.TextureIndex != -1)
+ materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID;
+ materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask;
+ materialOptions.Emissive.Color = material.Emissive.Color;
+ if (material.Emissive.TextureIndex != -1)
+ materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID;
+ materialOptions.Opacity.Value = material.Opacity.Value;
+ if (material.Opacity.TextureIndex != -1)
+ materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID;
+ if (material.Normals.TextureIndex != -1)
+ materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID;
+ if (material.TwoSided || material.Diffuse.HasAlphaMask)
+ materialOptions.Info.CullMode = CullMode::TwoSided;
+ if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1)
+ materialOptions.Info.BlendMode = MaterialBlendMode::Transparent;
+ AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions);
+ }
#endif
}
@@ -1091,7 +1130,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
#if COMPILE_WITH_PHYSICS_COOKING
// Create collision
CollisionCooking::Argument arg;
- arg.Type = CollisionDataType::TriangleMesh;
+ arg.Type = options.CollisionType;
arg.OverrideModelData = &collisionModel;
auto assetPath = autoImportOutput / StringUtils::GetFileNameWithoutExtension(path) + TEXT("Collision") ASSET_FILES_EXTENSION_WITH_DOT;
if (CreateCollisionData::CookMeshCollision(assetPath, arg))
@@ -1320,14 +1359,17 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
}
- // Apply import transform on root bones
- for (int32 i = 0; i < data.Skeleton.Bones.Count(); i++)
+ // Apply import transform on bones
+ Matrix importMatrixInv;
+ importTransform.GetWorld(importMatrixInv);
+ importMatrixInv.Invert();
+ for (SkeletonBone& bone : data.Skeleton.Bones)
{
- auto& bone = data.Skeleton.Bones.Get()[i];
if (bone.ParentIndex == -1)
{
bone.LocalTransform = importTransform.LocalToWorld(bone.LocalTransform);
}
+ bone.OffsetMatrix = importMatrixInv * bone.OffsetMatrix;
}
}
@@ -1363,41 +1405,32 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
// use SkeletonMapping to map bones?
// Calculate offset matrix (inverse bind pose transform) for every bone manually
+ /*for (SkeletonBone& bone : data.Skeleton.Bones)
{
- for (int32 i = 0; i < data.Skeleton.Bones.Count(); i++)
- {
- Matrix t = Matrix::Identity;
- int32 idx = data.Skeleton.Bones[i].NodeIndex;
- do
- {
- t *= data.Skeleton.Nodes[idx].LocalTransform.GetWorld();
- idx = data.Skeleton.Nodes[idx].ParentIndex;
- } while (idx != -1);
- t.Invert();
- data.Skeleton.Bones[i].OffsetMatrix = t;
- }
- }
+ CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex);
+ }*/
#if USE_SKELETON_NODES_SORTING
// Sort skeleton nodes and bones hierarchy (parents first)
- // Then it can be used with a simple linear loop update
- {
- const int32 nodesCount = data.Skeleton.Nodes.Count();
- const int32 bonesCount = data.Skeleton.Bones.Count();
- Array mapping;
- CreateLinearListFromTree(data.Skeleton.Nodes, mapping);
- for (int32 i = 0; i < nodesCount; i++)
- {
- auto& node = data.Skeleton.Nodes[i];
- node.ParentIndex = mapping[node.ParentIndex];
- }
- for (int32 i = 0; i < bonesCount; i++)
- {
- auto& bone = data.Skeleton.Bones[i];
- bone.NodeIndex = mapping[bone.NodeIndex];
- }
- }
- reorder_nodes_and_test_it_out!
+ // Then it can be used with a simple linear loop update
+ {
+ const int32 nodesCount = data.Skeleton.Nodes.Count();
+ const int32 bonesCount = data.Skeleton.Bones.Count();
+ Array mapping;
+ CreateLinearListFromTree(data.Skeleton.Nodes, mapping);
+ for (int32 i = 0; i < nodesCount; i++)
+ {
+ auto& node = data.Skeleton.Nodes[i];
+ node.ParentIndex = mapping[node.ParentIndex];
+ }
+ for (int32 i = 0; i < bonesCount; i++)
+ {
+ auto& bone = data.Skeleton.Bones[i];
+ bone.NodeIndex = mapping[bone.NodeIndex];
+ }
+ }
+ reorder_nodes_and_test_it_out
+ !
#endif
}
else if (options.Type == ModelType::Animation)
@@ -1753,6 +1786,19 @@ bool ModelTool::FindTexture(const String& sourcePath, const String& file, String
return false;
}
+void ModelTool::CalculateBoneOffsetMatrix(const Array& nodes, Matrix& offsetMatrix, int32 nodeIndex)
+{
+ offsetMatrix = Matrix::Identity;
+ int32 idx = nodeIndex;
+ do
+ {
+ const SkeletonNode& node = nodes[idx];
+ offsetMatrix *= node.LocalTransform.GetWorld();
+ idx = node.ParentIndex;
+ } while (idx != -1);
+ offsetMatrix.Invert();
+}
+
#endif
#endif
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h
index 9dc5bf067..36bb9b12c 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.h
+++ b/Source/Engine/Tools/ModelTool/ModelTool.h
@@ -6,11 +6,13 @@
#include "Engine/Core/Config.h"
#include "Engine/Content/Assets/ModelBase.h"
+#include "Engine/Physics/CollisionData.h"
#if USE_EDITOR
#include "Engine/Core/ISerializable.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Graphics/Models/SkeletonData.h"
#include "Engine/Animations/AnimationData.h"
+#include "Engine/Content/Assets/MaterialBase.h"
class JsonWriter;
@@ -262,6 +264,9 @@ public:
// If specified, all meshes which name starts with this prefix will be imported as a separate collision data (excluded used for rendering).
API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
String CollisionMeshesPrefix = TEXT("");
+ // The type of collision that should be generated if has collision prefix specified.
+ API_FIELD(Attributes = "EditorOrder(105), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
+ CollisionDataType CollisionType = CollisionDataType::TriangleMesh;
public: // Transform
@@ -334,11 +339,17 @@ public:
// If checked, the importer will create materials for model meshes as specified in the file.
API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
bool ImportMaterials = true;
+ // If checked, the importer will create the model's materials as instances of a base material.
+ API_FIELD(Attributes = "EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials))")
+ bool ImportMaterialsAsInstances = false;
+ // The material to import the model's materials as an instance of.
+ API_FIELD(Attributes = "EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances))")
+ AssetReference InstanceToImportAs;
// If checked, the importer will import texture files used by the model and any embedded texture resources.
API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
bool ImportTextures = true;
- // If checked, the importer will try to restore the model material slots.
- API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Restore Materials On Reimport\"), VisibleIf(nameof(ShowGeometry))")
+ // If checked, the importer will try to keep the model's current material slots, instead of importing materials from the source file.
+ API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Material Slots on Reimport\"), VisibleIf(nameof(ShowGeometry))")
bool RestoreMaterialsOnReimport = true;
public: // SDF
@@ -419,6 +430,7 @@ public:
}
private:
+ static void CalculateBoneOffsetMatrix(const Array& nodes, Matrix& offsetMatrix, int32 nodeIndex);
#if USE_ASSIMP
static bool ImportDataAssimp(const char* path, ImportedModelData& data, Options& options, String& errorMsg);
#endif
diff --git a/Source/ThirdParty/OpenFBX/ofbx.cpp b/Source/ThirdParty/OpenFBX/ofbx.cpp
index 3723043e9..e60211e3d 100644
--- a/Source/ThirdParty/OpenFBX/ofbx.cpp
+++ b/Source/ThirdParty/OpenFBX/ofbx.cpp
@@ -1567,6 +1567,7 @@ struct Scene : IScene
int getAnimationStackCount() const override { return (int)m_animation_stacks.size(); }
int getMeshCount() const override { return (int)m_meshes.size(); }
float getSceneFrameRate() const override { return m_scene_frame_rate; }
+ const GlobalInfo* getGlobalInfo() const override { return &m_info; }
const GlobalSettings* getGlobalSettings() const override { return &m_settings; }
const Object* const* getAllObjects() const override { return m_all_objects.empty() ? nullptr : &m_all_objects[0]; }
@@ -1629,6 +1630,7 @@ struct Scene : IScene
Element* m_root_element = nullptr;
Root* m_root = nullptr;
float m_scene_frame_rate = -1;
+ GlobalInfo m_info;
GlobalSettings m_settings;
std::unordered_map m_object_map;
std::vector