diff --git a/Content/Shaders/ColorGrading.flax b/Content/Shaders/ColorGrading.flax
index dae561361..ee620d1a2 100644
--- a/Content/Shaders/ColorGrading.flax
+++ b/Content/Shaders/ColorGrading.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ce60152b7076175eb50e07ad9895eacbfb4a9ed1365c1507266b21fcd3339958
-size 10925
+oid sha256:58bf83f2b334cd28a2db8a2c60a17c56f813edc9050a2d006456f8479cd05d13
+size 10629
diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs
index 484efb1d9..fac782cbd 100644
--- a/Source/Editor/Content/Import/ModelImportEntry.cs
+++ b/Source/Editor/Content/Import/ModelImportEntry.cs
@@ -16,6 +16,7 @@ namespace FlaxEngine.Tools
private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab;
private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab;
private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab;
+ private bool ShowRootMotion => ShowAnimation && RootMotion != RootMotionMode.None;
private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals;
private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents;
private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom;
diff --git a/Source/Editor/Content/Proxy/AssetProxy.cs b/Source/Editor/Content/Proxy/AssetProxy.cs
index 3bd23ad03..7dbd178a9 100644
--- a/Source/Editor/Content/Proxy/AssetProxy.cs
+++ b/Source/Editor/Content/Proxy/AssetProxy.cs
@@ -22,6 +22,22 @@ namespace FlaxEditor.Content
///
public abstract string TypeName { get; }
+ ///
+ /// Gets a value indicating whether this instance is virtual Proxy not linked to any asset.
+ ///
+ protected virtual bool IsVirtual { get; }
+
+ ///
+ /// Determines whether [is virtual proxy].
+ ///
+ ///
+ /// true if [is virtual proxy]; otherwise, false.
+ ///
+ public bool IsVirtualProxy()
+ {
+ return IsVirtual && CanExport == false;
+ }
+
///
/// Checks if this proxy supports the given asset type id at the given path.
///
diff --git a/Source/Editor/Cooker/Platform/Windows/WindowsPlatformTools.cpp b/Source/Editor/Cooker/Platform/Windows/WindowsPlatformTools.cpp
index feca5c0a0..447331ac9 100644
--- a/Source/Editor/Cooker/Platform/Windows/WindowsPlatformTools.cpp
+++ b/Source/Editor/Cooker/Platform/Windows/WindowsPlatformTools.cpp
@@ -5,11 +5,463 @@
#include "WindowsPlatformTools.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/Windows/WindowsPlatformSettings.h"
+#include "Engine/Core/Math/Color32.h"
#include "Engine/Core/Config/GameSettings.h"
#include "Editor/Utilities/EditorUtilities.h"
#include "Engine/Graphics/Textures/TextureData.h"
+#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/JsonAsset.h"
+#include
+
+#define MSDOS_SIGNATURE 0x5A4D
+#define PE_SIGNATURE 0x00004550
+#define PE_32BIT_SIGNATURE 0x10B
+#define PE_64BIT_SIGNATURE 0x20B
+#define PE_SECTION_UNINITIALIZED_DATA 0x00000080
+#define PE_IMAGE_DIRECTORY_ENTRY_RESOURCE 2
+#define PE_IMAGE_RT_ICON 3
+
+///
+/// MS-DOS header found at the beginning in a PE format file.
+///
+struct MSDOSHeader
+{
+ uint16 signature;
+ uint16 lastSize;
+ uint16 numBlocks;
+ uint16 numReloc;
+ uint16 hdrSize;
+ uint16 minAlloc;
+ uint16 maxAlloc;
+ uint16 ss;
+ uint16 sp;
+ uint16 checksum;
+ uint16 ip;
+ uint16 cs;
+ uint16 relocPos;
+ uint16 numOverlay;
+ uint16 reserved1[4];
+ uint16 oemId;
+ uint16 oemInfo;
+ uint16 reserved2[10];
+ uint32 lfanew;
+};
+
+///
+/// COFF header found in a PE format file.
+///
+struct COFFHeader
+{
+ uint16 machine;
+ uint16 numSections;
+ uint32 timeDateStamp;
+ uint32 ptrSymbolTable;
+ uint32 numSymbols;
+ uint16 sizeOptHeader;
+ uint16 characteristics;
+};
+
+///
+/// Contains address and size of data areas in a PE image.
+///
+struct PEDataDirectory
+{
+ uint32 virtualAddress;
+ uint32 size;
+};
+
+///
+/// Optional header in a 32-bit PE format file.
+///
+struct PEOptionalHeader32
+{
+ uint16 signature;
+ uint8 majorLinkerVersion;
+ uint8 minorLinkerVersion;
+ uint32 sizeCode;
+ uint32 sizeInitializedData;
+ uint32 sizeUninitializedData;
+ uint32 addressEntryPoint;
+ uint32 baseCode;
+ uint32 baseData;
+ uint32 baseImage;
+ uint32 alignmentSection;
+ uint32 alignmentFile;
+ uint16 majorOSVersion;
+ uint16 minorOSVersion;
+ uint16 majorImageVersion;
+ uint16 minorImageVersion;
+ uint16 majorSubsystemVersion;
+ uint16 minorSubsystemVersion;
+ uint32 reserved;
+ uint32 sizeImage;
+ uint32 sizeHeaders;
+ uint32 checksum;
+ uint16 subsystem;
+ uint16 characteristics;
+ uint32 sizeStackReserve;
+ uint32 sizeStackCommit;
+ uint32 sizeHeapReserve;
+ uint32 sizeHeapCommit;
+ uint32 loaderFlags;
+ uint32 NumRvaAndSizes;
+ PEDataDirectory dataDirectory[16];
+};
+
+///
+/// Optional header in a 64-bit PE format file.
+///
+struct PEOptionalHeader64
+{
+ uint16 signature;
+ uint8 majorLinkerVersion;
+ uint8 minorLinkerVersion;
+ uint32 sizeCode;
+ uint32 sizeInitializedData;
+ uint32 sizeUninitializedData;
+ uint32 addressEntryPoint;
+ uint32 baseCode;
+ uint64 baseImage;
+ uint32 alignmentSection;
+ uint32 alignmentFile;
+ uint16 majorOSVersion;
+ uint16 minorOSVersion;
+ uint16 majorImageVersion;
+ uint16 minorImageVersion;
+ uint16 majorSubsystemVersion;
+ uint16 minorSubsystemVersion;
+ uint32 reserved;
+ uint32 sizeImage;
+ uint32 sizeHeaders;
+ uint32 checksum;
+ uint16 subsystem;
+ uint16 characteristics;
+ uint64 sizeStackReserve;
+ uint64 sizeStackCommit;
+ uint64 sizeHeapReserve;
+ uint64 sizeHeapCommit;
+ uint32 loaderFlags;
+ uint32 NumRvaAndSizes;
+ PEDataDirectory dataDirectory[16];
+};
+
+///
+/// A section header in a PE format file.
+///
+struct PESectionHeader
+{
+ char name[8];
+ uint32 virtualSize;
+ uint32 relativeVirtualAddress;
+ uint32 physicalSize;
+ uint32 physicalAddress;
+ uint8 deprecated[12];
+ uint32 flags;
+};
+
+///
+/// A resource table header within a .rsrc section in a PE format file.
+///
+struct PEImageResourceDirectory
+{
+ uint32 flags;
+ uint32 timeDateStamp;
+ uint16 majorVersion;
+ uint16 minorVersion;
+ uint16 numNamedEntries;
+ uint16 numIdEntries;
+};
+
+///
+/// A single entry in a resource table within a .rsrc section in a PE format file.
+///
+struct PEImageResourceEntry
+{
+ uint32 type;
+ uint32 offsetDirectory : 31;
+ uint32 isDirectory : 1;
+};
+
+///
+/// An entry in a resource table referencing resource data. Found within a .rsrc section in a PE format file.
+///
+struct PEImageResourceEntryData
+{
+ uint32 offsetData;
+ uint32 size;
+ uint32 codePage;
+ uint32 resourceHandle;
+};
+
+///
+/// Header used in icon file format.
+///
+struct IconHeader
+{
+ uint32 size;
+ int32 width;
+ int32 height;
+ uint16 planes;
+ uint16 bitCount;
+ uint32 compression;
+ uint32 sizeImage;
+ int32 xPelsPerMeter;
+ int32 yPelsPerMeter;
+ uint32 clrUsed;
+ uint32 clrImportant;
+};
+
+void UpdateIconData(uint8* iconData, const TextureData* icon)
+{
+ IconHeader* iconHeader = (IconHeader*)iconData;
+ if (iconHeader->size != sizeof(IconHeader) || iconHeader->compression != 0 || iconHeader->planes != 1 || iconHeader->bitCount != 32)
+ {
+ // Unsupported format
+ return;
+ }
+ uint8* iconPixels = iconData + sizeof(IconHeader);
+ const uint32 width = iconHeader->width;
+ const uint32 height = iconHeader->height / 2;
+
+ // Try to pick a proper mip (require the same size)
+ int32 srcPixelsMip = 0;
+ const int32 mipLevels = icon->GetMipLevels();
+ for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
+ {
+ const uint32 iconWidth = Math::Max(1, icon->Width >> mipIndex);
+ const uint32 iconHeight = Math::Max(1, icon->Height >> mipIndex);
+ if (width == iconWidth && height == iconHeight)
+ {
+ srcPixelsMip = mipIndex;
+ break;
+ }
+ }
+ const TextureMipData* srcPixels = icon->GetData(0, srcPixelsMip);
+ const Color32* srcPixelsData = (Color32*)srcPixels->Data.Get();
+ const Int2 srcPixelsSize(Math::Max(1, icon->Width >> srcPixelsMip), Math::Max(1, icon->Height >> srcPixelsMip));
+ const auto sampler = TextureTool::GetSampler(icon->Format);
+ ASSERT_LOW_LAYER(sampler);
+
+ // Write colors
+ uint32* colorData = (uint32*)iconPixels;
+ uint32 idx = 0;
+ for (int32 y = (int32)height - 1; y >= 0; y--)
+ {
+ float v = (float)y / height;
+ for (uint32 x = 0; x < width; x++)
+ {
+ float u = (float)x / width;
+ const Color c = TextureTool::SampleLinear(sampler, Float2(u, v), srcPixelsData, srcPixelsSize, srcPixels->RowPitch);
+ colorData[idx++] = Color32(c).GetAsBGRA();
+ }
+ }
+
+ // Write AND mask
+ uint32 colorDataSize = width * height * sizeof(uint32);
+ uint8* maskData = iconPixels + colorDataSize;
+ uint32 numPackedPixels = width / 8; // One per bit in byte
+ for (int32 y = (int32)height - 1; y >= 0; y--)
+ {
+ uint8 mask = 0;
+ float v = (float)y / height;
+ for (uint32 packedX = 0; packedX < numPackedPixels; packedX++)
+ {
+ for (uint32 pixelIdx = 0; pixelIdx < 8; pixelIdx++)
+ {
+ uint32 x = packedX * 8 + pixelIdx;
+ float u = (float)x / width;
+ const Color c = TextureTool::SampleLinear(sampler, Float2(u, v), srcPixelsData, srcPixelsSize, srcPixels->RowPitch);
+ if (c.A < 0.25f)
+ mask |= 1 << (7 - pixelIdx);
+ }
+ *maskData = mask;
+ maskData++;
+ }
+ }
+}
+
+void SetIconData(PEImageResourceDirectory* base, PEImageResourceDirectory* current, uint8* imageData, uint32 sectionAddress, const TextureData* iconRGBA8)
+{
+ uint32 numEntries = current->numIdEntries; // Not supporting name entries
+ PEImageResourceEntry* entries = (PEImageResourceEntry*)(current + 1);
+ for (uint32 i = 0; i < numEntries; i++)
+ {
+ // Only at root does the type identify resource type
+ if (base == current && entries[i].type != PE_IMAGE_RT_ICON)
+ continue;
+
+ if (entries[i].isDirectory)
+ {
+ PEImageResourceDirectory* child = (PEImageResourceDirectory*)(((uint8*)base) + entries[i].offsetDirectory);
+ SetIconData(base, child, imageData, sectionAddress, iconRGBA8);
+ }
+ else
+ {
+ PEImageResourceEntryData* data = (PEImageResourceEntryData*)(((uint8*)base) + entries[i].offsetDirectory);
+ uint8* iconData = imageData + (data->offsetData - sectionAddress);
+ UpdateIconData(iconData, iconRGBA8);
+ }
+ }
+}
+
+bool UpdateExeIcon(const String& path, const TextureData& icon)
+{
+ if (!FileSystem::FileExists(path))
+ {
+ LOG(Warning, "Missing file");
+ return true;
+ }
+ if (icon.Width < 1 || icon.Height < 1 || icon.GetMipLevels() <= 0)
+ {
+ LOG(Warning, "Inalid icon data");
+ return true;
+ }
+
+ // Ensure that image format can be sampled
+ const TextureData* iconRGBA8 = &icon;
+ TextureData tmpData1;
+ //if (icon.Format != PixelFormat::R8G8B8A8_UNorm)
+ if (TextureTool::GetSampler(icon.Format) == nullptr)
+ {
+ if (TextureTool::Convert(tmpData1, *iconRGBA8, PixelFormat::R8G8B8A8_UNorm))
+ {
+ LOG(Warning, "Failed convert icon data.");
+ return true;
+ }
+ iconRGBA8 = &tmpData1;
+ }
+
+ // Use fixed-size input icon image
+ TextureData tmpData2;
+ if (iconRGBA8->Width != 256 || iconRGBA8->Height != 256)
+ {
+ if (TextureTool::Resize(tmpData2, *iconRGBA8, 256, 256))
+ {
+ LOG(Warning, "Failed resize icon data.");
+ return true;
+ }
+ iconRGBA8 = &tmpData2;
+ }
+
+ // A PE file is structured as such:
+ // - MSDOS Header
+ // - PE Signature
+ // - COFF Header
+ // - PE Optional Header
+ // - One or multiple sections
+ // - .code
+ // - .data
+ // - ...
+ // - .rsrc
+ // - icon/cursor/etc data
+
+ std::fstream stream;
+#if PLATFORM_WINDOWS
+ stream.open(path.Get(), std::ios::in | std::ios::out | std::ios::binary);
+#else
+ StringAsANSI<> pathAnsi(path.Get());
+ stream.open(pathAnsi.Get(), std::ios::in | std::ios::out | std::ios::binary);
+#endif
+ if (!stream.is_open())
+ {
+ LOG(Warning, "Cannot open file");
+ return true;
+ }
+
+ // First check magic number to ensure file is even an executable
+ uint16 magicNum;
+ stream.read((char*)&magicNum, sizeof(magicNum));
+ if (magicNum != MSDOS_SIGNATURE)
+ {
+ LOG(Warning, "Provided file is not a valid executable.");
+ return true;
+ }
+
+ // Read the MSDOS header and skip over it
+ stream.seekg(0);
+ MSDOSHeader msdosHeader;
+ stream.read((char*)&msdosHeader, sizeof(MSDOSHeader));
+
+ // Read PE signature
+ stream.seekg(msdosHeader.lfanew);
+ uint32 peSignature;
+ stream.read((char*)&peSignature, sizeof(peSignature));
+ if (peSignature != PE_SIGNATURE)
+ {
+ LOG(Warning, "Provided file is not in PE format.");
+ return true;
+ }
+
+ // Read COFF header
+ COFFHeader coffHeader;
+ stream.read((char*)&coffHeader, sizeof(COFFHeader));
+ if (coffHeader.sizeOptHeader == 0)
+ {
+ LOG(Warning, "Provided file is not a valid executable.");
+ return true;
+ }
+ uint32 sectionHeadersCount = coffHeader.numSections;
+
+ // Read optional header
+ auto optionalHeaderPos = stream.tellg();
+ uint16 optionalHeaderSignature;
+ stream.read((char*)&optionalHeaderSignature, sizeof(optionalHeaderSignature));
+ PEDataDirectory* dataDirectory = nullptr;
+ stream.seekg(optionalHeaderPos);
+ if (optionalHeaderSignature == PE_32BIT_SIGNATURE)
+ {
+ PEOptionalHeader32 optionalHeader;
+ stream.read((char*)&optionalHeader, sizeof(optionalHeader));
+ dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
+ }
+ else if (optionalHeaderSignature == PE_64BIT_SIGNATURE)
+ {
+ PEOptionalHeader64 optionalHeader;
+ stream.read((char*)&optionalHeader, sizeof(optionalHeader));
+ dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
+ }
+ else
+ {
+ LOG(Warning, "Unrecognized PE format.");
+ return true;
+ }
+
+ // Read section headers
+ auto sectionHeaderPos = optionalHeaderPos + (std::ifstream::pos_type)coffHeader.sizeOptHeader;
+ stream.seekg(sectionHeaderPos);
+ Array sectionHeaders;
+ sectionHeaders.Resize(sectionHeadersCount);
+ stream.read((char*)sectionHeaders.Get(), sizeof(PESectionHeader) * sectionHeadersCount);
+
+ // Look for .rsrc section header
+ for (uint32 i = 0; i < sectionHeadersCount; i++)
+ {
+ PESectionHeader& sectionHeader = sectionHeaders[i];
+ if (sectionHeader.flags & PE_SECTION_UNINITIALIZED_DATA)
+ continue;
+ if (strcmp(sectionHeader.name, ".rsrc") == 0)
+ {
+ uint32 imageSize = sectionHeader.physicalSize;
+ Array imageData;
+ imageData.Resize(imageSize);
+
+ stream.seekg(sectionHeader.physicalAddress);
+ stream.read((char*)imageData.Get(), imageSize);
+
+ uint32 resourceDirOffset = dataDirectory->virtualAddress - sectionHeader.relativeVirtualAddress;
+ PEImageResourceDirectory* resourceDirectory = (PEImageResourceDirectory*)&imageData.Get()[resourceDirOffset];
+
+ SetIconData(resourceDirectory, resourceDirectory, imageData.Get(), sectionHeader.relativeVirtualAddress, iconRGBA8);
+ stream.seekp(sectionHeader.physicalAddress);
+ stream.write((char*)imageData.Get(), imageSize);
+ }
+ }
+
+ stream.close();
+
+ return false;
+}
IMPLEMENT_ENGINE_SETTINGS_GETTER(WindowsPlatformSettings, WindowsPlatform);
@@ -50,7 +502,7 @@ bool WindowsPlatformTools::OnDeployBinaries(CookingData& data)
TextureData iconData;
if (!EditorUtilities::GetApplicationImage(platformSettings->OverrideIcon, iconData))
{
- if (EditorUtilities::UpdateExeIcon(files[0], iconData))
+ if (UpdateExeIcon(files[0], iconData))
{
data.Error(TEXT("Failed to change output executable file icon."));
return true;
diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs
index 242b4d28d..5c3fedd6c 100644
--- a/Source/Editor/CustomEditorWindow.cs
+++ b/Source/Editor/CustomEditorWindow.cs
@@ -1,9 +1,9 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors;
+using FlaxEditor.GUI.Docking;
using FlaxEditor.Windows;
using FlaxEngine.GUI;
-using DockState = FlaxEditor.GUI.Docking.DockState;
namespace FlaxEditor
{
@@ -97,9 +97,12 @@ namespace FlaxEditor
/// Shows the window.
///
/// Initial window state.
- public void Show(DockState state = DockState.Float)
+ /// The panel to dock to, if any.
+ /// Only used if is set. If true the window will be selected after docking it.
+ /// The splitter value to use if toDock is not null. If not specified, a default value will be used.
+ public void Show(DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null)
{
- _win.Show(state);
+ _win.Show(state, toDock, autoSelect, splitterValue);
}
}
}
diff --git a/Source/Editor/CustomEditors/Editors/BindableButtonEditor.cs b/Source/Editor/CustomEditors/Editors/BindableButtonEditor.cs
new file mode 100644
index 000000000..f8323bb49
--- /dev/null
+++ b/Source/Editor/CustomEditors/Editors/BindableButtonEditor.cs
@@ -0,0 +1,96 @@
+using FlaxEditor.GUI;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.CustomEditors.Editors
+{
+ ///
+ /// Base class for custom button editors.
+ /// See , and .
+ ///
+ public class BindableButtonEditor : EnumEditor
+ {
+ private bool _isListeningForInput;
+ private Button _button;
+
+ ///
+ /// Where or not we are currently listening for any input.
+ ///
+ protected bool IsListeningForInput
+ {
+ get => _isListeningForInput;
+ set
+ {
+ _isListeningForInput = value;
+ if (_isListeningForInput)
+ SetupButton();
+ else
+ ResetButton();
+ }
+ }
+
+ ///
+ /// The window this editor is attached to.
+ /// Useful to hook into for key pressed, mouse buttons etc.
+ ///
+ protected Window Window { get; private set; }
+
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ Window = layout.Control.RootWindow.Window;
+
+ var panel = layout.CustomContainer();
+ panel.CustomControl.SlotsHorizontally = 2;
+ panel.CustomControl.SlotsVertically = 1;
+
+ var button = panel.Button("Listen", "Press to listen for input events");
+ _button = button.Button;
+ _button.Clicked += OnButtonClicked;
+ ResetButton();
+
+ var padding = panel.CustomControl.SlotPadding;
+ panel.CustomControl.Height = ComboBox.DefaultHeight + padding.Height;
+
+ base.Initialize(panel);
+ }
+
+ ///
+ protected override void Deinitialize()
+ {
+ _button.Clicked -= OnButtonClicked;
+ base.Deinitialize();
+ }
+
+ private void ResetButton()
+ {
+ _button.Text = "Listen";
+ _button.BorderThickness = 1;
+
+ var style = FlaxEngine.GUI.Style.Current;
+ _button.BorderColor = style.BorderNormal;
+ _button.BorderColorHighlighted = style.BorderHighlighted;
+ _button.BorderColorSelected = style.BorderSelected;
+ }
+
+ private void SetupButton()
+ {
+ _button.Text = "Listening...";
+ _button.BorderThickness = 2;
+
+ var color = FlaxEngine.GUI.Style.Current.ProgressNormal;
+ _button.BorderColor = color;
+ _button.BorderColorHighlighted = color;
+ _button.BorderColorSelected = color;
+ }
+
+ private void OnButtonClicked()
+ {
+ _isListeningForInput = !_isListeningForInput;
+ if (_isListeningForInput)
+ SetupButton();
+ else
+ ResetButton();
+ }
+ }
+}
diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
index 919da4301..00322fc81 100644
--- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections;
using System.Linq;
-using FlaxEditor.Content;
-using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
+using FlaxEditor.GUI.Input;
+using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
@@ -54,8 +54,13 @@ namespace FlaxEditor.CustomEditors.Editors
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
- menu.AddSeparator();
+ menu.ItemsContainer.RemoveChildren();
+ menu.AddButton("Copy", linkedEditor.Copy);
+ var paste = menu.AddButton("Paste", linkedEditor.Paste);
+ paste.Enabled = linkedEditor.CanPaste;
+
+ menu.AddSeparator();
var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked);
moveUpButton.Enabled = Index > 0;
@@ -65,17 +70,100 @@ namespace FlaxEditor.CustomEditors.Editors
menu.AddButton("Remove", OnRemoveClicked);
}
- private void OnMoveUpClicked(ContextMenuButton button)
+ private void OnMoveUpClicked()
{
Editor.Move(Index, Index - 1);
}
- private void OnMoveDownClicked(ContextMenuButton button)
+ private void OnMoveDownClicked()
{
Editor.Move(Index, Index + 1);
}
- private void OnRemoveClicked(ContextMenuButton button)
+ private void OnRemoveClicked()
+ {
+ Editor.Remove(Index);
+ }
+ }
+
+ private class CollectionDropPanel : DropPanel
+ {
+ ///
+ /// The collection editor.
+ ///
+ public CollectionEditor Editor;
+
+ ///
+ /// The index of the item (zero-based).
+ ///
+ public int Index { get; private set; }
+
+ ///
+ /// The linked editor.
+ ///
+ public CustomEditor LinkedEditor;
+
+ private bool _canReorder = true;
+
+ public void Setup(CollectionEditor editor, int index, bool canReorder = true)
+ {
+ HeaderHeight = 18;
+ _canReorder = canReorder;
+ EnableDropDownIcon = true;
+ var icons = FlaxEditor.Editor.Instance.Icons;
+ ArrowImageClosed = new SpriteBrush(icons.ArrowRight12);
+ ArrowImageOpened = new SpriteBrush(icons.ArrowDown12);
+ HeaderText = $"Element {index}";
+ IsClosed = false;
+ Editor = editor;
+ Index = index;
+ Offsets = new Margin(7, 7, 0, 0);
+
+ MouseButtonRightClicked += OnMouseButtonRightClicked;
+ if (_canReorder)
+ {
+ // TODO: Drag drop
+ }
+ }
+
+ private void OnMouseButtonRightClicked(DropPanel panel, Float2 location)
+ {
+ if (LinkedEditor == null)
+ return;
+ var linkedEditor = LinkedEditor;
+ var menu = new ContextMenu();
+
+ menu.AddButton("Copy", linkedEditor.Copy);
+ var paste = menu.AddButton("Paste", linkedEditor.Paste);
+ paste.Enabled = linkedEditor.CanPaste;
+
+ if (_canReorder)
+ {
+ menu.AddSeparator();
+
+ var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked);
+ moveUpButton.Enabled = Index > 0;
+
+ var moveDownButton = menu.AddButton("Move down", OnMoveDownClicked);
+ moveDownButton.Enabled = Index + 1 < Editor.Count;
+ }
+
+ menu.AddButton("Remove", OnRemoveClicked);
+
+ menu.Show(panel, location);
+ }
+
+ private void OnMoveUpClicked()
+ {
+ Editor.Move(Index, Index - 1);
+ }
+
+ private void OnMoveDownClicked()
+ {
+ Editor.Move(Index, Index + 1);
+ }
+
+ private void OnRemoveClicked()
{
Editor.Remove(Index);
}
@@ -86,12 +174,12 @@ namespace FlaxEditor.CustomEditors.Editors
///
protected bool NotNullItems;
- private IntegerValueElement _size;
- private PropertyNameLabel _sizeLabel;
+ private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
private bool _readOnly;
private bool _canReorderItems;
+ private CollectionAttribute.DisplayType _displayType;
///
/// Gets the length of the collection.
@@ -124,12 +212,13 @@ namespace FlaxEditor.CustomEditors.Editors
_readOnly = false;
_canReorderItems = true;
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
+ _displayType = CollectionAttribute.DisplayType.Header;
NotNullItems = false;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
Type overrideEditorType = null;
- float spacing = 10.0f;
+ float spacing = 1.0f;
var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute);
if (collection != null)
{
@@ -140,6 +229,7 @@ namespace FlaxEditor.CustomEditors.Editors
_background = collection.BackgroundColor.Value;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
+ _displayType = collection.Display;
}
var dragArea = layout.CustomContainer();
@@ -172,76 +262,77 @@ namespace FlaxEditor.CustomEditors.Editors
}
// Size
- if (_readOnly || (NotNullItems && size == 0))
+ if (layout.ContainerControl is DropPanel dropPanel)
{
- dragArea.Label("Size", size.ToString());
- }
- else
- {
- var sizeProperty = dragArea.AddPropertyItem("Size");
- _sizeLabel = sizeProperty.Labels.Last();
- _size = sizeProperty.IntegerValue();
- _size.IntValue.MinValue = 0;
- _size.IntValue.MaxValue = ushort.MaxValue;
- _size.IntValue.Value = size;
- _size.IntValue.EditEnd += OnSizeChanged;
+ var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height;
+ var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
+ _sizeBox = new IntValueBox(size)
+ {
+ MinValue = 0,
+ MaxValue = ushort.MaxValue,
+ AnchorPreset = AnchorPresets.TopRight,
+ Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
+ Parent = dropPanel,
+ };
+
+ var label = new Label
+ {
+ Text = "Size",
+ AnchorPreset = AnchorPresets.TopRight,
+ Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height),
+ Parent = dropPanel
+ };
+
+ if (_readOnly || (NotNullItems && size == 0))
+ {
+ _sizeBox.IsReadOnly = true;
+ _sizeBox.Enabled = false;
+ }
+ else
+ {
+ _sizeBox.EditEnd += OnSizeChanged;
+ }
}
// Elements
if (size > 0)
{
var panel = dragArea.VerticalPanel();
+ panel.Panel.Offsets = new Margin(7, 7, 0, 0);
panel.Panel.BackgroundColor = _background;
var elementType = ElementType;
+ bool single = elementType.IsPrimitive ||
+ elementType.Equals(new ScriptType(typeof(string))) ||
+ elementType.IsEnum ||
+ (elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) ||
+ (elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) ||
+ elementType.Equals(new ScriptType(typeof(JsonAsset))) ||
+ elementType.Equals(new ScriptType(typeof(SettingsBase)));
- // Use separate layout cells for each collection items to improve layout updates for them in separation
- var useSharedLayout = elementType.IsPrimitive || elementType.IsEnum;
-
- if (_canReorderItems)
+ for (int i = 0; i < size; i++)
{
- for (int i = 0; i < size; i++)
- {
- if (i != 0 && spacing > 0f)
- {
- if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement)
- {
- if (propertiesListElement.Labels.Count > 0)
- {
- var label = propertiesListElement.Labels[propertiesListElement.Labels.Count - 1];
- var margin = label.Margin;
- margin.Bottom += spacing;
- label.Margin = margin;
- }
- propertiesListElement.Space(spacing);
- }
- else
- {
- panel.Space(spacing);
- }
- }
+ // Apply spacing
+ if (i > 0 && i < size && spacing > 0 && !single)
+ panel.Space(spacing);
- var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
- var property = panel.AddPropertyItem(new CollectionItemLabel(this, i));
- var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel();
- itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
+ var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
+ if (_displayType == CollectionAttribute.DisplayType.Inline || (collection == null && single) || (_displayType == CollectionAttribute.DisplayType.Default && single))
+ {
+ PropertyNameLabel itemLabel;
+ if (_canReorderItems)
+ itemLabel = new CollectionItemLabel(this, i);
+ else
+ itemLabel = new PropertyNameLabel("Element " + i);
+ var property = panel.AddPropertyItem(itemLabel);
+ var itemLayout = (LayoutElementsContainer)property;
+ itemLabel.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
}
- }
- else
- {
- for (int i = 0; i < size; i++)
+ else if (_displayType == CollectionAttribute.DisplayType.Header || (_displayType == CollectionAttribute.DisplayType.Default && !single))
{
- if (i != 0 && spacing > 0f)
- {
- if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement)
- propertiesListElement.Space(spacing);
- else
- panel.Space(spacing);
- }
-
- var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
- var property = panel.AddPropertyItem("Element " + i);
- var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel();
- itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
+ var cdp = panel.CustomContainer();
+ cdp.CustomControl.Setup(this, i, _canReorderItems);
+ var itemLayout = cdp.VerticalPanel();
+ cdp.CustomControl.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
}
}
}
@@ -283,8 +374,7 @@ namespace FlaxEditor.CustomEditors.Editors
///
protected override void Deinitialize()
{
- _size = null;
- _sizeLabel = null;
+ _sizeBox = null;
base.Deinitialize();
}
@@ -311,7 +401,8 @@ namespace FlaxEditor.CustomEditors.Editors
{
if (IsSetBlocked)
return;
- Resize(_size.IntValue.Value);
+
+ Resize(_sizeBox.Value);
}
///
@@ -384,14 +475,14 @@ namespace FlaxEditor.CustomEditors.Editors
return;
// Update reference/default value indicator
- if (_sizeLabel != null)
+ if (_sizeBox != null)
{
var color = Color.Transparent;
if (Values.HasReferenceValue && Values.ReferenceValue is IList referenceValue && referenceValue.Count != Count)
color = FlaxEngine.GUI.Style.Current.BackgroundSelected;
else if (Values.HasDefaultValue && Values.DefaultValue is IList defaultValue && defaultValue.Count != Count)
color = Color.Yellow * 0.8f;
- _sizeLabel.HighlightStripColor = color;
+ _sizeBox.BorderColor = color;
}
// Check if collection has been resized (by UI or from external source)
diff --git a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs
index 73089ff04..774f9b6b3 100644
--- a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs
@@ -7,6 +7,7 @@ using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.GUI.Input;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -149,13 +150,14 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
- private IntegerValueElement _size;
+ private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
private bool _readOnly;
private bool _notNullItems;
private bool _canEditKeys;
private bool _keyEdited;
+ private CollectionAttribute.DisplayType _displayType;
///
/// Gets the length of the collection.
@@ -178,6 +180,7 @@ namespace FlaxEditor.CustomEditors.Editors
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
_readOnly = false;
_notNullItems = false;
+ _displayType = CollectionAttribute.DisplayType.Header;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
@@ -192,20 +195,40 @@ namespace FlaxEditor.CustomEditors.Editors
_background = collection.BackgroundColor.Value;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
+ _displayType = collection.Display;
}
// Size
- if (_readOnly || !_canEditKeys)
+ if (layout.ContainerControl is DropPanel dropPanel)
{
- layout.Label("Size", size.ToString());
- }
- else
- {
- _size = layout.IntegerValue("Size");
- _size.IntValue.MinValue = 0;
- _size.IntValue.MaxValue = _notNullItems ? size : ushort.MaxValue;
- _size.IntValue.Value = size;
- _size.IntValue.EditEnd += OnSizeChanged;
+ var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height;
+ var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
+ _sizeBox = new IntValueBox(size)
+ {
+ MinValue = 0,
+ MaxValue = _notNullItems ? size : ushort.MaxValue,
+ AnchorPreset = AnchorPresets.TopRight,
+ Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
+ Parent = dropPanel,
+ };
+
+ var label = new Label
+ {
+ Text = "Size",
+ AnchorPreset = AnchorPresets.TopRight,
+ Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height),
+ Parent = dropPanel
+ };
+
+ if (_readOnly || !_canEditKeys)
+ {
+ _sizeBox.IsReadOnly = true;
+ _sizeBox.Enabled = false;
+ }
+ else
+ {
+ _sizeBox.EditEnd += OnSizeChanged;
+ }
}
// Elements
@@ -216,29 +239,23 @@ namespace FlaxEditor.CustomEditors.Editors
var keysEnumerable = ((IDictionary)Values[0]).Keys.OfType
public Tab SelectedTab
{
- get => _selectedIndex < 0 || Children.Count <= _selectedIndex ? null : Children[_selectedIndex + 1] as Tab;
+ get => _selectedIndex < 0 || Children.Count <= (_selectedIndex+1) ? null : Children[_selectedIndex + 1] as Tab;
set => SelectedTabIndex = value != null ? Children.IndexOf(value) - 1 : -1;
}
diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs
index 3a9780ed9..e86e1382a 100644
--- a/Source/Editor/GUI/Tree/Tree.cs
+++ b/Source/Editor/GUI/Tree/Tree.cs
@@ -19,7 +19,7 @@ namespace FlaxEditor.GUI.Tree
///
/// The key updates timeout in seconds.
///
- public static float KeyUpdateTimeout = 0.12f;
+ public static float KeyUpdateTimeout = 0.25f;
///
/// Delegate for selected tree nodes collection change.
@@ -113,7 +113,7 @@ namespace FlaxEditor.GUI.Tree
AutoFocus = false;
_supportMultiSelect = supportMultiSelect;
- _keyUpdateTime = KeyUpdateTimeout * 10;
+ _keyUpdateTime = KeyUpdateTimeout;
}
internal void OnRightClickInternal(TreeNode node, ref Float2 location)
@@ -347,10 +347,12 @@ namespace FlaxEditor.GUI.Tree
if (ContainsFocus && node != null && node.AutoFocus)
{
var window = Root;
+ if (window.GetKeyDown(KeyboardKeys.ArrowUp) || window.GetKeyDown(KeyboardKeys.ArrowDown))
+ _keyUpdateTime = KeyUpdateTimeout;
if (_keyUpdateTime >= KeyUpdateTimeout && window is WindowRootControl windowRoot && windowRoot.Window.IsFocused)
{
- bool keyUpArrow = window.GetKeyDown(KeyboardKeys.ArrowUp);
- bool keyDownArrow = window.GetKeyDown(KeyboardKeys.ArrowDown);
+ bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
+ bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
// Check if arrow flags are different
if (keyDownArrow != keyUpArrow)
diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs
index 80f419a6f..fa023bf7b 100644
--- a/Source/Editor/Modules/ContentDatabaseModule.cs
+++ b/Source/Editor/Modules/ContentDatabaseModule.cs
@@ -196,6 +196,25 @@ namespace FlaxEditor.Modules
return null;
}
+ ///
+ /// Gets the virtual proxy object from given path.
+ ///
use case if the asset u trying to display is not a flax asset but u like to add custom functionality
+ ///
to context menu,or display it the asset
+ ///
+ /// The asset path.
+ /// Asset proxy or null if cannot find.
+ public AssetProxy GetAssetVirtuallProxy(string path)
+ {
+ for (int i = 0; i < Proxy.Count; i++)
+ {
+ if (Proxy[i] is AssetProxy proxy && proxy.IsVirtualProxy() && path.EndsWith(proxy.FileExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ return proxy;
+ }
+ }
+
+ return null;
+ }
///
/// Refreshes the given item folder. Tries to find new content items and remove not existing ones.
@@ -996,7 +1015,14 @@ namespace FlaxEditor.Modules
item = proxy?.ConstructItem(path, assetInfo.TypeName, ref assetInfo.ID);
}
if (item == null)
- item = new FileItem(path);
+ {
+ var proxy = GetAssetVirtuallProxy(path);
+ item = proxy?.ConstructItem(path, assetInfo.TypeName, ref assetInfo.ID);
+ if (item == null)
+ {
+ item = new FileItem(path);
+ }
+ }
// Link
item.ParentFolder = parent.Folder;
diff --git a/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs b/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs
index a73c3d2ee..648ec74e3 100644
--- a/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs
+++ b/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs
@@ -45,6 +45,21 @@ namespace FlaxEditor.Modules.SourceCodeEditing
_checkAssembly = checkAssembly;
}
+ ///
+ /// Gets the type matching the certain Script.
+ ///
+ /// The content item.
+ /// The type matching that item, or null if not found.
+ public ScriptType Get(Content.ScriptItem script)
+ {
+ foreach (var type in Get())
+ {
+ if (type.ContentItem == script)
+ return type;
+ }
+ return ScriptType.Null;
+ }
+
///
/// Gets all the types from the all loaded assemblies (including project scripts and scripts from the plugins).
///
diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs
index 245fd4d5d..cf2715b48 100644
--- a/Source/Editor/Modules/WindowsModule.cs
+++ b/Source/Editor/Modules/WindowsModule.cs
@@ -36,6 +36,22 @@ namespace FlaxEditor.Modules
{
public string AssemblyName;
public string TypeName;
+
+ public DockState DockState;
+ public DockPanel DockedTo;
+ public float? SplitterValue = null;
+
+ public bool SelectOnShow = false;
+
+ public bool Maximize;
+ public bool Minimize;
+ public Float2 FloatSize;
+ public Float2 FloatPosition;
+
+ // Constructor, to allow for default values
+ public WindowRestoreData()
+ {
+ }
}
private readonly List _restoreWindows = new List();
@@ -709,9 +725,7 @@ namespace FlaxEditor.Modules
for (int i = 0; i < Windows.Count; i++)
{
if (string.Equals(Windows[i].SerializationTypename, typename, StringComparison.OrdinalIgnoreCase))
- {
return Windows[i];
- }
}
// Check if it's an asset ID
@@ -802,10 +816,38 @@ namespace FlaxEditor.Modules
if (constructor == null || type.IsGenericType)
return;
- WindowRestoreData winData;
+ var winData = new WindowRestoreData();
+ var panel = win.Window.ParentDockPanel;
+
+ // Ensure that this window is only selected following recompilation
+ // if it was the active tab in its dock panel. Otherwise, there is a
+ // risk of interrupting the user's workflow by potentially selecting
+ // background tabs.
+ winData.SelectOnShow = panel.SelectedTab == win.Window;
+ if (panel is FloatWindowDockPanel)
+ {
+ winData.DockState = DockState.Float;
+ var window = win.Window.RootWindow.Window;
+ winData.FloatPosition = window.Position;
+ winData.FloatSize = window.ClientSize;
+ winData.Maximize = window.IsMaximized;
+ winData.Minimize = window.IsMinimized;
+ }
+ else
+ {
+ if (panel.TabsCount > 1)
+ {
+ winData.DockState = DockState.DockFill;
+ winData.DockedTo = panel;
+ }else
+ {
+ winData.DockState = panel.TryGetDockState(out var splitterValue);
+ winData.DockedTo = panel.ParentDockPanel;
+ winData.SplitterValue = splitterValue;
+ }
+ }
winData.AssemblyName = type.Assembly.GetName().Name;
winData.TypeName = type.FullName;
- // TODO: cache and restore docking info
_restoreWindows.Add(winData);
}
@@ -824,7 +866,24 @@ namespace FlaxEditor.Modules
if (type != null)
{
var win = (CustomEditorWindow)Activator.CreateInstance(type);
- win.Show();
+ win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue);
+ if (winData.DockState == DockState.Float)
+ {
+ var window = win.Window.RootWindow.Window;
+ window.Position = winData.FloatPosition;
+ if (winData.Maximize)
+ {
+ window.Maximize();
+ }
+ else if (winData.Minimize)
+ {
+ window.Minimize();
+ }
+ else
+ {
+ window.ClientSize = winData.FloatSize;
+ }
+ }
}
}
}
diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
index 9cfe5b7bd..bf39a501b 100644
--- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
+++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
@@ -29,6 +29,7 @@ namespace FlaxEditor.SceneGraph.GUI
private DragScripts _dragScripts;
private DragAssets _dragAssets;
private DragActorType _dragActorType;
+ private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
private List _highlights;
private bool _hasSearchFilter;
@@ -395,6 +396,13 @@ namespace FlaxEditor.SceneGraph.GUI
}
if (_dragActorType.OnDragEnter(data))
return _dragActorType.Effect;
+ if (_dragScriptItems == null)
+ {
+ _dragScriptItems = new DragScriptItems(ValidateDragScriptItem);
+ _dragHandlers.Add(_dragScriptItems);
+ }
+ if (_dragScriptItems.OnDragEnter(data))
+ return _dragScriptItems.Effect;
return DragDropEffect.None;
}
@@ -673,7 +681,34 @@ namespace FlaxEditor.SceneGraph.GUI
actor.Transform = Actor.Transform;
ActorNode.Root.Spawn(actor, Actor);
}
+ result = DragDropEffect.Move;
+ }
+ // Drag script item
+ else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag)
+ {
+ var spawnParent = myActor;
+ if (DragOverMode == DragItemPositioning.Above || DragOverMode == DragItemPositioning.Below)
+ spawnParent = newParent;
+ for (int i = 0; i < _dragScriptItems.Objects.Count; i++)
+ {
+ var item = _dragScriptItems.Objects[i];
+ var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
+ if (actorType != ScriptType.Null)
+ {
+ var actor = actorType.CreateInstance() as Actor;
+ if (actor == null)
+ {
+ Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName);
+ continue;
+ }
+ actor.StaticFlags = spawnParent.StaticFlags;
+ actor.Name = actorType.Name;
+ actor.Transform = spawnParent.Transform;
+ ActorNode.Root.Spawn(actor, spawnParent);
+ actor.OrderInParent = newOrder;
+ }
+ }
result = DragDropEffect.Move;
}
@@ -728,6 +763,11 @@ namespace FlaxEditor.SceneGraph.GUI
return true;
}
+ private static bool ValidateDragScriptItem(ScriptItem script)
+ {
+ return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
+ }
+
///
protected override void DoDragDrop()
{
@@ -768,6 +808,7 @@ namespace FlaxEditor.SceneGraph.GUI
_dragScripts = null;
_dragAssets = null;
_dragActorType = null;
+ _dragScriptItems = null;
_dragHandlers?.Clear();
_dragHandlers = null;
_highlights = null;
diff --git a/Source/Editor/Surface/AnimGraphSurface.cs b/Source/Editor/Surface/AnimGraphSurface.cs
index d7746662f..56764bafd 100644
--- a/Source/Editor/Surface/AnimGraphSurface.cs
+++ b/Source/Editor/Surface/AnimGraphSurface.cs
@@ -113,6 +113,40 @@ namespace FlaxEditor.Surface
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
}
+ internal AnimGraphTraceEvent[] LastTraceEvents;
+
+ internal unsafe bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent)
+ {
+ if (LastTraceEvents != null)
+ {
+ foreach (var e in LastTraceEvents)
+ {
+ // Node IDs must match
+ if (e.NodeId == node.ID)
+ {
+ uint* nodePath = e.NodePath0;
+
+ // Get size of the path
+ int nodePathSize = 0;
+ while (nodePathSize < 8 && nodePath[nodePathSize] != 0)
+ nodePathSize++;
+
+ // Follow input node contexts path to verify if it matches with the path in the event
+ var c = node.Context;
+ for (int i = nodePathSize - 1; i >= 0 && c != null; i--)
+ c = c.OwnerNodeID == nodePath[i] ? c.Parent : null;
+ if (c != null)
+ {
+ traceEvent = e;
+ return true;
+ }
+ }
+ }
+ }
+ traceEvent = default;
+ return false;
+ }
+
private static SurfaceStyle CreateStyle()
{
var editor = Editor.Instance;
@@ -383,6 +417,7 @@ namespace FlaxEditor.Surface
}
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
_nodesCache.Wait();
+ LastTraceEvents = null;
base.OnDestroy();
}
diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs
index 9d4367303..bb4bc724d 100644
--- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs
@@ -191,9 +191,7 @@ namespace FlaxEditor.Surface.Archetypes
var value = title;
int count = 1;
while (!OnRenameValidate(null, value))
- {
value = title + " " + count++;
- }
Values[0] = value;
Title = value;
@@ -655,6 +653,7 @@ namespace FlaxEditor.Surface.Archetypes
protected Rectangle _renameButtonRect;
private bool _cursorChanged = false;
private bool _textRectHovered = false;
+ private bool _debugActive;
///
/// The transitions list from this state to the others.
@@ -1092,6 +1091,16 @@ namespace FlaxEditor.Surface.Archetypes
// TODO: maybe update only on actual transitions change?
UpdateTransitions();
+
+ // Debug current state
+ if (((AnimGraphSurface)Surface).TryGetTraceEvent(this, out var traceEvent))
+ {
+ _debugActive = true;
+ }
+ else
+ {
+ _debugActive = false;
+ }
}
///
@@ -1132,6 +1141,10 @@ namespace FlaxEditor.Surface.Archetypes
// Close button
Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey);
+
+ // Debug outline
+ if (_debugActive)
+ Render2D.DrawRectangle(_textRect.MakeExpanded(1.0f), style.ProgressNormal);
}
///
@@ -1583,14 +1596,24 @@ namespace FlaxEditor.Surface.Archetypes
None = 0,
///
- /// Transition rule will be rechecked during active transition with option to interrupt transition.
+ /// Transition rule will be rechecked during active transition with option to interrupt transition (to go back to the source state).
///
RuleRechecking = 1,
///
- /// Interrupted transition is immediately stopped without blending out.
+ /// Interrupted transition is immediately stopped without blending out (back to the source/destination state).
///
Instant = 2,
+
+ ///
+ /// Enables checking other transitions in the source state that might interrupt this one.
+ ///
+ SourceState = 4,
+
+ ///
+ /// Enables checking transitions in the destination state that might interrupt this one.
+ ///
+ DestinationState = 8,
}
///
@@ -1613,6 +1636,8 @@ namespace FlaxEditor.Surface.Archetypes
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
+ InterruptionSourceState = 32,
+ InterruptionDestinationState = 64,
}
///
@@ -1773,7 +1798,7 @@ namespace FlaxEditor.Surface.Archetypes
}
///
- /// Transition interruption options.
+ /// Transition interruption options (flags, can select multiple values).
///
[EditorOrder(70), DefaultValue(InterruptionFlags.None)]
public InterruptionFlags Interruption
@@ -1785,12 +1810,18 @@ namespace FlaxEditor.Surface.Archetypes
flags |= InterruptionFlags.RuleRechecking;
if (_data.HasFlag(Data.FlagTypes.InterruptionInstant))
flags |= InterruptionFlags.Instant;
+ if (_data.HasFlag(Data.FlagTypes.InterruptionSourceState))
+ flags |= InterruptionFlags.SourceState;
+ if (_data.HasFlag(Data.FlagTypes.InterruptionDestinationState))
+ flags |= InterruptionFlags.DestinationState;
return flags;
}
set
{
_data.SetFlag(Data.FlagTypes.InterruptionRuleRechecking, value.HasFlag(InterruptionFlags.RuleRechecking));
_data.SetFlag(Data.FlagTypes.InterruptionInstant, value.HasFlag(InterruptionFlags.Instant));
+ _data.SetFlag(Data.FlagTypes.InterruptionSourceState, value.HasFlag(InterruptionFlags.SourceState));
+ _data.SetFlag(Data.FlagTypes.InterruptionDestinationState, value.HasFlag(InterruptionFlags.DestinationState));
SourceState.SaveTransitions(true);
}
}
diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs
index 446883406..cbe9c3d85 100644
--- a/Source/Editor/Surface/Archetypes/Animation.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.cs
@@ -28,6 +28,51 @@ namespace FlaxEditor.Surface.Archetypes
}
}
+ ///
+ /// Customized for Blend with Mask node.
+ ///
+ public class SkeletonMaskSample : SurfaceNode
+ {
+ private AssetSelect _assetSelect;
+ private Box _assetBox;
+
+ ///
+ public SkeletonMaskSample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ }
+
+ ///
+ public override void OnSurfaceLoaded(SurfaceNodeActions action)
+ {
+ base.OnSurfaceLoaded(action);
+
+ if (Surface != null)
+ {
+ _assetSelect = GetChild();
+
+ // 4 is the id of skeleton mask parameter node.
+ if (TryGetBox(4, out var box))
+ {
+ _assetBox = box;
+ _assetSelect.Visible = !_assetBox.HasAnyConnection;
+ }
+ }
+ }
+
+ ///
+ public override void ConnectionTick(Box box)
+ {
+ base.ConnectionTick(box);
+
+ if (_assetBox == null)
+ return;
+ if (box.ID != _assetBox.ID)
+ return;
+ _assetSelect.Visible = !box.HasAnyConnection;
+ }
+ }
+
///
/// Customized for the animation sampling nodes
///
@@ -36,6 +81,7 @@ namespace FlaxEditor.Surface.Archetypes
{
private AssetSelect _assetSelect;
private Box _assetBox;
+ private ProgressBar _playbackPos;
///
public Sample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
@@ -75,7 +121,7 @@ namespace FlaxEditor.Surface.Archetypes
Title = _assetBox.HasAnyConnection || asset == null ? "Animation" : asset.ShortName;
else
Title = asset?.ShortName ?? "Animation";
-
+
var style = Style.Current;
Resize(Mathf.Max(230, style.FontLarge.MeasureText(Title).X + 30), 160);
}
@@ -93,6 +139,36 @@ namespace FlaxEditor.Surface.Archetypes
_assetSelect.Visible = !box.HasAnyConnection;
UpdateTitle();
}
+
+ ///
+ public override void Update(float deltaTime)
+ {
+ // Debug current playback position
+ if (((AnimGraphSurface)Surface).TryGetTraceEvent(this, out var traceEvent) && traceEvent.Asset is FlaxEngine.Animation anim)
+ {
+ if (_playbackPos == null)
+ {
+ _playbackPos = new ProgressBar
+ {
+ SmoothingScale = 0.0f,
+ Offsets = Margin.Zero,
+ AnchorPreset = AnchorPresets.HorizontalStretchBottom,
+ Parent = this,
+ Height = 12.0f,
+ };
+ _playbackPos.Y -= 16.0f;
+ }
+ _playbackPos.Visible = true;
+ _playbackPos.Maximum = anim.Duration;
+ _playbackPos.Value = traceEvent.Value; // AnimGraph reports position in animation frames, not time
+ }
+ else if (_playbackPos != null)
+ {
+ _playbackPos.Visible = false;
+ }
+
+ base.Update(deltaTime);
+ }
}
///
@@ -493,7 +569,7 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 10,
Title = "Blend Additive",
- Description =
+ Description =
"Blend animation poses (with additive mode)" +
"\n" +
"\nNote: " +
@@ -521,6 +597,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 11,
Title = "Blend with Mask",
Description = "Blend animation poses using skeleton mask",
+ Create = (id, context, arch, groupArch) => new SkeletonMaskSample(id, context, arch, groupArch),
Flags = NodeFlags.AnimGraph,
Size = new Float2(180, 140),
DefaultValues = new object[]
@@ -534,7 +611,8 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Input(0, "Pose A", true, typeof(void), 1),
NodeElementArchetype.Factory.Input(1, "Pose B", true, typeof(void), 2),
NodeElementArchetype.Factory.Input(2, "Alpha", true, typeof(float), 3, 0),
- NodeElementArchetype.Factory.Asset(0, 70, 1, typeof(SkeletonMask)),
+ NodeElementArchetype.Factory.Input(3, "Skeleton Mask Asset", true, typeof(SkeletonMask), 4),
+ NodeElementArchetype.Factory.Asset(0, Surface.Constants.LayoutOffsetY * 4, 1, typeof(SkeletonMask)),
}
},
new NodeArchetype
diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs
index 53950dad2..e8b92d121 100644
--- a/Source/Editor/Surface/Archetypes/Function.cs
+++ b/Source/Editor/Surface/Archetypes/Function.cs
@@ -763,6 +763,17 @@ namespace FlaxEditor.Surface.Archetypes
public string Name;
public ScriptType Type;
public bool IsOut;
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ if (IsOut)
+ sb.Append("out ");
+ sb.Append(Type.ToString());
+ sb.Append(" ");
+ sb.Append(Name);
+ return sb.ToString();
+ }
}
private struct SignatureInfo
@@ -892,7 +903,7 @@ namespace FlaxEditor.Surface.Archetypes
{
ref var param = ref signature.Params[i];
ref var paramMember = ref memberParameters[i];
- if (param.Type != paramMember.Type || param.IsOut != paramMember.IsOut)
+ if (!SurfaceUtils.AreScriptTypesEqual(param.Type, paramMember.Type) || param.IsOut != paramMember.IsOut)
{
// Special case: param.Type is serialized as just a type while paramMember.Type might be a reference for output parameters (eg. `out Int32` vs `out Int32&`)
var paramMemberTypeName = paramMember.Type.TypeName;
@@ -1660,7 +1671,7 @@ namespace FlaxEditor.Surface.Archetypes
SaveSignature();
// Check if return type has been changed
- if (_signature.ReturnType != prevReturnType)
+ if (!SurfaceUtils.AreScriptTypesEqual(_signature.ReturnType, prevReturnType))
{
// Update all return nodes used by this function to match the new type
var usedNodes = DepthFirstTraversal(false);
@@ -2158,7 +2169,7 @@ namespace FlaxEditor.Surface.Archetypes
return false;
for (int i = 0; i < _signature.Length; i++)
{
- if (_signature[i].Type != sig.Parameters[i].Type)
+ if (!SurfaceUtils.AreScriptTypesEqual(_signature[i].Type, sig.Parameters[i].Type))
return false;
}
return true;
diff --git a/Source/Editor/Surface/MaterialSurface.cs b/Source/Editor/Surface/MaterialSurface.cs
index ba7f2e01e..a7fa6cb38 100644
--- a/Source/Editor/Surface/MaterialSurface.cs
+++ b/Source/Editor/Surface/MaterialSurface.cs
@@ -105,7 +105,7 @@ namespace FlaxEditor.Surface
if (node != null)
{
- args.SurfaceLocation.X += node.Width + 10;
+ args.SurfaceLocation.Y += node.Height + 10;
}
}
diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs
index 5f4c3ef07..2c8448e92 100644
--- a/Source/Editor/Surface/SurfaceUtils.cs
+++ b/Source/Editor/Surface/SurfaceUtils.cs
@@ -532,5 +532,24 @@ namespace FlaxEditor.Surface
value = new Double4(i);
return value;
}
+
+ private static bool AreScriptTypesEqualInner(ScriptType left, ScriptType right)
+ {
+ // Special case for Vector types that use typedefs and might overlap
+ if (left.Type == typeof(Vector2) && (right.Type == typeof(Float2) || right.Type == typeof(Double2)))
+ return true;
+ if (left.Type == typeof(Vector3) && (right.Type == typeof(Float3) || right.Type == typeof(Double3)))
+ return true;
+ if (left.Type == typeof(Vector4) && (right.Type == typeof(Float4) || right.Type == typeof(Double4)))
+ return true;
+ return false;
+ }
+
+ internal static bool AreScriptTypesEqual(ScriptType left, ScriptType right)
+ {
+ if (left == right)
+ return true;
+ return AreScriptTypesEqualInner(left, right) || AreScriptTypesEqualInner(right, left);
+ }
}
}
diff --git a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs
index 72b9242a4..07118228e 100644
--- a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs
+++ b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs
@@ -24,6 +24,8 @@ namespace FlaxEditor.Surface.Undo
public ConnectBoxesAction(InputBox iB, OutputBox oB, bool connect)
{
+ if (iB == null || oB == null || iB.ParentNode == null || oB.ParentNode == null)
+ throw new System.ArgumentNullException();
_surface = iB.Surface;
_context = new ContextHandle(iB.ParentNode.Context);
_connect = connect;
diff --git a/Source/Editor/Surface/VisjectSurface.Context.cs b/Source/Editor/Surface/VisjectSurface.Context.cs
index 1d8b97729..0cb8e3ead 100644
--- a/Source/Editor/Surface/VisjectSurface.Context.cs
+++ b/Source/Editor/Surface/VisjectSurface.Context.cs
@@ -33,6 +33,30 @@ namespace FlaxEditor.Surface
///
public event Action ContextChanged;
+ ///
+ /// Finds the surface context with the given owning nodes IDs path.
+ ///
+ /// The node ids path.
+ /// Found context or null if cannot.
+ public VisjectSurfaceContext FindContext(Span nodePath)
+ {
+ // Get size of the path
+ int nodePathSize = 0;
+ while (nodePathSize < nodePath.Length && nodePath[nodePathSize] != 0)
+ nodePathSize++;
+
+ // Follow each context path to verify if it matches with the path in the input path
+ foreach (var e in _contextCache)
+ {
+ var c = e.Value;
+ for (int i = nodePathSize - 1; i >= 0 && c != null; i--)
+ c = c.OwnerNodeID == nodePath[i] ? c.Parent : null;
+ if (c != null)
+ return e.Value;
+ }
+ return null;
+ }
+
///
/// Creates the Visject surface context for the given surface data source context.
///
@@ -62,6 +86,8 @@ namespace FlaxEditor.Surface
surfaceContext = CreateContext(_context, context);
_context?.Children.Add(surfaceContext);
_contextCache.Add(contextHandle, surfaceContext);
+ if (context is SurfaceNode asNode)
+ surfaceContext.OwnerNodeID = asNode.ID;
context.OnContextCreated(surfaceContext);
diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs
index 0886996b6..9902f224a 100644
--- a/Source/Editor/Surface/VisjectSurfaceContext.cs
+++ b/Source/Editor/Surface/VisjectSurfaceContext.cs
@@ -156,6 +156,11 @@ namespace FlaxEditor.Surface
///
public event Action ControlDeleted;
+ ///
+ /// Identifier of the node that 'owns' this context (eg. State Machine which created this graph of state nodes).
+ ///
+ public uint OwnerNodeID;
+
///
/// Initializes a new instance of the class.
///
diff --git a/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs b/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
index 610a336b7..bf80eb34d 100644
--- a/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
+++ b/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs
@@ -18,7 +18,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public float TargetHeight = 0.0f;
///
- public override unsafe void Apply(ref ApplyParams p)
+ public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// If used with invert mode pick the target height level
if (p.Options.Invert)
diff --git a/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs b/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
index d4c10f00e..3d75a56f2 100644
--- a/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
+++ b/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs
@@ -26,7 +26,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
}
///
- public override unsafe void Apply(ref ApplyParams p)
+ public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
var strength = p.Strength * -10.0f;
var brushPosition = p.Gizmo.CursorPosition;
diff --git a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs
index 9a42d2ae8..cce2f982f 100644
--- a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs
+++ b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEngine;
@@ -50,18 +51,20 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public virtual bool EditHoles => false;
///
- /// Applies the modification to the terrain.
+ /// Gets all patches that will be affected by the brush
///
/// The brush.
/// The options.
/// The gizmo.
/// The terrain.
- public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
+ public virtual unsafe List GetAffectedPatches(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
{
+ List affectedPatches = new();
+
// Combine final apply strength
float strength = Strength * options.Strength * options.DeltaTime;
if (strength <= 0.0f)
- return;
+ return affectedPatches;
if (options.Invert && SupportsNegativeApply)
strength *= -1;
@@ -72,20 +75,10 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
- ApplyParams p = new ApplyParams
- {
- Terrain = terrain,
- Brush = brush,
- Gizmo = gizmo,
- Options = options,
- Strength = strength,
- HeightmapSize = heightmapSize,
- TempBuffer = tempBuffer,
- };
// Get brush bounds in terrain local space
var brushBounds = gizmo.CursorBrushBounds;
- terrain.GetLocalToWorldMatrix(out p.TerrainWorld);
+ terrain.GetLocalToWorldMatrix(out var terrainWorld);
terrain.GetWorldToLocalMatrix(out var terrainInvWorld);
BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal);
@@ -131,26 +124,78 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
if (sourceHeights == null && sourceHoles == null)
throw new Exception("Cannot modify terrain. Loading heightmap failed. See log for more info.");
- // Record patch data before editing it
- if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
+ ApplyParams p = new ApplyParams
{
- gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord);
- }
+ Terrain = terrain,
+ TerrainWorld = terrainWorld,
+ Brush = brush,
+ Gizmo = gizmo,
+ Options = options,
+ Strength = strength,
+ HeightmapSize = heightmapSize,
+ TempBuffer = tempBuffer,
+ ModifiedOffset = modifiedOffset,
+ ModifiedSize = modifiedSize,
+ PatchCoord = patch.PatchCoord,
+ PatchPositionLocal = patchPositionLocal,
+ SourceHeightMap = sourceHeights,
+ SourceHolesMask = sourceHoles,
+ };
- // Apply modification
- p.ModifiedOffset = modifiedOffset;
- p.ModifiedSize = modifiedSize;
- p.PatchCoord = patch.PatchCoord;
- p.PatchPositionLocal = patchPositionLocal;
- p.SourceHeightMap = sourceHeights;
- p.SourceHolesMask = sourceHoles;
- Apply(ref p);
+ affectedPatches.Add(p);
}
+ return affectedPatches;
+ }
+
+ ///
+ /// Applies the modification to the terrain.
+ ///
+ /// The brush.
+ /// The options.
+ /// The gizmo.
+ /// The terrain.
+ public void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
+ {
+ var affectedPatches = GetAffectedPatches(brush, ref options, gizmo, terrain);
+
+ if (affectedPatches.Count == 0)
+ {
+ return;
+ }
+
+ ApplyBrush(gizmo, affectedPatches);
+
// Auto NavMesh rebuild
+ var brushBounds = gizmo.CursorBrushBounds;
gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds);
}
+ ///
+ /// Applies the brush to all affected patches
+ ///
+ ///
+ ///
+ public virtual void ApplyBrush(SculptTerrainGizmoMode gizmo, List affectedPatches)
+ {
+ for (int i = 0; i < affectedPatches.Count; i++)
+ {
+ ApplyParams patchApplyParams = affectedPatches[i];
+
+ // Record patch data before editing it
+ if (!gizmo.CurrentEditUndoAction.HashPatch(ref patchApplyParams.PatchCoord))
+ {
+ gizmo.CurrentEditUndoAction.AddPatch(ref patchApplyParams.PatchCoord);
+ }
+
+ ApplyBrushToPatch(ref patchApplyParams);
+
+ // Auto NavMesh rebuild
+ var brushBounds = gizmo.CursorBrushBounds;
+ gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds);
+ }
+ }
+
///
/// The mode apply parameters.
///
@@ -231,6 +276,6 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
/// Applies the modification to the terrain.
///
/// The parameters to use.
- public abstract void Apply(ref ApplyParams p);
+ public abstract void ApplyBrushToPatch(ref ApplyParams p);
}
}
diff --git a/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs b/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
index 32d02bb3f..7fcb09288 100644
--- a/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
+++ b/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs
@@ -29,7 +29,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public override bool SupportsNegativeApply => true;
///
- public override unsafe void Apply(ref ApplyParams p)
+ public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// Prepare
var brushPosition = p.Gizmo.CursorPosition;
diff --git a/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs b/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
index c3409f30c..381b39ebd 100644
--- a/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
+++ b/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs
@@ -15,7 +15,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public override bool SupportsNegativeApply => true;
///
- public override unsafe void Apply(ref ApplyParams p)
+ public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
var strength = p.Strength * 1000.0f;
var brushPosition = p.Gizmo.CursorPosition;
diff --git a/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs b/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
index d23eae8dd..387bb555e 100644
--- a/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
+++ b/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs
@@ -1,7 +1,8 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
-using System;
+using System.Collections.Generic;
using FlaxEngine;
+using System;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
@@ -19,43 +20,109 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public float FilterRadius = 0.4f;
///
- public override unsafe void Apply(ref ApplyParams p)
+ public override unsafe void ApplyBrush(SculptTerrainGizmoMode gizmo, List affectedPatches)
{
- // Prepare
- var brushPosition = p.Gizmo.CursorPosition;
- var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * p.Brush.Size), 2);
- var max = p.HeightmapSize - 1;
- var strength = Mathf.Saturate(p.Strength);
-
- // Apply brush modification
Profiler.BeginEvent("Apply Brush");
- for (int z = 0; z < p.ModifiedSize.Y; z++)
+
+ // TODO: don't need these on each patch; just need them once
+ var heightmapSize = affectedPatches[0].HeightmapSize;
+ var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * affectedPatches[0].Brush.Size), 2);
+
+ // Calculate bounding coordinates of the total affected area
+ Int2 modifieedAreaMinCoord = Int2.Maximum;
+ Int2 modifiedAreaMaxCoord = Int2.Minimum;
+ for (int i = 0; i < affectedPatches.Count; i++)
{
- var zz = z + p.ModifiedOffset.Y;
- for (int x = 0; x < p.ModifiedSize.X; x++)
+ var patch = affectedPatches[i];
+
+ var tl = (patch.PatchCoord * (heightmapSize - 1)) + patch.ModifiedOffset;
+ var br = tl + patch.ModifiedSize;
+
+ if (tl.X <= modifieedAreaMinCoord.X && tl.Y <= modifieedAreaMinCoord.Y)
+ modifieedAreaMinCoord = tl;
+ if (br.X >= modifiedAreaMaxCoord.X && br.Y >= modifiedAreaMaxCoord.Y)
+ modifiedAreaMaxCoord = br;
+ }
+ var totalModifiedSize = modifiedAreaMaxCoord - modifieedAreaMinCoord;
+
+ // Build map of heights in affected area
+ var modifiedHeights = new float[totalModifiedSize.X * totalModifiedSize.Y];
+ for (int i = 0; i < affectedPatches.Count; i++)
+ {
+ var patch = affectedPatches[i];
+
+ for (int z = 0; z < patch.ModifiedSize.Y; z++)
{
- var xx = x + p.ModifiedOffset.X;
- var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
-
- var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
- Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
-
- var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
-
- if (paintAmount > 0)
+ for (int x = 0; x < patch.ModifiedSize.X; x++)
{
+ // Read height from current patch
+ var localCoordX = (x + patch.ModifiedOffset.X);
+ var localCoordY = (z + patch.ModifiedOffset.Y);
+ var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX];
+
+ // Calculate the absolute coordinate of the terrain point
+ var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z);
+ var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord;
+
+ // Store height
+ var index = (currentPointCoordRelativeToModifiedArea.Y * totalModifiedSize.X) + currentPointCoordRelativeToModifiedArea.X;
+ modifiedHeights[index] = coordHeight;
+ }
+ }
+ }
+
+ // Iterate through modified points and smooth now that we have height information for all necessary points
+ for (int i = 0; i < affectedPatches.Count; i++)
+ {
+ var patch = affectedPatches[i];
+ var brushPosition = patch.Gizmo.CursorPosition;
+ var strength = Mathf.Saturate(patch.Strength);
+
+ for (int z = 0; z < patch.ModifiedSize.Y; z++)
+ {
+ for (int x = 0; x < patch.ModifiedSize.X; x++)
+ {
+ // Read height from current patch
+ var localCoordX = (x + patch.ModifiedOffset.X);
+ var localCoordY = (z + patch.ModifiedOffset.Y);
+ var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX];
+
+ // Calculate the absolute coordinate of the terrain point
+ var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z);
+ var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord;
+
+ // Calculate brush influence at the current position
+ var samplePositionLocal = patch.PatchPositionLocal + new Vector3(localCoordX * FlaxEngine.Terrain.UnitsPerVertex, coordHeight, localCoordY * FlaxEngine.Terrain.UnitsPerVertex);
+ Vector3.Transform(ref samplePositionLocal, ref patch.TerrainWorld, out Vector3 samplePositionWorld);
+ var paintAmount = patch.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
+
+ if (paintAmount == 0)
+ {
+ patch.TempBuffer[z * patch.ModifiedSize.X + x] = coordHeight;
+ continue;
+ }
+
+ // Record patch data before editing it
+ if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
+ {
+ gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord);
+ }
+
// Sum the nearby values
float smoothValue = 0;
int smoothValueSamples = 0;
- int minX = Math.Max(x - radius + p.ModifiedOffset.X, 0);
- int minZ = Math.Max(z - radius + p.ModifiedOffset.Y, 0);
- int maxX = Math.Min(x + radius + p.ModifiedOffset.X, max);
- int maxZ = Math.Min(z + radius + p.ModifiedOffset.Y, max);
+
+ var minX = Math.Max(0, currentPointCoordRelativeToModifiedArea.X - radius);
+ var maxX = Math.Min(totalModifiedSize.X - 1, currentPointCoordRelativeToModifiedArea.X + radius);
+ var minZ = Math.Max(0, currentPointCoordRelativeToModifiedArea.Y - radius);
+ var maxZ = Math.Min(totalModifiedSize.Y - 1, currentPointCoordRelativeToModifiedArea.Y + radius);
+
for (int dz = minZ; dz <= maxZ; dz++)
{
for (int dx = minX; dx <= maxX; dx++)
{
- var height = p.SourceHeightMap[dz * p.HeightmapSize + dx];
+ var coordIndex = (dz * totalModifiedSize.X) + dx;
+ var height = modifiedHeights[coordIndex];
smoothValue += height;
smoothValueSamples++;
}
@@ -65,18 +132,26 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
smoothValue /= smoothValueSamples;
// Blend between the height and smooth value
- p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, smoothValue, paintAmount);
- }
- else
- {
- p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight;
+ var newHeight = Mathf.Lerp(coordHeight, smoothValue, paintAmount);
+ patch.TempBuffer[z * patch.ModifiedSize.X + x] = newHeight;
}
}
- }
- Profiler.EndEvent();
- // Update terrain patch
- TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
+ // Update terrain patch
+ TerrainTools.ModifyHeightMap(patch.Terrain, ref patch.PatchCoord, patch.TempBuffer, ref patch.ModifiedOffset, ref patch.ModifiedSize);
+ }
+
+ // Auto NavMesh rebuild
+ var brushBounds = gizmo.CursorBrushBounds;
+ gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds);
+
+ Profiler.EndEvent();
+ }
+
+ ///
+ public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
+ {
+ // noop; unused
}
}
}
diff --git a/Source/Editor/Utilities/EditorUtilities.cpp b/Source/Editor/Utilities/EditorUtilities.cpp
index a2418ceb1..a4000a324 100644
--- a/Source/Editor/Utilities/EditorUtilities.cpp
+++ b/Source/Editor/Utilities/EditorUtilities.cpp
@@ -9,7 +9,6 @@
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
-#include "Engine/Core/Math/Color32.h"
#include "Engine/Core/Config/GameSettings.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/AssetReference.h"
@@ -20,497 +19,6 @@
#endif
#include
-#define MSDOS_SIGNATURE 0x5A4D
-#define PE_SIGNATURE 0x00004550
-#define PE_32BIT_SIGNATURE 0x10B
-#define PE_64BIT_SIGNATURE 0x20B
-#define PE_NUM_DIRECTORY_ENTRIES 16
-#define PE_SECTION_UNINITIALIZED_DATA 0x00000080
-#define PE_IMAGE_DIRECTORY_ENTRY_RESOURCE 2
-#define PE_IMAGE_RT_ICON 3
-
-///
-/// MS-DOS header found at the beginning in a PE format file.
-///
-struct MSDOSHeader
-{
- uint16 signature;
- uint16 lastSize;
- uint16 numBlocks;
- uint16 numReloc;
- uint16 hdrSize;
- uint16 minAlloc;
- uint16 maxAlloc;
- uint16 ss;
- uint16 sp;
- uint16 checksum;
- uint16 ip;
- uint16 cs;
- uint16 relocPos;
- uint16 numOverlay;
- uint16 reserved1[4];
- uint16 oemId;
- uint16 oemInfo;
- uint16 reserved2[10];
- uint32 lfanew;
-};
-
-///
-/// COFF header found in a PE format file.
-///
-struct COFFHeader
-{
- uint16 machine;
- uint16 numSections;
- uint32 timeDateStamp;
- uint32 ptrSymbolTable;
- uint32 numSymbols;
- uint16 sizeOptHeader;
- uint16 characteristics;
-};
-
-///
-/// Contains address and size of data areas in a PE image.
-///
-struct PEDataDirectory
-{
- uint32 virtualAddress;
- uint32 size;
-};
-
-///
-/// Optional header in a 32-bit PE format file.
-///
-struct PEOptionalHeader32
-{
- uint16 signature;
- uint8 majorLinkerVersion;
- uint8 minorLinkerVersion;
- uint32 sizeCode;
- uint32 sizeInitializedData;
- uint32 sizeUninitializedData;
- uint32 addressEntryPoint;
- uint32 baseCode;
- uint32 baseData;
- uint32 baseImage;
- uint32 alignmentSection;
- uint32 alignmentFile;
- uint16 majorOSVersion;
- uint16 minorOSVersion;
- uint16 majorImageVersion;
- uint16 minorImageVersion;
- uint16 majorSubsystemVersion;
- uint16 minorSubsystemVersion;
- uint32 reserved;
- uint32 sizeImage;
- uint32 sizeHeaders;
- uint32 checksum;
- uint16 subsystem;
- uint16 characteristics;
- uint32 sizeStackReserve;
- uint32 sizeStackCommit;
- uint32 sizeHeapReserve;
- uint32 sizeHeapCommit;
- uint32 loaderFlags;
- uint32 NumRvaAndSizes;
- PEDataDirectory dataDirectory[16];
-};
-
-///
-/// Optional header in a 64-bit PE format file.
-///
-struct PEOptionalHeader64
-{
- uint16 signature;
- uint8 majorLinkerVersion;
- uint8 minorLinkerVersion;
- uint32 sizeCode;
- uint32 sizeInitializedData;
- uint32 sizeUninitializedData;
- uint32 addressEntryPoint;
- uint32 baseCode;
- uint64 baseImage;
- uint32 alignmentSection;
- uint32 alignmentFile;
- uint16 majorOSVersion;
- uint16 minorOSVersion;
- uint16 majorImageVersion;
- uint16 minorImageVersion;
- uint16 majorSubsystemVersion;
- uint16 minorSubsystemVersion;
- uint32 reserved;
- uint32 sizeImage;
- uint32 sizeHeaders;
- uint32 checksum;
- uint16 subsystem;
- uint16 characteristics;
- uint64 sizeStackReserve;
- uint64 sizeStackCommit;
- uint64 sizeHeapReserve;
- uint64 sizeHeapCommit;
- uint32 loaderFlags;
- uint32 NumRvaAndSizes;
- PEDataDirectory dataDirectory[16];
-};
-
-///
-/// A section header in a PE format file.
-///
-struct PESectionHeader
-{
- char name[8];
- uint32 virtualSize;
- uint32 relativeVirtualAddress;
- uint32 physicalSize;
- uint32 physicalAddress;
- uint8 deprecated[12];
- uint32 flags;
-};
-
-///
-/// A resource table header within a .rsrc section in a PE format file.
-///
-struct PEImageResourceDirectory
-{
- uint32 flags;
- uint32 timeDateStamp;
- uint16 majorVersion;
- uint16 minorVersion;
- uint16 numNamedEntries;
- uint16 numIdEntries;
-};
-
-///
-/// A single entry in a resource table within a .rsrc section in a PE format file.
-///
-struct PEImageResourceEntry
-{
- uint32 type;
- uint32 offsetDirectory : 31;
- uint32 isDirectory : 1;
-};
-
-///
-/// An entry in a resource table referencing resource data. Found within a .rsrc section in a PE format file.
-///
-struct PEImageResourceEntryData
-{
- uint32 offsetData;
- uint32 size;
- uint32 codePage;
- uint32 resourceHandle;
-};
-
-///
-/// Header used in icon file format.
-///
-struct IconHeader
-{
- uint32 size;
- int32 width;
- int32 height;
- uint16 planes;
- uint16 bitCount;
- uint32 compression;
- uint32 sizeImage;
- int32 xPelsPerMeter;
- int32 yPelsPerMeter;
- uint32 clrUsed;
- uint32 clrImportant;
-};
-
-void UpdateIconData(uint8* iconData, const TextureData* icon)
-{
- IconHeader* iconHeader = (IconHeader*)iconData;
-
- if (iconHeader->size != sizeof(IconHeader) || iconHeader->compression != 0 || iconHeader->planes != 1 || iconHeader->bitCount != 32)
- {
- // Unsupported format
- return;
- }
-
- uint8* iconPixels = iconData + sizeof(IconHeader);
- uint32 width = iconHeader->width;
- uint32 height = iconHeader->height / 2;
-
- // Check if can use mip from texture data or sample different mip
- uint32 iconTexSize;
- if (width != height)
- {
- // Only square icons are supported
- return;
- }
- if (Math::IsPowerOfTwo(width))
- {
- // Use mip
- iconTexSize = width;
- }
- else
- {
- // Use resized mip
- iconTexSize = Math::RoundUpToPowerOf2(width);
- }
-
- // Try to pick a proper mip (require the same size)
- const TextureMipData* srcPixels = nullptr;
- int32 mipLevels = icon->GetMipLevels();
- for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
- {
- uint32 iconWidth = Math::Max(1, icon->Width >> mipIndex);
- uint32 iconHeight = Math::Max(1, icon->Height >> mipIndex);
-
- if (iconTexSize == iconWidth && iconTexSize == iconHeight)
- {
- srcPixels = icon->GetData(0, mipIndex);
- break;
- }
- }
- if (srcPixels == nullptr)
- {
- // No icon of this size provided
- return;
- }
-
- // Write colors
- uint32* colorData = (uint32*)iconPixels;
-
- uint32 idx = 0;
- for (int32 y = (int32)height - 1; y >= 0; y--)
- {
- float v = (float)y / height;
- for (uint32 x = 0; x < width; x++)
- {
- float u = (float)x / width;
-
- int32 i = (int32)(v * iconTexSize) * srcPixels->RowPitch + (int32)(u * iconTexSize) * sizeof(Color32);
- colorData[idx++] = ((Color32*)&srcPixels->Data.Get()[i])->GetAsBGRA();
- }
- }
-
- // Write AND mask
- uint32 colorDataSize = width * height * sizeof(uint32);
- uint8* maskData = iconPixels + colorDataSize;
-
- // One per bit in byte
- uint32 numPackedPixels = width / 8;
-
- for (int32 y = (int32)height - 1; y >= 0; y--)
- {
- uint8 mask = 0;
- float v = (float)y / height;
- for (uint32 packedX = 0; packedX < numPackedPixels; packedX++)
- {
- for (uint32 pixelIdx = 0; pixelIdx < 8; pixelIdx++)
- {
- uint32 x = packedX * 8 + pixelIdx;
- float u = (float)x / width;
- int32 i = (int32)(v * iconTexSize) * srcPixels->RowPitch + (int32)(u * iconTexSize) * sizeof(Color32);
- Color32 color = *((Color32*)&srcPixels->Data.Get()[i]);
- if (color.A < 64)
- mask |= 1 << (7 - pixelIdx);
- }
-
- *maskData = mask;
- maskData++;
- }
- }
-}
-
-void SetIconData(PEImageResourceDirectory* base, PEImageResourceDirectory* current, uint8* imageData, uint32 sectionAddress, const TextureData* iconRGBA8)
-{
- uint32 numEntries = current->numIdEntries; // Not supporting name entries
- PEImageResourceEntry* entries = (PEImageResourceEntry*)(current + 1);
-
- for (uint32 i = 0; i < numEntries; i++)
- {
- // Only at root does the type identify resource type
- if (base == current && entries[i].type != PE_IMAGE_RT_ICON)
- continue;
-
- if (entries[i].isDirectory)
- {
- PEImageResourceDirectory* child = (PEImageResourceDirectory*)(((uint8*)base) + entries[i].offsetDirectory);
- SetIconData(base, child, imageData, sectionAddress, iconRGBA8);
- }
- else
- {
- PEImageResourceEntryData* data = (PEImageResourceEntryData*)(((uint8*)base) + entries[i].offsetDirectory);
-
- uint8* iconData = imageData + (data->offsetData - sectionAddress);
- UpdateIconData(iconData, iconRGBA8);
- }
- }
-}
-
-bool EditorUtilities::UpdateExeIcon(const String& path, const TextureData& icon)
-{
- // Validate input
- if (!FileSystem::FileExists(path))
- {
- LOG(Warning, "Missing file");
- return true;
- }
- if (icon.Width < 1 || icon.Height < 1 || icon.GetMipLevels() <= 0)
- {
- LOG(Warning, "Inalid icon data");
- return true;
- }
-
- // Convert to RGBA8 format if need to
- const TextureData* iconRGBA8 = &icon;
- TextureData tmpData1;
- if (icon.Format != PixelFormat::R8G8B8A8_UNorm)
- {
- if (TextureTool::Convert(tmpData1, *iconRGBA8, PixelFormat::R8G8B8A8_UNorm))
- {
- LOG(Warning, "Failed convert icon data.");
- return true;
- }
- iconRGBA8 = &tmpData1;
- }
-
- // Resize if need to
- TextureData tmpData2;
- if (iconRGBA8->Width > 256 || iconRGBA8->Height > 256)
- {
- if (TextureTool::Resize(tmpData2, *iconRGBA8, 256, 256))
- {
- LOG(Warning, "Failed resize icon data.");
- return true;
- }
- iconRGBA8 = &tmpData2;
- }
-
- // A PE file is structured as such:
- // - MSDOS Header
- // - PE Signature
- // - COFF Header
- // - PE Optional Header
- // - One or multiple sections
- // - .code
- // - .data
- // - ...
- // - .rsrc
- // - icon/cursor/etc data
-
- std::fstream stream;
-#if PLATFORM_WINDOWS
- stream.open(path.Get(), std::ios::in | std::ios::out | std::ios::binary);
-#else
- StringAsANSI<> pathAnsi(path.Get());
- stream.open(pathAnsi.Get(), std::ios::in | std::ios::out | std::ios::binary);
-#endif
- if (!stream.is_open())
- {
- LOG(Warning, "Cannot open file");
- return true;
- }
-
- // First check magic number to ensure file is even an executable
- uint16 magicNum;
- stream.read((char*)&magicNum, sizeof(magicNum));
- if (magicNum != MSDOS_SIGNATURE)
- {
- LOG(Warning, "Provided file is not a valid executable.");
- return true;
- }
-
- // Read the MSDOS header and skip over it
- stream.seekg(0);
-
- MSDOSHeader msdosHeader;
- stream.read((char*)&msdosHeader, sizeof(MSDOSHeader));
-
- // Read PE signature
- stream.seekg(msdosHeader.lfanew);
-
- uint32 peSignature;
- stream.read((char*)&peSignature, sizeof(peSignature));
-
- if (peSignature != PE_SIGNATURE)
- {
- LOG(Warning, "Provided file is not in PE format.");
- return true;
- }
-
- // Read COFF header
- COFFHeader coffHeader;
- stream.read((char*)&coffHeader, sizeof(COFFHeader));
-
- // .exe files always have an optional header
- if (coffHeader.sizeOptHeader == 0)
- {
- LOG(Warning, "Provided file is not a valid executable.");
- return true;
- }
-
- uint32 numSectionHeaders = coffHeader.numSections;
-
- // Read optional header
- auto optionalHeaderPos = stream.tellg();
-
- uint16 optionalHeaderSignature;
- stream.read((char*)&optionalHeaderSignature, sizeof(optionalHeaderSignature));
-
- PEDataDirectory* dataDirectory = nullptr;
- stream.seekg(optionalHeaderPos);
- if (optionalHeaderSignature == PE_32BIT_SIGNATURE)
- {
- PEOptionalHeader32 optionalHeader;
- stream.read((char*)&optionalHeader, sizeof(optionalHeader));
-
- dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
- }
- else if (optionalHeaderSignature == PE_64BIT_SIGNATURE)
- {
- PEOptionalHeader64 optionalHeader;
- stream.read((char*)&optionalHeader, sizeof(optionalHeader));
-
- dataDirectory = optionalHeader.dataDirectory + PE_IMAGE_DIRECTORY_ENTRY_RESOURCE;
- }
- else
- {
- LOG(Warning, "Unrecognized PE format.");
- return true;
- }
-
- // Read section headers
- auto sectionHeaderPos = optionalHeaderPos + (std::ifstream::pos_type)coffHeader.sizeOptHeader;
- stream.seekg(sectionHeaderPos);
-
- Array sectionHeaders;
- sectionHeaders.Resize(numSectionHeaders);
- stream.read((char*)sectionHeaders.Get(), sizeof(PESectionHeader) * numSectionHeaders);
-
- // Look for .rsrc section header
- for (uint32 i = 0; i < numSectionHeaders; i++)
- {
- if (sectionHeaders[i].flags & PE_SECTION_UNINITIALIZED_DATA)
- continue;
-
- if (strcmp(sectionHeaders[i].name, ".rsrc") == 0)
- {
- uint32 imageSize = sectionHeaders[i].physicalSize;
- Array imageData;
- imageData.Resize(imageSize);
-
- stream.seekg(sectionHeaders[i].physicalAddress);
- stream.read((char*)imageData.Get(), imageSize);
-
- uint32 resourceDirOffset = dataDirectory->virtualAddress - sectionHeaders[i].relativeVirtualAddress;
- PEImageResourceDirectory* resourceDirectory = (PEImageResourceDirectory*)&imageData.Get()[resourceDirOffset];
-
- SetIconData(resourceDirectory, resourceDirectory, imageData.Get(), sectionHeaders[i].relativeVirtualAddress, iconRGBA8);
- stream.seekp(sectionHeaders[i].physicalAddress);
- stream.write((char*)imageData.Get(), imageSize);
- }
- }
-
- stream.close();
-
- return false;
-}
-
bool EditorUtilities::FormatAppPackageName(String& packageName)
{
const auto gameSettings = GameSettings::Get();
diff --git a/Source/Editor/Utilities/EditorUtilities.h b/Source/Editor/Utilities/EditorUtilities.h
index 881bfe3b8..10e09cee1 100644
--- a/Source/Editor/Utilities/EditorUtilities.h
+++ b/Source/Editor/Utilities/EditorUtilities.h
@@ -22,14 +22,6 @@ public:
SplashScreen,
};
- ///
- /// Updates the Win32 executable file icon.
- ///
- /// The exe path.
- /// The icon image data.
- /// True if fails, otherwise false.
- static bool UpdateExeIcon(const String& path, const TextureData& icon);
-
static bool FormatAppPackageName(String& packageName);
static bool GetApplicationImage(const Guid& imageId, TextureData& imageData, ApplicationImageType type = ApplicationImageType::Icon);
static bool GetTexture(const Guid& textureId, TextureData& textureData);
diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs
index 7184391d5..3d790ff81 100644
--- a/Source/Editor/Utilities/Utils.cs
+++ b/Source/Editor/Utilities/Utils.cs
@@ -1310,8 +1310,17 @@ namespace FlaxEditor.Utilities
inputActions.Add(options => options.BuildSDF, Editor.Instance.BuildAllMeshesSDF);
inputActions.Add(options => options.TakeScreenshot, Editor.Instance.Windows.TakeScreenshot);
inputActions.Add(options => options.ProfilerWindow, () => Editor.Instance.Windows.ProfilerWin.FocusOrShow());
- inputActions.Add(options => options.ProfilerStartStop, () => { Editor.Instance.Windows.ProfilerWin.LiveRecording = !Editor.Instance.Windows.ProfilerWin.LiveRecording; Editor.Instance.UI.AddStatusMessage($"Profiling {(Editor.Instance.Windows.ProfilerWin.LiveRecording ? "started" : "stopped")}."); });
- inputActions.Add(options => options.ProfilerClear, () => { Editor.Instance.Windows.ProfilerWin.Clear(); Editor.Instance.UI.AddStatusMessage($"Profiling results cleared."); });
+ inputActions.Add(options => options.ProfilerStartStop, () =>
+ {
+ bool recording = !Editor.Instance.Windows.ProfilerWin.LiveRecording;
+ Editor.Instance.Windows.ProfilerWin.LiveRecording = recording;
+ Editor.Instance.UI.AddStatusMessage($"Profiling {(recording ? "started" : "stopped")}.");
+ });
+ inputActions.Add(options => options.ProfilerClear, () =>
+ {
+ Editor.Instance.Windows.ProfilerWin.Clear();
+ Editor.Instance.UI.AddStatusMessage($"Profiling results cleared.");
+ });
inputActions.Add(options => options.SaveScenes, () => Editor.Instance.Scene.SaveScenes());
inputActions.Add(options => options.CloseScenes, () => Editor.Instance.Scene.CloseAllScenes());
inputActions.Add(options => options.OpenScriptsProject, () => Editor.Instance.CodeEditing.OpenSolution());
diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs
index 9ac9b5571..526a84c45 100644
--- a/Source/Editor/Viewport/EditorViewport.cs
+++ b/Source/Editor/Viewport/EditorViewport.cs
@@ -1113,7 +1113,7 @@ namespace FlaxEditor.Viewport
private void OnFarPlaneChanged(FloatValueBox control)
{
_farPlane = control.Value;
- _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _farPlane.ToString());
+ _editor.ProjectCache.SetCustomData("CameraFarPlaneValue", _farPlane.ToString());
}
///
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index 24c8f1c4c..ed6bd4915 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
@@ -194,7 +195,7 @@ namespace FlaxEditor.Viewport
: base(Object.New(), editor.Undo, editor.Scene.Root)
{
_editor = editor;
- DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType);
+ DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
var inputOptions = editor.Options.Options.Input;
// Prepare rendering task
@@ -940,6 +941,11 @@ namespace FlaxEditor.Viewport
return Level.IsAnySceneLoaded;
}
+ private static bool ValidateDragScriptItem(ScriptItem script)
+ {
+ return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
+ }
+
///
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs
index 6e111f588..6c1509de8 100644
--- a/Source/Editor/Viewport/PrefabWindowViewport.cs
+++ b/Source/Editor/Viewport/PrefabWindowViewport.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
@@ -81,7 +82,7 @@ namespace FlaxEditor.Viewport
_window.SelectionChanged += OnSelectionChanged;
Undo = window.Undo;
ViewportCamera = new FPSCamera();
- DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType);
+ DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
ShowDebugDraw = true;
ShowEditorPrimitives = true;
Gizmos = new GizmosCollection(this);
@@ -702,6 +703,11 @@ namespace FlaxEditor.Viewport
return true;
}
+ private static bool ValidateDragScriptItem(ScriptItem script)
+ {
+ return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
+ }
+
///
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs
index 8a1b4f183..bf4ec7979 100644
--- a/Source/Editor/Viewport/ViewportDraggingHelper.cs
+++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs
@@ -39,17 +39,19 @@ namespace FlaxEditor.Viewport
private readonly EditorViewport _viewport;
private readonly DragAssets _dragAssets;
private readonly DragActorType _dragActorType;
+ private readonly DragScriptItems _dragScriptItem;
private StaticModel _previewStaticModel;
private int _previewModelEntryIndex;
private BrushSurface _previewBrushSurface;
- internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func validateAsset, Func validateDragActorType)
+ internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func validateAsset, Func validateDragActorType, Func validateDragScriptItem)
{
_owner = owner;
_viewport = viewport;
Add(_dragAssets = new DragAssets(validateAsset));
Add(_dragActorType = new DragActorType(validateDragActorType));
+ Add(_dragScriptItem = new DragScriptItems(validateDragScriptItem));
}
internal void ClearDragEffects()
@@ -102,7 +104,12 @@ namespace FlaxEditor.Viewport
foreach (var actorType in _dragActorType.Objects)
Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal);
}
-
+ else if (_dragScriptItem.HasValidDrag)
+ {
+ result = _dragScriptItem.Effect;
+ foreach (var scripItem in _dragScriptItem.Objects)
+ Spawn(scripItem, hit, ref location, ref hitLocation, ref hitNormal);
+ }
OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation });
return result;
@@ -193,6 +200,15 @@ namespace FlaxEditor.Viewport
_viewport.Focus();
}
+ private void Spawn(ScriptItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
+ {
+ var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
+ if (actorType != ScriptType.Null)
+ {
+ Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal);
+ }
+ }
+
private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
{
var actor = item.CreateInstance() as Actor;
diff --git a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
index 12eac22b5..93eea95e1 100644
--- a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
@@ -154,10 +154,11 @@ namespace FlaxEditor.Windows.Assets
}
[StructLayout(LayoutKind.Sequential)]
- private struct AnimGraphDebugFlowInfo
+ private unsafe struct AnimGraphDebugFlowInfo
{
public uint NodeId;
public int BoxId;
+ public fixed uint NodePath[8];
}
private FlaxObjectRefPickerControl _debugPicker;
@@ -252,25 +253,26 @@ namespace FlaxEditor.Windows.Assets
return obj is AnimatedModel player && player.AnimationGraph == OriginalAsset;
}
- private void OnDebugFlow(Asset asset, Object obj, uint nodeId, uint boxId)
+ private unsafe void OnDebugFlow(Animations.DebugFlowInfo flowInfo)
{
// Filter the flow
if (_debugPicker.Value != null)
{
- if (asset != OriginalAsset || _debugPicker.Value != obj)
+ if (flowInfo.Asset != OriginalAsset || _debugPicker.Value != flowInfo.Instance)
return;
}
else
{
- if (asset != Asset || _preview.PreviewActor != obj)
+ if (flowInfo.Asset != Asset || _preview.PreviewActor != flowInfo.Instance)
return;
}
// Register flow to show it in UI on a surface
- var flowInfo = new AnimGraphDebugFlowInfo { NodeId = nodeId, BoxId = (int)boxId };
+ var flow = new AnimGraphDebugFlowInfo { NodeId = flowInfo.NodeId, BoxId = (int)flowInfo.BoxId };
+ Utils.MemoryCopy(new IntPtr(flow.NodePath), new IntPtr(flowInfo.NodePath0), sizeof(uint) * 8ul);
lock (_debugFlows)
{
- _debugFlows.Add(flowInfo);
+ _debugFlows.Add(flow);
}
}
@@ -394,8 +396,18 @@ namespace FlaxEditor.Windows.Assets
}
///
- public override void OnUpdate()
+ public override unsafe void OnUpdate()
{
+ // Extract animations playback state from the events tracing
+ var debugActor = _debugPicker.Value as AnimatedModel;
+ if (debugActor == null)
+ debugActor = _preview.PreviewActor;
+ if (debugActor != null)
+ {
+ debugActor.EnableTracing = true;
+ Surface.LastTraceEvents = debugActor.TraceEvents;
+ }
+
base.OnUpdate();
// Update graph execution flow debugging visualization
@@ -403,7 +415,8 @@ namespace FlaxEditor.Windows.Assets
{
foreach (var debugFlow in _debugFlows)
{
- var node = Surface.Context.FindNode(debugFlow.NodeId);
+ var context = Surface.FindContext(new Span(debugFlow.NodePath, 8));
+ var node = context?.FindNode(debugFlow.NodeId);
var box = node?.GetBox(debugFlow.BoxId);
box?.HighlightConnections();
}
@@ -416,6 +429,8 @@ namespace FlaxEditor.Windows.Assets
///
public override void OnDestroy()
{
+ if (IsDisposing)
+ return;
Animations.DebugFlow -= OnDebugFlow;
_properties = null;
diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs
index 8f88d93f6..c512d287f 100644
--- a/Source/Editor/Windows/Assets/AnimationWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationWindow.cs
@@ -48,7 +48,7 @@ namespace FlaxEditor.Windows.Assets
// Use virtual animation graph to playback the animation
_animGraph = FlaxEngine.Content.CreateVirtualAsset();
- _animGraph.InitAsAnimation(model, _window.Asset);
+ _animGraph.InitAsAnimation(model, _window.Asset, true, true);
PreviewActor.AnimationGraph = _animGraph;
}
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
index f263d4734..9418e7b75 100644
--- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
+++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
@@ -64,6 +65,7 @@ namespace FlaxEditor.Windows.Assets
private PrefabWindow _window;
private DragAssets _dragAssets;
private DragActorType _dragActorType;
+ private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
public SceneTreePanel(PrefabWindow window)
@@ -84,6 +86,11 @@ namespace FlaxEditor.Windows.Assets
return true;
}
+ private static bool ValidateDragScriptItem(ScriptItem script)
+ {
+ return Editor.Instance.CodeEditing.Actors.Get(script);
+ }
+
///
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
@@ -106,6 +113,13 @@ namespace FlaxEditor.Windows.Assets
}
if (_dragActorType.OnDragEnter(data))
return _dragActorType.Effect;
+ if (_dragScriptItems == null)
+ {
+ _dragScriptItems = new DragScriptItems(ValidateDragScriptItem);
+ _dragHandlers.Add(_dragScriptItems);
+ }
+ if (_dragScriptItems.OnDragEnter(data))
+ return _dragScriptItems.Effect;
}
return result;
}
@@ -162,7 +176,27 @@ namespace FlaxEditor.Windows.Assets
}
result = DragDropEffect.Move;
}
-
+ // Drag script item
+ else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag)
+ {
+ for (int i = 0; i < _dragScriptItems.Objects.Count; i++)
+ {
+ var item = _dragScriptItems.Objects[i];
+ var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
+ if (actorType != ScriptType.Null)
+ {
+ var actor = actorType.CreateInstance() as Actor;
+ if (actor == null)
+ {
+ Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName);
+ continue;
+ }
+ actor.Name = actorType.Name;
+ _window.Spawn(actor);
+ }
+ }
+ result = DragDropEffect.Move;
+ }
_dragHandlers.OnDragDrop(null);
}
return result;
@@ -173,6 +207,7 @@ namespace FlaxEditor.Windows.Assets
_window = null;
_dragAssets = null;
_dragActorType = null;
+ _dragScriptItems = null;
_dragHandlers?.Clear();
_dragHandlers = null;
diff --git a/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs b/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs
index 7d09620d5..a564144c6 100644
--- a/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs
+++ b/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs
@@ -83,14 +83,14 @@ namespace FlaxEditor.Windows.Assets
set => Sprite.Name = value;
}
- [EditorOrder(1), Limit(-4096, 4096)]
+ [EditorOrder(1)]
public Float2 Location
{
get => Sprite.Location;
set => Sprite.Location = value;
}
- [EditorOrder(3), Limit(0, 4096)]
+ [EditorOrder(3), Limit(0)]
public Float2 Size
{
get => Sprite.Size;
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index d43174cf3..68aa03678 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -799,7 +799,10 @@ namespace FlaxEditor.Windows
if (proxy == null)
throw new ArgumentNullException(nameof(proxy));
+ // Setup name
string name = initialName ?? proxy.NewItemName;
+ if (!proxy.IsFileNameValid(name) || Utilities.Utils.HasInvalidPathChar(name))
+ name = proxy.NewItemName;
// If the proxy can not be created in the current folder, then navigate to the content folder
if (!proxy.CanCreate(CurrentViewFolder))
diff --git a/Source/Editor/Windows/EditorWindow.cs b/Source/Editor/Windows/EditorWindow.cs
index ff0673c11..df70a0b33 100644
--- a/Source/Editor/Windows/EditorWindow.cs
+++ b/Source/Editor/Windows/EditorWindow.cs
@@ -226,6 +226,8 @@ namespace FlaxEditor.Windows
///
public override void OnDestroy()
{
+ if (IsDisposing)
+ return;
OnExit();
// Unregister
diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs
index eb04f07b8..01e356187 100644
--- a/Source/Editor/Windows/PluginsWindow.cs
+++ b/Source/Editor/Windows/PluginsWindow.cs
@@ -95,6 +95,7 @@ namespace FlaxEditor.Windows
Bounds = new Rectangle(nameLabel.X, tmp1, nameLabel.Width, Height - tmp1 - margin),
};
+ var xOffset = nameLabel.Width;
string versionString = string.Empty;
if (desc.IsAlpha)
versionString = "ALPHA ";
@@ -109,7 +110,7 @@ namespace FlaxEditor.Windows
AnchorPreset = AnchorPresets.TopRight,
Text = versionString,
Parent = this,
- Bounds = new Rectangle(Width - 140 - margin, margin, 140, 14),
+ Bounds = new Rectangle(Width - 140 - margin - xOffset, margin, 140, 14),
};
string url = null;
@@ -129,7 +130,7 @@ namespace FlaxEditor.Windows
AnchorPreset = AnchorPresets.TopRight,
Text = desc.Author,
Parent = this,
- Bounds = new Rectangle(Width - authorWidth - margin, versionLabel.Bottom + margin, authorWidth, 14),
+ Bounds = new Rectangle(Width - authorWidth - margin - xOffset, versionLabel.Bottom + margin, authorWidth, 14),
};
if (url != null)
{
@@ -671,11 +672,11 @@ namespace FlaxEditor.Windows
Editor.Log($"Using plugin code type name: {pluginCodeName}");
var oldPluginPath = Path.Combine(extractPath, "ExamplePlugin-master");
- var newPluginPath = Path.Combine(extractPath, pluginName);
+ var newPluginPath = Path.Combine(extractPath, pluginCodeName);
Directory.Move(oldPluginPath, newPluginPath);
var oldFlaxProjFile = Path.Combine(newPluginPath, "ExamplePlugin.flaxproj");
- var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginName}.flaxproj");
+ var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginCodeName}.flaxproj");
File.Move(oldFlaxProjFile, newFlaxProjFile);
var readme = Path.Combine(newPluginPath, "README.md");
@@ -687,7 +688,7 @@ namespace FlaxEditor.Windows
// Flax plugin project file
var flaxPluginProjContents = JsonSerializer.Deserialize(await File.ReadAllTextAsync(newFlaxProjFile));
- flaxPluginProjContents.Name = pluginName;
+ flaxPluginProjContents.Name = pluginCodeName;
if (!string.IsNullOrEmpty(pluginVersion))
flaxPluginProjContents.Version = new Version(pluginVersion);
if (!string.IsNullOrEmpty(companyName))
@@ -751,7 +752,7 @@ namespace FlaxEditor.Windows
}
Editor.Log($"Plugin project {pluginName} has successfully been created.");
- await AddReferenceToProject(pluginName, pluginName);
+ await AddReferenceToProject(pluginCodeName, pluginCodeName);
MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
}
@@ -775,8 +776,12 @@ namespace FlaxEditor.Windows
var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs");
if (File.Exists(pluginModuleScriptPath))
{
- gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");");
- modifiedAny = true;
+ var text = await File.ReadAllTextAsync(pluginModuleScriptPath);
+ if (!text.Contains("GameEditorModule", StringComparison.OrdinalIgnoreCase))
+ {
+ gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");");
+ modifiedAny = true;
+ }
}
}
diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs
index ba569f04f..7638485e4 100644
--- a/Source/Editor/Windows/Profiler/CPU.cs
+++ b/Source/Editor/Windows/Profiler/CPU.cs
@@ -20,9 +20,7 @@ namespace FlaxEngine
get
{
fixed (short* name = Name0)
- {
return new string((char*)name);
- }
}
}
@@ -31,9 +29,7 @@ namespace FlaxEngine
fixed (short* name = Name0)
{
fixed (char* p = prefix)
- {
return Utils.MemoryCompare(new IntPtr(name), new IntPtr(p), (ulong)(prefix.Length * 2)) == 0;
- }
}
}
}
diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs
index d2c34d335..6ddd5e704 100644
--- a/Source/Editor/Windows/Profiler/GPU.cs
+++ b/Source/Editor/Windows/Profiler/GPU.cs
@@ -321,8 +321,7 @@ namespace FlaxEditor.Windows.Profiler
var data = _events.Get(_drawTimeCPU.SelectedSampleIndex);
if (data == null || data.Length == 0)
return;
-
- float totalTimeMs = _drawTimeCPU.SelectedSample;
+ float totalTimeMs = _drawTimeGPU.SelectedSample;
// Add rows
var rowColor2 = Style.Current.Background * 1.4f;
@@ -343,14 +342,19 @@ namespace FlaxEditor.Windows.Profiler
row = new Row
{
Values = new object[6],
+ BackgroundColors = new Color[6],
};
+ for (int k = 0; k < row.BackgroundColors.Length; k++)
+ row.BackgroundColors[k] = Color.Transparent;
}
{
// Event
row.Values[0] = name;
// Total (%)
- row.Values[1] = (int)(e.Time / totalTimeMs * 1000.0f) / 10.0f;
+ float rowTimePerc = (float)(e.Time / totalTimeMs);
+ row.Values[1] = (int)(rowTimePerc * 1000.0f) / 10.0f;
+ row.BackgroundColors[1] = Color.Red.AlphaMultiplied(Mathf.Min(1, rowTimePerc) * 0.5f);
// GPU ms
row.Values[2] = (e.Time * 10000.0f) / 10000.0f;
diff --git a/Source/Editor/Windows/Profiler/ProfilerWindow.cs b/Source/Editor/Windows/Profiler/ProfilerWindow.cs
index f97e943ad..7a3924d1b 100644
--- a/Source/Editor/Windows/Profiler/ProfilerWindow.cs
+++ b/Source/Editor/Windows/Profiler/ProfilerWindow.cs
@@ -38,6 +38,7 @@ namespace FlaxEditor.Windows.Profiler
if (value != LiveRecording)
{
_liveRecordingButton.Checked = value;
+ OnLiveRecordingChanged();
}
}
}
diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs
index 63ba7b960..97bf85b35 100644
--- a/Source/Editor/Windows/SceneTreeWindow.cs
+++ b/Source/Editor/Windows/SceneTreeWindow.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using FlaxEditor.Gizmo;
using FlaxEditor.Content;
using FlaxEditor.GUI.Tree;
@@ -31,6 +32,7 @@ namespace FlaxEditor.Windows
private DragAssets _dragAssets;
private DragActorType _dragActorType;
+ private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
///
@@ -273,6 +275,11 @@ namespace FlaxEditor.Windows
return true;
}
+ private static bool ValidateDragScriptItem(ScriptItem script)
+ {
+ return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
+ }
+
///
public override void Draw()
{
@@ -380,6 +387,13 @@ namespace FlaxEditor.Windows
}
if (_dragActorType.OnDragEnter(data) && result == DragDropEffect.None)
return _dragActorType.Effect;
+ if (_dragScriptItems == null)
+ {
+ _dragScriptItems = new DragScriptItems(ValidateDragScriptItem);
+ _dragHandlers.Add(_dragScriptItems);
+ }
+ if (_dragScriptItems.OnDragEnter(data) && result == DragDropEffect.None)
+ return _dragScriptItems.Effect;
}
return result;
}
@@ -445,6 +459,28 @@ namespace FlaxEditor.Windows
}
result = DragDropEffect.Move;
}
+ // Drag script item
+ else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag)
+ {
+ for (int i = 0; i < _dragScriptItems.Objects.Count; i++)
+ {
+ var item = _dragScriptItems.Objects[i];
+ var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
+ if (actorType != ScriptType.Null)
+ {
+ var actor = actorType.CreateInstance() as Actor;
+ if (actor == null)
+ {
+ Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName);
+ continue;
+ }
+ actor.Name = actorType.Name;
+ Level.SpawnActor(actor);
+ Editor.Scene.MarkSceneEdited(actor.Scene);
+ }
+ }
+ result = DragDropEffect.Move;
+ }
_dragHandlers.OnDragDrop(null);
}
@@ -456,6 +492,7 @@ namespace FlaxEditor.Windows
{
_dragAssets = null;
_dragActorType = null;
+ _dragScriptItems = null;
_dragHandlers?.Clear();
_dragHandlers = null;
_tree = null;
diff --git a/Source/Engine/Animations/AnimationData.cpp b/Source/Engine/Animations/AnimationData.cpp
new file mode 100644
index 000000000..8c3611d7a
--- /dev/null
+++ b/Source/Engine/Animations/AnimationData.cpp
@@ -0,0 +1,84 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+#include "AnimationData.h"
+
+void NodeAnimationData::Evaluate(float time, Transform* result, bool loop) const
+{
+ if (Position.GetKeyframes().HasItems())
+#if USE_LARGE_WORLDS
+ {
+ Float3 position;
+ Position.Evaluate(position, time, loop);
+ result->Translation = position;
+ }
+#else
+ Position.Evaluate(result->Translation, time, loop);
+#endif
+ if (Rotation.GetKeyframes().HasItems())
+ Rotation.Evaluate(result->Orientation, time, loop);
+ if (Scale.GetKeyframes().HasItems())
+ Scale.Evaluate(result->Scale, time, loop);
+}
+
+void NodeAnimationData::EvaluateAll(float time, Transform* result, bool loop) const
+{
+ Float3 position;
+ Position.Evaluate(position, time, loop);
+ result->Translation = position;
+ Rotation.Evaluate(result->Orientation, time, loop);
+ Scale.Evaluate(result->Scale, time, loop);
+}
+
+int32 NodeAnimationData::GetKeyframesCount() const
+{
+ return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count();
+}
+
+uint64 NodeAnimationData::GetMemoryUsage() const
+{
+ return NodeName.Length() * sizeof(Char) + Position.GetMemoryUsage() + Rotation.GetMemoryUsage() + Scale.GetMemoryUsage();
+}
+
+uint64 AnimationData::GetMemoryUsage() const
+{
+ uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
+ for (const auto& e : Channels)
+ result += e.GetMemoryUsage();
+ return result;
+}
+
+int32 AnimationData::GetKeyframesCount() const
+{
+ int32 result = 0;
+ for (int32 i = 0; i < Channels.Count(); i++)
+ result += Channels[i].GetKeyframesCount();
+ return result;
+}
+
+NodeAnimationData* AnimationData::GetChannel(const StringView& name)
+{
+ for (auto& e : Channels)
+ if (e.NodeName == name)
+ return &e;
+ return nullptr;
+}
+
+void AnimationData::Swap(AnimationData& other)
+{
+ ::Swap(Duration, other.Duration);
+ ::Swap(FramesPerSecond, other.FramesPerSecond);
+ ::Swap(RootMotionFlags, other.RootMotionFlags);
+ ::Swap(Name, other.Name);
+ ::Swap(RootNodeName, other.RootNodeName);
+ Channels.Swap(other.Channels);
+}
+
+void AnimationData::Dispose()
+{
+ Name.Clear();
+ Duration = 0.0;
+ FramesPerSecond = 0.0;
+ RootNodeName.Clear();
+ RootMotionFlags = AnimationRootMotionFlags::None;
+ Channels.Resize(0);
+}
diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h
index c69de8afa..00717b05f 100644
--- a/Source/Engine/Animations/AnimationData.h
+++ b/Source/Engine/Animations/AnimationData.h
@@ -3,8 +3,8 @@
#pragma once
#include "Engine/Core/Types/String.h"
-#include "Engine/Animations/Curve.h"
#include "Engine/Core/Math/Transform.h"
+#include "Engine/Animations/Curve.h"
///
/// Single node animation data container.
@@ -50,19 +50,7 @@ public:
/// The time to evaluate the curves at.
/// The interpolated value from the curve at provided time.
/// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.
- void Evaluate(float time, Transform* result, bool loop = true) const
- {
- if (Position.GetKeyframes().HasItems())
- {
- Float3 position;
- Position.Evaluate(position, time, loop);
- result->Translation = position;
- }
- if (Rotation.GetKeyframes().HasItems())
- Rotation.Evaluate(result->Orientation, time, loop);
- if (Scale.GetKeyframes().HasItems())
- Scale.Evaluate(result->Scale, time, loop);
- }
+ void Evaluate(float time, Transform* result, bool loop = true) const;
///
/// Evaluates the animation transformation at the specified time.
@@ -70,29 +58,37 @@ public:
/// The time to evaluate the curves at.
/// The interpolated value from the curve at provided time.
/// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.
- void EvaluateAll(float time, Transform* result, bool loop = true) const
- {
- Float3 position;
- Position.Evaluate(position, time, loop);
- result->Translation = position;
- Rotation.Evaluate(result->Orientation, time, loop);
- Scale.Evaluate(result->Scale, time, loop);
- }
+ void EvaluateAll(float time, Transform* result, bool loop = true) const;
///
/// Gets the total amount of keyframes in the animation curves.
///
- int32 GetKeyframesCount() const
- {
- return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count();
- }
+ int32 GetKeyframesCount() const;
- uint64 GetMemoryUsage() const
- {
- return NodeName.Length() * sizeof(Char) + Position.GetMemoryUsage() + Rotation.GetMemoryUsage() + Scale.GetMemoryUsage();
- }
+ uint64 GetMemoryUsage() const;
};
+///
+/// Root Motion modes that can be applied by the animation. Used as flags for selective behavior.
+///
+API_ENUM(Attributes="Flags") enum class AnimationRootMotionFlags : byte
+{
+ // No root motion.
+ None = 0,
+ // Root node position along XZ plane. Applies horizontal movement. Good for stationary animations (eg. idle).
+ RootPositionXZ = 1 << 0,
+ // Root node position along Y axis (up). Applies vertical movement. Good for all 'grounded' animations unless jumping is handled from code.
+ RootPositionY = 1 << 1,
+ // Root node rotation. Applies orientation changes. Good for animations that have baked-in root rotation (eg. turn animations).
+ RootRotation = 1 << 2,
+ // Root node position.
+ RootPosition = RootPositionXZ | RootPositionY,
+ // Root node position and rotation.
+ RootTransform = RootPosition | RootRotation,
+};
+
+DECLARE_ENUM_OPERATORS(AnimationRootMotionFlags);
+
///
/// Skeleton nodes animation data container. Includes metadata about animation sampling, duration and node animations curves.
///
@@ -111,7 +107,7 @@ struct AnimationData
///
/// Enables root motion extraction support from this animation.
///
- bool EnableRootMotion = false;
+ AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::None;
///
/// The animation name.
@@ -140,49 +136,23 @@ public:
return static_cast(Duration / FramesPerSecond);
}
- uint64 GetMemoryUsage() const
- {
- uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
- for (const auto& e : Channels)
- result += e.GetMemoryUsage();
- return result;
- }
+ uint64 GetMemoryUsage() const;
///
/// Gets the total amount of keyframes in the all animation channels.
///
- int32 GetKeyframesCount() const
- {
- int32 result = 0;
- for (int32 i = 0; i < Channels.Count(); i++)
- result += Channels[i].GetKeyframesCount();
- return result;
- }
+ int32 GetKeyframesCount() const;
+
+ NodeAnimationData* GetChannel(const StringView& name);
///
/// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange.
///
/// The other object.
- void Swap(AnimationData& other)
- {
- ::Swap(Duration, other.Duration);
- ::Swap(FramesPerSecond, other.FramesPerSecond);
- ::Swap(EnableRootMotion, other.EnableRootMotion);
- ::Swap(Name, other.Name);
- ::Swap(RootNodeName, other.RootNodeName);
- Channels.Swap(other.Channels);
- }
+ void Swap(AnimationData& other);
///
/// Releases data.
///
- void Dispose()
- {
- Name.Clear();
- Duration = 0.0;
- FramesPerSecond = 0.0;
- RootNodeName.Clear();
- EnableRootMotion = false;
- Channels.Resize(0);
- }
+ void Dispose();
};
diff --git a/Source/Engine/Animations/Animations.cpp b/Source/Engine/Animations/Animations.cpp
index e571af162..59acc5860 100644
--- a/Source/Engine/Animations/Animations.cpp
+++ b/Source/Engine/Animations/Animations.cpp
@@ -52,7 +52,7 @@ namespace
AnimationsService AnimationManagerInstance;
TaskGraphSystem* Animations::System = nullptr;
#if USE_EDITOR
-Delegate Animations::DebugFlow;
+Delegate Animations::DebugFlow;
#endif
AnimEvent::AnimEvent(const SpawnParams& params)
@@ -127,7 +127,7 @@ void AnimationsSystem::Execute(TaskGraph* graph)
#if USE_EDITOR
// If debug flow is registered, then warm it up (eg. static cached method inside DebugFlow_ManagedWrapper) so it doesn't crash on highly multi-threaded code
if (Animations::DebugFlow.IsBinded())
- Animations::DebugFlow(nullptr, nullptr, 0, 0);
+ Animations::DebugFlow(Animations::DebugFlowInfo());
#endif
// Schedule work to update all animated models in async
diff --git a/Source/Engine/Animations/Animations.h b/Source/Engine/Animations/Animations.h
index d82b9d05f..038d20d9d 100644
--- a/Source/Engine/Animations/Animations.h
+++ b/Source/Engine/Animations/Animations.h
@@ -22,8 +22,25 @@ API_CLASS(Static) class FLAXENGINE_API Animations
API_FIELD(ReadOnly) static TaskGraphSystem* System;
#if USE_EDITOR
- // Custom event that is called every time the Anim Graph signal flows over the graph (including the data connections). Can be used to read and visualize the animation blending logic. Args are: anim graph asset, animated object, node id, box id
- API_EVENT() static Delegate DebugFlow;
+ // Data wrapper for the debug flow information.
+ API_STRUCT(NoDefault) struct DebugFlowInfo
+ {
+ DECLARE_SCRIPTING_TYPE_MINIMAL(DebugFlowInfo);
+
+ // Anim Graph asset
+ API_FIELD() Asset* Asset = nullptr;
+ // Animated actor
+ API_FIELD() ScriptingObject* Instance = nullptr;
+ // Graph node id.
+ API_FIELD() uint32 NodeId = 0;
+ // Graph box id.
+ API_FIELD() uint32 BoxId = 0;
+ // Ids of graph nodes (call of hierarchy).
+ API_FIELD(Internal, NoArray) uint32 NodePath[8] = {};
+ };
+
+ // Custom event that is called every time the Anim Graph signal flows over the graph (including the data connections). Can be used to read and visualize the animation blending logic.
+ API_EVENT() static Delegate DebugFlow;
#endif
///
diff --git a/Source/Engine/Animations/Curve.h b/Source/Engine/Animations/Curve.h
index 74ad4168d..e0e7bd6b5 100644
--- a/Source/Engine/Animations/Curve.h
+++ b/Source/Engine/Animations/Curve.h
@@ -730,7 +730,7 @@ public:
void TransformTime(float timeScale, float timeOffset)
{
for (int32 i = 0; i < _keyframes.Count(); i++)
- _keyframes[i].Time = _keyframes[i].Time * timeScale + timeOffset;;
+ _keyframes[i].Time = _keyframes[i].Time * timeScale + timeOffset;
}
uint64 GetMemoryUsage() const
diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
index 48fa7fed1..af5508705 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
@@ -99,10 +99,7 @@ void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket)
void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
- bucket.StateMachine.LastUpdateFrame = 0;
- bucket.StateMachine.CurrentState = nullptr;
- bucket.StateMachine.ActiveTransition = nullptr;
- bucket.StateMachine.TransitionPosition = 0.0f;
+ Platform::MemoryClear(&bucket.StateMachine, sizeof(bucket.StateMachine));
}
void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket)
@@ -160,7 +157,7 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
if (_rootNode->Values.Count() < 1)
{
_rootNode->Values.Resize(1);
- _rootNode->Values[0] = (int32)RootMotionMode::NoExtraction;
+ _rootNode->Values[0] = (int32)RootMotionExtraction::NoExtraction;
}
break;
// Animation
diff --git a/Source/Engine/Animations/Graph/AnimGraph.Custom.cpp b/Source/Engine/Animations/Graph/AnimGraph.Custom.cpp
index cdd5b03eb..860ac99d4 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.Custom.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.Custom.cpp
@@ -89,7 +89,7 @@ void AnimGraphExecutor::initRuntime()
void AnimGraphExecutor::ProcessGroupCustom(Box* boxBase, Node* nodeBase, Value& value)
{
#if USE_CSHARP
- auto& context = Context.Get();
+ auto& context = *Context.Get();
if (context.ValueCache.TryGet(boxBase, value))
return;
auto box = (AnimGraphBox*)boxBase;
diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp
index 5aa720d31..93bfc2609 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.cpp
@@ -9,7 +9,7 @@
extern void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 i);
-ThreadLocal AnimGraphExecutor::Context;
+ThreadLocal AnimGraphExecutor::Context;
Transform AnimGraphImpulse::GetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex) const
{
@@ -39,6 +39,7 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32
void AnimGraphInstanceData::Clear()
{
ClearState();
+ Slots.Clear();
Parameters.Resize(0);
}
@@ -55,7 +56,7 @@ void AnimGraphInstanceData::ClearState()
RootMotion = Transform::Identity;
State.Resize(0);
NodesPose.Resize(0);
- Slots.Clear();
+ TraceEvents.Clear();
}
void AnimGraphInstanceData::Invalidate()
@@ -103,7 +104,7 @@ AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(Ani
AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor)
{
- auto& context = AnimGraphExecutor::Context.Get();
+ auto& context = *AnimGraphExecutor::Context.Get();
const int32 count = executor->_skeletonNodesCount;
if (context.PoseCacheSize == context.PoseCache.Count())
context.PoseCache.AddOne();
@@ -203,17 +204,21 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
// Initialize
auto& skeleton = _graph.BaseModel->Skeleton;
- auto& context = Context.Get();
+ auto& contextPtr = Context.Get();
+ if (!contextPtr)
+ contextPtr = New();
+ auto& context = *contextPtr;
{
ANIM_GRAPH_PROFILE_EVENT("Init");
// Init data from base model
_skeletonNodesCount = skeleton.Nodes.Count();
- _rootMotionMode = (RootMotionMode)(int32)_graph._rootNode->Values[0];
+ _rootMotionMode = (RootMotionExtraction)(int32)_graph._rootNode->Values[0];
// Prepare context data for the evaluation
context.GraphStack.Clear();
context.GraphStack.Push((Graph*)&_graph);
+ context.NodePath.Clear();
context.Data = &data;
context.DeltaTime = dt;
context.CurrentFrameIndex = ++data.CurrentFrame;
@@ -238,6 +243,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
}
for (auto& e : data.ActiveEvents)
e.Hit = false;
+ data.TraceEvents.Clear();
// Init empty nodes data
context.EmptyNodes.RootMotion = Transform::Identity;
@@ -282,6 +288,20 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
}
}
}
+#if !BUILD_RELEASE
+ {
+ // Perform sanity check on nodes pose to prevent crashes due to NaNs
+ bool anyInvalid = animResult->RootMotion.IsNanOrInfinity();
+ for (int32 i = 0; i < animResult->Nodes.Count(); i++)
+ anyInvalid |= animResult->Nodes.Get()[i].IsNanOrInfinity();
+ if (anyInvalid)
+ {
+ LOG(Error, "Animated Model pose contains NaNs due to animations sampling/blending bug.");
+ context.Data = nullptr;
+ return;
+ }
+ }
+#endif
SkeletonData* animResultSkeleton = &skeleton;
// Retarget animation when using output pose from other skeleton
@@ -361,12 +381,12 @@ void AnimGraphExecutor::GetInputValue(Box* box, Value& result)
AnimGraphImpulse* AnimGraphExecutor::GetEmptyNodes()
{
- return &Context.Get().EmptyNodes;
+ return &Context.Get()->EmptyNodes;
}
void AnimGraphExecutor::InitNodes(AnimGraphImpulse* nodes) const
{
- const auto& emptyNodes = Context.Get().EmptyNodes;
+ const auto& emptyNodes = Context.Get()->EmptyNodes;
Platform::MemoryCopy(nodes->Nodes.Get(), emptyNodes.Nodes.Get(), sizeof(Transform) * _skeletonNodesCount);
nodes->RootMotion = emptyNodes.RootMotion;
nodes->Position = emptyNodes.Position;
@@ -388,7 +408,7 @@ void AnimGraphExecutor::ResetBuckets(AnimGraphContext& context, AnimGraphBase* g
VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
// Check if graph is looped or is too deep
if (context.CallStack.Count() >= ANIM_GRAPH_MAX_CALL_STACK)
@@ -408,7 +428,15 @@ VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
context.CallStack.Add(caller);
#if USE_EDITOR
- Animations::DebugFlow(_graph._owner, context.Data->Object, box->GetParent()->ID, box->ID);
+ Animations::DebugFlowInfo flowInfo;
+ flowInfo.Asset = _graph._owner;
+ flowInfo.Instance = context.Data->Object;
+ flowInfo.NodeId = box->GetParent()->ID;
+ flowInfo.BoxId = box->ID;
+ const auto* nodePath = context.NodePath.Get();
+ for (int32 i = 0; i < context.NodePath.Count(); i++)
+ flowInfo.NodePath[i] = nodePath[i];
+ Animations::DebugFlow(flowInfo);
#endif
// Call per group custom processing event
@@ -425,6 +453,6 @@ VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
VisjectExecutor::Graph* AnimGraphExecutor::GetCurrentGraph() const
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
return context.GraphStack.Peek();
}
diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h
index afaa4441b..b570cf9fe 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.h
+++ b/Source/Engine/Animations/Graph/AnimGraph.h
@@ -92,9 +92,9 @@ enum class BoneTransformMode
};
///
-/// The animated model root motion mode.
+/// The animated model root motion extraction modes.
///
-enum class RootMotionMode
+enum class RootMotionExtraction
{
///
/// Don't extract nor apply the root motion.
@@ -129,6 +129,8 @@ public:
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
+ InterruptionSourceState = 32,
+ InterruptionDestinationState = 64,
};
public:
@@ -197,6 +199,24 @@ struct FLAXENGINE_API AnimGraphSlot
float BlendOutTime = 0.0f;
int32 LoopCount = 0;
bool Pause = false;
+ bool Reset = false;
+};
+
+///
+/// The animation graph state container for a single node playback trace (eg. animation sample info or state transition). Can be used by Anim Graph debugging or custom scripting.
+///
+API_STRUCT(NoDefault) struct FLAXENGINE_API AnimGraphTraceEvent
+{
+ DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent);
+
+ // Contextual asset used. For example, sampled animation.
+ API_FIELD() Asset* Asset = nullptr;
+ // Generic value contextual to playback type (eg. animation sample position).
+ API_FIELD() float Value = 0;
+ // Identifier of the node in the graph.
+ API_FIELD() uint32 NodeId = 0;
+ // Ids of graph nodes (call of hierarchy).
+ API_FIELD(Internal, NoArray) uint32 NodePath[8] = {};
};
///
@@ -240,7 +260,10 @@ public:
uint64 LastUpdateFrame;
AnimGraphNode* CurrentState;
AnimGraphStateTransition* ActiveTransition;
+ AnimGraphStateTransition* BaseTransition;
+ AnimGraphNode* BaseTransitionState;
float TransitionPosition;
+ float BaseTransitionPosition;
};
struct SlotBucket
@@ -357,6 +380,12 @@ public:
///
void InvokeAnimEvents();
+public:
+ // Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations.
+ bool EnableTracing = false;
+ // Trace events collected when using EnableTracing option.
+ Array TraceEvents;
+
private:
struct OutgoingEvent
{
@@ -769,10 +798,13 @@ struct AnimGraphContext
AnimGraphTransitionData TransitionData;
Array> CallStack;
Array> GraphStack;
+ Array > NodePath;
Dictionary Functions;
ChunkedArray PoseCache;
int32 PoseCacheSize;
Dictionary ValueCache;
+
+ AnimGraphTraceEvent& AddTraceEvent(const AnimGraphNode* node);
};
///
@@ -783,11 +815,11 @@ class AnimGraphExecutor : public VisjectExecutor
friend AnimGraphNode;
private:
AnimGraph& _graph;
- RootMotionMode _rootMotionMode = RootMotionMode::NoExtraction;
+ RootMotionExtraction _rootMotionMode = RootMotionExtraction::NoExtraction;
int32 _skeletonNodesCount = 0;
// Per-thread context to allow async execution
- static ThreadLocal Context;
+ static ThreadLocal Context;
public:
///
@@ -836,7 +868,7 @@ public:
}
///
- /// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children).
+ /// Resets all the state bucket used by the given graph including sub-graphs (total). Can be used to reset the animation state of the nested graph (including children).
///
void ResetBuckets(AnimGraphContext& context, AnimGraphBase* graph);
@@ -864,6 +896,8 @@ private:
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, float speedA, float speedB, float alpha);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);
Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode);
- Variant SampleState(AnimGraphNode* state);
+ Variant SampleState(AnimGraphContext& context, const AnimGraphNode* state);
+ void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr);
+ AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr);
void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData);
};
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index 8a7751ecb..30b1bb022 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -21,13 +21,13 @@ namespace
base += additive;
}
- FORCE_INLINE void NormalizeRotations(AnimGraphImpulse* nodes, RootMotionMode rootMotionMode)
+ FORCE_INLINE void NormalizeRotations(AnimGraphImpulse* nodes, RootMotionExtraction rootMotionMode)
{
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
nodes->Nodes[i].Orientation.Normalize();
}
- if (rootMotionMode != RootMotionMode::NoExtraction)
+ if (rootMotionMode != RootMotionExtraction::NoExtraction)
{
nodes->RootMotion.Orientation.Normalize();
}
@@ -52,6 +52,17 @@ void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData
node = value;
}
+AnimGraphTraceEvent& AnimGraphContext::AddTraceEvent(const AnimGraphNode* node)
+{
+ auto& trace = Data->TraceEvents.AddOne();
+ trace.Value = 0.0f;
+ trace.NodeId = node->ID;
+ const auto* nodePath = NodePath.Get();
+ for (int32 i = 0; i < NodePath.Count(); i++)
+ trace.NodePath[i] = nodePath[i];
+ return trace;
+}
+
int32 AnimGraphExecutor::GetRootNodeIndex(Animation* anim)
{
// TODO: cache the root node index (use dictionary with Animation* -> int32 for fast lookups)
@@ -76,7 +87,7 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
if (anim->Events.Count() == 0)
return;
ANIM_GRAPH_PROFILE_EVENT("Events");
- auto& context = Context.Get();
+ auto& context = *Context.Get();
float eventTimeMin = animPrevPos;
float eventTimeMax = animPos;
if (loop && context.DeltaTime * speed < 0)
@@ -219,6 +230,15 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const float animPos = GetAnimSamplePos(length, anim, pos, speed);
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
+ // Add to trace
+ auto& context = *Context.Get();
+ if (context.Data->EnableTracing)
+ {
+ auto& trace = context.AddTraceEvent(node);
+ trace.Asset = anim;
+ trace.Value = animPos;
+ }
+
// Evaluate nested animations
bool hasNested = false;
if (anim->NestedAnims.Count() != 0)
@@ -303,16 +323,21 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
}
// Handle root motion
- if (_rootMotionMode != RootMotionMode::NoExtraction && anim->Data.EnableRootMotion)
+ if (_rootMotionMode != RootMotionExtraction::NoExtraction && anim->Data.RootMotionFlags != AnimationRootMotionFlags::None)
{
// Calculate the root motion node transformation
+ const bool motionPositionXZ = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionXZ);
+ const bool motionPositionY = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionY);
+ const bool motionRotation = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootRotation);
+ const Vector3 motionPositionMask(motionPositionXZ ? 1.0f : 0.0f, motionPositionY ? 1.0f : 0.0f, motionPositionXZ ? 1.0f : 0.0f);
+ const bool motionPosition = motionPositionXZ | motionPositionY;
const int32 rootNodeIndex = GetRootNodeIndex(anim);
const Transform& refPose = emptyNodes->Nodes[rootNodeIndex];
Transform& rootNode = nodes->Nodes[rootNodeIndex];
Transform& dstNode = nodes->RootMotion;
Transform srcNode = Transform::Identity;
const int32 nodeToChannel = mapping.NodesMapping[rootNodeIndex];
- if (_rootMotionMode == RootMotionMode::Enable && nodeToChannel != -1)
+ if (_rootMotionMode == RootMotionExtraction::Enable && nodeToChannel != -1)
{
// Get the root bone transformation
Transform rootBefore = refPose;
@@ -336,16 +361,20 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
// Complex motion calculation to preserve the looped movement
// (end - before + now - begin)
// It sums the motion since the last update to anim end and since the start to now
- srcNode.Translation = rootEnd.Translation - rootBefore.Translation + rootNode.Translation - rootBegin.Translation;
- srcNode.Orientation = rootEnd.Orientation * rootBefore.Orientation.Conjugated() * (rootNode.Orientation * rootBegin.Orientation.Conjugated());
+ if (motionPosition)
+ srcNode.Translation = (rootEnd.Translation - rootBefore.Translation + rootNode.Translation - rootBegin.Translation) * motionPositionMask;
+ if (motionRotation)
+ srcNode.Orientation = rootEnd.Orientation * rootBefore.Orientation.Conjugated() * (rootNode.Orientation * rootBegin.Orientation.Conjugated());
//srcNode.Orientation = Quaternion::Identity;
}
else
{
// Simple motion delta
// (now - before)
- srcNode.Translation = rootNode.Translation - rootBefore.Translation;
- srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation;
+ if (motionPosition)
+ srcNode.Translation = (rootNode.Translation - rootBefore.Translation) * motionPositionMask;
+ if (motionRotation)
+ srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation;
}
// Convert root motion from local-space to the actor-space (eg. if root node is not actually a root and its parents have rotation/scale)
@@ -359,28 +388,40 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
}
}
- // Remove root node motion after extraction
- rootNode = refPose;
+ // Remove root node motion after extraction (only extracted components)
+ if (motionPosition)
+ rootNode.Translation = refPose.Translation * motionPositionMask + rootNode.Translation * (Vector3::One - motionPositionMask);
+ if (motionRotation)
+ rootNode.Orientation = refPose.Orientation;
// Blend root motion
if (mode == ProcessAnimationMode::BlendAdditive)
{
- dstNode.Translation += srcNode.Translation * weight;
- BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight);
+ if (motionPosition)
+ dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
+ if (motionRotation)
+ BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight);
}
else if (mode == ProcessAnimationMode::Add)
{
- dstNode.Translation += srcNode.Translation * weight;
- dstNode.Orientation += srcNode.Orientation * weight;
+ if (motionPosition)
+ dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
+ if (motionRotation)
+ dstNode.Orientation += srcNode.Orientation * weight;
}
else if (weighted)
{
- dstNode.Translation = srcNode.Translation * weight;
- dstNode.Orientation = srcNode.Orientation * weight;
+ if (motionPosition)
+ dstNode.Translation = srcNode.Translation * weight * motionPositionMask;
+ if (motionRotation)
+ dstNode.Orientation = srcNode.Orientation * weight;
}
else
{
- dstNode = srcNode;
+ if (motionPosition)
+ dstNode.Translation = srcNode.Translation * motionPositionMask;
+ if (motionRotation)
+ dstNode.Orientation = srcNode.Orientation;
}
}
@@ -460,10 +501,12 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
{
ANIM_GRAPH_PROFILE_EVENT("Blend Pose");
+ if (isnan(alpha) || isinf(alpha))
+ alpha = 0;
+ alpha = Math::Saturate(alpha);
alpha = AlphaBlend::Process(alpha, alphaMode);
const auto nodes = node->GetNodes(this);
-
auto nodesA = static_cast(poseA.AsPointer);
auto nodesB = static_cast(poseB.AsPointer);
if (!ANIM_GRAPH_IS_VALID_PTR(poseA))
@@ -482,34 +525,53 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
return nodes;
}
-Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
+Variant AnimGraphExecutor::SampleState(AnimGraphContext& context, const AnimGraphNode* state)
{
- // Prepare
auto& data = state->Data.State;
if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr)
- {
- // Invalid state graph
return Value::Null;
+
+ // Add to trace
+ if (context.Data->EnableTracing)
+ {
+ auto& trace = context.AddTraceEvent(state);
}
ANIM_GRAPH_PROFILE_EVENT("Evaluate State");
-
- // Evaluate state
+ context.NodePath.Add(state->ID);
auto rootNode = data.Graph->GetRootNode();
auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]);
+ context.NodePath.Pop();
return result;
}
-void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
+void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition)
+{
+ // Reset transition
+ stateMachineBucket.ActiveTransition = transition;
+ stateMachineBucket.TransitionPosition = 0.0f;
+
+ // End base transition
+ if (stateMachineBucket.BaseTransition)
+ {
+ ResetBuckets(context, stateMachineBucket.BaseTransitionState->Data.State.Graph);
+ stateMachineBucket.BaseTransition = nullptr;
+ stateMachineBucket.BaseTransitionState = nullptr;
+ stateMachineBucket.BaseTransitionPosition = 0.0f;
+ }
+}
+
+AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState)
{
int32 transitionIndex = 0;
+ const AnimGraphNode::StateBaseData& stateData = state->Data.State;
while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex)
{
const uint16 idx = stateData.Transitions[transitionIndex];
ASSERT(idx < stateMachineData.Graph->StateTransitions.Count());
auto& transition = stateMachineData.Graph->StateTransitions[idx];
- if (transition.Destination == stateMachineBucket.CurrentState)
+ if (transition.Destination == state || transition.Destination == ignoreState)
{
// Ignore transition to the current state
transitionIndex++;
@@ -517,7 +579,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
}
// Evaluate source state transition data (position, length, etc.)
- const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState);
+ const Value sourceStatePtr = SampleState(context, state);
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
{
@@ -538,6 +600,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
if (transition.RuleGraph && !useDefaultRule)
{
// Execute transition rule
+ ANIM_GRAPH_PROFILE_EVENT("Rule");
auto rootNode = transition.RuleGraph->GetRootNode();
ASSERT(rootNode);
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
@@ -560,10 +623,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
canEnter = true;
if (canEnter)
{
- // Start transition
- stateMachineBucket.ActiveTransition = &transition;
- stateMachineBucket.TransitionPosition = 0.0f;
- break;
+ return &transition;
}
// Skip after Solo transition
@@ -573,6 +633,18 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
transitionIndex++;
}
+
+ // No transition
+ return nullptr;
+}
+
+void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
+{
+ AnimGraphStateTransition* transition = UpdateStateTransitions(context, stateMachineData, stateMachineBucket.CurrentState);
+ if (transition)
+ {
+ InitStateTransition(context, stateMachineBucket, transition);
+ }
}
void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
@@ -604,7 +676,7 @@ void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
switch (node->TypeID)
{
// Get
@@ -715,7 +787,7 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu
void AnimGraphExecutor::ProcessGroupTools(Box* box, Node* nodeBase, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
auto node = (AnimGraphNode*)nodeBase;
switch (node->TypeID)
{
@@ -739,7 +811,7 @@ void AnimGraphExecutor::ProcessGroupTools(Box* box, Node* nodeBase, Value& value
void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
if (context.ValueCache.TryGet(boxBase, value))
return;
auto box = (AnimGraphBox*)boxBase;
@@ -1101,6 +1173,17 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
{
const float alpha = Math::Saturate((float)tryGetValue(node->GetBox(3), node->Values[0]));
auto mask = node->Assets[0].As();
+ auto maskAssetBox = node->GetBox(4); // 4 is the id of skeleton mask parameter node.
+
+ // Check if have some mask asset connected with the mask node
+ if (maskAssetBox->HasConnection())
+ {
+ const Value assetBoxValue = tryGetValue(maskAssetBox, Value::Null);
+
+ // Use the mask connected with this node instead of default mask asset
+ if (assetBoxValue != Value::Null)
+ mask = (SkeletonMask*)assetBoxValue.AsAsset;
+ }
// Only A or missing/invalid mask
if (Math::NearEqual(alpha, 0.0f, ANIM_GRAPH_BLEND_THRESHOLD) || mask == nullptr || mask->WaitForLoaded())
@@ -1367,33 +1450,29 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
// Use 1D blend if points are on the same line (degenerated triangle)
- // TODO: simplify this code
+ struct BlendData
+ {
+ float AlphaX, AlphaY;
+ Animation* AnimA, *AnimB;
+ const Float4* AnimAd, *AnimBd;
+ };
+ BlendData blendData;
if (v1.Y >= v0.Y)
{
if (p.Y < v0.Y && v1.Y >= v0.Y)
- {
- const float alpha = p.Y / v0.Y;
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, aAnim, bAnim, aData.W, bData.W, alpha);
- }
+ blendData = { p.Y, v0.Y, aAnim, bAnim, &aData, &bData };
else
- {
- const float alpha = (p.Y - v0.Y) / (v1.Y - v0.Y);
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, bAnim, cAnim, bData.W, cData.W, alpha);
- }
+ blendData = { p.Y - v0.Y, v1.Y - v0.Y, bAnim, cAnim, &bData, &cData };
}
else
{
if (p.Y < v1.Y)
- {
- const float alpha = p.Y / v1.Y;
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, aAnim, cAnim, aData.W, cData.W, alpha);
- }
+ blendData = { p.Y, v1.Y, aAnim, cAnim, &aData, &cData };
else
- {
- const float alpha = (p.Y - v1.Y) / (v0.Y - v1.Y);
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, cAnim, bAnim, cData.W, bData.W, alpha);
- }
+ blendData = { p.Y - v1.Y, v0.Y - v1.Y, cAnim, bAnim, &cData, &bData };
}
+ const float alpha = Math::IsZero(blendData.AlphaY) ? 0.0f : blendData.AlphaX / blendData.AlphaY;
+ value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, blendData.AnimA, blendData.AnimB, blendData.AnimAd->W, blendData.AnimBd->W, alpha);
}
else
{
@@ -1505,10 +1584,9 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend two animations
{
- const float alpha = Math::Saturate(bucket.TransitionPosition / blendDuration);
+ const float alpha = bucket.TransitionPosition / blendDuration;
const auto valueA = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + bucket.PreviousBlendPoseIndex), Value::Null);
const auto valueB = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + poseIndex), Value::Null);
-
value = Blend(node, valueA, valueB, alpha, mode);
}
@@ -1614,22 +1692,23 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Enter to the first state pointed by the Entry node (without transitions)
bucket.CurrentState = data.Graph->GetRootNode();
- bucket.ActiveTransition = nullptr;
- bucket.TransitionPosition = 0.0f;
+ InitStateTransition(context, bucket);
- // Reset all state buckets pof the graphs and nodes included inside the state machine
+ // Reset all state buckets of the graphs and nodes included inside the state machine
ResetBuckets(context, data.Graph);
}
#define END_TRANSITION() \
ResetBuckets(context, bucket.CurrentState->Data.State.Graph); \
bucket.CurrentState = bucket.ActiveTransition->Destination; \
- bucket.ActiveTransition = nullptr; \
- bucket.TransitionPosition = 0.0f
+ InitStateTransition(context, bucket)
+
+ context.NodePath.Push(node->ID);
// Update the active transition
if (bucket.ActiveTransition)
{
bucket.TransitionPosition += context.DeltaTime;
+ ASSERT(bucket.CurrentState);
// Check for transition end
if (bucket.TransitionPosition >= bucket.ActiveTransition->BlendDuration)
@@ -1637,38 +1716,70 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
END_TRANSITION();
}
// Check for transition interruption
- else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking))
+ else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) &&
+ EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) &&
+ bucket.ActiveTransition->RuleGraph)
{
- const bool useDefaultRule = EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule);
- if (bucket.ActiveTransition->RuleGraph && !useDefaultRule)
+ // Execute transition rule
+ auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
+ if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
{
- // Execute transition rule
- auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
- if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
+ bool cancelTransition = false;
+ if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
- bool cancelTransition = false;
- if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
+ cancelTransition = true;
+ }
+ else
+ {
+ // Blend back to the source state (remove currently applied delta and rewind transition)
+ bucket.TransitionPosition -= context.DeltaTime;
+ bucket.TransitionPosition -= context.DeltaTime;
+ if (bucket.TransitionPosition <= ZeroTolerance)
{
cancelTransition = true;
}
- else
- {
- // Blend back to the source state (remove currently applied delta and rewind transition)
- bucket.TransitionPosition -= context.DeltaTime;
- bucket.TransitionPosition -= context.DeltaTime;
- if (bucket.TransitionPosition <= ZeroTolerance)
- {
- cancelTransition = true;
- }
- }
- if (cancelTransition)
- {
- // Go back to the source state
- ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
- bucket.ActiveTransition = nullptr;
- bucket.TransitionPosition = 0.0f;
- }
}
+ if (cancelTransition)
+ {
+ // Go back to the source state
+ ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
+ InitStateTransition(context, bucket);
+ }
+ }
+ }
+ if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionSourceState))
+ {
+ // Try to interrupt with any other transition in the source state (except the current transition)
+ if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.CurrentState, bucket.ActiveTransition->Destination))
+ {
+ // Change active transition to the interrupted one
+ if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
+ {
+ // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
+ bucket.BaseTransition = bucket.ActiveTransition;
+ bucket.BaseTransitionState = bucket.CurrentState;
+ bucket.BaseTransitionPosition = bucket.TransitionPosition;
+ }
+ bucket.ActiveTransition = transition;
+ bucket.TransitionPosition = 0.0f;
+ }
+ }
+ if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionDestinationState))
+ {
+ // Try to interrupt with any other transition in the destination state (except the transition back to the current state if exists)
+ if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.ActiveTransition->Destination, bucket.CurrentState))
+ {
+ // Change active transition to the interrupted one
+ if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
+ {
+ // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
+ bucket.BaseTransition = bucket.ActiveTransition;
+ bucket.BaseTransitionState = bucket.CurrentState;
+ bucket.BaseTransitionPosition = bucket.TransitionPosition;
+ }
+ bucket.CurrentState = bucket.ActiveTransition->Destination;
+ bucket.ActiveTransition = transition;
+ bucket.TransitionPosition = 0.0f;
}
}
}
@@ -1697,25 +1808,38 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
}
- // Sample the current state
- const auto currentState = SampleState(bucket.CurrentState);
- value = currentState;
+ if (bucket.BaseTransitionState)
+ {
+ // Sample the other state (eg. when blending from interrupted state to the another state from the old destination)
+ value = SampleState(context, bucket.BaseTransitionState);
+ if (bucket.BaseTransition)
+ {
+ // Evaluate the base pose from the time when transition was interrupted
+ const auto destinationState = SampleState(context, bucket.BaseTransition->Destination);
+ const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration;
+ value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode);
+ }
+ }
+ else
+ {
+ // Sample the current state
+ value = SampleState(context, bucket.CurrentState);
+ }
// Handle active transition blending
if (bucket.ActiveTransition)
{
// Sample the active transition destination state
- const auto destinationState = SampleState(bucket.ActiveTransition->Destination);
+ const auto destinationState = SampleState(context, bucket.ActiveTransition->Destination);
// Perform blending
- const float alpha = Math::Saturate(bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration);
- value = Blend(node, currentState, destinationState, alpha, bucket.ActiveTransition->BlendMode);
+ const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration;
+ value = Blend(node, value, destinationState, alpha, bucket.ActiveTransition->BlendMode);
}
- // Update bucket
bucket.LastUpdateFrame = context.CurrentFrameIndex;
+ context.NodePath.Pop();
#undef END_TRANSITION
-
break;
}
// Entry
@@ -2102,6 +2226,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
auto& slot = slots[bucket.Index];
Animation* anim = slot.Animation;
ASSERT(slot.Animation && slot.Animation->IsLoaded());
+ if (slot.Reset)
+ {
+ // Start from the begining
+ slot.Reset = false;
+ bucket.TimePosition = 0.0f;
+ }
const float deltaTime = slot.Pause ? 0.0f : context.DeltaTime * slot.Speed;
const float length = anim->GetLength();
const bool loop = bucket.LoopsLeft != 0;
@@ -2130,7 +2260,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend out
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendOutPosition += deltaTime;
- const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime);
+ const float alpha = bucket.BlendOutPosition / slot.BlendOutTime;
value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic);
}
else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime)
@@ -2138,7 +2268,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend in
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendInPosition += deltaTime;
- const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime);
+ const float alpha = bucket.BlendInPosition / slot.BlendInTime;
value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic);
}
break;
@@ -2163,7 +2293,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
void AnimGraphExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
if (context.ValueCache.TryGet(boxBase, value))
return;
switch (node->TypeID)
diff --git a/Source/Engine/Audio/AudioSource.cpp b/Source/Engine/Audio/AudioSource.cpp
index cb32e0967..89d1d0a5b 100644
--- a/Source/Engine/Audio/AudioSource.cpp
+++ b/Source/Engine/Audio/AudioSource.cpp
@@ -19,6 +19,7 @@ AudioSource::AudioSource(const SpawnParams& params)
, _minDistance(1000.0f)
, _loop(false)
, _playOnStart(false)
+ , _startTime(0.0f)
, _allowSpatialization(true)
{
Clip.Changed.Bind(this);
@@ -71,6 +72,11 @@ void AudioSource::SetPlayOnStart(bool value)
_playOnStart = value;
}
+void AudioSource::SetStartTime(float value)
+{
+ _startTime = value;
+}
+
void AudioSource::SetMinDistance(float value)
{
value = Math::Max(0.0f, value);
@@ -361,6 +367,7 @@ void AudioSource::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_MEMBER(DopplerFactor, _dopplerFactor);
SERIALIZE_MEMBER(Loop, _loop);
SERIALIZE_MEMBER(PlayOnStart, _playOnStart);
+ SERIALIZE_MEMBER(StartTime, _startTime);
SERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization);
}
@@ -377,6 +384,7 @@ void AudioSource::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE_MEMBER(DopplerFactor, _dopplerFactor);
DESERIALIZE_MEMBER(Loop, _loop);
DESERIALIZE_MEMBER(PlayOnStart, _playOnStart);
+ DESERIALIZE_MEMBER(StartTime, _startTime);
DESERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization);
DESERIALIZE(Clip);
}
@@ -540,5 +548,7 @@ void AudioSource::BeginPlay(SceneBeginData* data)
return;
#endif
Play();
+ if (GetStartTime() > 0)
+ SetTime(GetStartTime());
}
}
diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h
index 70cdb4180..5c0c23c03 100644
--- a/Source/Engine/Audio/AudioSource.h
+++ b/Source/Engine/Audio/AudioSource.h
@@ -52,6 +52,7 @@ private:
float _dopplerFactor = 1.0f;
bool _loop;
bool _playOnStart;
+ float _startTime;
bool _allowSpatialization;
bool _clipChanged = false;
@@ -148,11 +149,25 @@ public:
return _playOnStart;
}
+ ///
+ /// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled.
+ ///
+ API_PROPERTY(Attributes = "EditorOrder(51), DefaultValue(0.0f), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Audio Source\", \"Start Time\"), VisibleIf(nameof(PlayOnStart))")
+ FORCE_INLINE float GetStartTime() const
+ {
+ return _startTime;
+ }
+
///
/// Determines whether the audio clip should auto play on game start.
///
API_PROPERTY() void SetPlayOnStart(bool value);
+ ///
+ /// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled.
+ ///
+ API_PROPERTY() void SetStartTime(float value);
+
///
/// Gets the minimum distance at which audio attenuation starts. When the listener is closer to the source than this value, audio is heard at full volume. Once farther away the audio starts attenuating.
///
diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
index ace8b6591..3bd709259 100644
--- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
+++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
@@ -116,7 +116,7 @@ namespace ALC
{
AudioBackend::Listener::TransformChanged(listener);
- const Vector3 velocity = listener->GetVelocity();
+ const Float3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
alListenerf(AL_GAIN, Audio::GetVolume());
}
@@ -319,8 +319,6 @@ void AudioBackendOAL::Listener_OnAdd(AudioListener* listener)
ALC::RebuildContexts(false);
#else
AudioBackend::Listener::TransformChanged(listener);
- const Vector3 velocity = listener->GetVelocity();
- alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
alListenerf(AL_GAIN, Audio::GetVolume());
#endif
}
@@ -336,7 +334,7 @@ void AudioBackendOAL::Listener_VelocityChanged(AudioListener* listener)
{
ALC_GET_LISTENER_CONTEXT(listener)
- const Vector3 velocity = listener->GetVelocity();
+ const Float3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
}
@@ -344,15 +342,15 @@ void AudioBackendOAL::Listener_TransformChanged(AudioListener* listener)
{
ALC_GET_LISTENER_CONTEXT(listener)
- const Vector3 position = listener->GetPosition();
+ const Float3 position = listener->GetPosition();
const Quaternion orientation = listener->GetOrientation();
- const Vector3 flipX(-1, 1, 1);
- const Vector3 alOrientation[2] =
+ const Float3 flipX(-1, 1, 1);
+ const Float3 alOrientation[2] =
{
// Forward
- orientation * Vector3::Forward * flipX,
+ orientation * Float3::Forward * flipX,
// Up
- orientation * Vector3::Up * flipX
+ orientation * Float3::Up * flipX
};
alListenerfv(AL_ORIENTATION, (float*)alOrientation);
diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp
index 8aa635244..ec418c5fb 100644
--- a/Source/Engine/Content/Assets/Animation.cpp
+++ b/Source/Engine/Content/Assets/Animation.cpp
@@ -413,10 +413,10 @@ bool Animation::Save(const StringView& path)
MemoryWriteStream stream(4096);
// Info
- stream.WriteInt32(102);
+ stream.WriteInt32(103);
stream.WriteDouble(Data.Duration);
stream.WriteDouble(Data.FramesPerSecond);
- stream.WriteBool(Data.EnableRootMotion);
+ stream.WriteByte((byte)Data.RootMotionFlags);
stream.WriteString(Data.RootNodeName, 13);
// Animation channels
@@ -532,17 +532,22 @@ Asset::LoadResult Animation::load()
int32 headerVersion = *(int32*)stream.GetPositionHandle();
switch (headerVersion)
{
- case 100:
- case 101:
- case 102:
- {
+ case 103:
stream.ReadInt32(&headerVersion);
stream.ReadDouble(&Data.Duration);
stream.ReadDouble(&Data.FramesPerSecond);
- Data.EnableRootMotion = stream.ReadBool();
+ stream.ReadByte((byte*)&Data.RootMotionFlags);
+ stream.ReadString(&Data.RootNodeName, 13);
+ break;
+ case 100:
+ case 101:
+ case 102:
+ stream.ReadInt32(&headerVersion);
+ stream.ReadDouble(&Data.Duration);
+ stream.ReadDouble(&Data.FramesPerSecond);
+ Data.RootMotionFlags = stream.ReadBool() ? AnimationRootMotionFlags::RootPositionXZ : AnimationRootMotionFlags::None;
stream.ReadString(&Data.RootNodeName, 13);
break;
- }
default:
stream.ReadDouble(&Data.Duration);
stream.ReadDouble(&Data.FramesPerSecond);
diff --git a/Source/Engine/Content/Assets/AnimationGraph.cpp b/Source/Engine/Content/Assets/AnimationGraph.cpp
index fe87abfe4..9b431aefa 100644
--- a/Source/Engine/Content/Assets/AnimationGraph.cpp
+++ b/Source/Engine/Content/Assets/AnimationGraph.cpp
@@ -67,7 +67,7 @@ void AnimationGraph::OnDependencyModified(BinaryAsset* asset)
#endif
-bool AnimationGraph::InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop)
+bool AnimationGraph::InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop, bool rootMotion)
{
if (!IsVirtual())
{
@@ -89,7 +89,7 @@ bool AnimationGraph::InitAsAnimation(SkinnedModel* baseModel, Animation* anim, b
rootNode.Type = GRAPH_NODE_MAKE_TYPE(9, 1);
rootNode.ID = 1;
rootNode.Values.Resize(1);
- rootNode.Values[0] = (int32)RootMotionMode::NoExtraction;
+ rootNode.Values[0] = (int32)(rootMotion ? RootMotionExtraction::Enable : RootMotionExtraction::Ignore);
rootNode.Boxes.Resize(1);
rootNode.Boxes[0] = AnimGraphBox(&rootNode, 0, VariantType::Void);
auto& animNode = graph.Nodes[1];
diff --git a/Source/Engine/Content/Assets/AnimationGraph.h b/Source/Engine/Content/Assets/AnimationGraph.h
index f378b9bbe..716329347 100644
--- a/Source/Engine/Content/Assets/AnimationGraph.h
+++ b/Source/Engine/Content/Assets/AnimationGraph.h
@@ -37,8 +37,9 @@ public:
/// The base model asset.
/// The animation to play.
/// True if play animation in a loop.
+ /// True if apply root motion. Otherwise it will be ignored.
/// True if failed, otherwise false.
- API_FUNCTION() bool InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop = true);
+ API_FUNCTION() bool InitAsAnimation(SkinnedModel* baseModel, Animation* anim, bool loop = true, bool rootMotion = false);
///
/// Tries to load surface graph from the asset.
diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp
index 61e73a3f2..47bdcd3b2 100644
--- a/Source/Engine/Content/Storage/ContentStorageManager.cpp
+++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp
@@ -58,8 +58,8 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
Locker.Lock();
// Try fast lookup
- FlaxStorage* result;
- if (!StorageMap.TryGet(path, result))
+ FlaxStorage* storage;
+ if (!StorageMap.TryGet(path, storage))
{
// Detect storage type and create object
const bool isPackage = path.EndsWith(StringView(PACKAGE_FILES_EXTENSION));
@@ -67,39 +67,42 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
{
auto package = New(path);
Packages.Add(package);
- result = package;
+ storage = package;
}
else
{
auto file = New(path);
Files.Add(file);
- result = file;
+ storage = file;
}
// Register storage container
- StorageMap.Add(path, result);
+ StorageMap.Add(path, storage);
}
+ // Build reference (before releasing the lock so ContentStorageSystem::Job won't delete it when running from async thread)
+ FlaxStorageReference result(storage);
+
Locker.Unlock();
if (loadIt)
{
// Initialize storage container
- result->LockChunks();
- const bool loadFailed = result->Load();
- result->UnlockChunks();
+ storage->LockChunks();
+ const bool loadFailed = storage->Load();
+ storage->UnlockChunks();
if (loadFailed)
{
LOG(Error, "Failed to load {0}.", path);
Locker.Lock();
StorageMap.Remove(path);
- if (result->IsPackage())
- Packages.Remove((FlaxPackage*)result);
+ if (storage->IsPackage())
+ Packages.Remove((FlaxPackage*)storage);
else
- Files.Remove((FlaxFile*)result);
+ Files.Remove((FlaxFile*)storage);
Locker.Unlock();
- Delete(result);
- return nullptr;
+ result = nullptr;
+ Delete(storage);
}
}
diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp
index df99418bb..17cf2d193 100644
--- a/Source/Engine/Content/Storage/FlaxStorage.cpp
+++ b/Source/Engine/Content/Storage/FlaxStorage.cpp
@@ -211,7 +211,13 @@ FlaxStorage::~FlaxStorage()
#if USE_EDITOR
// Ensure to close any outstanding file handles to prevent file locking in case it failed to load
- _file.DeleteAll();
+ Array streams;
+ _file.GetValues(streams);
+ for (FileReadStream* stream : streams)
+ {
+ if (stream)
+ Delete(stream);
+ }
#endif
}
@@ -1264,7 +1270,6 @@ bool FlaxStorage::LoadAssetHeader(const Entry& e, AssetInitData& data)
}
#if ASSETS_LOADING_EXTRA_VERIFICATION
-
// Validate loaded header (asset ID and type ID must be the same)
if (e.ID != data.Header.ID)
{
@@ -1274,7 +1279,6 @@ bool FlaxStorage::LoadAssetHeader(const Entry& e, AssetInitData& data)
{
LOG(Error, "Loading asset header data mismatch! Expected Type Name: {0}, loaded header: {1}.\nSource: {2}", e.TypeName, data.Header.ToString(), ToString());
}
-
#endif
return false;
@@ -1337,7 +1341,14 @@ bool FlaxStorage::CloseFileHandles()
return true; // Failed, someone is still accessing the file
// Close file handles (from all threads)
- _file.DeleteAll();
+ Array streams;
+ _file.GetValues(streams);
+ for (FileReadStream* stream : streams)
+ {
+ if (stream)
+ Delete(stream);
+ }
+ _file.Clear();
return false;
}
diff --git a/Source/Engine/Content/Storage/FlaxStorage.h b/Source/Engine/Content/Storage/FlaxStorage.h
index 77c912c5a..842511430 100644
--- a/Source/Engine/Content/Storage/FlaxStorage.h
+++ b/Source/Engine/Content/Storage/FlaxStorage.h
@@ -94,7 +94,7 @@ protected:
CriticalSection _loadLocker;
// Storage
- ThreadLocalObject _file;
+ ThreadLocal _file;
Array _chunks;
// Metadata
diff --git a/Source/Engine/Content/Storage/FlaxStorageReference.h b/Source/Engine/Content/Storage/FlaxStorageReference.h
index 03f4d0c8d..e4a5df079 100644
--- a/Source/Engine/Content/Storage/FlaxStorageReference.h
+++ b/Source/Engine/Content/Storage/FlaxStorageReference.h
@@ -58,17 +58,17 @@ public:
return _storage != nullptr;
}
- FORCE_INLINE bool operator ==(const FlaxStorageReference& other) const
+ FORCE_INLINE bool operator==(const FlaxStorageReference& other) const
{
return _storage == other._storage;
}
- FORCE_INLINE bool operator !=(const FlaxStorageReference& other) const
+ FORCE_INLINE bool operator!=(const FlaxStorageReference& other) const
{
return _storage != other._storage;
}
- FORCE_INLINE FlaxStorage* operator ->() const
+ FORCE_INLINE FlaxStorage* operator->() const
{
return _storage;
}
diff --git a/Source/Engine/ContentImporters/CreateAnimationGraph.cpp b/Source/Engine/ContentImporters/CreateAnimationGraph.cpp
index 695e5c2a7..e9c88b68c 100644
--- a/Source/Engine/ContentImporters/CreateAnimationGraph.cpp
+++ b/Source/Engine/ContentImporters/CreateAnimationGraph.cpp
@@ -20,7 +20,7 @@ CreateAssetResult CreateAnimationGraph::Create(CreateAssetContext& context)
rootNode.Type = GRAPH_NODE_MAKE_TYPE(9, 1);
rootNode.ID = 1;
rootNode.Values.Resize(1);
- rootNode.Values[0] = (int32)RootMotionMode::NoExtraction;
+ rootNode.Values[0] = (int32)RootMotionExtraction::NoExtraction;
rootNode.Boxes.Resize(1);
rootNode.Boxes[0] = AnimGraphBox(&rootNode, 0, VariantType::Void);
diff --git a/Source/Engine/Core/Collections/Sorting.cpp b/Source/Engine/Core/Collections/Sorting.cpp
index 49ce0f3d4..85c0fc9f1 100644
--- a/Source/Engine/Core/Collections/Sorting.cpp
+++ b/Source/Engine/Core/Collections/Sorting.cpp
@@ -5,11 +5,14 @@
#include "Engine/Threading/ThreadLocal.h"
// Use a cached storage for the sorting (one per thread to reduce locking)
-ThreadLocal SortingStacks;
+ThreadLocal SortingStacks;
Sorting::SortingStack& Sorting::SortingStack::Get()
{
- return SortingStacks.Get();
+ SortingStack*& stack = SortingStacks.Get();
+ if (!stack)
+ stack = New();
+ return *stack;
}
Sorting::SortingStack::SortingStack()
diff --git a/Source/Engine/Core/Config/LayersAndTagsSettings.cs b/Source/Engine/Core/Config/LayersAndTagsSettings.cs
index 008f70504..d5ebbe19b 100644
--- a/Source/Engine/Core/Config/LayersAndTagsSettings.cs
+++ b/Source/Engine/Core/Config/LayersAndTagsSettings.cs
@@ -18,7 +18,7 @@ namespace FlaxEditor.Content.Settings
///
/// The layers names.
///
- [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true)]
+ [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true, Display = CollectionAttribute.DisplayType.Inline)]
public string[] Layers = new string[32];
///
diff --git a/Source/Engine/Core/Math/Matrix3x3.cs b/Source/Engine/Core/Math/Matrix3x3.cs
index f4487bb1e..44af4542b 100644
--- a/Source/Engine/Core/Math/Matrix3x3.cs
+++ b/Source/Engine/Core/Math/Matrix3x3.cs
@@ -56,13 +56,7 @@ using System.Runtime.InteropServices;
namespace FlaxEngine
{
- ///
- /// Represents a 3x3 Matrix ( contains only Scale and Rotation ).
- ///
- [Serializable]
- [StructLayout(LayoutKind.Sequential, Pack = 4)]
- // ReSharper disable once InconsistentNaming
- public struct Matrix3x3 : IEquatable, IFormattable
+ partial struct Matrix3x3 : IEquatable, IFormattable
{
///
/// The size of the type, in bytes.
@@ -135,9 +129,7 @@ namespace FlaxEngine
/// The value that will be assigned to all components.
public Matrix3x3(float value)
{
- M11 = M12 = M13 =
- M21 = M22 = M23 =
- M31 = M32 = M33 = value;
+ M11 = M12 = M13 = M21 = M22 = M23 = M31 = M32 = M33 = value;
}
///
diff --git a/Source/Engine/Core/Math/Matrix3x3.h b/Source/Engine/Core/Math/Matrix3x3.h
index 344f68455..0680e7735 100644
--- a/Source/Engine/Core/Math/Matrix3x3.h
+++ b/Source/Engine/Core/Math/Matrix3x3.h
@@ -9,8 +9,9 @@
///
/// Represents a 3x3 mathematical matrix.
///
-API_STRUCT(InBuild) struct FLAXENGINE_API Matrix3x3
+API_STRUCT() struct FLAXENGINE_API Matrix3x3
{
+ DECLARE_SCRIPTING_TYPE_MINIMAL(Matrix3x3);
public:
union
{
diff --git a/Source/Engine/Core/Types/DateTime.cpp b/Source/Engine/Core/Types/DateTime.cpp
index a0d873b43..85b42378c 100644
--- a/Source/Engine/Core/Types/DateTime.cpp
+++ b/Source/Engine/Core/Types/DateTime.cpp
@@ -12,22 +12,22 @@ const int32 CachedDaysToMonth[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273,
DateTime::DateTime(int32 year, int32 month, int32 day, int32 hour, int32 minute, int32 second, int32 millisecond)
{
ASSERT_LOW_LAYER(Validate(year, month, day, hour, minute, second, millisecond));
- int32 totalDays = 0;
+ int32 daysSum = 0;
if (month > 2 && IsLeapYear(year))
- totalDays++;
+ daysSum++;
year--;
month--;
- totalDays += year * 365 + year / 4 - year / 100 + year / 400 + CachedDaysToMonth[month] + day - 1;
- Ticks = totalDays * Constants::TicksPerDay
- + hour * Constants::TicksPerHour
- + minute * Constants::TicksPerMinute
- + second * Constants::TicksPerSecond
- + millisecond * Constants::TicksPerMillisecond;
+ daysSum += year * 365 + year / 4 - year / 100 + year / 400 + CachedDaysToMonth[month] + day - 1;
+ Ticks = daysSum * TimeSpan::TicksPerDay
+ + hour * TimeSpan::TicksPerHour
+ + minute * TimeSpan::TicksPerMinute
+ + second * TimeSpan::TicksPerSecond
+ + millisecond * TimeSpan::TicksPerMillisecond;
}
DateTime DateTime::GetDate() const
{
- return DateTime(Ticks - Ticks % Constants::TicksPerDay);
+ return DateTime(Ticks - Ticks % TimeSpan::TicksPerDay);
}
void DateTime::GetDate(int32& year, int32& month, int32& day) const
@@ -35,8 +35,7 @@ void DateTime::GetDate(int32& year, int32& month, int32& day) const
// Based on:
// Fliegel, H. F. and van Flandern, T. C.,
// Communications of the ACM, Vol. 11, No. 10 (October 1968).
-
- int32 l = Math::FloorToInt(static_cast(GetJulianDay() + 0.5)) + 68569;
+ int32 l = Math::FloorToInt((float)(GetDate().GetJulianDay() + 0.5)) + 68569;
const int32 n = 4 * l / 146097;
l = l - (146097 * n + 3) / 4;
int32 i = 4000 * (l + 1) / 1461001;
@@ -46,7 +45,6 @@ void DateTime::GetDate(int32& year, int32& month, int32& day) const
l = j / 11;
j = j + 2 - 12 * l;
i = 100 * (n - 49) + i + l;
-
year = i;
month = j;
day = k;
@@ -61,7 +59,7 @@ int32 DateTime::GetDay() const
DayOfWeek DateTime::GetDayOfWeek() const
{
- return static_cast((Ticks / Constants::TicksPerDay) % 7);
+ return static_cast((Ticks / TimeSpan::TicksPerDay) % 7);
}
int32 DateTime::GetDayOfYear() const
@@ -75,7 +73,7 @@ int32 DateTime::GetDayOfYear() const
int32 DateTime::GetHour() const
{
- return static_cast(Ticks / Constants::TicksPerHour % 24);
+ return static_cast(Ticks / TimeSpan::TicksPerHour % 24);
}
int32 DateTime::GetHour12() const
@@ -90,7 +88,7 @@ int32 DateTime::GetHour12() const
double DateTime::GetJulianDay() const
{
- return 1721425.5 + static_cast(Ticks) / Constants::TicksPerDay;
+ return 1721425.5 + static_cast(Ticks) / TimeSpan::TicksPerDay;
}
double DateTime::GetModifiedJulianDay() const
@@ -100,12 +98,12 @@ double DateTime::GetModifiedJulianDay() const
int32 DateTime::GetMillisecond() const
{
- return static_cast(Ticks / Constants::TicksPerMillisecond % 1000);
+ return static_cast(Ticks / TimeSpan::TicksPerMillisecond % 1000);
}
int32 DateTime::GetMinute() const
{
- return static_cast(Ticks / Constants::TicksPerMinute % 60);
+ return static_cast(Ticks / TimeSpan::TicksPerMinute % 60);
}
int32 DateTime::GetMonth() const
@@ -122,12 +120,12 @@ MonthOfYear DateTime::GetMonthOfYear() const
int32 DateTime::GetSecond() const
{
- return static_cast(Ticks / Constants::TicksPerSecond % 60);
+ return static_cast(Ticks / TimeSpan::TicksPerSecond % 60);
}
TimeSpan DateTime::GetTimeOfDay() const
{
- return TimeSpan(Ticks % Constants::TicksPerDay);
+ return TimeSpan(Ticks % TimeSpan::TicksPerDay);
}
int32 DateTime::GetYear() const
@@ -137,11 +135,6 @@ int32 DateTime::GetYear() const
return year;
}
-int32 DateTime::ToUnixTimestamp() const
-{
- return static_cast((Ticks - DateTime(1970, 1, 1).Ticks) / Constants::TicksPerSecond);
-}
-
int32 DateTime::DaysInMonth(int32 year, int32 month)
{
ASSERT_LOW_LAYER((month >= 1) && (month <= 12));
@@ -155,16 +148,6 @@ int32 DateTime::DaysInYear(int32 year)
return IsLeapYear(year) ? 366 : 365;
}
-DateTime DateTime::FromJulianDay(double julianDay)
-{
- return DateTime(static_cast((julianDay - 1721425.5) * Constants::TicksPerDay));
-}
-
-DateTime DateTime::FromUnixTimestamp(int32 unixTime)
-{
- return DateTime(1970, 1, 1) + TimeSpan(static_cast(unixTime) * Constants::TicksPerSecond);
-}
-
bool DateTime::IsLeapYear(int32 year)
{
if ((year % 4) == 0)
@@ -176,7 +159,7 @@ bool DateTime::IsLeapYear(int32 year)
DateTime DateTime::MaxValue()
{
- return DateTime(3652059 * Constants::TicksPerDay - 1);
+ return DateTime(3652059 * TimeSpan::TicksPerDay - 1);
}
DateTime DateTime::Now()
diff --git a/Source/Engine/Core/Types/DateTime.h b/Source/Engine/Core/Types/DateTime.h
index 67b2d70ff..a89a69fcf 100644
--- a/Source/Engine/Core/Types/DateTime.h
+++ b/Source/Engine/Core/Types/DateTime.h
@@ -199,11 +199,6 @@ public:
///
int32 GetYear() const;
- ///
- /// Gets this date as the number of seconds since the Unix Epoch (January 1st of 1970).
- ///
- int32 ToUnixTimestamp() const;
-
public:
///
/// Gets the number of days in the year and month.
@@ -220,20 +215,6 @@ public:
/// The number of days.
static int32 DaysInYear(int32 year);
- ///
- /// Returns the proleptic Gregorian date for the given Julian Day.
- ///
- /// The Julian Day.
- /// Gregorian date and time.
- static DateTime FromJulianDay(double julianDay);
-
- ///
- /// Returns the date from Unix time (seconds from midnight 1970-01-01).
- ///
- /// The Unix time (seconds from midnight 1970-01-01).
- /// The Gregorian date and time.
- static DateTime FromUnixTimestamp(int32 unixTime);
-
///
/// Determines whether the specified year is a leap year.
///
diff --git a/Source/Engine/Core/Types/TimeSpan.cpp b/Source/Engine/Core/Types/TimeSpan.cpp
index 0e4aed407..287097672 100644
--- a/Source/Engine/Core/Types/TimeSpan.cpp
+++ b/Source/Engine/Core/Types/TimeSpan.cpp
@@ -6,38 +6,53 @@
TimeSpan TimeSpan::FromDays(double days)
{
ASSERT_LOW_LAYER((days >= MinValue().GetTotalDays()) && (days <= MaxValue().GetTotalDays()));
- return TimeSpan(static_cast(days * Constants::TicksPerDay));
+ return TimeSpan(static_cast(days * TicksPerDay));
}
TimeSpan TimeSpan::FromHours(double hours)
{
ASSERT_LOW_LAYER((hours >= MinValue().GetTotalHours()) && (hours <= MaxValue().GetTotalHours()));
- return TimeSpan(static_cast(hours * Constants::TicksPerHour));
+ return TimeSpan(static_cast(hours * TicksPerHour));
}
TimeSpan TimeSpan::FromMilliseconds(double milliseconds)
{
ASSERT_LOW_LAYER((milliseconds >= MinValue().GetTotalMilliseconds()) && (milliseconds <= MaxValue().GetTotalMilliseconds()));
- return TimeSpan(static_cast(milliseconds * Constants::TicksPerMillisecond));
+ return TimeSpan(static_cast(milliseconds * TicksPerMillisecond));
}
TimeSpan TimeSpan::FromMinutes(double minutes)
{
ASSERT_LOW_LAYER((minutes >= MinValue().GetTotalMinutes()) && (minutes <= MaxValue().GetTotalMinutes()));
- return TimeSpan(static_cast(minutes * Constants::TicksPerMinute));
+ return TimeSpan(static_cast(minutes * TicksPerMinute));
}
TimeSpan TimeSpan::FromSeconds(double seconds)
{
ASSERT_LOW_LAYER((seconds >= MinValue().GetTotalSeconds()) && (seconds <= MaxValue().GetTotalSeconds()));
- return TimeSpan(static_cast(seconds * Constants::TicksPerSecond));
+ return TimeSpan(static_cast(seconds * TicksPerSecond));
+}
+
+TimeSpan TimeSpan::MaxValue()
+{
+ return TimeSpan(9223372036854775807);
+}
+
+TimeSpan TimeSpan::MinValue()
+{
+ return TimeSpan(-9223372036854775807 - 1);
+}
+
+TimeSpan TimeSpan::Zero()
+{
+ return TimeSpan(0);
}
void TimeSpan::Set(int32 days, int32 hours, int32 minutes, int32 seconds, int32 milliseconds)
{
const int64 totalMs = 1000 * (60 * 60 * 24 * (int64)days + 60 * 60 * (int64)hours + 60 * (int64)minutes + (int64)seconds) + (int64)milliseconds;
ASSERT_LOW_LAYER((totalMs >= MinValue().GetTotalMilliseconds()) && (totalMs <= MaxValue().GetTotalMilliseconds()));
- Ticks = totalMs * Constants::TicksPerMillisecond;
+ Ticks = totalMs * TicksPerMillisecond;
}
String TimeSpan::ToString() const
diff --git a/Source/Engine/Core/Types/TimeSpan.h b/Source/Engine/Core/Types/TimeSpan.h
index 7545a21c0..f14a622a0 100644
--- a/Source/Engine/Core/Types/TimeSpan.h
+++ b/Source/Engine/Core/Types/TimeSpan.h
@@ -6,32 +6,30 @@
#include "Engine/Core/Formatting.h"
#include "Engine/Core/Templates.h"
-namespace Constants
-{
- // The number of timespan ticks per day.
- const int64 TicksPerDay = 864000000000;
-
- // The number of timespan ticks per hour.
- const int64 TicksPerHour = 36000000000;
-
- // The number of timespan ticks per millisecond.
- const int64 TicksPerMillisecond = 10000;
-
- // The number of timespan ticks per minute.
- const int64 TicksPerMinute = 600000000;
-
- // The number of timespan ticks per second.
- const int64 TicksPerSecond = 10000000;
-
- // The number of timespan ticks per week.
- const int64 TicksPerWeek = 6048000000000;
-}
-
///
/// Represents the difference between two dates and times.
///
API_STRUCT(InBuild, Namespace="System") struct FLAXENGINE_API TimeSpan
{
+public:
+ // The number of timespan ticks per day.
+ static constexpr int64 TicksPerDay = 864000000000;
+
+ // The number of timespan ticks per hour.
+ static constexpr int64 TicksPerHour = 36000000000;
+
+ // The number of timespan ticks per millisecond.
+ static constexpr int64 TicksPerMillisecond = 10000;
+
+ // The number of timespan ticks per minute.
+ static constexpr int64 TicksPerMinute = 600000000;
+
+ // The number of timespan ticks per second.
+ static constexpr int64 TicksPerSecond = 10000000;
+
+ // The number of timespan ticks per week.
+ static constexpr int64 TicksPerWeek = 6048000000000;
+
public:
///
/// Time span in 100 nanoseconds resolution.
@@ -170,7 +168,7 @@ public:
///
FORCE_INLINE int32 GetDays() const
{
- return (int32)(Ticks / Constants::TicksPerDay);
+ return (int32)(Ticks / TicksPerDay);
}
///
@@ -186,7 +184,7 @@ public:
///
FORCE_INLINE int32 GetHours() const
{
- return (int32)(Ticks / Constants::TicksPerHour % 24);
+ return (int32)(Ticks / TicksPerHour % 24);
}
///
@@ -194,7 +192,7 @@ public:
///
FORCE_INLINE int32 GetMilliseconds() const
{
- return (int32)(Ticks / Constants::TicksPerMillisecond % 1000);
+ return (int32)(Ticks / TicksPerMillisecond % 1000);
}
///
@@ -202,7 +200,7 @@ public:
///
FORCE_INLINE int32 GetMinutes() const
{
- return (int32)(Ticks / Constants::TicksPerMinute % 60);
+ return (int32)(Ticks / TicksPerMinute % 60);
}
///
@@ -210,7 +208,7 @@ public:
///
FORCE_INLINE int32 GetSeconds() const
{
- return (int32)(Ticks / Constants::TicksPerSecond % 60);
+ return (int32)(Ticks / TicksPerSecond % 60);
}
///
@@ -218,7 +216,7 @@ public:
///
FORCE_INLINE double GetTotalDays() const
{
- return (double)Ticks / Constants::TicksPerDay;
+ return (double)Ticks / TicksPerDay;
}
///
@@ -226,7 +224,7 @@ public:
///
FORCE_INLINE double GetTotalHours() const
{
- return (double)Ticks / Constants::TicksPerHour;
+ return (double)Ticks / TicksPerHour;
}
///
@@ -234,7 +232,7 @@ public:
///
FORCE_INLINE double GetTotalMilliseconds() const
{
- return (double)Ticks / Constants::TicksPerMillisecond;
+ return (double)Ticks / TicksPerMillisecond;
}
///
@@ -242,7 +240,7 @@ public:
///
FORCE_INLINE double GetTotalMinutes() const
{
- return (double)Ticks / Constants::TicksPerMinute;
+ return (double)Ticks / TicksPerMinute;
}
///
@@ -250,7 +248,7 @@ public:
///
FORCE_INLINE float GetTotalSeconds() const
{
- return static_cast(Ticks) / Constants::TicksPerSecond;
+ return static_cast(Ticks) / TicksPerSecond;
}
public:
@@ -293,29 +291,17 @@ public:
///
/// Returns the maximum time span value.
///
- /// The time span.
- static TimeSpan MaxValue()
- {
- return TimeSpan(9223372036854775807);
- }
+ static TimeSpan MaxValue();
///
/// Returns the minimum time span value.
///
- /// The time span.
- static TimeSpan MinValue()
- {
- return TimeSpan(-9223372036854775807 - 1);
- }
+ static TimeSpan MinValue();
///
/// Returns the zero time span value.
///
- /// The time span.
- static TimeSpan Zero()
- {
- return TimeSpan(0);
- }
+ static TimeSpan Zero();
private:
void Set(int32 days, int32 hours, int32 minutes, int32 seconds, int32 milliseconds);
diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp
index 952e648e6..9e8b69b2c 100644
--- a/Source/Engine/Core/Types/Variant.cpp
+++ b/Source/Engine/Core/Types/Variant.cpp
@@ -139,6 +139,24 @@ VariantType::VariantType(const StringAnsiView& typeName)
return;
}
}
+ {
+ // Aliases
+ if (typeName == "FlaxEngine.Vector2")
+ {
+ new(this) VariantType(Vector2);
+ return;
+ }
+ if (typeName == "FlaxEngine.Vector3")
+ {
+ new(this) VariantType(Vector3);
+ return;
+ }
+ if (typeName == "FlaxEngine.Vector4")
+ {
+ new(this) VariantType(Vector4);
+ return;
+ }
+ }
// Check case for array
if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive))
@@ -3985,15 +4003,32 @@ void Variant::CopyStructure(void* src)
{
if (AsBlob.Data && src)
{
- const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(Type.TypeName));
+ const StringAnsiView typeName(Type.TypeName);
+ const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName);
if (typeHandle)
{
auto& type = typeHandle.GetType();
type.Struct.Copy(AsBlob.Data, src);
}
+#if USE_CSHARP
+ else if (const auto mclass = Scripting::FindClass(typeName))
+ {
+ // Fallback to C#-only types
+ MCore::Thread::Attach();
+ if (MANAGED_GC_HANDLE && mclass->IsValueType())
+ {
+ MObject* instance = MCore::GCHandle::GetTarget(MANAGED_GC_HANDLE);
+ void* data = MCore::Object::Unbox(instance);
+ Platform::MemoryCopy(data, src, mclass->GetInstanceSize());
+ }
+ }
+#endif
else
{
- Platform::MemoryCopy(AsBlob.Data, src, AsBlob.Length);
+ if (typeName.Length() != 0)
+ {
+ LOG(Warning, "Missing scripting type \'{0}\'", String(typeName));
+ }
}
}
}
diff --git a/Source/Engine/Engine/EngineService.cpp b/Source/Engine/Engine/EngineService.cpp
index 7eea66853..c400ef943 100644
--- a/Source/Engine/Engine/EngineService.cpp
+++ b/Source/Engine/Engine/EngineService.cpp
@@ -72,9 +72,6 @@ void EngineService::OnInit()
// Init services from front to back
auto& services = GetServices();
-#if TRACY_ENABLE
- Char nameBuffer[100];
-#endif
for (int32 i = 0; i < services.Count(); i++)
{
const auto service = services[i];
@@ -82,6 +79,7 @@ void EngineService::OnInit()
#if TRACY_ENABLE
ZoneScoped;
int32 nameBufferLength = 0;
+ Char nameBuffer[100];
for (int32 j = 0; j < name.Length(); j++)
if (name[j] != ' ')
nameBuffer[nameBufferLength++] = name[j];
@@ -114,6 +112,18 @@ void EngineService::OnDispose()
const auto service = services[i];
if (service->IsInitialized)
{
+#if TRACY_ENABLE
+ ZoneScoped;
+ const StringView name(service->Name);
+ int32 nameBufferLength = 0;
+ Char nameBuffer[100];
+ for (int32 j = 0; j < name.Length(); j++)
+ if (name[j] != ' ')
+ nameBuffer[nameBufferLength++] = name[j];
+ Platform::MemoryCopy(nameBuffer + nameBufferLength, TEXT("::Dispose"), 10 * sizeof(Char));
+ nameBufferLength += 10;
+ ZoneName(nameBuffer, nameBufferLength);
+#endif
service->IsInitialized = false;
service->Dispose();
}
diff --git a/Source/Engine/Engine/Time.cpp b/Source/Engine/Engine/Time.cpp
index 3adccdbd9..bb0c856f8 100644
--- a/Source/Engine/Engine/Time.cpp
+++ b/Source/Engine/Engine/Time.cpp
@@ -67,7 +67,7 @@ void Time::TickData::OnBeforeRun(float targetFps, double currentTime)
{
Time = UnscaledTime = TimeSpan::Zero();
DeltaTime = UnscaledDeltaTime = targetFps > ZeroTolerance ? TimeSpan::FromSeconds(1.0f / targetFps) : TimeSpan::Zero();
- LastLength = static_cast(DeltaTime.Ticks) / Constants::TicksPerSecond;
+ LastLength = static_cast(DeltaTime.Ticks) / TimeSpan::TicksPerSecond;
LastBegin = currentTime - LastLength;
LastEnd = currentTime;
NextBegin = targetFps > ZeroTolerance ? LastBegin + (1.0f / targetFps) : 0.0;
@@ -76,7 +76,7 @@ void Time::TickData::OnBeforeRun(float targetFps, double currentTime)
void Time::TickData::OnReset(float targetFps, double currentTime)
{
DeltaTime = UnscaledDeltaTime = targetFps > ZeroTolerance ? TimeSpan::FromSeconds(1.0f / targetFps) : TimeSpan::Zero();
- LastLength = static_cast(DeltaTime.Ticks) / Constants::TicksPerSecond;
+ LastLength = static_cast(DeltaTime.Ticks) / TimeSpan::TicksPerSecond;
LastBegin = currentTime - LastLength;
LastEnd = currentTime;
}
diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h
index bbf9c0de8..6ae9ad4ec 100644
--- a/Source/Engine/Graphics/Enums.h
+++ b/Source/Engine/Graphics/Enums.h
@@ -236,6 +236,27 @@ API_ENUM(Attributes="Flags") enum class ShadowsCastingMode
DECLARE_ENUM_OPERATORS(ShadowsCastingMode);
+///
+/// The partitioning mode for shadow cascades.
+///
+API_ENUM() enum class PartitionMode
+{
+ ///
+ /// Internally defined cascade splits.
+ ///
+ Manual = 0,
+
+ ///
+ /// Logarithmic cascade splits.
+ ///
+ Logarithmic = 1,
+
+ ///
+ /// PSSM cascade splits.
+ ///
+ PSSM = 2,
+};
+
///
/// Identifies expected GPU resource use during rendering. The usage directly reflects whether a resource is accessible by the CPU and/or the GPU.
///
diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp
index 6e6ae04b5..cc95ed6ba 100644
--- a/Source/Engine/Graphics/Models/ModelData.cpp
+++ b/Source/Engine/Graphics/Models/ModelData.cpp
@@ -911,10 +911,10 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
}
// Info
- stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change)
+ stream->WriteInt32(103); // Header version (for fast version upgrades without serialization format change)
stream->WriteDouble(anim.Duration);
stream->WriteDouble(anim.FramesPerSecond);
- stream->WriteBool(anim.EnableRootMotion);
+ stream->WriteByte((byte)anim.RootMotionFlags);
stream->WriteString(anim.RootNodeName, 13);
// Animation channels
@@ -928,6 +928,12 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
Serialization::Serialize(*stream, channel.Scale);
}
+ // Animation events
+ stream->WriteInt32(0);
+
+ // Nested animations
+ stream->WriteInt32(0);
+
return false;
}
diff --git a/Source/Engine/Graphics/Models/SkeletonData.h b/Source/Engine/Graphics/Models/SkeletonData.h
index 816f69c85..83908d18a 100644
--- a/Source/Engine/Graphics/Models/SkeletonData.h
+++ b/Source/Engine/Graphics/Models/SkeletonData.h
@@ -91,7 +91,7 @@ public:
FORCE_INLINE SkeletonNode& RootNode()
{
ASSERT(Nodes.HasItems());
- return Nodes[0];
+ return Nodes.Get()[0];
}
///
@@ -100,52 +100,24 @@ public:
FORCE_INLINE const SkeletonNode& RootNode() const
{
ASSERT(Nodes.HasItems());
- return Nodes[0];
+ return Nodes.Get()[0];
}
///
/// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange.
///
- void Swap(SkeletonData& other)
- {
- Nodes.Swap(other.Nodes);
- Bones.Swap(other.Bones);
- }
+ void Swap(SkeletonData& other);
- int32 FindNode(const StringView& name) const
- {
- for (int32 i = 0; i < Nodes.Count(); i++)
- {
- if (Nodes[i].Name == name)
- return i;
- }
- return -1;
- }
+ Transform GetNodeTransform(int32 nodeIndex) const;
+ void SetNodeTransform(int32 nodeIndex, const Transform& value);
- int32 FindBone(int32 nodeIndex) const
- {
- for (int32 i = 0; i < Bones.Count(); i++)
- {
- if (Bones[i].NodeIndex == nodeIndex)
- return i;
- }
- return -1;
- }
+ int32 FindNode(const StringView& name) const;
+ int32 FindBone(int32 nodeIndex) const;
- uint64 GetMemoryUsage() const
- {
- uint64 result = Nodes.Capacity() * sizeof(SkeletonNode) + Bones.Capacity() * sizeof(SkeletonBone);
- for (const auto& e : Nodes)
- result += (e.Name.Length() + 1) * sizeof(Char);
- return result;
- }
+ uint64 GetMemoryUsage() const;
///
/// Releases data.
///
- void Dispose()
- {
- Nodes.Resize(0);
- Bones.Resize(0);
- }
+ void Dispose();
};
diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp
index 48ff53549..fc0dccf8a 100644
--- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp
+++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp
@@ -18,6 +18,69 @@
#include "Engine/Threading/Task.h"
#include "Engine/Threading/Threading.h"
+void SkeletonData::Swap(SkeletonData& other)
+{
+ Nodes.Swap(other.Nodes);
+ Bones.Swap(other.Bones);
+}
+
+Transform SkeletonData::GetNodeTransform(int32 nodeIndex) const
+{
+ const int32 parentIndex = Nodes[nodeIndex].ParentIndex;
+ if (parentIndex == -1)
+ {
+ return Nodes[nodeIndex].LocalTransform;
+ }
+ const Transform parentTransform = GetNodeTransform(parentIndex);
+ return parentTransform.LocalToWorld(Nodes[nodeIndex].LocalTransform);
+}
+
+void SkeletonData::SetNodeTransform(int32 nodeIndex, const Transform& value)
+{
+ const int32 parentIndex = Nodes[nodeIndex].ParentIndex;
+ if (parentIndex == -1)
+ {
+ Nodes[nodeIndex].LocalTransform = value;
+ return;
+ }
+ const Transform parentTransform = GetNodeTransform(parentIndex);
+ parentTransform.WorldToLocal(value, Nodes[nodeIndex].LocalTransform);
+}
+
+int32 SkeletonData::FindNode(const StringView& name) const
+{
+ for (int32 i = 0; i < Nodes.Count(); i++)
+ {
+ if (Nodes[i].Name == name)
+ return i;
+ }
+ return -1;
+}
+
+int32 SkeletonData::FindBone(int32 nodeIndex) const
+{
+ for (int32 i = 0; i < Bones.Count(); i++)
+ {
+ if (Bones[i].NodeIndex == nodeIndex)
+ return i;
+ }
+ return -1;
+}
+
+uint64 SkeletonData::GetMemoryUsage() const
+{
+ uint64 result = Nodes.Capacity() * sizeof(SkeletonNode) + Bones.Capacity() * sizeof(SkeletonBone);
+ for (const auto& e : Nodes)
+ result += (e.Name.Length() + 1) * sizeof(Char);
+ return result;
+}
+
+void SkeletonData::Dispose()
+{
+ Nodes.Resize(0);
+ Bones.Resize(0);
+}
+
void SkinnedMesh::Init(SkinnedModel* model, int32 lodIndex, int32 index, int32 materialSlotIndex, const BoundingBox& box, const BoundingSphere& sphere)
{
_model = model;
diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp
index ff40cc208..7e2329389 100644
--- a/Source/Engine/Level/Actor.cpp
+++ b/Source/Engine/Level/Actor.cpp
@@ -1359,7 +1359,7 @@ bool Actor::IsPrefabRoot() const
Actor* Actor::FindActor(const StringView& name) const
{
Actor* result = nullptr;
- if (StringUtils::Compare(*_name, *name) == 0)
+ if (_name == name)
{
result = const_cast(this);
}
@@ -1393,7 +1393,7 @@ Actor* Actor::FindActor(const MClass* type) const
Actor* Actor::FindActor(const MClass* type, const StringView& name) const
{
CHECK_RETURN(type, nullptr);
- if (GetClass()->IsSubClassOf(type) && StringUtils::Compare(*_name, *name) == 0)
+ if (GetClass()->IsSubClassOf(type) && _name == name)
return const_cast(this);
for (auto child : Children)
{
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index c1b5af398..319def475 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -225,6 +225,17 @@ void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose)
_masterPose->AnimationUpdated.Bind(this);
}
+const Array& AnimatedModel::GetTraceEvents() const
+{
+#if !BUILD_RELEASE
+ if (!GetEnableTracing())
+ {
+ LOG(Warning, "Accessing AnimatedModel.TraceEvents with tracing disabled.");
+ }
+#endif
+ return GraphInstance.TraceEvents;
+}
+
#define CHECK_ANIM_GRAPH_PARAM_ACCESS() \
if (!AnimationGraph) \
{ \
@@ -494,6 +505,7 @@ void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* ani
if (slot.Animation == anim && slot.Name == slotName)
{
slot.Animation = nullptr;
+ slot.Reset = true;
break;
}
}
diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h
index 029e17b62..bd03e824e 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.h
+++ b/Source/Engine/Level/Actors/AnimatedModel.h
@@ -259,6 +259,27 @@ public:
/// The master pose actor to use.
API_FUNCTION() void SetMasterPoseModel(AnimatedModel* masterPose);
+ ///
+ /// Enables extracting animation playback insights for debugging or custom scripting.
+ ///
+ API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetEnableTracing() const
+ {
+ return GraphInstance.EnableTracing;
+ }
+
+ ///
+ /// Enables extracting animation playback insights for debugging or custom scripting.
+ ///
+ API_PROPERTY() void SetEnableTracing(bool value)
+ {
+ GraphInstance.EnableTracing = value;
+ }
+
+ ///
+ /// Gets the trace events from the last animation update. Valid only when EnableTracing is active.
+ ///
+ API_PROPERTY(Attributes="HideInEditor, NoSerialize") const Array& GetTraceEvents() const;
+
public:
///
/// Gets the anim graph instance parameters collection.
diff --git a/Source/Engine/Level/Actors/DirectionalLight.cpp b/Source/Engine/Level/Actors/DirectionalLight.cpp
index 43450fd8e..32bb61653 100644
--- a/Source/Engine/Level/Actors/DirectionalLight.cpp
+++ b/Source/Engine/Level/Actors/DirectionalLight.cpp
@@ -41,6 +41,12 @@ void DirectionalLight::Draw(RenderContext& renderContext)
data.RenderedVolumetricFog = 0;
data.ShadowsMode = ShadowsMode;
data.CascadeCount = CascadeCount;
+ data.Cascade1Spacing = Cascade1Spacing;
+ data.Cascade2Spacing = Cascade2Spacing;
+ data.Cascade3Spacing = Cascade3Spacing;
+ data.Cascade4Spacing = Cascade4Spacing;
+
+ data.PartitionMode = PartitionMode;
data.ContactShadowsLength = ContactShadowsLength;
data.StaticFlags = GetStaticFlags();
data.ID = GetID();
@@ -56,6 +62,12 @@ void DirectionalLight::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_GET_OTHER_OBJ(DirectionalLight);
SERIALIZE(CascadeCount);
+ SERIALIZE(Cascade1Spacing);
+ SERIALIZE(Cascade2Spacing);
+ SERIALIZE(Cascade3Spacing);
+ SERIALIZE(Cascade4Spacing);
+
+ SERIALIZE(PartitionMode);
}
void DirectionalLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
@@ -64,6 +76,12 @@ void DirectionalLight::Deserialize(DeserializeStream& stream, ISerializeModifier
LightWithShadow::Deserialize(stream, modifier);
DESERIALIZE(CascadeCount);
+ DESERIALIZE(Cascade1Spacing);
+ DESERIALIZE(Cascade2Spacing);
+ DESERIALIZE(Cascade3Spacing);
+ DESERIALIZE(Cascade4Spacing);
+
+ DESERIALIZE(PartitionMode);
}
bool DirectionalLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)
diff --git a/Source/Engine/Level/Actors/DirectionalLight.h b/Source/Engine/Level/Actors/DirectionalLight.h
index d5f31324a..cb29112c0 100644
--- a/Source/Engine/Level/Actors/DirectionalLight.h
+++ b/Source/Engine/Level/Actors/DirectionalLight.h
@@ -12,12 +12,42 @@ class FLAXENGINE_API DirectionalLight : public LightWithShadow
{
DECLARE_SCENE_OBJECT(DirectionalLight);
public:
+ ///
+ /// The partitioning mode for the shadow cascades.
+ ///
+ API_FIELD(Attributes = "EditorOrder(64), DefaultValue(PartitionMode.Manual), EditorDisplay(\"Shadow\")")
+ PartitionMode PartitionMode = PartitionMode::Manual;
+
///
/// The number of cascades used for slicing the range of depth covered by the light during shadow rendering. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades.
///
API_FIELD(Attributes="EditorOrder(65), DefaultValue(4), Limit(1, 4), EditorDisplay(\"Shadow\")")
int32 CascadeCount = 4;
+ ///
+ /// Percentage of the shadow distance used by the first cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(66), DefaultValue(0.05f), VisibleIf(nameof(ShowCascade1)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade1Spacing = 0.05f;
+
+ ///
+ /// Percentage of the shadow distance used by the second cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(67), DefaultValue(0.15f), VisibleIf(nameof(ShowCascade2)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade2Spacing = 0.15f;
+
+ ///
+ /// Percentage of the shadow distance used by the third cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(68), DefaultValue(0.50f), VisibleIf(nameof(ShowCascade3)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade3Spacing = 0.50f;
+
+ ///
+ /// Percentage of the shadow distance used by the fourth cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(69), DefaultValue(1.0f), VisibleIf(nameof(ShowCascade4)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade4Spacing = 1.0f;
+
public:
// [LightWithShadow]
void Draw(RenderContext& renderContext) override;
diff --git a/Source/Engine/Level/DirectionalLight.cs b/Source/Engine/Level/DirectionalLight.cs
new file mode 100644
index 000000000..201d35dc1
--- /dev/null
+++ b/Source/Engine/Level/DirectionalLight.cs
@@ -0,0 +1,10 @@
+namespace FlaxEngine
+{
+ public partial class DirectionalLight
+ {
+ bool ShowCascade1 => CascadeCount >= 1 && PartitionMode == PartitionMode.Manual;
+ bool ShowCascade2 => CascadeCount >= 2 && PartitionMode == PartitionMode.Manual;
+ bool ShowCascade3 => CascadeCount >= 3 && PartitionMode == PartitionMode.Manual;
+ bool ShowCascade4 => CascadeCount >= 4 && PartitionMode == PartitionMode.Manual;
+ }
+}
diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp
index d9c86d250..006e54aa6 100644
--- a/Source/Engine/Level/SceneObjectsFactory.cpp
+++ b/Source/Engine/Level/SceneObjectsFactory.cpp
@@ -63,7 +63,7 @@ SceneObjectsFactory::Context::~Context()
{
if (Async)
{
- Array> modifiers;
+ Array> modifiers;
Modifiers.GetValues(modifiers);
for (ISerializeModifier* e : modifiers)
{
diff --git a/Source/Engine/Localization/LocalizationSettings.h b/Source/Engine/Localization/LocalizationSettings.h
index e2c546c8b..22a4f03e4 100644
--- a/Source/Engine/Localization/LocalizationSettings.h
+++ b/Source/Engine/Localization/LocalizationSettings.h
@@ -16,7 +16,7 @@ public:
///
/// The list of the string localization tables used by the game.
///
- API_FIELD()
+ API_FIELD(Attributes="Collection(Display = CollectionAttribute.DisplayType.Inline)")
Array> LocalizedStringTables;
///
diff --git a/Source/Engine/Networking/NetworkConfig.h b/Source/Engine/Networking/NetworkConfig.h
index b92d822cb..0768d3402 100644
--- a/Source/Engine/Networking/NetworkConfig.h
+++ b/Source/Engine/Networking/NetworkConfig.h
@@ -43,7 +43,7 @@ API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkConfi
///
/// Object is managed by the created network peer (will be deleted on peer shutdown).
API_FIELD()
- ScriptingObject* NetworkDriver;
+ ScriptingObject* NetworkDriver = nullptr;
///
/// The upper limit on how many peers can join when we're listening.
diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp
index c6311cd7a..407bd8c58 100644
--- a/Source/Engine/Networking/NetworkReplicator.cpp
+++ b/Source/Engine/Networking/NetworkReplicator.cpp
@@ -714,6 +714,7 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b
stream->SenderId = senderClientId;
// Deserialize object
+ Scripting::ObjectsLookupIdMapping.Set(&IdsRemappingTable);
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, false);
if (failed)
{
@@ -2151,6 +2152,18 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie
NetworkMessageObjectRpc msgData;
event.Message.ReadStructure(msgData);
ScopeLock lock(ObjectsLock);
+
+ // Find RPC info
+ NetworkRpcName name;
+ name.First = Scripting::FindScriptingType(msgData.RpcTypeName);
+ name.Second = msgData.RpcName;
+ const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name);
+ if (!info)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(msgData.RpcTypeName), String(msgData.RpcName), msgData.ObjectId);
+ return;
+ }
+
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.ObjectTypeName);
if (e)
{
@@ -2159,17 +2172,7 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie
if (!obj)
return;
- // Find RPC info
- NetworkRpcName name;
- name.First = Scripting::FindScriptingType(msgData.RpcTypeName);
- name.Second = msgData.RpcName;
- const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name);
- if (!info)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(msgData.RpcTypeName), String(msgData.RpcName), msgData.ObjectId);
- return;
- }
-
+
// Validate RPC
if (info->Server && NetworkManager::IsClient())
{
@@ -2192,7 +2195,7 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie
// Execute RPC
info->Execute(obj, stream, info->Tag);
}
- else
+ else if(info->Channel != static_cast(NetworkChannelType::Unreliable) && info->Channel != static_cast(NetworkChannelType::UnreliableOrdered))
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", msgData.ObjectId, String(msgData.RpcTypeName), String(msgData.RpcName));
}
diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp
index 6ea4b8b4e..f198fdcd7 100644
--- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp
+++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp
@@ -52,7 +52,7 @@ namespace
int32 ParticleEmitterGraphCPUExecutor::ProcessSpawnModule(int32 index)
{
const auto node = _graph.SpawnModules[index];
- auto& context = Context.Get();
+ auto& context = *Context.Get();
auto& data = context.Data->SpawnModulesData[index];
// Accumulate the previous frame fraction
@@ -120,7 +120,7 @@ int32 ParticleEmitterGraphCPUExecutor::ProcessSpawnModule(int32 index)
void ParticleEmitterGraphCPUExecutor::ProcessModule(ParticleEmitterGraphCPUNode* node, int32 particlesStart, int32 particlesEnd)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
auto stride = context.Data->Buffer->Stride;
auto start = context.Data->Buffer->GetParticleCPU(particlesStart);
diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp
index cb2d7004e..4c30746c3 100644
--- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp
+++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp
@@ -12,7 +12,7 @@
void ParticleEmitterGraphCPUExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
switch (node->TypeID)
{
// Get
@@ -168,7 +168,7 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupTextures(Box* box, Node* node,
void ParticleEmitterGraphCPUExecutor::ProcessGroupTools(Box* box, Node* node, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
switch (node->TypeID)
{
// Linearize Depth
@@ -202,7 +202,7 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupTools(Box* box, Node* node, Va
void ParticleEmitterGraphCPUExecutor::ProcessGroupParticles(Box* box, Node* nodeBase, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
auto node = (ParticleEmitterGraphCPUNode*)nodeBase;
switch (node->TypeID)
{
@@ -468,7 +468,7 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupParticles(Box* box, Node* node
void ParticleEmitterGraphCPUExecutor::ProcessGroupFunction(Box* box, Node* node, Value& value)
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
switch (node->TypeID)
{
// Function Input
diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp
index a8f7898e1..12ecd054b 100644
--- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp
+++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp
@@ -8,7 +8,7 @@
#include "Engine/Engine/Time.h"
#include "Engine/Profiler/ProfilerCPU.h"
-ThreadLocal ParticleEmitterGraphCPUExecutor::Context;
+ThreadLocal ParticleEmitterGraphCPUExecutor::Context;
namespace
{
@@ -122,7 +122,10 @@ ParticleEmitterGraphCPUExecutor::ParticleEmitterGraphCPUExecutor(ParticleEmitter
void ParticleEmitterGraphCPUExecutor::Init(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, float dt)
{
- auto& context = Context.Get();
+ auto& contextPtr = Context.Get();
+ if (!contextPtr)
+ contextPtr = New();
+ auto& context = *contextPtr;
context.GraphStack.Clear();
context.GraphStack.Push(&_graph);
context.Data = &data;
@@ -252,8 +255,8 @@ bool ParticleEmitterGraphCPUExecutor::ComputeBounds(ParticleEmitter* emitter, Pa
case 401:
{
// Prepare graph data
- auto& context = Context.Get();
Init(emitter, effect, data);
+ auto& context = *Context.Get();
// Find the maximum radius of the particle light
float maxRadius = 0.0f;
@@ -377,7 +380,7 @@ void ParticleEmitterGraphCPUExecutor::Draw(ParticleEmitter* emitter, ParticleEff
// Prepare graph data
Init(emitter, effect, data);
- auto& context = Context.Get();
+ auto& context = *Context.Get();
// Draw lights
for (int32 moduleIndex = 0; moduleIndex < emitter->Graph.LightModules.Count(); moduleIndex++)
@@ -571,7 +574,6 @@ int32 ParticleEmitterGraphCPUExecutor::UpdateSpawn(ParticleEmitter* emitter, Par
PROFILE_CPU_NAMED("Spawn");
// Prepare data
- auto& context = Context.Get();
Init(emitter, effect, data, dt);
// Spawn particles
@@ -587,7 +589,7 @@ int32 ParticleEmitterGraphCPUExecutor::UpdateSpawn(ParticleEmitter* emitter, Par
VisjectExecutor::Value ParticleEmitterGraphCPUExecutor::eatBox(Node* caller, Box* box)
{
// Check if graph is looped or is too deep
- auto& context = Context.Get();
+ auto& context = *Context.Get();
if (context.CallStackSize >= PARTICLE_EMITTER_MAX_CALL_STACK)
{
OnError(caller, box, TEXT("Graph is looped or too deep!"));
@@ -618,6 +620,6 @@ VisjectExecutor::Value ParticleEmitterGraphCPUExecutor::eatBox(Node* caller, Box
VisjectExecutor::Graph* ParticleEmitterGraphCPUExecutor::GetCurrentGraph() const
{
- auto& context = Context.Get();
+ auto& context = *Context.Get();
return (Graph*)context.GraphStack.Peek();
}
diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h
index 4f55da6e2..34a65d721 100644
--- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h
+++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h
@@ -133,7 +133,7 @@ private:
ParticleEmitterGraphCPU& _graph;
// Per-thread context to allow async execution
- static ThreadLocal Context;
+ static ThreadLocal Context;
public:
///
diff --git a/Source/Engine/Physics/Actors/RigidBody.cpp b/Source/Engine/Physics/Actors/RigidBody.cpp
index afbac6611..71151866a 100644
--- a/Source/Engine/Physics/Actors/RigidBody.cpp
+++ b/Source/Engine/Physics/Actors/RigidBody.cpp
@@ -300,6 +300,44 @@ void RigidBody::ClosestPoint(const Vector3& position, Vector3& result) const
}
}
+void RigidBody::AddMovement(const Vector3& translation, const Quaternion& rotation)
+{
+ // filter rotation according to constraints
+ Quaternion allowedRotation;
+ if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockRotation))
+ allowedRotation = Quaternion::Identity;
+ else
+ {
+ Float3 euler = rotation.GetEuler();
+ if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationX))
+ euler.X = 0;
+ if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationY))
+ euler.Y = 0;
+ if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationZ))
+ euler.Z = 0;
+ allowedRotation = Quaternion::Euler(euler);
+ }
+
+ // filter translation according to the constraints
+ auto allowedTranslation = translation;
+ if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockPosition))
+ allowedTranslation = Vector3::Zero;
+ else
+ {
+ if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionX))
+ allowedTranslation.X = 0;
+ if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionY))
+ allowedTranslation.Y = 0;
+ if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionZ))
+ allowedTranslation.Z = 0;
+ }
+ Transform t;
+ t.Translation = _transform.Translation + allowedTranslation;
+ t.Orientation = _transform.Orientation * allowedRotation;
+ t.Scale = _transform.Scale;
+ SetTransform(t);
+}
+
void RigidBody::OnCollisionEnter(const Collision& c)
{
CollisionEnter(c);
diff --git a/Source/Engine/Physics/Actors/RigidBody.h b/Source/Engine/Physics/Actors/RigidBody.h
index 2ef50ca7a..ca1428810 100644
--- a/Source/Engine/Physics/Actors/RigidBody.h
+++ b/Source/Engine/Physics/Actors/RigidBody.h
@@ -486,6 +486,7 @@ public:
// [Actor]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
+ void AddMovement(const Vector3& translation, const Quaternion& rotation) override;
// [IPhysicsActor]
void* GetPhysicsActor() const override;
diff --git a/Source/Engine/Physics/Collisions.h b/Source/Engine/Physics/Collisions.h
index 7336e8d5c..230337bc9 100644
--- a/Source/Engine/Physics/Collisions.h
+++ b/Source/Engine/Physics/Collisions.h
@@ -9,7 +9,7 @@ class PhysicsColliderActor;
///
/// Contains a contact point data for the collision location.
///
-API_STRUCT() struct FLAXENGINE_API ContactPoint
+API_STRUCT(NoDefault) struct FLAXENGINE_API ContactPoint
{
DECLARE_SCRIPTING_TYPE_MINIMAL(ContactPoint);
@@ -41,7 +41,7 @@ struct TIsPODType
///
/// Contains a collision information passed to the OnCollisionEnter/OnCollisionExit events.
///
-API_STRUCT() struct FLAXENGINE_API Collision
+API_STRUCT(NoDefault) struct FLAXENGINE_API Collision
{
DECLARE_SCRIPTING_TYPE_MINIMAL(Collision);
@@ -81,7 +81,7 @@ API_STRUCT() struct FLAXENGINE_API Collision
///
/// The contacts locations.
///
- API_FIELD(Private, NoArray) ContactPoint Contacts[COLLISION_NAX_CONTACT_POINTS];
+ API_FIELD(Internal, NoArray) ContactPoint Contacts[COLLISION_NAX_CONTACT_POINTS];
public:
///
diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
index cbc381142..99a5abc56 100644
--- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
+++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
@@ -2260,7 +2260,13 @@ void PhysicsBackend::SetRigidActorPose(void* actor, const Vector3& position, con
if (kinematic)
{
auto actorPhysX = (PxRigidDynamic*)actor;
- actorPhysX->setKinematicTarget(trans);
+ if (actorPhysX->getActorFlags() & PxActorFlag::eDISABLE_SIMULATION)
+ {
+ // Ensures the disabled kinematic actor ends up in the correct pose after enabling simulation
+ actorPhysX->setGlobalPose(trans, wakeUp);
+ }
+ else
+ actorPhysX->setKinematicTarget(trans);
}
else
{
@@ -3306,7 +3312,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto drive4W = PxVehicleDrive4W::allocate(wheels.Count());
drive4W->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, Math::Max(wheels.Count() - 4, 0));
- drive4W->setToRestState();
drive4W->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
drive4W->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
vehicle = drive4W;
@@ -3349,7 +3354,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto driveNW = PxVehicleDriveNW::allocate(wheels.Count());
driveNW->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, wheels.Count());
- driveNW->setToRestState();
driveNW->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
driveNW->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
vehicle = driveNW;
@@ -3360,7 +3364,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto driveNo = PxVehicleNoDrive::allocate(wheels.Count());
driveNo->setup(PhysX, actorPhysX, *wheelsSimData);
- driveNo->setToRestState();
vehicle = driveNo;
break;
}
diff --git a/Source/Engine/Physics/Physics.cpp b/Source/Engine/Physics/Physics.cpp
index 7087b2e70..5d9b218ec 100644
--- a/Source/Engine/Physics/Physics.cpp
+++ b/Source/Engine/Physics/Physics.cpp
@@ -203,8 +203,11 @@ void Physics::Simulate(float dt)
void Physics::CollectResults()
{
- if (DefaultScene)
- DefaultScene->CollectResults();
+ for (PhysicsScene* scene : Scenes)
+ {
+ if (scene->GetAutoSimulation())
+ scene->CollectResults();
+ }
}
bool Physics::IsDuringSimulation()
diff --git a/Source/Engine/Platform/Android/AndroidFileSystem.cpp b/Source/Engine/Platform/Android/AndroidFileSystem.cpp
index 6ea5b394b..0d40f7517 100644
--- a/Source/Engine/Platform/Android/AndroidFileSystem.cpp
+++ b/Source/Engine/Platform/Android/AndroidFileSystem.cpp
@@ -517,7 +517,7 @@ DateTime AndroidFileSystem::GetFileLastEditTime(const StringView& path)
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
return DateTime::MinValue();
- const TimeSpan timeSinceEpoch(0, 0, fileInfo.st_mtime);
+ const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
const DateTime UnixEpoch(1970, 1, 1);
return UnixEpoch + timeSinceEpoch;
}
diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp
index 0d36f1dfb..8f2930ea9 100644
--- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp
+++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp
@@ -498,7 +498,7 @@ DateTime AppleFileSystem::GetFileLastEditTime(const StringView& path)
return DateTime::MinValue();
}
- const TimeSpan timeSinceEpoch(0, 0, fileInfo.st_mtime);
+ const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp
index 9a6b1b9dc..5c1b2ca28 100644
--- a/Source/Engine/Platform/Base/PlatformBase.cpp
+++ b/Source/Engine/Platform/Base/PlatformBase.cpp
@@ -41,6 +41,10 @@ static_assert(sizeof(bool) == 1, "Invalid bool type size.");
static_assert(sizeof(float) == 4, "Invalid float type size.");
static_assert(sizeof(double) == 8, "Invalid double type size.");
+// Check configuration
+static_assert((PLATFORM_THREADS_LIMIT & (PLATFORM_THREADS_LIMIT - 1)) == 0, "Threads limit must be power of two.");
+static_assert(PLATFORM_THREADS_LIMIT % 4 == 0, "Threads limit must be multiple of 4.");
+
float PlatformBase::CustomDpiScale = 1.0f;
Array> PlatformBase::Users;
Delegate PlatformBase::UserAdded;
diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp
index 23d47a029..a814c5dab 100644
--- a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp
+++ b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp
@@ -657,7 +657,7 @@ DateTime LinuxFileSystem::GetFileLastEditTime(const StringView& path)
return DateTime::MinValue();
}
- const TimeSpan timeSinceEpoch(0, 0, fileInfo.st_mtime);
+ const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
diff --git a/Source/Engine/Platform/Unix/UnixFile.cpp b/Source/Engine/Platform/Unix/UnixFile.cpp
index 5fbc75f2e..9cf629d2c 100644
--- a/Source/Engine/Platform/Unix/UnixFile.cpp
+++ b/Source/Engine/Platform/Unix/UnixFile.cpp
@@ -137,7 +137,7 @@ DateTime UnixFile::GetLastWriteTime() const
{
return DateTime::MinValue();
}
- const TimeSpan timeSinceEpoch(0, 0, fileInfo.st_mtime);
+ const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
const DateTime unixEpoch(1970, 1, 1);
return unixEpoch + timeSinceEpoch;
}
diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp
index 181077125..fec322bc3 100644
--- a/Source/Engine/Platform/Windows/WindowsWindow.cpp
+++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp
@@ -324,7 +324,19 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized)
lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION;
SetWindowLong(_handle, GWL_STYLE, lStyle);
- SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
+ const Float2 clientSize = GetClientSize();
+ const Float2 desktopSize = Platform::GetDesktopSize();
+ // Move window and half size if it is larger than desktop size
+ if (clientSize.X >= desktopSize.X && clientSize.Y >= desktopSize.Y)
+ {
+ const Float2 halfSize = desktopSize * 0.5f;
+ const Float2 middlePos = halfSize * 0.5f;
+ SetWindowPos(_handle, nullptr, (int)middlePos.X, (int)middlePos.Y, (int)halfSize.X, (int)halfSize.Y, SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
+ }
+ else
+ {
+ SetWindowPos(_handle, nullptr, 0, 0, (int)clientSize.X, (int)clientSize.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
+ }
if (maximized)
{
diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h
index a2cd48696..3f9a25694 100644
--- a/Source/Engine/Renderer/RenderList.h
+++ b/Source/Engine/Renderer/RenderList.h
@@ -46,6 +46,12 @@ struct RendererDirectionalLightData
float ShadowsDistance;
int32 CascadeCount;
+ float Cascade1Spacing;
+ float Cascade2Spacing;
+ float Cascade3Spacing;
+ float Cascade4Spacing;
+
+ PartitionMode PartitionMode;
float ContactShadowsLength;
ShadowsCastingMode ShadowsMode;
diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp
index ba326b728..937cd9e99 100644
--- a/Source/Engine/Renderer/ShadowsPass.cpp
+++ b/Source/Engine/Renderer/ShadowsPass.cpp
@@ -247,19 +247,12 @@ void ShadowsPass::SetupLight(RenderContext& renderContext, RenderContextBatch& r
minDistance = cameraNear;
maxDistance = cameraNear + shadowsDistance;
- // TODO: expose partition mode?
- enum class PartitionMode
- {
- Manual = 0,
- Logarithmic = 1,
- PSSM = 2,
- };
- PartitionMode partitionMode = PartitionMode::Manual;
+ PartitionMode partitionMode = light.PartitionMode;
float pssmFactor = 0.5f;
- float splitDistance0 = 0.05f;
- float splitDistance1 = 0.15f;
- float splitDistance2 = 0.50f;
- float splitDistance3 = 1.00f;
+ float splitDistance0 = light.Cascade1Spacing;
+ float splitDistance1 = Math::Max(splitDistance0, light.Cascade2Spacing);
+ float splitDistance2 = Math::Max(splitDistance1, light.Cascade3Spacing);
+ float splitDistance3 = Math::Max(splitDistance2, light.Cascade4Spacing);
// Compute the split distances based on the partitioning mode
if (partitionMode == PartitionMode::Manual)
diff --git a/Source/Engine/Scripting/Attributes/CollectionAttribute.cs b/Source/Engine/Scripting/Attributes/CollectionAttribute.cs
index ca8f45d39..cd2e962e6 100644
--- a/Source/Engine/Scripting/Attributes/CollectionAttribute.cs
+++ b/Source/Engine/Scripting/Attributes/CollectionAttribute.cs
@@ -10,6 +10,32 @@ namespace FlaxEngine
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)]
public sealed class CollectionAttribute : Attribute
{
+ ///
+ /// The display type for collections.
+ ///
+ public enum DisplayType
+ {
+ ///
+ /// Displays the default display type.
+ ///
+ Default,
+
+ ///
+ /// Displays a header.
+ ///
+ Header,
+
+ ///
+ /// Displays inline.
+ ///
+ Inline,
+ }
+
+ ///
+ /// Gets or sets the display type.
+ ///
+ public DisplayType Display;
+
///
/// Gets or sets whether this collection is read-only. If true, applications using this collection should not allow to add or remove items.
///
diff --git a/Source/Engine/Scripting/Attributes/Editor/VisibleIfAttribute.cs b/Source/Engine/Scripting/Attributes/Editor/VisibleIfAttribute.cs
index 5e690caa8..3d606370a 100644
--- a/Source/Engine/Scripting/Attributes/Editor/VisibleIfAttribute.cs
+++ b/Source/Engine/Scripting/Attributes/Editor/VisibleIfAttribute.cs
@@ -5,11 +5,11 @@ using System;
namespace FlaxEngine
{
///
- /// Shows property/field in the editor only if the specified member has a given value. Can be used to hide properties based on other properties (also private properties). The given member has to be bool type.
+ /// Shows property/field in the editor only if the specified member has a given value. Can be used to hide properties based on other properties (also private properties). The given member has to be bool type. Multiple VisibleIf attributes can be added for additional conditions to be met.
///
///
[Serializable]
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class VisibleIfAttribute : Attribute
{
///
diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp
index 44ef32503..0cded22b4 100644
--- a/Source/Engine/Scripting/BinaryModule.cpp
+++ b/Source/Engine/Scripting/BinaryModule.cpp
@@ -533,7 +533,12 @@ void ScriptingType::HackObjectVTable(void* object, ScriptingTypeHandle baseTypeH
if (!Script.VTable)
{
// Ensure to have valid Script VTable hacked
- SetupScriptObjectVTable(object, baseTypeHandle, wrapperIndex);
+ BinaryModule::Locker.Lock();
+ if (!Script.VTable)
+ {
+ SetupScriptObjectVTable(object, baseTypeHandle, wrapperIndex);
+ }
+ BinaryModule::Locker.Unlock();
}
// Override object vtable with hacked one that has calls to overriden scripting functions
diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp
index 6cabbc0dd..e3a0991e3 100644
--- a/Source/Engine/Scripting/Scripting.cpp
+++ b/Source/Engine/Scripting/Scripting.cpp
@@ -115,7 +115,7 @@ Action Scripting::ScriptsLoaded;
Action Scripting::ScriptsUnload;
Action Scripting::ScriptsReloading;
Action Scripting::ScriptsReloaded;
-ThreadLocal Scripting::ObjectsLookupIdMapping;
+ThreadLocal Scripting::ObjectsLookupIdMapping;
ScriptingService ScriptingServiceInstance;
bool initFlaxEngine();
diff --git a/Source/Engine/Scripting/Scripting.h b/Source/Engine/Scripting/Scripting.h
index a22349928..6ed2beca4 100644
--- a/Source/Engine/Scripting/Scripting.h
+++ b/Source/Engine/Scripting/Scripting.h
@@ -6,7 +6,7 @@
#include "Engine/Scripting/ScriptingType.h"
#include "Types.h"
-template
+template
class ThreadLocal;
///
@@ -114,7 +114,7 @@ public:
///
/// The objects lookup identifier mapping used to override the object ids on FindObject call (used by the object references deserialization).
///
- static ThreadLocal ObjectsLookupIdMapping;
+ static ThreadLocal ObjectsLookupIdMapping;
///
/// Finds the object by the given identifier. Searches registered scene objects and optionally assets. Logs warning if fails.
diff --git a/Source/Engine/Serialization/FileReadStream.h b/Source/Engine/Serialization/FileReadStream.h
index 40a14185b..57287abcf 100644
--- a/Source/Engine/Serialization/FileReadStream.h
+++ b/Source/Engine/Serialization/FileReadStream.h
@@ -12,7 +12,6 @@
class FLAXENGINE_API FileReadStream : public ReadStream
{
private:
-
File* _file;
uint32 _virtualPosInBuffer; // Current position in the buffer (index)
uint32 _bufferSize; // Amount of loaded bytes from the file to the buffer
@@ -33,11 +32,9 @@ public:
~FileReadStream();
public:
-
///
/// Gets the file handle.
///
- /// File
FORCE_INLINE const File* GetFile() const
{
return _file;
@@ -49,7 +46,6 @@ public:
void Unlink();
public:
-
///
/// Open file to write data to it
///
@@ -58,7 +54,6 @@ public:
static FileReadStream* Open(const StringView& path);
public:
-
// [ReadStream]
void Flush() final override;
void Close() final override;
diff --git a/Source/Engine/Tests/TestTime.cpp b/Source/Engine/Tests/TestTime.cpp
new file mode 100644
index 000000000..0419c5e13
--- /dev/null
+++ b/Source/Engine/Tests/TestTime.cpp
@@ -0,0 +1,26 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+#include "Engine/Core/Types/DateTime.h"
+#include
+
+TEST_CASE("DateTime")
+{
+ SECTION("Test Convertion")
+ {
+ constexpr int year = 2023;
+ constexpr int month = 12;
+ constexpr int day = 16;
+ constexpr int hour = 23;
+ constexpr int minute = 50;
+ constexpr int second = 13;
+ constexpr int millisecond = 5;
+ const DateTime dt1(year, month, day, hour, minute, second, millisecond);
+ CHECK(dt1.GetYear() == year);
+ CHECK(dt1.GetMonth() == month);
+ CHECK(dt1.GetDay() == day);
+ CHECK(dt1.GetHour() == hour);
+ CHECK(dt1.GetMinute() == minute);
+ CHECK(dt1.GetSecond() == second);
+ CHECK(dt1.GetMillisecond() == millisecond);
+ }
+}
diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp
index c89e6aca5..dea8298ce 100644
--- a/Source/Engine/Threading/JobSystem.cpp
+++ b/Source/Engine/Threading/JobSystem.cpp
@@ -93,7 +93,7 @@ struct TIsPODType
namespace
{
JobSystemService JobSystemInstance;
- Thread* Threads[PLATFORM_THREADS_LIMIT] = {};
+ Thread* Threads[PLATFORM_THREADS_LIMIT / 2] = {};
int32 ThreadsCount = 0;
bool JobStartingOnDispatch = true;
volatile int64 ExitFlag = 0;
diff --git a/Source/Engine/Threading/ThreadLocal.h b/Source/Engine/Threading/ThreadLocal.h
index 4de8ced57..b766d0430 100644
--- a/Source/Engine/Threading/ThreadLocal.h
+++ b/Source/Engine/Threading/ThreadLocal.h
@@ -5,15 +5,16 @@
#include "Engine/Core/Types/BaseTypes.h"
#include "Engine/Platform/Platform.h"
+#define THREAD_LOCAL_USE_DYNAMIC_BUCKETS (PLATFORM_DESKTOP)
+
///
-/// Per-thread local variable storage.
-/// Implemented using atomic with per-thread storage indexed via thread id hashing.
-/// ForConsider using 'THREADLOCAL' define before the variable instead.
+/// Per-thread local variable storage for basic types (POD). Implemented using atomic with per-thread storage indexed via thread id hashing. Consider using 'THREADLOCAL' define before the variable instead.
///
-template
+template
class ThreadLocal
{
protected:
+ static_assert(TIsPODType::Value, "Only POD types are supported");
struct Bucket
{
@@ -21,34 +22,34 @@ protected:
T Value;
};
- Bucket _buckets[MaxThreads];
+ Bucket _staticBuckets[MaxThreads];
+#if THREAD_LOCAL_USE_DYNAMIC_BUCKETS
+ Bucket* _dynamicBuckets = nullptr;
+ constexpr static int32 DynamicMaxThreads = 1024;
+#endif
public:
-
ThreadLocal()
{
- // Clear buckets
- if (ClearMemory)
- {
- Platform::MemoryClear(_buckets, sizeof(_buckets));
- }
- else
- {
- for (int32 i = 0; i < MaxThreads; i++)
- _buckets[i].ThreadID = 0;
- }
+ Platform::MemoryClear(_staticBuckets, sizeof(_staticBuckets));
}
+#if THREAD_LOCAL_USE_DYNAMIC_BUCKETS
+ ~ThreadLocal()
+ {
+ Platform::Free(_dynamicBuckets);
+ }
+#endif
+
public:
-
- T& Get()
+ FORCE_INLINE T& Get()
{
- return _buckets[GetIndex()].Value;
+ return GetBucket().Value;
}
- void Set(const T& value)
+ FORCE_INLINE void Set(const T& value)
{
- _buckets[GetIndex()].Value = value;
+ GetBucket().Value = value;
}
int32 Count() const
@@ -56,9 +57,19 @@ public:
int32 result = 0;
for (int32 i = 0; i < MaxThreads; i++)
{
- if (Platform::AtomicRead((int64 volatile*)&_buckets[i].ThreadID) != 0)
+ if (Platform::AtomicRead((int64 volatile*)&_staticBuckets[i].ThreadID) != 0)
result++;
}
+#if THREAD_LOCAL_USE_DYNAMIC_BUCKETS
+ if (auto dynamicBuckets = (Bucket*)Platform::AtomicRead((intptr volatile*)&_dynamicBuckets))
+ {
+ for (int32 i = 0; i < MaxThreads; i++)
+ {
+ if (Platform::AtomicRead((int64 volatile*)&dynamicBuckets[i].ThreadID) != 0)
+ result++;
+ }
+ }
+#endif
return result;
}
@@ -67,89 +78,79 @@ public:
{
for (int32 i = 0; i < MaxThreads; i++)
{
- if (Platform::AtomicRead((int64 volatile*)&_buckets[i].ThreadID) != 0)
- result.Add(_buckets[i].Value);
+ if (Platform::AtomicRead((int64 volatile*)&_staticBuckets[i].ThreadID) != 0)
+ result.Add(_staticBuckets[i].Value);
}
+#if THREAD_LOCAL_USE_DYNAMIC_BUCKETS
+ if (auto dynamicBuckets = (Bucket*)Platform::AtomicRead((intptr volatile*)&_dynamicBuckets))
+ {
+ for (int32 i = 0; i < MaxThreads; i++)
+ {
+ if (Platform::AtomicRead((int64 volatile*)&dynamicBuckets[i].ThreadID) != 0)
+ result.Add(dynamicBuckets[i].Value);
+ }
+ }
+#endif
+ }
+
+ void Clear()
+ {
+ Platform::MemoryClear(_staticBuckets, sizeof(_staticBuckets));
+#if THREAD_LOCAL_USE_DYNAMIC_BUCKETS
+ Platform::Free(_dynamicBuckets);
+ _dynamicBuckets = nullptr;
+#endif
}
protected:
-
- FORCE_INLINE static int32 Hash(const int64 value)
+ Bucket& GetBucket()
{
- return value & (MaxThreads - 1);
- }
+ const int64 key = (int64)Platform::GetCurrentThreadID();
- FORCE_INLINE int32 GetIndex()
- {
- int64 key = (int64)Platform::GetCurrentThreadID();
- auto index = Hash(key);
- while (true)
+ // Search statically allocated buckets
+ int32 index = (int32)(key & (MaxThreads - 1));
+ int32 spaceLeft = MaxThreads;
+ while (spaceLeft)
{
- const int64 value = Platform::AtomicRead(&_buckets[index].ThreadID);
+ const int64 value = Platform::AtomicRead(&_staticBuckets[index].ThreadID);
if (value == key)
- break;
- if (value == 0 && Platform::InterlockedCompareExchange(&_buckets[index].ThreadID, key, 0) == 0)
- break;
- index = Hash(index + 1);
+ return _staticBuckets[index];
+ if (value == 0 && Platform::InterlockedCompareExchange(&_staticBuckets[index].ThreadID, key, 0) == 0)
+ return _staticBuckets[index];
+ index = (index + 1) & (MaxThreads - 1);
+ spaceLeft--;
}
- return index;
- }
-};
-///
-/// Per thread local object
-///
-template
-class ThreadLocalObject : public ThreadLocal
-{
-public:
-
- typedef ThreadLocal Base;
-
-public:
-
- void Delete()
- {
- auto value = Base::Get();
- Base::SetAll(nullptr);
- ::Delete(value);
- }
-
- void DeleteAll()
- {
- for (int32 i = 0; i < MaxThreads; i++)
+#if THREAD_LOCAL_USE_DYNAMIC_BUCKETS
+ // Allocate dynamic buckets if missing
+ DYNAMIC:
+ auto dynamicBuckets = (Bucket*)Platform::AtomicRead((intptr volatile*)&_dynamicBuckets);
+ if (!dynamicBuckets)
{
- auto& bucket = Base::_buckets[i];
- if (bucket.Value != nullptr)
+ dynamicBuckets = (Bucket*)Platform::Allocate(DynamicMaxThreads * sizeof(Bucket), 16);
+ Platform::MemoryClear(dynamicBuckets, DynamicMaxThreads * sizeof(Bucket));
+ if (Platform::InterlockedCompareExchange((intptr volatile*)&_dynamicBuckets, (intptr)dynamicBuckets, 0) != 0)
{
- ::Delete(bucket.Value);
- bucket.ThreadID = 0;
- bucket.Value = nullptr;
+ Platform::Free(dynamicBuckets);
+ goto DYNAMIC;
}
}
- }
- template
- void GetNotNullValues(Array& result) const
- {
- result.EnsureCapacity(MaxThreads);
- for (int32 i = 0; i < MaxThreads; i++)
+ // Search dynamically allocated buckets
+ index = (int32)(key & (DynamicMaxThreads - 1));
+ spaceLeft = DynamicMaxThreads;
+ while (spaceLeft)
{
- if (Base::_buckets[i].Value != nullptr)
- {
- result.Add(Base::_buckets[i].Value);
- }
+ const int64 value = Platform::AtomicRead(&dynamicBuckets[index].ThreadID);
+ if (value == key)
+ return dynamicBuckets[index];
+ if (value == 0 && Platform::InterlockedCompareExchange(&dynamicBuckets[index].ThreadID, key, 0) == 0)
+ return dynamicBuckets[index];
+ index = (index + 1) & (DynamicMaxThreads - 1);
+ spaceLeft--;
}
- }
+#endif
- int32 CountNotNullValues() const
- {
- int32 result = 0;
- for (int32 i = 0; i < MaxThreads; i++)
- {
- if (Base::_buckets[i].Value != nullptr)
- result++;
- }
- return result;
+ return *(Bucket*)nullptr;
}
};
diff --git a/Source/Engine/Threading/ThreadPool.cpp b/Source/Engine/Threading/ThreadPool.cpp
index b7db81ffa..2b6ed5e26 100644
--- a/Source/Engine/Threading/ThreadPool.cpp
+++ b/Source/Engine/Threading/ThreadPool.cpp
@@ -58,7 +58,7 @@ ThreadPoolService ThreadPoolServiceInstance;
bool ThreadPoolService::Init()
{
// Spawn threads
- const int32 numThreads = Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 2, PLATFORM_THREADS_LIMIT);
+ const int32 numThreads = Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 2, PLATFORM_THREADS_LIMIT / 2);
LOG(Info, "Spawning {0} Thread Pool workers", numThreads);
for (int32 i = ThreadPoolImpl::Threads.Count(); i < numThreads; i++)
{
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
index ed79b5edc..a24c5e108 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
@@ -584,7 +584,6 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
{
// Procedural Texture Sample
textureBox->Cache = writeLocal(Value::InitForZero(ValueType::Float4), node);
- createGradients(node);
auto proceduralSample = String::Format(TEXT(
" {{\n"
" float3 weights;\n"
@@ -613,19 +612,19 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
" uv1 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex1)) * 43758.5453);\n"
" uv2 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex2)) * 43758.5453);\n"
" uv3 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex3)) * 43758.5453);\n"
- " float4 tex1 = {1}.SampleGrad({4}, uv1, {2}, {3}, {6}) * weights.x;\n"
- " float4 tex2 = {1}.SampleGrad({4}, uv2, {2}, {3}, {6}) * weights.y;\n"
- " float4 tex3 = {1}.SampleGrad({4}, uv3, {2}, {3}, {6}) * weights.z;\n"
- " {5} = tex1 + tex2 + tex3;\n"
+ " float2 fdx = ddx({0});\n"
+ " float2 fdy = ddy({0});\n"
+ " float4 tex1 = {1}.SampleGrad({2}, uv1, fdx, fdy, {4}) * weights.x;\n"
+ " float4 tex2 = {1}.SampleGrad({2}, uv2, fdx, fdy, {4}) * weights.y;\n"
+ " float4 tex3 = {1}.SampleGrad({2}, uv3, fdx, fdy, {4}) * weights.z;\n"
+ " {3} = tex1 + tex2 + tex3;\n"
" }}\n"
),
uvs.Value, // {0}
texture.Value, // {1}
- _ddx.Value, // {2}
- _ddy.Value, // {3}
- samplerName, // {4}
- textureBox->Cache.Value, // {5}
- offset.Value // {6}
+ samplerName, // {2}
+ textureBox->Cache.Value, // {3}
+ offset.Value // {4}
);
_writer.Write(*proceduralSample);
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index 8d5db063f..53448ec27 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -19,15 +19,15 @@
#include "Engine/Content/Content.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#if USE_EDITOR
+#include "Engine/Core/Utilities.h"
+#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Types/Pair.h"
#include "Engine/Core/Types/Variant.h"
+#include "Engine/Platform/FileSystem.h"
#include "Engine/Graphics/Models/SkeletonUpdater.h"
#include "Engine/Graphics/Models/SkeletonMapping.h"
-#include "Engine/Core/Utilities.h"
-#include "Engine/Core/Types/StringView.h"
-#include "Engine/Platform/FileSystem.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/ContentImporters/CreateMaterial.h"
@@ -35,6 +35,7 @@
#include "Engine/ContentImporters/CreateCollisionData.h"
#include "Engine/Serialization/Serialization.h"
#include "Editor/Utilities/EditorUtilities.h"
+#include "Engine/Animations/Graph/AnimGraph.h"
#include
#endif
@@ -361,7 +362,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(SkipEmptyCurves);
SERIALIZE(OptimizeKeyframes);
SERIALIZE(ImportScaleTracks);
- SERIALIZE(EnableRootMotion);
+ SERIALIZE(RootMotion);
+ SERIALIZE(RootMotionFlags);
SERIALIZE(RootNodeName);
SERIALIZE(GenerateLODs);
SERIALIZE(BaseLOD);
@@ -374,6 +376,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(InstanceToImportAs);
SERIALIZE(ImportTextures);
SERIALIZE(RestoreMaterialsOnReimport);
+ SERIALIZE(SkipExistingMaterialsOnReimport);
SERIALIZE(GenerateSDF);
SERIALIZE(SDFResolution);
SERIALIZE(SplitObjects);
@@ -409,7 +412,8 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(SkipEmptyCurves);
DESERIALIZE(OptimizeKeyframes);
DESERIALIZE(ImportScaleTracks);
- DESERIALIZE(EnableRootMotion);
+ DESERIALIZE(RootMotion);
+ DESERIALIZE(RootMotionFlags);
DESERIALIZE(RootNodeName);
DESERIALIZE(GenerateLODs);
DESERIALIZE(BaseLOD);
@@ -422,6 +426,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(InstanceToImportAs);
DESERIALIZE(ImportTextures);
DESERIALIZE(RestoreMaterialsOnReimport);
+ DESERIALIZE(SkipExistingMaterialsOnReimport);
DESERIALIZE(GenerateSDF);
DESERIALIZE(SDFResolution);
DESERIALIZE(SplitObjects);
@@ -433,6 +438,15 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(AnimationIndex);
if (AnimationIndex != -1)
ObjectIndex = AnimationIndex;
+
+ // [Deprecated on 08.02.2024, expires on 08.02.2026]
+ bool EnableRootMotion = false;
+ DESERIALIZE(EnableRootMotion);
+ if (EnableRootMotion)
+ {
+ RootMotion = RootMotionMode::ExtractNode;
+ RootMotionFlags = AnimationRootMotionFlags::RootPositionXZ;
+ }
}
void RemoveNamespace(String& name)
@@ -807,6 +821,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
break;
case ModelType::Animation:
options.ImportTypes = ImportDataTypes::Animations;
+ if (options.RootMotion == RootMotionMode::ExtractCenterOfMass)
+ options.ImportTypes |= ImportDataTypes::Skeleton;
break;
case ModelType::Prefab:
options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations;
@@ -1154,6 +1170,18 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
continue;
}
+ // Skip any materials that already exist from the model.
+ // This allows the use of "import as material instances" without material properties getting overridden on each import.
+ if (options.SkipExistingMaterialsOnReimport)
+ {
+ AssetInfo info;
+ if (Content::GetAssetInfo(assetPath, info))
+ {
+ material.AssetID = info.ID;
+ continue;
+ }
+ }
+
if (options.ImportMaterialsAsInstances)
{
// Create material instance
@@ -1396,6 +1424,129 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
+ // Process root motion setup
+ animation.RootMotionFlags = options.RootMotion != RootMotionMode::None ? options.RootMotionFlags : AnimationRootMotionFlags::None;
+ animation.RootNodeName = options.RootNodeName.TrimTrailing();
+ if (animation.RootMotionFlags != AnimationRootMotionFlags::None && animation.Channels.HasItems())
+ {
+ if (options.RootMotion == RootMotionMode::ExtractNode)
+ {
+ if (animation.RootNodeName.HasChars() && animation.GetChannel(animation.RootNodeName) == nullptr)
+ {
+ LOG(Warning, "Missing Root Motion node '{}'", animation.RootNodeName);
+ }
+ }
+ else if (options.RootMotion == RootMotionMode::ExtractCenterOfMass && data.Skeleton.Nodes.HasItems()) // TODO: finish implementing this
+ {
+ // Setup root node animation track
+ const auto& rootName = data.Skeleton.Nodes.First().Name;
+ auto rootChannelPtr = animation.GetChannel(rootName);
+ if (!rootChannelPtr)
+ {
+ animation.Channels.Insert(0, NodeAnimationData());
+ rootChannelPtr = &animation.Channels[0];
+ rootChannelPtr->NodeName = rootName;
+ }
+ animation.RootNodeName = rootName;
+ auto& rootChannel = *rootChannelPtr;
+ rootChannel.Position.Clear();
+
+ // Calculate skeleton center of mass position over the animation frames
+ const int32 frames = (int32)animation.Duration;
+ const int32 nodes = data.Skeleton.Nodes.Count();
+ Array centerOfMass;
+ centerOfMass.Resize(frames);
+ for (int32 frame = 0; frame < frames; frame++)
+ {
+ auto& key = centerOfMass[frame];
+
+ // Evaluate skeleton pose at the animation frame
+ AnimGraphImpulse pose;
+ pose.Nodes.Resize(nodes);
+ for (int32 nodeIndex = 0; nodeIndex < nodes; nodeIndex++)
+ {
+ Transform srcNode = data.Skeleton.Nodes[nodeIndex].LocalTransform;
+ auto& node = data.Skeleton.Nodes[nodeIndex];
+ if (auto* channel = animation.GetChannel(node.Name))
+ channel->Evaluate(frame, &srcNode, false);
+ pose.Nodes[nodeIndex] = srcNode;
+ }
+
+ // Calculate average location of the pose (center of mass)
+ key = Float3::Zero;
+ for (int32 nodeIndex = 0; nodeIndex < nodes; nodeIndex++)
+ key += pose.GetNodeModelTransformation(data.Skeleton, nodeIndex).Translation;
+ key /= nodes;
+ }
+
+ // Calculate skeleton center of mass movement over the animation frames
+ rootChannel.Position.Resize(frames);
+ const Float3 centerOfMassRefPose = centerOfMass[0];
+ for (int32 frame = 0; frame < frames; frame++)
+ {
+ auto& key = rootChannel.Position[frame];
+ key.Time = frame;
+ key.Value = centerOfMass[frame] - centerOfMassRefPose;
+ }
+
+ // Remove root motion from the children (eg. if Root moves, then Hips should skip that movement delta)
+ Float3 maxMotion = Float3::Zero;
+ for (int32 i = 0; i < animation.Channels.Count(); i++)
+ {
+ auto& anim = animation.Channels[i];
+ const int32 animNodeIndex = data.Skeleton.FindNode(anim.NodeName);
+
+ // Skip channels that have one of their parents already animated
+ {
+ int32 nodeIndex = animNodeIndex;
+ nodeIndex = data.Skeleton.Nodes[nodeIndex].ParentIndex;
+ while (nodeIndex > 0)
+ {
+ const String& nodeName = data.Skeleton.Nodes[nodeIndex].Name;
+ if (animation.GetChannel(nodeName) != nullptr)
+ break;
+ nodeIndex = data.Skeleton.Nodes[nodeIndex].ParentIndex;
+ }
+ if (nodeIndex > 0 || &anim == rootChannelPtr)
+ continue;
+ }
+
+ // Remove motion
+ auto& animPos = anim.Position.GetKeyframes();
+ for (int32 frame = 0; frame < animPos.Count(); frame++)
+ {
+ auto& key = animPos[frame];
+
+ // Evaluate root motion at the keyframe location
+ Float3 rootMotion;
+ rootChannel.Position.Evaluate(rootMotion, key.Time, false);
+ Float3::Max(maxMotion, rootMotion, maxMotion);
+
+ // Evaluate skeleton pose at the animation frame
+ AnimGraphImpulse pose;
+ pose.Nodes.Resize(nodes);
+ pose.Nodes[0] = data.Skeleton.Nodes[0].LocalTransform; // Use ref pose of root
+ for (int32 nodeIndex = 1; nodeIndex < nodes; nodeIndex++) // Skip new root
+ {
+ Transform srcNode = data.Skeleton.Nodes[nodeIndex].LocalTransform;
+ auto& node = data.Skeleton.Nodes[nodeIndex];
+ if (auto* channel = animation.GetChannel(node.Name))
+ channel->Evaluate(frame, &srcNode, false);
+ pose.Nodes[nodeIndex] = srcNode;
+ }
+
+ // Convert root motion to the local space of this node so the node stays at the same location after adding new root channel
+ Transform parentNodeTransform = pose.GetNodeModelTransformation(data.Skeleton, data.Skeleton.Nodes[animNodeIndex].ParentIndex);
+ rootMotion = parentNodeTransform.WorldToLocal(rootMotion);
+
+ // Remove motion
+ key.Value -= rootMotion;
+ }
+ }
+ LOG(Info, "Calculated root motion: {}", maxMotion);
+ }
+ }
+
// Optimize the keyframes
if (options.OptimizeKeyframes)
{
@@ -1418,9 +1569,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
const int32 after = animation.GetKeyframesCount();
LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before));
}
-
- animation.EnableRootMotion = options.EnableRootMotion;
- animation.RootNodeName = options.RootNodeName;
}
}
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h
index 416505631..6468c1a43 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.h
+++ b/Source/Engine/Tools/ModelTool/ModelTool.h
@@ -129,6 +129,19 @@ public:
Custom = 1,
};
+ ///
+ /// Declares the imported animation Root Motion modes.
+ ///
+ API_ENUM(Attributes="HideInEditor") enum class RootMotionMode
+ {
+ // Root Motion feature is disabled.
+ None = 0,
+ // Motion is extracted from the root node (or node specified by name).
+ ExtractNode = 1,
+ // Motion is extracted from the center of mass movement (estimated based on the skeleton pose animation).
+ ExtractCenterOfMass = 2,
+ };
+
///
/// Model import options.
///
@@ -228,9 +241,12 @@ public:
bool ImportScaleTracks = false;
// Enables root motion extraction support from this animation.
API_FIELD(Attributes="EditorOrder(1060), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation))")
- bool EnableRootMotion = false;
+ RootMotionMode RootMotion = RootMotionMode::None;
+ // Adjusts root motion applying flags. Can customize how root node animation can affect target actor movement (eg. apply both position and rotation changes).
+ API_FIELD(Attributes="EditorOrder(1060), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowRootMotion))")
+ AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::RootPositionXZ;
// The custom node name to be used as a root motion source. If not specified the actual root node will be used.
- API_FIELD(Attributes="EditorOrder(1070), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation))")
+ API_FIELD(Attributes="EditorOrder(1070), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowRootMotion))")
String RootNodeName = TEXT("");
public: // Level Of Detail
@@ -251,7 +267,7 @@ public:
API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry))")
bool SloppyOptimization = false;
// Only used if Sloppy is false. Target error is an approximate measure of the deviation from the original mesh using distance normalized to [0..1] range (e.g. 1e-2f means that simplifier will try to maintain the error to be below 1% of the mesh extents).
- API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(SloppyOptimization), true), Limit(0.01f, 1, 0.001f)")
+ API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGeometry)), Limit(0.01f, 1, 0.001f)")
float LODTargetError = 0.05f;
public: // Materials
@@ -260,17 +276,20 @@ public:
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))")
+ API_FIELD(Attributes = "EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry))")
bool ImportMaterialsAsInstances = false;
// The material used as the base material that will be instanced as the imported model's material.
- API_FIELD(Attributes = "EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances))")
+ API_FIELD(Attributes = "EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry))")
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 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))")
+ // If checked, the importer will try to keep the model's current overridden material slots, instead of importing materials from the source file.
+ API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Overridden Materials\"), VisibleIf(nameof(ShowGeometry))")
bool RestoreMaterialsOnReimport = true;
+ // If checked, the importer will not reimport any material from this model which already exist in the sub-asset folder.
+ API_FIELD(Attributes = "EditorOrder(421), EditorDisplay(\"Materials\", \"Skip Existing Materials\"), VisibleIf(nameof(ShowGeometry))")
+ bool SkipExistingMaterialsOnReimport = true;
public: // SDF
diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs
index 31ea87948..90454b586 100644
--- a/Source/Engine/UI/GUI/Common/Dropdown.cs
+++ b/Source/Engine/UI/GUI/Common/Dropdown.cs
@@ -606,14 +606,15 @@ namespace FlaxEngine.GUI
_popup.LostFocus += DestroyPopup;
// Show dropdown popup
- var locationRootSpace = Location + new Float2(0, Height);
+ var locationRootSpace = Location + new Float2(0, Height - Height * (1 - Scale.Y) / 2);
var parent = Parent;
while (parent != null && parent != root)
{
locationRootSpace = parent.PointToParent(ref locationRootSpace);
parent = parent.Parent;
}
- _popup.Location = locationRootSpace;
+ _popup.Scale = Scale;
+ _popup.Location = locationRootSpace - new Float2(0, _popup.Height * (1 - _popup.Scale.Y) / 2);
_popup.Parent = root;
_popup.Focus();
_popup.StartMouseCapture();
diff --git a/Source/Engine/UI/GUI/Panels/Panel.cs b/Source/Engine/UI/GUI/Panels/Panel.cs
index bae165fba..05cb9bd80 100644
--- a/Source/Engine/UI/GUI/Panels/Panel.cs
+++ b/Source/Engine/UI/GUI/Panels/Panel.cs
@@ -132,6 +132,22 @@ namespace FlaxEngine.GUI
if (_alwaysShowScrollbars != value)
{
_alwaysShowScrollbars = value;
+ switch (_scrollBars)
+ {
+ case ScrollBars.None:
+ break;
+ case ScrollBars.Horizontal:
+ HScrollBar.Visible = value;
+ break;
+ case ScrollBars.Vertical:
+ VScrollBar.Visible = value;
+ break;
+ case ScrollBars.Both:
+ HScrollBar.Visible = value;
+ VScrollBar.Visible = value;
+ break;
+ default: break;
+ }
PerformLayout();
}
}
diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp
index 64164ecec..ea2824e77 100644
--- a/Source/Engine/Visject/VisjectGraph.cpp
+++ b/Source/Engine/Visject/VisjectGraph.cpp
@@ -685,9 +685,9 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
case 36:
{
// Get value with structure data
- Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
if (!node->GetBox(0)->HasConnection())
return;
+ Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
// Find type
const StringView typeName(node->Values[0]);
@@ -741,6 +741,12 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
return;
}
const ScriptingType& type = typeHandle.GetType();
+ if (structureValue.Type.Type != VariantType::Structure) // If structureValue is eg. Float we can try to cast it to a required structure type
+ {
+ VariantType typeVariantType(typeNameAnsiView);
+ if (Variant::CanCast(structureValue, typeVariantType))
+ structureValue = Variant::Cast(structureValue, typeVariantType);
+ }
structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format
const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName());
if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle)
diff --git a/Source/Shaders/ColorGrading.shader b/Source/Shaders/ColorGrading.shader
index ae639fc4d..9d9175614 100644
--- a/Source/Shaders/ColorGrading.shader
+++ b/Source/Shaders/ColorGrading.shader
@@ -243,7 +243,7 @@ float4 CombineLUTs(float2 uv, uint layerIndex)
// Apply LDR LUT color grading
{
- float3 uvw = color * (15.0 / 16.0) + (0.5f / 16.0);
+ float3 uvw = saturate(color) * (15.0 / 16.0) + (0.5f / 16.0);
float3 lutColor = SampleUnwrappedTexture3D(LutTexture, SamplerLinearClamp, uvw, 16).rgb;
color = lerp(color, lutColor, LutWeight);
}
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
index 51ce5b647..845e4e62b 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
@@ -90,6 +90,15 @@ namespace Flax.Build.Bindings
"Int4",
};
+ private static bool GenerateCSharpUseFixedBuffer(string managedType)
+ {
+ return managedType == "byte" || managedType == "char" ||
+ managedType == "short" || managedType == "ushort" ||
+ managedType == "int" || managedType == "uint" ||
+ managedType == "long" || managedType == "ulong" ||
+ managedType == "float" || managedType == "double";
+ }
+
private static string GenerateCSharpDefaultValueNativeToManaged(BuildData buildData, string value, ApiTypeInfo caller, TypeInfo valueType = null, bool attribute = false, string managedType = null)
{
if (string.IsNullOrEmpty(value))
@@ -1435,10 +1444,10 @@ namespace Flax.Build.Bindings
indent += " ";
- StringBuilder toManagedContent = new StringBuilder();
- StringBuilder toNativeContent = new StringBuilder();
- StringBuilder freeContents = new StringBuilder();
- StringBuilder freeContents2 = new StringBuilder();
+ var toManagedContent = GetStringBuilder();
+ var toNativeContent = GetStringBuilder();
+ var freeContents = GetStringBuilder();
+ var freeContents2 = GetStringBuilder();
{
// Native struct begin
@@ -1453,10 +1462,9 @@ namespace Flax.Build.Bindings
contents.Append(indent + "{");
indent += " ";
- toNativeContent.Append($"return new {structureInfo.Name}Internal() {{ ");
- toManagedContent.Append($"return new {structureInfo.Name}() {{ ");
+ toNativeContent.Append($"var unmanaged = new {structureInfo.Name}Internal();").AppendLine();
+ toManagedContent.Append($"var managed = new {structureInfo.Name}();").AppendLine();
- bool useSeparator = false;
contents.AppendLine();
foreach (var fieldInfo in structureInfo.Fields)
{
@@ -1474,11 +1482,7 @@ namespace Flax.Build.Bindings
else
originalType = type = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, structureInfo);
- contents.Append(indent).Append(GenerateCSharpAccessLevel(fieldInfo.Access));
- if (fieldInfo.IsConstexpr)
- contents.Append("const ");
- else if (fieldInfo.IsStatic)
- contents.Append("static ");
+ contents.Append(indent).Append("public ");
var apiType = FindApiTypeInfo(buildData, fieldInfo.Type, structureInfo);
bool internalType = apiType is StructureInfo fieldStructureInfo && UseCustomMarshalling(buildData, fieldStructureInfo, structureInfo);
@@ -1486,16 +1490,43 @@ namespace Flax.Build.Bindings
if (fieldInfo.Type.IsArray && (fieldInfo.NoArray || structureInfo.IsPod))
{
- contents.Append(type).Append(' ').Append(fieldInfo.Name + "0;").AppendLine();
- for (int i = 1; i < fieldInfo.Type.ArraySize; i++)
+#if USE_NETCORE
+ if (GenerateCSharpUseFixedBuffer(originalType))
{
- contents.AppendLine();
- GenerateCSharpAttributes(buildData, contents, indent, structureInfo, fieldInfo, fieldInfo.IsStatic);
- contents.Append(indent).Append(GenerateCSharpAccessLevel(fieldInfo.Access));
- if (fieldInfo.IsStatic)
- contents.Append("static ");
- contents.Append(type).Append(' ').Append(fieldInfo.Name + i).Append(';').AppendLine();
+ // Use fixed statement with primitive types of buffers
+ contents.Append($"fixed {originalType} {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}];").AppendLine();
+
+ // Copy fixed-size array
+ toManagedContent.AppendLine($"FlaxEngine.Utils.MemoryCopy(new IntPtr(managed.{fieldInfo.Name}0), new IntPtr(unmanaged.{fieldInfo.Name}0), sizeof({originalType}) * {fieldInfo.Type.ArraySize}ul);");
+ toNativeContent.AppendLine($"FlaxEngine.Utils.MemoryCopy(new IntPtr(unmanaged.{fieldInfo.Name}0), new IntPtr(managed.{fieldInfo.Name}0), sizeof({originalType}) * {fieldInfo.Type.ArraySize}ul);");
}
+ else
+#endif
+ {
+ // Padding in structs for fixed-size array
+ contents.Append(type).Append(' ').Append(fieldInfo.Name).Append("0;").AppendLine();
+ for (int i = 1; i < fieldInfo.Type.ArraySize; i++)
+ {
+ GenerateCSharpAttributes(buildData, contents, indent, structureInfo, fieldInfo, fieldInfo.IsStatic);
+ contents.Append(indent).Append("public ");
+ contents.Append(type).Append(' ').Append(fieldInfo.Name + i).Append(';').AppendLine();
+ }
+
+ // Copy fixed-size array item one-by-one
+ if (fieldInfo.Access == AccessLevel.Public || fieldInfo.Access == AccessLevel.Internal)
+ {
+ for (int i = 0; i < fieldInfo.Type.ArraySize; i++)
+ {
+ toManagedContent.AppendLine($"managed.{fieldInfo.Name}{i} = unmanaged.{fieldInfo.Name}{i};");
+ toNativeContent.AppendLine($"unmanaged.{fieldInfo.Name}{i} = managed.{fieldInfo.Name}{i};");
+ }
+ }
+ else
+ {
+ throw new NotImplementedException("TODO: generate utility method to copy private/protected array data items");
+ }
+ }
+ continue;
}
else
{
@@ -1525,34 +1556,17 @@ namespace Flax.Build.Bindings
//else if (type == "Guid")
// type = "GuidNative";
- contents.Append(type).Append(' ').Append(fieldInfo.Name);
- contents.Append(';').AppendLine();
+ contents.Append(type).Append(' ').Append(fieldInfo.Name).Append(';').AppendLine();
}
// Generate struct constructor/getter and deconstructor/setter function
- if (fieldInfo.NoArray && fieldInfo.Type.IsArray)
- continue;
-
- if (useSeparator)
- {
- toManagedContent.Append(", ");
- toNativeContent.Append(", ");
- freeContents2.Append("");
- freeContents.Append("");
- }
- useSeparator = true;
-
- toManagedContent.Append(fieldInfo.Name);
- toManagedContent.Append(" = ");
-
- toNativeContent.Append(fieldInfo.Name);
- toNativeContent.Append(" = ");
-
+ toManagedContent.Append("managed.").Append(fieldInfo.Name).Append(" = ");
+ toNativeContent.Append("unmanaged.").Append(fieldInfo.Name).Append(" = ");
if (fieldInfo.Type.IsObjectRef)
{
var managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type.GenericArgs[0], structureInfo);
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{managedType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
- toNativeContent.Append($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{managedType}>(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target) : null;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero;");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
// Permanent ScriptingObject handle is passed from native side, do not release it
@@ -1560,8 +1574,8 @@ namespace Flax.Build.Bindings
}
else if (fieldInfo.Type.Type == "ScriptingObject")
{
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
- toNativeContent.Append($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target) : null;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero;");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
// Permanent ScriptingObject handle is passed from native side, do not release it
@@ -1569,8 +1583,8 @@ namespace Flax.Build.Bindings
}
else if (fieldInfo.Type.IsPtr && originalType != "IntPtr" && !originalType.EndsWith("*"))
{
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{originalType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
- toNativeContent.Append($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{originalType}>(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target) : null;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero;");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
// Permanent ScriptingObject handle is passed from native side, do not release it
@@ -1578,8 +1592,8 @@ namespace Flax.Build.Bindings
}
else if (fieldInfo.Type.Type == "Dictionary")
{
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{originalType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
- toNativeContent.Append($"ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak)");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{originalType}>(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target) : null;");
+ toNativeContent.AppendLine($"ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak);");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
freeContents2.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
}
@@ -1591,16 +1605,16 @@ namespace Flax.Build.Bindings
// Marshal blittable array elements back to original non-blittable elements
string originalElementTypeMarshaller = originalElementType + "Marshaller";
string internalElementType = $"{originalElementTypeMarshaller}.{originalElementType}Internal";
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? NativeInterop.ConvertArray((Unsafe.As(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target)).ToSpan<{internalElementType}>(), {originalElementTypeMarshaller}.ToManaged) : null");
- toNativeContent.Append($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(NativeInterop.ConvertArray(managed.{fieldInfo.Name}, {originalElementTypeMarshaller}.ToNative)), GCHandleType.Weak) : IntPtr.Zero");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? NativeInterop.ConvertArray((Unsafe.As(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target)).ToSpan<{internalElementType}>(), {originalElementTypeMarshaller}.ToManaged) : null;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(NativeInterop.ConvertArray(managed.{fieldInfo.Name}, {originalElementTypeMarshaller}.ToNative)), GCHandleType.Weak) : IntPtr.Zero;");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); Span<{internalElementType}> values = (Unsafe.As(handle.Target)).ToSpan<{internalElementType}>(); foreach (var value in values) {{ {originalElementTypeMarshaller}.Free(value); }} (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
freeContents2.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); Span<{internalElementType}> values = (Unsafe.As(handle.Target)).ToSpan<{internalElementType}>(); foreach (var value in values) {{ {originalElementTypeMarshaller}.Free(value); }} (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
}
else if (fieldInfo.Type.GenericArgs[0].IsObjectRef)
{
// Array elements passed as GCHandles
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? NativeInterop.GCHandleArrayToManagedArray<{originalElementType}>(Unsafe.As(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target)) : null");
- toNativeContent.Append($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(NativeInterop.ManagedArrayToGCHandleWrappedArray(managed.{fieldInfo.Name}), GCHandleType.Weak) : IntPtr.Zero");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? NativeInterop.GCHandleArrayToManagedArray<{originalElementType}>(Unsafe.As(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target)) : null;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(NativeInterop.ManagedArrayToGCHandleWrappedArray(managed.{fieldInfo.Name}), GCHandleType.Weak) : IntPtr.Zero;");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); Span ptrs = (Unsafe.As(handle.Target)).ToSpan(); foreach (var ptr in ptrs) {{ if (ptr != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(ptr).Free(); }} }} (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
// Permanent ScriptingObject handle is passed from native side, do not release it
@@ -1609,53 +1623,53 @@ namespace Flax.Build.Bindings
else
{
// Blittable array elements
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? (Unsafe.As(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target)).ToArray<{originalElementType}>() : null");
- toNativeContent.Append($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(managed.{fieldInfo.Name}), GCHandleType.Weak) : IntPtr.Zero");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? (Unsafe.As(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target)).ToArray<{originalElementType}>() : null;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(managed.{fieldInfo.Name}), GCHandleType.Weak) : IntPtr.Zero;");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
freeContents2.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
}
}
else if (fieldInfo.Type.Type == "Version")
{
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{originalType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
- toNativeContent.Append($"ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak)");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{originalType}>(ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Target) : null;");
+ toNativeContent.AppendLine($"ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak);");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
freeContents2.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
}
else if (originalType == "string")
{
- toManagedContent.Append($"ManagedString.ToManaged(managed.{fieldInfo.Name})");
- toNativeContent.Append($"ManagedString.ToNative(managed.{fieldInfo.Name})");
+ toManagedContent.AppendLine($"ManagedString.ToManaged(unmanaged.{fieldInfo.Name});");
+ toNativeContent.AppendLine($"ManagedString.ToNative(managed.{fieldInfo.Name});");
freeContents.AppendLine($"ManagedString.Free(unmanaged.{fieldInfo.Name});");
freeContents2.AppendLine($"ManagedString.Free(unmanaged.{fieldInfo.Name});");
}
else if (originalType == "bool")
{
- toManagedContent.Append($"managed.{fieldInfo.Name} != 0");
- toNativeContent.Append($"managed.{fieldInfo.Name} ? (byte)1 : (byte)0");
+ toManagedContent.AppendLine($"unmanaged.{fieldInfo.Name} != 0;");
+ toNativeContent.AppendLine($"managed.{fieldInfo.Name} ? (byte)1 : (byte)0;");
}
else if (fieldInfo.Type.Type == "Variant")
{
// Variant passed as boxed object handle
- toManagedContent.Append($"ManagedHandleMarshaller.NativeToManaged.ConvertToManaged(managed.{fieldInfo.Name})");
- toNativeContent.Append($"ManagedHandleMarshaller.NativeToManaged.ConvertToUnmanaged(managed.{fieldInfo.Name})");
+ toManagedContent.AppendLine($"ManagedHandleMarshaller.NativeToManaged.ConvertToManaged(unmanaged.{fieldInfo.Name});");
+ toNativeContent.AppendLine($"ManagedHandleMarshaller.NativeToManaged.ConvertToUnmanaged(managed.{fieldInfo.Name});");
}
else if (internalType)
{
- toManagedContent.Append($"{internalTypeMarshaller}.ToManaged(managed.{fieldInfo.Name})");
- toNativeContent.Append($"{internalTypeMarshaller}.ToNative(managed.{fieldInfo.Name})");
+ toManagedContent.AppendLine($"{internalTypeMarshaller}.ToManaged(unmanaged.{fieldInfo.Name});");
+ toNativeContent.AppendLine($"{internalTypeMarshaller}.ToNative(managed.{fieldInfo.Name});");
freeContents.AppendLine($"{internalTypeMarshaller}.Free(unmanaged.{fieldInfo.Name});");
freeContents2.AppendLine($"{internalTypeMarshaller}.Free(unmanaged.{fieldInfo.Name});");
}
/*else if (originalType == "Guid")
{
- toManagedContent.Append("(Guid)managed.").Append(fieldInfo.Name);
+ toManagedContent.Append("(Guid)unmanaged.").Append(fieldInfo.Name);
toNativeContent.Append("(GuidNative)managed.").Append(fieldInfo.Name);
}*/
else
{
- toManagedContent.Append("managed.").Append(fieldInfo.Name);
- toNativeContent.Append("managed.").Append(fieldInfo.Name);
+ toManagedContent.Append("unmanaged.").Append(fieldInfo.Name).AppendLine(";");
+ toNativeContent.Append("managed.").Append(fieldInfo.Name).AppendLine(";");
}
}
@@ -1663,8 +1677,8 @@ namespace Flax.Build.Bindings
indent = indent.Substring(0, indent.Length - 4);
contents.AppendLine(indent + "}").AppendLine();
- toManagedContent.AppendLine(" };");
- toNativeContent.AppendLine(" };");
+ toNativeContent.Append("return unmanaged;");
+ toManagedContent.Append("return managed;");
}
var indent2 = indent + " ";
@@ -1712,7 +1726,7 @@ namespace Flax.Build.Bindings
contents.Append(indent).AppendLine("{").Append(indent2).AppendLine(freeContents.Replace("\n", "\n" + indent2).ToString().TrimEnd()).Append(indent).AppendLine("}");
// Managed/native converters
- contents.Append(indent).AppendLine($"internal static {structureInfo.Name} ToManaged({structureInfo.Name}Internal managed)");
+ contents.Append(indent).AppendLine($"internal static {structureInfo.Name} ToManaged({structureInfo.Name}Internal unmanaged)");
contents.Append(indent).AppendLine("{").Append(indent2).AppendLine(toManagedContent.Replace("\n", "\n" + indent2).ToString().TrimEnd()).Append(indent).AppendLine("}");
contents.Append(indent).AppendLine($"internal static {structureInfo.Name}Internal ToNative({structureInfo.Name} managed)");
contents.Append(indent).AppendLine("{").Append(indent2).AppendLine(toNativeContent.Replace("\n", "\n" + indent2).ToString().TrimEnd()).Append(indent).AppendLine("}");
@@ -1720,6 +1734,11 @@ namespace Flax.Build.Bindings
contents.AppendLine("#pragma warning restore 1591");
indent = indent.Substring(0, indent.Length - 4);
contents.Append(indent).AppendLine("}").AppendLine();
+
+ PutStringBuilder(toManagedContent);
+ PutStringBuilder(toNativeContent);
+ PutStringBuilder(freeContents);
+ PutStringBuilder(freeContents2);
}
#endif
// Struct docs
@@ -1767,15 +1786,12 @@ namespace Flax.Build.Bindings
managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, structureInfo);
fieldInfo.Type.IsArray = true;
#if USE_NETCORE
- // Use fixed statement with primitive types of buffers
- if (managedType == "char")
+ if (GenerateCSharpUseFixedBuffer(managedType))
{
- // char's are not blittable, store as short instead
- contents.Append($"fixed short {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine();
- }
- else if (managedType == "byte")
- {
- contents.Append($"fixed byte {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine();
+ // Use fixed statement with primitive types of buffers
+ if (managedType == "char")
+ managedType = "short"; // char's are not blittable, store as short instead
+ contents.Append($"fixed {managedType} {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}];").AppendLine();
}
else
#endif
@@ -1785,7 +1801,6 @@ namespace Flax.Build.Bindings
for (int i = 1; i < fieldInfo.Type.ArraySize; i++)
{
contents.AppendLine();
- GenerateCSharpComment(contents, indent, fieldInfo.Comment);
GenerateCSharpAttributes(buildData, contents, indent, structureInfo, fieldInfo, fieldInfo.IsStatic);
contents.Append(indent).Append(GenerateCSharpAccessLevel(fieldInfo.Access));
if (fieldInfo.IsStatic)
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
index 5e63f489d..4e632014a 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
@@ -1970,7 +1970,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
- contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i);
+ contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)).Append(" arg" + i);
}
contents.Append(')').AppendLine();
contents.Append(" {").AppendLine();
@@ -2058,7 +2058,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
- contents.Append(eventInfo.Type.GenericArgs[i]);
+ contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo));
}
contents.Append(")> f;").AppendLine();
if (eventInfo.IsStatic)
@@ -2084,7 +2084,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
- contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i);
+ contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)).Append(" arg" + i);
}
contents.Append(')').AppendLine();
contents.Append(" {").AppendLine();
@@ -2111,7 +2111,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
- contents.Append(eventInfo.Type.GenericArgs[i]);
+ contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo));
}
contents.Append(")> f;").AppendLine();
if (eventInfo.IsStatic)
@@ -2423,28 +2423,29 @@ namespace Flax.Build.Bindings
// Getter for structure field
contents.AppendLine(" static void GetField(void* ptr, const String& name, Variant& value)");
contents.AppendLine(" {");
- for (var i = 0; i < structureInfo.Fields.Count; i++)
+ for (int i = 0, count = 0; i < structureInfo.Fields.Count; i++)
{
var fieldInfo = structureInfo.Fields[i];
if (fieldInfo.IsReadOnly || fieldInfo.IsStatic || fieldInfo.IsConstexpr || fieldInfo.Access == AccessLevel.Private)
continue;
- if (i == 0)
+ if (count == 0)
contents.AppendLine($" if (name == TEXT(\"{fieldInfo.Name}\"))");
else
contents.AppendLine($" else if (name == TEXT(\"{fieldInfo.Name}\"))");
contents.AppendLine($" value = {GenerateCppWrapperNativeToVariant(buildData, fieldInfo.Type, structureInfo, $"(({structureTypeNameNative}*)ptr)->{fieldInfo.Name}")};");
+ count++;
}
contents.AppendLine(" }").AppendLine();
// Setter for structure field
contents.AppendLine(" static void SetField(void* ptr, const String& name, const Variant& value)");
contents.AppendLine(" {");
- for (var i = 0; i < structureInfo.Fields.Count; i++)
+ for (int i = 0, count = 0; i < structureInfo.Fields.Count; i++)
{
var fieldInfo = structureInfo.Fields[i];
if (fieldInfo.IsReadOnly || fieldInfo.IsStatic || fieldInfo.IsConstexpr || fieldInfo.Access == AccessLevel.Private)
continue;
- if (i == 0)
+ if (count == 0)
contents.AppendLine($" if (name == TEXT(\"{fieldInfo.Name}\"))");
else
contents.AppendLine($" else if (name == TEXT(\"{fieldInfo.Name}\"))");
@@ -2460,6 +2461,7 @@ namespace Flax.Build.Bindings
}
else
contents.AppendLine($" (({structureTypeNameNative}*)ptr)->{fieldInfo.Name} = {GenerateCppWrapperVariantToNative(buildData, fieldInfo.Type, structureInfo, "value")};");
+ count++;
}
contents.AppendLine(" }");
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs
index 843bb60fa..0b71af086 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs
@@ -47,6 +47,20 @@ namespace Flax.Build.Bindings
}
}
+ private class ParseException : Exception
+ {
+ public ParseException(ref ParsingContext context, string msg)
+ : base(GetParseErrorLocation(ref context, msg))
+ {
+ }
+ }
+
+ private static string GetParseErrorLocation(ref ParsingContext context, string msg)
+ {
+ // Make it a link clickable in Visual Studio build output
+ return $"{context.File.Name}({context.Tokenizer.CurrentLine}): {msg}";
+ }
+
private static string[] ParseComment(ref ParsingContext context)
{
if (context.StringCache == null)
@@ -180,7 +194,7 @@ namespace Flax.Build.Bindings
case TokenType.RightParent:
parameters.Add(tag);
return parameters;
- default: throw new Exception($"Expected right parent or next argument, but got {token.Type}.");
+ default: throw new ParseException(ref context, $"Expected right parent or next argument, but got {token.Type}.");
}
}
}
@@ -302,7 +316,7 @@ namespace Flax.Build.Bindings
if (context.PreprocessorDefines.TryGetValue(length, out var define))
length = define;
if (!int.TryParse(length, out type.ArraySize))
- throw new Exception($"Failed to parse the field {entry} array size '{length}'");
+ throw new ParseException(ref context, $"Failed to parse the field {entry} array size '{length}'");
}
private static List ParseFunctionParameters(ref ParsingContext context)
@@ -354,7 +368,7 @@ namespace Flax.Build.Bindings
if (valid)
break;
var location = "function parameter";
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {location} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{location}'"));
break;
}
}
@@ -483,8 +497,7 @@ namespace Flax.Build.Bindings
desc.Inheritance = new List();
desc.Inheritance.Add(inheritType);
token = context.Tokenizer.NextToken();
- while (token.Type == TokenType.CommentSingleLine
- || token.Type == TokenType.CommentMultiLine)
+ while (token.Type == TokenType.CommentSingleLine || token.Type == TokenType.CommentMultiLine)
{
token = context.Tokenizer.NextToken();
}
@@ -563,7 +576,7 @@ namespace Flax.Build.Bindings
// Read 'class' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "class")
- throw new Exception($"Invalid {ApiTokens.Class} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
+ throw new ParseException(ref context, $"Invalid {ApiTokens.Class} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read specifiers
while (true)
@@ -644,7 +657,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -672,7 +685,7 @@ namespace Flax.Build.Bindings
// Read 'class' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "class")
- throw new Exception($"Invalid {ApiTokens.Interface} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
+ throw new ParseException(ref context, $"Invalid {ApiTokens.Interface} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read specifiers
while (true)
@@ -735,7 +748,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -815,13 +828,13 @@ namespace Flax.Build.Bindings
{
case "const":
if (desc.IsConst)
- throw new Exception($"Invalid double 'const' specifier in function {desc.Name} at line {context.Tokenizer.CurrentLine}.");
+ throw new ParseException(ref context, $"Invalid double 'const' specifier in function {desc.Name}");
desc.IsConst = true;
break;
case "override":
desc.IsVirtual = true;
break;
- default: throw new Exception($"Unknown identifier '{token.Value}' in function {desc.Name} at line {context.Tokenizer.CurrentLine}.");
+ default: throw new ParseException(ref context, $"Unknown identifier '{token.Value}' in function {desc.Name}");
}
}
else if (token.Type == TokenType.LeftCurlyBrace)
@@ -875,7 +888,7 @@ namespace Flax.Build.Bindings
ParseMemberTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -892,11 +905,11 @@ namespace Flax.Build.Bindings
var classInfo = context.ScopeInfo as ClassInfo;
if (classInfo == null)
- throw new Exception($"Found property {propertyName} outside the class at line {context.Tokenizer.CurrentLine}.");
+ throw new ParseException(ref context, $"Found property {propertyName} outside the class");
var isGetter = !functionInfo.ReturnType.IsVoid;
if (!isGetter && functionInfo.Parameters.Count == 0)
- throw new Exception($"Property {propertyName} setter method in class {classInfo.Name} has to have value parameter to set (line {context.Tokenizer.CurrentLine}).");
+ throw new ParseException(ref context, $"Property {propertyName} setter method in class {classInfo.Name} has to have value parameter to set (line {context.Tokenizer.CurrentLine}).");
var propertyType = isGetter ? functionInfo.ReturnType : functionInfo.Parameters[0].Type;
var propertyInfo = classInfo.Properties.FirstOrDefault(x => x.Name == propertyName);
@@ -917,7 +930,7 @@ namespace Flax.Build.Bindings
else
{
if (propertyInfo.IsStatic != functionInfo.IsStatic)
- throw new Exception($"Property {propertyName} in class {classInfo.Name} has to have both getter and setter methods static or non-static (line {context.Tokenizer.CurrentLine}).");
+ throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} has to have both getter and setter methods static or non-static (line {context.Tokenizer.CurrentLine}).");
}
if (functionInfo.Tags != null)
{
@@ -934,9 +947,9 @@ namespace Flax.Build.Bindings
}
if (isGetter && propertyInfo.Getter != null)
- throw new Exception($"Property {propertyName} in class {classInfo.Name} cannot have multiple getter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
+ throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} cannot have multiple getter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
if (!isGetter && propertyInfo.Setter != null)
- throw new Exception($"Property {propertyName} in class {classInfo.Name} cannot have multiple setter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
+ throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} cannot have multiple setter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
if (isGetter)
propertyInfo.Getter = functionInfo;
@@ -963,7 +976,7 @@ namespace Flax.Build.Bindings
return propertyInfo;
if (getterType.Type == "Array" && setterType.Type == "Span" && getterType.GenericArgs?.Count == 1 && setterType.GenericArgs?.Count == 1 && getterType.GenericArgs[0].Equals(setterType.GenericArgs[0]))
return propertyInfo;
- throw new Exception($"Property {propertyName} in class {classInfo.Name} (line {context.Tokenizer.CurrentLine}) has mismatching getter return type ({getterType}) and setter parameter type ({setterType}). Both getter and setter methods must use the same value type used for property.");
+ throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} (line {context.Tokenizer.CurrentLine}) has mismatching getter return type ({getterType}) and setter parameter type ({setterType}). Both getter and setter methods must use the same value type used for property.");
}
if (propertyInfo.Comment != null)
@@ -996,7 +1009,7 @@ namespace Flax.Build.Bindings
// Read 'enum' or `enum class` keywords
var token = context.Tokenizer.NextToken();
if (token.Value != "enum")
- throw new Exception($"Invalid {ApiTokens.Enum} usage at line {context.Tokenizer.CurrentLine} (expected 'enum' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
+ throw new ParseException(ref context, $"Invalid {ApiTokens.Enum} usage (expected 'enum' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
token = context.Tokenizer.NextToken();
if (token.Value != "class")
context.Tokenizer.PreviousToken();
@@ -1079,7 +1092,7 @@ namespace Flax.Build.Bindings
entry.Attributes = tag.Value;
break;
default:
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name + " enum entry"} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1153,7 +1166,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1176,7 +1189,7 @@ namespace Flax.Build.Bindings
// Read 'struct' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "struct")
- throw new Exception($"Invalid {ApiTokens.Struct} usage (expected 'struct' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
+ throw new ParseException(ref context, $"Invalid {ApiTokens.Struct} usage (expected 'struct' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read name
desc.Name = desc.NativeName = ParseName(ref context);
@@ -1233,7 +1246,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1366,7 +1379,7 @@ namespace Flax.Build.Bindings
ParseMemberTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1396,7 +1409,7 @@ namespace Flax.Build.Bindings
if (desc.Type.Type == "Action")
desc.Type = new TypeInfo { Type = "Delegate", GenericArgs = new List() };
else if (desc.Type.Type != "Delegate")
- throw new Exception($"Invalid {ApiTokens.Event} type. Only Action and Delegate<> types are supported. '{desc.Type}' used on event at line {context.Tokenizer.CurrentLine}.");
+ throw new ParseException(ref context, $"Invalid {ApiTokens.Event} type. Only Action and Delegate<> types are supported. '{desc.Type}' used on event.");
// Read name
desc.Name = ParseName(ref context);
@@ -1438,7 +1451,7 @@ namespace Flax.Build.Bindings
ParseMemberTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1474,7 +1487,7 @@ namespace Flax.Build.Bindings
// Read 'typedef' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "typedef")
- throw new Exception($"Invalid {ApiTokens.Typedef} usage (expected 'typedef' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
+ throw new ParseException(ref context, $"Invalid {ApiTokens.Typedef} usage (expected 'typedef' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read type definition
desc.Type = ParseType(ref context);
@@ -1513,7 +1526,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
- Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
+ Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
diff --git a/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs b/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs
index d646a81cd..085c56680 100644
--- a/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs
+++ b/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs
@@ -802,9 +802,12 @@ namespace Flax.Build.Plugins
// Serialize base type
if (type.BaseType != null && type.BaseType.FullName != "System.ValueType" && type.BaseType.FullName != "FlaxEngine.Object" && type.BaseType.CanBeResolved())
{
- GenerateSerializeCallback(module, il, type.BaseType.Resolve(), serialize);
+ GenerateSerializeCallback(module, il, type.BaseType, serialize);
}
+ if (type.HasGenericParameters) // TODO: implement network replication for generic classes
+ MonoCecil.CompilationError($"Not supported generic type '{type.FullName}' for network replication.");
+
var ildContext = new DotnetIlContext(il);
// Serialize all type fields marked with NetworkReplicated attribute
@@ -874,12 +877,13 @@ namespace Flax.Build.Plugins
return m;
}
- private static void GenerateSerializeCallback(ModuleDefinition module, ILProcessor il, TypeDefinition type, bool serialize)
+ private static void GenerateSerializeCallback(ModuleDefinition module, ILProcessor il, TypeReference type, bool serialize)
{
if (type.IsScriptingObject())
{
// NetworkReplicator.InvokeSerializer(typeof(), instance, stream, )
- il.Emit(OpCodes.Ldtoken, module.ImportReference(type));
+ module.ImportReference(type);
+ il.Emit(OpCodes.Ldtoken, type);
module.GetType("System.Type", out var typeType);
var getTypeFromHandle = typeType.Resolve().GetMethod("GetTypeFromHandle");
il.Emit(OpCodes.Call, module.ImportReference(getTypeFromHandle));
diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs
index 4eba06cb0..4facc73c0 100644
--- a/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs
+++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs
@@ -179,7 +179,10 @@ namespace Flax.Build.Projects.VisualStudio
customizer.WriteVisualStudioBuildProperties(vsProject, platform, toolchain, configuration, vcProjectFileContent, vcFiltersFileContent, vcUserFileContent);
vcProjectFileContent.AppendLine(string.Format(" {0}", targetBuildOptions.IntermediateFolder));
vcProjectFileContent.AppendLine(string.Format(" {0}", targetBuildOptions.OutputFolder));
- vcProjectFileContent.AppendLine(" ");
+ if (includePaths.Count != 0)
+ vcProjectFileContent.AppendLine(string.Format(" $(IncludePath);{0}", string.Join(";", includePaths)));
+ else
+ vcProjectFileContent.AppendLine(" ");
vcProjectFileContent.AppendLine(" ");
vcProjectFileContent.AppendLine(" ");
vcProjectFileContent.AppendLine(" ");
diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs
index 5cc1b2205..3438f8960 100644
--- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs
+++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs
@@ -296,7 +296,7 @@ namespace Flax.Build.Projects.VisualStudio
var folderIdMatches = new Regex("Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"(.*?)\", \"(.*?)\", \"{(.*?)}\"").Matches(contents);
foreach (Match match in folderIdMatches)
{
- var folder = match.Groups[1].Value;
+ var folder = match.Groups[2].Value;
var folderId = Guid.ParseExact(match.Groups[3].Value, "D");
folderIds[folder] = folderId;
}
@@ -385,8 +385,7 @@ namespace Flax.Build.Projects.VisualStudio
{
if (!folderIds.TryGetValue(folderPath, out project.FolderGuid))
{
- if (!folderIds.TryGetValue(folderParents[i], out project.FolderGuid))
- project.FolderGuid = Guid.NewGuid();
+ project.FolderGuid = Guid.NewGuid();
folderIds.Add(folderPath, project.FolderGuid);
}
folderNames.Add(folderPath);
@@ -401,7 +400,7 @@ namespace Flax.Build.Projects.VisualStudio
var lastSplit = folder.LastIndexOf('\\');
var name = lastSplit != -1 ? folder.Substring(lastSplit + 1) : folder;
- vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, name, folderGuid));
+ vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, folder, folderGuid));
vcSolutionFileContent.AppendLine("EndProject");
}
}