Merge remote-tracking branch 'origin/master' into 1.8

# Conflicts:
#	Source/Editor/Utilities/EditorUtilities.cpp
#	Source/Editor/Utilities/EditorUtilities.h
This commit is contained in:
Wojtek Figat
2024-02-19 22:26:16 +01:00
219 changed files with 4189 additions and 2372 deletions

1
.gitignore vendored
View File

@@ -11,7 +11,6 @@ Source/*.Gen.*
Source/*.csproj
/Package_*/
!Source/Engine/Debug
/Source/Platforms/Editor/Linux/Mono/etc/mono/registry
PackageEditor_Cert.command
PackageEditor_Cert.bat
PackagePlatforms_Cert.bat

BIN
Content/Editor/Fonts/NotoSansSC-Regular.flax (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Content/Shaders/ColorGrading.flax (Stored with Git LFS)

Binary file not shown.

View File

@@ -73,8 +73,12 @@
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TYPEDEF/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION_005FMEMBER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LO/@EntryIndexedValue">LO</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RPC/@EntryIndexedValue">RPC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SDK/@EntryIndexedValue">SDK</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=VS/@EntryIndexedValue">VS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FCONSTANT/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FFUNCTION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>

View File

@@ -81,7 +81,7 @@ namespace FlaxEditor.Content.Create
switch (_options.Template)
{
case Templates.Empty:
return Editor.CreateAsset(Editor.NewAssetType.ParticleEmitter, ResultUrl);
return Editor.CreateAsset("ParticleEmitter", ResultUrl);
case Templates.ConstantBurst:
templateName = "Constant Burst";
break;

View File

@@ -56,6 +56,9 @@ namespace FlaxEditor.Content.GUI
private float _viewScale = 1.0f;
private ContentViewType _viewType = ContentViewType.Tiles;
private bool _isRubberBandSpanning = false;
private Float2 _mousePresslocation;
private Rectangle _rubberBandRectangle;
#region External Events
@@ -600,6 +603,12 @@ namespace FlaxEditor.Content.GUI
{
Render2D.DrawText(style.FontSmall, IsSearching ? "No results" : "Empty", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
}
if (_isRubberBandSpanning)
{
Render2D.FillRectangle(_rubberBandRectangle, Color.Orange * 0.4f);
Render2D.DrawRectangle(_rubberBandRectangle, Color.Orange);
}
}
/// <inheritdoc />
@@ -607,9 +616,54 @@ namespace FlaxEditor.Content.GUI
{
if (base.OnMouseDown(location, button))
return true;
if (button == MouseButton.Left)
{
_mousePresslocation = location;
_rubberBandRectangle = new Rectangle(_mousePresslocation, 0, 0);
_isRubberBandSpanning = true;
StartMouseCapture();
}
return AutoFocus && Focus(this);
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
if (_isRubberBandSpanning)
{
_rubberBandRectangle.Width = location.X - _mousePresslocation.X;
_rubberBandRectangle.Height = location.Y - _mousePresslocation.Y;
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (_isRubberBandSpanning)
{
_isRubberBandSpanning = false;
EndMouseCapture();
if (_rubberBandRectangle.Width < 0 || _rubberBandRectangle.Height < 0)
{
// make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner
var size = _rubberBandRectangle.Size;
_rubberBandRectangle.X = Mathf.Min(_rubberBandRectangle.X, _rubberBandRectangle.X + _rubberBandRectangle.Width);
_rubberBandRectangle.Y = Mathf.Min(_rubberBandRectangle.Y, _rubberBandRectangle.Y + _rubberBandRectangle.Height);
size.X = Mathf.Abs(size.X);
size.Y = Mathf.Abs(size.Y);
_rubberBandRectangle.Size = size;
}
var itemsInRectangle = _items.Where(t => _rubberBandRectangle.Intersects(t.Bounds)).ToList();
Select(itemsInRectangle, Input.GetKey(KeyboardKeys.Shift) || Input.GetKey(KeyboardKeys.Control));
return true;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseWheel(Float2 location, float delta)
{

View File

@@ -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;

View File

@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.AnimationGraphFunction, outputPath))
if (Editor.CreateAsset("AnimationGraphFunction", outputPath))
throw new Exception("Failed to create new asset.");
}
}

View File

@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.AnimationGraph, outputPath))
if (Editor.CreateAsset("AnimationGraph", outputPath))
throw new Exception("Failed to create new asset.");
}
}

View File

@@ -47,7 +47,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.Animation, outputPath))
if (Editor.CreateAsset("Animation", outputPath))
throw new Exception("Failed to create new asset.");
}

View File

@@ -47,7 +47,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.BehaviorTree, outputPath))
if (Editor.CreateAsset("BehaviorTree", outputPath))
throw new Exception("Failed to create new asset.");
}

View File

@@ -71,7 +71,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.CollisionData, outputPath))
if (Editor.CreateAsset("CollisionData", outputPath))
throw new Exception("Failed to create new asset.");
}

View File

@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.MaterialFunction, outputPath))
if (Editor.CreateAsset("MaterialFunction", outputPath))
throw new Exception("Failed to create new asset.");
}
}

View File

@@ -43,7 +43,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.MaterialInstance, outputPath))
if (Editor.CreateAsset("MaterialInstance", outputPath))
throw new Exception("Failed to create new asset.");
}

View File

@@ -44,7 +44,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.Material, outputPath))
if (Editor.CreateAsset("Material", outputPath))
throw new Exception("Failed to create new asset.");
}

View File

@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.ParticleEmitterFunction, outputPath))
if (Editor.CreateAsset("ParticleEmitterFunction", outputPath))
throw new Exception("Failed to create new asset.");
}
}

View File

@@ -75,7 +75,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.ParticleSystem, outputPath))
if (Editor.CreateAsset("ParticleSystem", outputPath))
throw new Exception("Failed to create new asset.");
}

View File

@@ -69,7 +69,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.SceneAnimation, outputPath))
if (Editor.CreateAsset("SceneAnimation", outputPath))
throw new Exception("Failed to create new asset.");
}
}

View File

@@ -69,8 +69,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override bool IsFileNameValid(string filename)
{
// Scripts cannot start with digit.
if (Char.IsDigit(filename[0]))
if (char.IsDigit(filename[0]))
return false;
if (filename.Equals("Script"))
return false;

View File

@@ -38,7 +38,7 @@ namespace FlaxEditor.Content
/// <inheritdoc />
public override void Create(string outputPath, object arg)
{
if (Editor.CreateAsset(Editor.NewAssetType.SkeletonMask, outputPath))
if (Editor.CreateAsset("SkeletonMask", outputPath))
throw new Exception("Failed to create new asset.");
}
}

View File

@@ -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 <fstream>
#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
/// <summary>
/// MS-DOS header found at the beginning in a PE format file.
/// </summary>
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;
};
/// <summary>
/// COFF header found in a PE format file.
/// </summary>
struct COFFHeader
{
uint16 machine;
uint16 numSections;
uint32 timeDateStamp;
uint32 ptrSymbolTable;
uint32 numSymbols;
uint16 sizeOptHeader;
uint16 characteristics;
};
/// <summary>
/// Contains address and size of data areas in a PE image.
/// </summary>
struct PEDataDirectory
{
uint32 virtualAddress;
uint32 size;
};
/// <summary>
/// Optional header in a 32-bit PE format file.
/// </summary>
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];
};
/// <summary>
/// Optional header in a 64-bit PE format file.
/// </summary>
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];
};
/// <summary>
/// A section header in a PE format file.
/// </summary>
struct PESectionHeader
{
char name[8];
uint32 virtualSize;
uint32 relativeVirtualAddress;
uint32 physicalSize;
uint32 physicalAddress;
uint8 deprecated[12];
uint32 flags;
};
/// <summary>
/// A resource table header within a .rsrc section in a PE format file.
/// </summary>
struct PEImageResourceDirectory
{
uint32 flags;
uint32 timeDateStamp;
uint16 majorVersion;
uint16 minorVersion;
uint16 numNamedEntries;
uint16 numIdEntries;
};
/// <summary>
/// A single entry in a resource table within a .rsrc section in a PE format file.
/// </summary>
struct PEImageResourceEntry
{
uint32 type;
uint32 offsetDirectory : 31;
uint32 isDirectory : 1;
};
/// <summary>
/// An entry in a resource table referencing resource data. Found within a .rsrc section in a PE format file.
/// </summary>
struct PEImageResourceEntryData
{
uint32 offsetData;
uint32 size;
uint32 codePage;
uint32 resourceHandle;
};
/// <summary>
/// Header used in icon file format.
/// </summary>
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<PESectionHeader> 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<uint8> 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;

View File

@@ -543,10 +543,11 @@ namespace FlaxEditor.CustomEditors
try
{
string text;
var value = Values[0];
if (ParentEditor is Dedicated.ScriptsEditor)
{
// Script
text = JsonSerializer.Serialize(Values[0]);
text = JsonSerializer.Serialize(value);
// Remove properties that should be ignored when copy/pasting data
if (text == null)
@@ -576,12 +577,12 @@ namespace FlaxEditor.CustomEditors
else if (ScriptType.FlaxObject.IsAssignableFrom(Values.Type))
{
// Object reference
text = JsonSerializer.GetStringID(Values[0] as FlaxEngine.Object);
text = JsonSerializer.GetStringID(value as FlaxEngine.Object);
}
else
{
// Default
text = JsonSerializer.Serialize(Values[0]);
text = JsonSerializer.Serialize(value, TypeUtils.GetType(Values.Type));
}
Clipboard.Text = text;
}

View File

@@ -3,6 +3,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FlaxEditor.Actions;
using FlaxEditor.Content;
using FlaxEditor.GUI;
@@ -16,6 +18,27 @@ using Object = FlaxEngine.Object;
namespace FlaxEditor.CustomEditors.Dedicated
{
internal class NewScriptItem : ItemsListContextMenu.Item
{
private string _scriptName;
public string ScriptName
{
get => _scriptName;
set
{
_scriptName = value;
Name = $"Create script '{value}'";
}
}
public NewScriptItem(string scriptName)
{
ScriptName = scriptName;
TooltipText = "Create a new script";
}
}
/// <summary>
/// Drag and drop scripts area control.
/// </summary>
@@ -74,7 +97,43 @@ namespace FlaxEditor.CustomEditors.Dedicated
{
cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i]));
}
cm.ItemClicked += item => AddScript((ScriptType)item.Tag);
cm.TextChanged += text =>
{
if (!IsValidScriptName(text))
return;
if (!cm.ItemsPanel.Children.Any(x => x.Visible && x is not NewScriptItem))
{
// If there are no visible items, that means the search failed so we can find the create script button or create one if it's the first time
var newScriptItem = (NewScriptItem)cm.ItemsPanel.Children.FirstOrDefault(x => x is NewScriptItem);
if (newScriptItem != null)
{
newScriptItem.Visible = true;
newScriptItem.ScriptName = text;
}
else
{
cm.AddItem(new NewScriptItem(text));
}
}
else
{
// Make sure to hide the create script button if there
var newScriptItem = cm.ItemsPanel.Children.FirstOrDefault(x => x is NewScriptItem);
if (newScriptItem != null)
newScriptItem.Visible = false;
}
};
cm.ItemClicked += item =>
{
if (item.Tag is ScriptType script)
{
AddScript(script);
}
else if (item is NewScriptItem newScriptItem)
{
CreateScript(newScriptItem);
}
};
cm.SortItems();
cm.Show(this, button.BottomLeft - new Float2((cm.Width - button.Width) / 2, 0));
}
@@ -113,6 +172,19 @@ namespace FlaxEditor.CustomEditors.Dedicated
return false;
}
private static bool IsValidScriptName(string text)
{
if (string.IsNullOrEmpty(text))
return false;
if (text.Contains(' '))
return false;
if (char.IsDigit(text[0]))
return false;
if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_'))
return false;
return Editor.Instance.ContentDatabase.GetProxy("cs").IsFileNameValid(text);
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
@@ -163,6 +235,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
if (_dragScripts.HasValidDrag)
{
result = _dragScripts.Effect;
AddScripts(_dragScripts.Objects);
}
else if (_dragAssets.HasValidDrag)
@@ -177,7 +250,43 @@ namespace FlaxEditor.CustomEditors.Dedicated
return result;
}
private void AddScript(ScriptType item)
private void CreateScript(NewScriptItem item)
{
ScriptsEditor.NewScriptName = item.ScriptName;
var paths = Directory.GetFiles(Globals.ProjectSourceFolder, "*.Build.cs");
string moduleName = null;
foreach (var p in paths)
{
var file = File.ReadAllText(p);
if (!file.Contains("GameProjectTarget"))
continue; // Skip
if (file.Contains("Modules.Add(\"Game\")"))
{
// Assume Game represents the main game module
moduleName = "Game";
break;
}
}
// Ensure the path slashes are correct for the OS
var correctedPath = Path.GetFullPath(Globals.ProjectSourceFolder);
if (string.IsNullOrEmpty(moduleName))
{
var error = FileSystem.ShowBrowseFolderDialog(Editor.Instance.Windows.MainWindow, correctedPath, "Select a module folder to put the new script in", out moduleName);
if (error)
return;
}
var path = Path.Combine(Globals.ProjectSourceFolder, moduleName, item.ScriptName + ".cs");
Editor.Instance.ContentDatabase.GetProxy("cs").Create(path, null);
}
/// <summary>
/// Attach a script to the actor.
/// </summary>
/// <param name="item">The script.</param>
public void AddScript(ScriptType item)
{
var list = new List<ScriptType>(1) { item };
AddScripts(list);
@@ -224,16 +333,67 @@ namespace FlaxEditor.CustomEditors.Dedicated
private void AddScripts(List<ScriptType> items)
{
var actions = new List<IUndoAction>(4);
var actions = new List<IUndoAction>();
for (int i = 0; i < items.Count; i++)
{
var scriptType = items[i];
RequireScriptAttribute scriptAttribute = null;
if (scriptType.HasAttribute(typeof(RequireScriptAttribute), false))
{
foreach (var e in scriptType.GetAttributes(false))
{
if (e is not RequireScriptAttribute requireScriptAttribute)
continue;
scriptAttribute = requireScriptAttribute;
break;
}
}
// See if script requires a specific actor type
RequireActorAttribute actorAttribute = null;
if (scriptType.HasAttribute(typeof(RequireActorAttribute), false))
{
foreach (var e in scriptType.GetAttributes(false))
{
if (e is not RequireActorAttribute requireActorAttribute)
continue;
actorAttribute = requireActorAttribute;
break;
}
}
var actors = ScriptsEditor.ParentEditor.Values;
for (int j = 0; j < actors.Count; j++)
{
var actor = (Actor)actors[j];
// If required actor exists but is not this actor type then skip adding to actor
if (actorAttribute != null)
{
if (actor.GetType() != actorAttribute.RequiredType && !actor.GetType().IsSubclassOf(actorAttribute.RequiredType))
{
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` not added to `{actor}` due to script requiring an Actor type of `{actorAttribute.RequiredType}`.");
continue;
}
}
actions.Add(AddRemoveScript.Add(actor, scriptType));
// Check if actor has required scripts and add them if the actor does not.
if (scriptAttribute != null)
{
foreach (var type in scriptAttribute.RequiredTypes)
{
if (!type.IsSubclassOf(typeof(Script)))
{
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(type.Name)}` not added to `{actor}` due to the class not being a subclass of Script.");
continue;
}
if (actor.GetScript(type) != null)
continue;
actions.Add(AddRemoveScript.Add(actor, new ScriptType(type)));
}
}
}
}
@@ -440,6 +600,11 @@ namespace FlaxEditor.CustomEditors.Dedicated
/// <inheritdoc />
public override IEnumerable<object> UndoObjects => _scripts;
/// <summary>
/// Cached the newly created script name - used to add script after compilation.
/// </summary>
internal static string NewScriptName;
private void AddMissingScript(int index, LayoutElementsContainer layout)
{
var group = layout.Group("Missing script");
@@ -548,6 +713,21 @@ namespace FlaxEditor.CustomEditors.Dedicated
var dragArea = layout.CustomContainer<DragAreaControl>();
dragArea.CustomControl.ScriptsEditor = this;
// If the initialization is triggered by an editor recompilation, check if it was due to script generation from DragAreaControl
if (NewScriptName != null)
{
var script = Editor.Instance.CodeEditing.Scripts.Get().FirstOrDefault(x => x.Name == NewScriptName);
NewScriptName = null;
if (script != null)
{
dragArea.CustomControl.AddScript(script);
}
else
{
Editor.LogWarning("Failed to find newly created script.");
}
}
// No support for showing scripts from multiple actors that have different set of scripts
var scripts = (Script[])Values[0];
_scripts.Clear();
@@ -586,19 +766,71 @@ namespace FlaxEditor.CustomEditors.Dedicated
var scriptType = TypeUtils.GetObjectType(script);
var editor = CustomEditorsUtil.CreateEditor(scriptType, false);
// Check if actor has all the required scripts
bool hasAllRequirements = true;
if (scriptType.HasAttribute(typeof(RequireScriptAttribute), false))
{
RequireScriptAttribute scriptAttribute = null;
foreach (var e in scriptType.GetAttributes(false))
{
if (e is not RequireScriptAttribute requireScriptAttribute)
continue;
scriptAttribute = requireScriptAttribute;
}
if (scriptAttribute != null)
{
foreach (var type in scriptAttribute.RequiredTypes)
{
if (!type.IsSubclassOf(typeof(Script)))
continue;
var requiredScript = script.Actor.GetScript(type);
if (requiredScript == null)
{
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` on `{script.Actor}` is missing a required Script of type `{type}`.");
hasAllRequirements = false;
}
}
}
}
if (scriptType.HasAttribute(typeof(RequireActorAttribute), false))
{
RequireActorAttribute attribute = null;
foreach (var e in scriptType.GetAttributes(false))
{
if (e is not RequireActorAttribute requireActorAttribute)
continue;
attribute = requireActorAttribute;
break;
}
if (attribute != null)
{
var actor = script.Actor;
if (actor.GetType() != attribute.RequiredType && !actor.GetType().IsSubclassOf(attribute.RequiredType))
{
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` on `{script.Actor}` is missing a required Actor of type `{attribute.RequiredType}`.");
hasAllRequirements = false;
// Maybe call to remove script here?
}
}
}
// Create group
var title = Utilities.Utils.GetPropertyNameUI(scriptType.Name);
var group = layout.Group(title, editor);
if (!hasAllRequirements)
group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.Statusbar.Failed;
if ((Presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
{
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
group.Panel.Close(false);
if (Editor.Instance.ProjectCache.IsGroupToggled(title))
group.Panel.Close();
else
group.Panel.Open(false);
group.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
group.Panel.Open();
group.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetGroupToggle(panel.HeaderText, panel.IsClosed);
}
else
group.Panel.Open(false);
group.Panel.Open();
// Customize
group.Panel.TooltipText = Editor.Instance.CodeDocs.GetTooltip(scriptType);

View File

@@ -51,10 +51,13 @@ namespace FlaxEditor.CustomEditors.Editors
return;
Picker = layout.Custom<AssetPicker>().CustomControl;
_valueType = Values.Type.Type != typeof(object) || Values[0] == null ? Values.Type : TypeUtils.GetObjectType(Values[0]);
var value = Values[0];
_valueType = Values.Type.Type != typeof(object) || value == null ? Values.Type : TypeUtils.GetObjectType(value);
var assetType = _valueType;
if (assetType == typeof(string))
assetType = new ScriptType(typeof(Asset));
else if (_valueType.Type != null && _valueType.Type.Name == typeof(JsonAssetReference<>).Name)
assetType = new ScriptType(_valueType.Type.GenericTypeArguments[0]);
float height = 48;
var attributes = Values.GetAttributes();
@@ -102,6 +105,12 @@ namespace FlaxEditor.CustomEditors.Editors
SetValue(new SceneReference(Picker.Validator.SelectedID));
else if (_valueType.Type == typeof(string))
SetValue(Picker.Validator.SelectedPath);
else if (_valueType.Type.Name == typeof(JsonAssetReference<>).Name)
{
var value = Values[0];
value.GetType().GetField("Asset").SetValue(value, Picker.Validator.SelectedAsset as JsonAsset);
SetValue(value);
}
else
SetValue(Picker.Validator.SelectedAsset);
}
@@ -114,16 +123,19 @@ namespace FlaxEditor.CustomEditors.Editors
if (!HasDifferentValues)
{
_isRefreshing = true;
if (Values[0] is AssetItem assetItem)
var value = Values[0];
if (value is AssetItem assetItem)
Picker.Validator.SelectedItem = assetItem;
else if (Values[0] is Guid guid)
else if (value is Guid guid)
Picker.Validator.SelectedID = guid;
else if (Values[0] is SceneReference sceneAsset)
else if (value is SceneReference sceneAsset)
Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID);
else if (Values[0] is string path)
else if (value is string path)
Picker.Validator.SelectedPath = path;
else if (value != null && value.GetType().Name == typeof(JsonAssetReference<>).Name)
Picker.Validator.SelectedAsset = value.GetType().GetField("Asset").GetValue(value) as JsonAsset;
else
Picker.Validator.SelectedAsset = Values[0] as Asset;
Picker.Validator.SelectedAsset = value as Asset;
_isRefreshing = false;
}
}

View File

@@ -176,8 +176,8 @@ namespace FlaxEditor.CustomEditors.Editors
private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
private bool _readOnly;
private int _elementsCount, _minCount, _maxCount;
private bool _canResize;
private bool _canReorderItems;
private CollectionAttribute.DisplayType _displayType;
@@ -209,8 +209,10 @@ namespace FlaxEditor.CustomEditors.Editors
return;
var size = Count;
_readOnly = false;
_canResize = true;
_canReorderItems = true;
_minCount = 0;
_maxCount = 0;
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
_displayType = CollectionAttribute.DisplayType.Header;
NotNullItems = false;
@@ -222,7 +224,9 @@ namespace FlaxEditor.CustomEditors.Editors
var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute);
if (collection != null)
{
_readOnly = collection.ReadOnly;
_canResize = !collection.ReadOnly;
_minCount = collection.MinCount;
_maxCount = collection.MaxCount;
_canReorderItems = collection.CanReorderItems;
NotNullItems = collection.NotNullItems;
if (collection.BackgroundColor.HasValue)
@@ -231,6 +235,9 @@ namespace FlaxEditor.CustomEditors.Editors
spacing = collection.Spacing;
_displayType = collection.Display;
}
if (_maxCount == 0)
_maxCount = ushort.MaxValue;
_canResize &= _minCount < _maxCount;
var dragArea = layout.CustomContainer<DragAreaControl>();
dragArea.CustomControl.Editor = this;
@@ -268,8 +275,8 @@ namespace FlaxEditor.CustomEditors.Editors
var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
_sizeBox = new IntValueBox(size)
{
MinValue = 0,
MaxValue = ushort.MaxValue,
MinValue = _minCount,
MaxValue = _maxCount,
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
Parent = dropPanel,
@@ -283,7 +290,7 @@ namespace FlaxEditor.CustomEditors.Editors
Parent = dropPanel
};
if (_readOnly || (NotNullItems && size == 0))
if (!_canResize || (NotNullItems && size == 0))
{
_sizeBox.IsReadOnly = true;
_sizeBox.Enabled = false;
@@ -339,7 +346,7 @@ namespace FlaxEditor.CustomEditors.Editors
_elementsCount = size;
// Add/Remove buttons
if (!_readOnly)
if (_canResize)
{
var panel = dragArea.HorizontalPanel();
panel.Panel.Size = new Float2(0, 20);
@@ -347,25 +354,23 @@ namespace FlaxEditor.CustomEditors.Editors
var removeButton = panel.Button("-", "Remove last item");
removeButton.Button.Size = new Float2(16, 16);
removeButton.Button.Enabled = size > 0;
removeButton.Button.Enabled = size > _minCount;
removeButton.Button.AnchorPreset = AnchorPresets.TopRight;
removeButton.Button.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count - 1);
};
var addButton = panel.Button("+", "Add new item");
addButton.Button.Size = new Float2(16, 16);
addButton.Button.Enabled = !NotNullItems || size > 0;
addButton.Button.Enabled = (!NotNullItems || size > 0) && size < _maxCount;
addButton.Button.AnchorPreset = AnchorPresets.TopRight;
addButton.Button.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count + 1);
};
}

View File

@@ -25,6 +25,11 @@ namespace FlaxEditor.CustomEditors
/// </summary>
internal bool isRootGroup = true;
/// <summary>
/// Parent container who created this one.
/// </summary>
internal LayoutElementsContainer _parent;
/// <summary>
/// The children.
/// </summary>
@@ -40,6 +45,24 @@ namespace FlaxEditor.CustomEditors
/// </summary>
public abstract ContainerControl ContainerControl { get; }
/// <summary>
/// Gets the Custom Editors layout presenter.
/// </summary>
internal CustomEditorPresenter Presenter
{
get
{
CustomEditorPresenter result;
var container = this;
do
{
result = container as CustomEditorPresenter;
container = container._parent;
} while (container != null);
return result;
}
}
/// <summary>
/// Adds new group element.
/// </summary>
@@ -81,17 +104,31 @@ namespace FlaxEditor.CustomEditors
public GroupElement Group(string title, bool useTransparentHeader = false)
{
var element = new GroupElement();
if (!isRootGroup)
var presenter = Presenter;
var isSubGroup = !isRootGroup;
if (isSubGroup)
element.Panel.Close();
if (presenter != null && (presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
{
element.Panel.Close(false);
// Build group identifier (made of path from group titles)
var expandPath = title;
var container = this;
while (container != null && !(container is CustomEditorPresenter))
{
if (container.ContainerControl is DropPanel dropPanel)
expandPath = dropPanel.HeaderText + "/" + expandPath;
container = container._parent;
}
else if (this is CustomEditorPresenter presenter && (presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
{
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
element.Panel.Close(false);
element.Panel.IsClosedChanged += OnPanelIsClosedChanged;
// Caching/restoring expanded groups (non-root groups cache expanded state so invert boolean expression)
if (Editor.Instance.ProjectCache.IsGroupToggled(expandPath) ^ isSubGroup)
element.Panel.Close();
else
element.Panel.Open();
element.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetGroupToggle(expandPath, panel.IsClosed ^ isSubGroup);
}
element.isRootGroup = false;
element._parent = this;
element.Panel.HeaderText = title;
if (useTransparentHeader)
{
@@ -103,11 +140,6 @@ namespace FlaxEditor.CustomEditors
return element;
}
private void OnPanelIsClosedChanged(DropPanel panel)
{
Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
}
/// <summary>
/// Adds new horizontal panel element.
/// </summary>
@@ -627,7 +659,6 @@ namespace FlaxEditor.CustomEditors
if (style == DisplayStyle.Group)
{
var group = Group(name, editor, true);
group.Panel.Close(false);
group.Panel.TooltipText = tooltip;
return group.Object(values, editor);
}
@@ -657,7 +688,6 @@ namespace FlaxEditor.CustomEditors
if (style == DisplayStyle.Group)
{
var group = Group(label.Text, editor, true);
group.Panel.Close(false);
group.Panel.TooltipText = tooltip;
return group.Object(values, editor);
}

View File

@@ -869,7 +869,9 @@ namespace FlaxEditor
/// <summary>
/// New asset types allowed to create.
/// [Deprecated in v1.8]
/// </summary>
[Obsolete("Use CreateAsset with named tag.")]
public enum NewAssetType
{
/// <summary>
@@ -1046,12 +1048,59 @@ namespace FlaxEditor
/// <summary>
/// Creates new asset at the target location.
/// [Deprecated in v1.8]
/// </summary>
/// <param name="type">New asset type.</param>
/// <param name="outputPath">Output asset path.</param>
[Obsolete("Use CreateAsset with named tag.")]
public static bool CreateAsset(NewAssetType type, string outputPath)
{
return Internal_CreateAsset(type, outputPath);
// [Deprecated on 18.02.2024, expires on 18.02.2025]
string tag;
switch (type)
{
case NewAssetType.Material:
tag = "Material";
break;
case NewAssetType.MaterialInstance:
tag = "MaterialInstance";
break;
case NewAssetType.CollisionData:
tag = "CollisionData";
break;
case NewAssetType.AnimationGraph:
tag = "AnimationGraph";
break;
case NewAssetType.SkeletonMask:
tag = "SkeletonMask";
break;
case NewAssetType.ParticleEmitter:
tag = "ParticleEmitter";
break;
case NewAssetType.ParticleSystem:
tag = "ParticleSystem";
break;
case NewAssetType.SceneAnimation:
tag = "SceneAnimation";
break;
case NewAssetType.MaterialFunction:
tag = "MaterialFunction";
break;
case NewAssetType.ParticleEmitterFunction:
tag = "ParticleEmitterFunction";
break;
case NewAssetType.AnimationGraphFunction:
tag = "AnimationGraphFunction";
break;
case NewAssetType.Animation:
tag = "Animation";
break;
case NewAssetType.BehaviorTree:
tag = "BehaviorTree";
break;
default: return true;
}
return CreateAsset(tag, outputPath);
}
/// <summary>
@@ -1588,10 +1637,6 @@ namespace FlaxEditor
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_CloseSplashScreen", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
internal static partial void Internal_CloseSplashScreen();
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_CreateAsset", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
[return: MarshalAs(UnmanagedType.U1)]
internal static partial bool Internal_CreateAsset(NewAssetType type, string outputPath);
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_CreateVisualScript", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
[return: MarshalAs(UnmanagedType.U1)]
internal static partial bool Internal_CreateVisualScript(string outputPath, string baseTypename);

View File

@@ -54,6 +54,11 @@ namespace FlaxEditor
/// </summary>
public static string PrimaryFont = "Editor/Fonts/Roboto-Regular";
/// <summary>
/// The secondary (fallback) font to use for missing characters rendering (CJK - Chinese/Japanese/Korean characters).
/// </summary>
public static string FallbackFont = "Editor/Fonts/NotoSansSC-Regular";
/// <summary>
/// The Inconsolata Regular font.
/// </summary>

View File

@@ -189,6 +189,11 @@ namespace FlaxEditor.GUI
/// </summary>
public event Action<Item> ItemClicked;
/// <summary>
/// Event fired when search text in this popup menu gets changed.
/// </summary>
public event Action<string> TextChanged;
/// <summary>
/// The panel control where you should add your items.
/// </summary>
@@ -263,6 +268,7 @@ namespace FlaxEditor.GUI
UnlockChildrenRecursive();
PerformLayout(true);
_searchBox.Focus();
TextChanged?.Invoke(_searchBox.Text);
}
/// <summary>
@@ -439,6 +445,7 @@ namespace FlaxEditor.GUI
Hide();
return true;
case KeyboardKeys.ArrowDown:
{
if (RootWindow.FocusedControl == null)
{
// Focus search box if nothing is focused
@@ -447,7 +454,6 @@ namespace FlaxEditor.GUI
}
// Focus the first visible item or then next one
{
var items = GetVisibleItems();
var focusedIndex = items.IndexOf(focusedItem);
if (focusedIndex == -1)
@@ -459,8 +465,8 @@ namespace FlaxEditor.GUI
_scrollPanel.ScrollViewTo(item);
return true;
}
}
break;
}
case KeyboardKeys.ArrowUp:
if (focusedItem != null)
{

View File

@@ -43,8 +43,9 @@ namespace FlaxEditor.GUI
{
Depth = -1;
if (Height < Style.Current.FontMedium.Height)
Height = Style.Current.FontMedium.Height + 4;
var fontHeight = Style.Current.FontMedium.Height;
if (Height < fontHeight)
Height = fontHeight + 4;
}
/// <inheritdoc />

View File

@@ -239,7 +239,7 @@ namespace FlaxEditor.GUI.Tabs
/// </summary>
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;
}

View File

@@ -170,78 +170,6 @@ DEFINE_INTERNAL_CALL(bool) EditorInternal_CloneAssetFile(MString* dstPathObj, MS
return Content::CloneAssetFile(dstPath, srcPath, *dstId);
}
enum class NewAssetType
{
Material = 0,
MaterialInstance = 1,
CollisionData = 2,
AnimationGraph = 3,
SkeletonMask = 4,
ParticleEmitter = 5,
ParticleSystem = 6,
SceneAnimation = 7,
MaterialFunction = 8,
ParticleEmitterFunction = 9,
AnimationGraphFunction = 10,
Animation = 11,
BehaviorTree = 12,
};
DEFINE_INTERNAL_CALL(bool) EditorInternal_CreateAsset(NewAssetType type, MString* outputPathObj)
{
String tag;
switch (type)
{
case NewAssetType::Material:
tag = AssetsImportingManager::CreateMaterialTag;
break;
case NewAssetType::MaterialInstance:
tag = AssetsImportingManager::CreateMaterialInstanceTag;
break;
case NewAssetType::CollisionData:
tag = AssetsImportingManager::CreateCollisionDataTag;
break;
case NewAssetType::AnimationGraph:
tag = AssetsImportingManager::CreateAnimationGraphTag;
break;
case NewAssetType::SkeletonMask:
tag = AssetsImportingManager::CreateSkeletonMaskTag;
break;
case NewAssetType::ParticleEmitter:
tag = AssetsImportingManager::CreateParticleEmitterTag;
break;
case NewAssetType::ParticleSystem:
tag = AssetsImportingManager::CreateParticleSystemTag;
break;
case NewAssetType::SceneAnimation:
tag = AssetsImportingManager::CreateSceneAnimationTag;
break;
case NewAssetType::MaterialFunction:
tag = AssetsImportingManager::CreateMaterialFunctionTag;
break;
case NewAssetType::ParticleEmitterFunction:
tag = AssetsImportingManager::CreateParticleEmitterFunctionTag;
break;
case NewAssetType::AnimationGraphFunction:
tag = AssetsImportingManager::CreateAnimationGraphFunctionTag;
break;
case NewAssetType::Animation:
tag = AssetsImportingManager::CreateAnimationTag;
break;
case NewAssetType::BehaviorTree:
tag = AssetsImportingManager::CreateBehaviorTreeTag;
break;
default:
return true;
}
String outputPath;
MUtils::ToString(outputPathObj, outputPath);
FileSystem::NormalizePath(outputPath);
return AssetsImportingManager::Create(tag, outputPath);
}
DEFINE_INTERNAL_CALL(bool) EditorInternal_CreateVisualScript(MString* outputPathObj, MString* baseTypenameObj)
{
String outputPath;
@@ -639,8 +567,6 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String
{
options.GenerateSDF = graphicsSettings->GenerateSDFOnModelImport;
}
// Get options from model
FileSystem::NormalizePath(assetPath);
return ImportModel::TryGetImportOptions(assetPath, options);
}
@@ -652,7 +578,12 @@ bool ManagedEditor::Import(const String& inputPath, const String& outputPath, co
bool ManagedEditor::TryRestoreImportOptions(AudioTool::Options& options, String assetPath)
{
// Get options from model
FileSystem::NormalizePath(assetPath);
return ImportAudio::TryGetImportOptions(assetPath, options);
}
bool ManagedEditor::CreateAsset(const String& tag, String outputPath)
{
FileSystem::NormalizePath(outputPath);
return AssetsImportingManager::Create(tag, outputPath);
}

View File

@@ -210,6 +210,13 @@ public:
API_FUNCTION() static bool TryRestoreImportOptions(API_PARAM(Ref) AudioTool::Options& options, String assetPath);
#endif
/// <summary>
/// Creates a new asset at the target location.
/// </summary>
/// <param name="tag">New asset type.</param>
/// <param name="outputPath">Output asset path.</param>
API_FUNCTION() static bool CreateAsset(const String& tag, String outputPath);
public:
API_STRUCT(Internal, NoDefault) struct VisualScriptStackFrame
{

View File

@@ -18,7 +18,7 @@ namespace FlaxEditor.Modules
private DateTime _lastSaveTime;
private readonly HashSet<Guid> _expandedActors = new HashSet<Guid>();
private readonly HashSet<string> _collapsedGroups = new HashSet<string>();
private readonly HashSet<string> _toggledGroups = new HashSet<string>();
private readonly Dictionary<string, string> _customData = new Dictionary<string, string>();
/// <summary>
@@ -62,26 +62,26 @@ namespace FlaxEditor.Modules
}
/// <summary>
/// Determines whether group identified by the given title is collapsed in the UI.
/// Determines whether group identified by the given title is collapsed/opened in the UI.
/// </summary>
/// <param name="title">The group title.</param>
/// <returns><c>true</c> if group is collapsed; otherwise, <c>false</c>.</returns>
public bool IsCollapsedGroup(string title)
/// <returns><c>true</c> if group is toggled; otherwise, <c>false</c>.</returns>
public bool IsGroupToggled(string title)
{
return _collapsedGroups.Contains(title);
return _toggledGroups.Contains(title);
}
/// <summary>
/// Sets the group collapsed cached value.
/// Sets the group collapsed/opened cached value.
/// </summary>
/// <param name="title">The group title.</param>
/// <param name="isCollapsed">If set to <c>true</c> group will be cached as an collapsed, otherwise false.</param>
public void SetCollapsedGroup(string title, bool isCollapsed)
/// <param name="isToggled">If set to <c>true</c> group will be cached as a toggled, otherwise false.</param>
public void SetGroupToggle(string title, bool isToggled)
{
if (isCollapsed)
_collapsedGroups.Add(title);
if (isToggled)
_toggledGroups.Add(title);
else
_collapsedGroups.Remove(title);
_toggledGroups.Remove(title);
_isDirty = true;
}
@@ -160,7 +160,7 @@ namespace FlaxEditor.Modules
_expandedActors.Add(new Guid(bytes16));
}
_collapsedGroups.Clear();
_toggledGroups.Clear();
_customData.Clear();
break;
@@ -176,7 +176,7 @@ namespace FlaxEditor.Modules
_expandedActors.Add(new Guid(bytes16));
}
_collapsedGroups.Clear();
_toggledGroups.Clear();
_customData.Clear();
int customDataCount = reader.ReadInt32();
@@ -201,11 +201,9 @@ namespace FlaxEditor.Modules
}
int collapsedGroupsCount = reader.ReadInt32();
_collapsedGroups.Clear();
_toggledGroups.Clear();
for (int i = 0; i < collapsedGroupsCount; i++)
{
_collapsedGroups.Add(reader.ReadString());
}
_toggledGroups.Add(reader.ReadString());
_customData.Clear();
int customDataCount = reader.ReadInt32();
@@ -259,11 +257,9 @@ namespace FlaxEditor.Modules
writer.Write(e.ToByteArray());
}
writer.Write(_collapsedGroups.Count);
foreach (var e in _collapsedGroups)
{
writer.Write(_toggledGroups.Count);
foreach (var e in _toggledGroups)
writer.Write(e);
}
writer.Write(_customData.Count);
foreach (var e in _customData)
@@ -284,7 +280,6 @@ namespace FlaxEditor.Modules
try
{
SaveGuarded();
_isDirty = false;
}
catch (Exception ex)

View File

@@ -534,6 +534,41 @@ namespace FlaxEditor.Modules
Delete();
}
/// <summary>
/// Create parent for selected actors.
/// </summary>
public void CreateParentForSelectedActors()
{
Actor actor = new EmptyActor();
Editor.SceneEditing.Spawn(actor, null, false);
List<SceneGraphNode> selection = Editor.SceneEditing.Selection;
var actors = selection.Where(x => x is ActorNode).Select(x => ((ActorNode)x).Actor);
using (new UndoMultiBlock(Undo, actors, "Reparent actors"))
{
for (int i = 0; i < selection.Count; i++)
{
if (selection[i] is ActorNode node)
{
if (node.ParentNode != node.ParentScene) // If parent node is not a scene
{
if (selection.Contains(node.ParentNode))
{
continue; // If parent and child nodes selected together, don't touch child nodes
}
// Put created node as child of the Parent Node of node
int parentOrder = node.Actor.OrderInParent;
actor.Parent = node.Actor.Parent;
actor.OrderInParent = parentOrder;
}
node.Actor.Parent = actor;
}
}
}
Editor.SceneEditing.Select(actor);
Editor.Scene.GetActorNode(actor).TreeNode.StartRenaming(Editor.Windows.SceneWin, Editor.Windows.SceneWin.SceneTreePanel);
}
/// <summary>
/// Duplicates the selected objects. Supports undo/redo.
/// </summary>

View File

@@ -50,6 +50,7 @@ namespace FlaxEditor.Modules
private ContextMenuButton _menuEditCut;
private ContextMenuButton _menuEditCopy;
private ContextMenuButton _menuEditPaste;
private ContextMenuButton _menuCreateParentForSelectedActors;
private ContextMenuButton _menuEditDelete;
private ContextMenuButton _menuEditDuplicate;
private ContextMenuButton _menuEditSelectAll;
@@ -535,6 +536,7 @@ namespace FlaxEditor.Modules
_menuFileRecompileScripts = cm.AddButton("Recompile scripts", inputOptions.RecompileScripts, ScriptsBuilder.Compile);
cm.AddSeparator();
cm.AddButton("Open project...", OpenProject);
cm.AddButton("Reload project", ReloadProject);
cm.AddSeparator();
cm.AddButton("Exit", "Alt+F4", () => Editor.Windows.MainWindow.Close(ClosingReason.User));
@@ -548,11 +550,11 @@ namespace FlaxEditor.Modules
_menuEditCut = cm.AddButton("Cut", inputOptions.Cut, Editor.SceneEditing.Cut);
_menuEditCopy = cm.AddButton("Copy", inputOptions.Copy, Editor.SceneEditing.Copy);
_menuEditPaste = cm.AddButton("Paste", inputOptions.Paste, Editor.SceneEditing.Paste);
cm.AddSeparator();
_menuEditDelete = cm.AddButton("Delete", inputOptions.Delete, Editor.SceneEditing.Delete);
_menuEditDuplicate = cm.AddButton("Duplicate", inputOptions.Duplicate, Editor.SceneEditing.Duplicate);
cm.AddSeparator();
_menuEditSelectAll = cm.AddButton("Select all", inputOptions.SelectAll, Editor.SceneEditing.SelectAllScenes);
_menuCreateParentForSelectedActors = cm.AddButton("Create parent for selected actors", Editor.SceneEditing.CreateParentForSelectedActors);
_menuEditFind = cm.AddButton("Find", inputOptions.Search, Editor.Windows.SceneWin.Search);
cm.AddSeparator();
cm.AddButton("Game Settings", () =>
@@ -822,6 +824,13 @@ namespace FlaxEditor.Modules
}
}
private void ReloadProject()
{
// Open project, then close it
Editor.OpenProject(Editor.GameProject.ProjectPath);
Editor.Windows.MainWindow.Close(ClosingReason.User);
}
private void OnMenuFileShowHide(Control control)
{
if (control.Visible == false)
@@ -858,6 +867,7 @@ namespace FlaxEditor.Modules
_menuEditCut.Enabled = hasSthSelected;
_menuEditCopy.Enabled = hasSthSelected;
_menuEditPaste.Enabled = canEditScene;
_menuCreateParentForSelectedActors.Enabled = canEditScene && hasSthSelected;
_menuEditDelete.Enabled = hasSthSelected;
_menuEditDuplicate.Enabled = hasSthSelected;
_menuEditSelectAll.Enabled = Level.IsAnySceneLoaded;

View File

@@ -725,10 +725,8 @@ 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
if (Guid.TryParse(typename, out Guid id))

View File

@@ -172,9 +172,9 @@ namespace FlaxEditor.Options
set
{
if (value == null)
_outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
_outputLogFont = new FontReference(ConsoleFont, 10);
else if (!value.Font)
_outputLogFont.Font = FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont);
_outputLogFont.Font = ConsoleFont;
else
_outputLogFont = value;
}
@@ -237,11 +237,19 @@ namespace FlaxEditor.Options
public int NumberOfGameClientsToLaunch = 1;
private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.PrimaryFont);
private static FontAsset ConsoleFont => FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont);
private FontReference _titleFont = new FontReference(DefaultFont, 18);
private FontReference _largeFont = new FontReference(DefaultFont, 14);
private FontReference _mediumFont = new FontReference(DefaultFont, 9);
private FontReference _smallFont = new FontReference(DefaultFont, 9);
private FontReference _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
private FontReference _outputLogFont = new FontReference(ConsoleFont, 10);
/// <summary>
/// The list of fallback fonts to use when main text font is missing certain characters. Empty to use fonts from GraphicsSettings.
/// </summary>
[EditorDisplay("Fonts"), EditorOrder(650)]
public FontAsset[] FallbackFonts = new FontAsset[1] { FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.FallbackFont) };
/// <summary>
/// Gets or sets the title font for editor UI.

View File

@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FlaxEditor.Content.Settings;
using FlaxEditor.Modules;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -223,6 +225,12 @@ namespace FlaxEditor.Options
Style.Current = CreateDefaultStyle();
}
}
// Set fallback fonts
var fallbackFonts = Options.Interface.FallbackFonts;
if (fallbackFonts == null || fallbackFonts.Length == 0 || fallbackFonts.All(x => x == null))
fallbackFonts = GameSettings.Load<GraphicsSettings>().FallbackFonts;
Font.FallbackFonts = fallbackFonts;
}
/// <summary>

View File

@@ -23,7 +23,7 @@ namespace FlaxEditor.SceneGraph.Actors
base.OnDebugDraw(data);
var transform = Actor.Transform;
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 1.0f, Color.Red, 0.0f, false);
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 1.0f, 0.5f, Color.Red, 0.0f, false);
}
}
}

View File

@@ -7,11 +7,14 @@ using Real = System.Single;
#endif
using System;
using FlaxEditor.GUI.ContextMenu;
using System.Collections.Generic;
using FlaxEditor.Actions;
using FlaxEditor.Modules;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Windows;
using FlaxEngine;
using FlaxEngine.Json;
using FlaxEngine.Utilities;
using Object = FlaxEngine.Object;
namespace FlaxEditor.SceneGraph.Actors
@@ -288,6 +291,8 @@ namespace FlaxEditor.SceneGraph.Actors
private const Real PointNodeSize = 1.5f;
private const Real TangentNodeSize = 1.0f;
private const Real SnapIndicatorSize = 1.7f;
private const Real SnapPointIndicatorSize = 2f;
/// <inheritdoc />
public SplineNode(Actor actor)
@@ -297,9 +302,26 @@ namespace FlaxEditor.SceneGraph.Actors
FlaxEngine.Scripting.Update += OnUpdate;
}
private unsafe void OnUpdate()
private void OnUpdate()
{
// If this node's point is selected
var selection = Editor.Instance.SceneEditing.Selection;
if (selection.Count == 1 && selection[0] is SplinePointNode selectedPoint && selectedPoint.ParentNode == this)
{
if (Input.Keyboard.GetKey(KeyboardKeys.Shift))
EditSplineWithSnap(selectedPoint);
var canAddSplinePoint = Input.Mouse.PositionDelta == Float2.Zero && Input.Mouse.Position != Float2.Zero;
var requestAddSplinePoint = Input.Keyboard.GetKey(KeyboardKeys.Control) && Input.Mouse.GetButtonDown(MouseButton.Right);
if (requestAddSplinePoint && canAddSplinePoint)
AddSplinePoint(selectedPoint);
}
SyncSplineKeyframeWithNodes();
}
private unsafe void SyncSplineKeyframeWithNodes()
{
// Sync spline points with gizmo handles
var actor = (Spline)Actor;
var dstCount = actor.SplinePointsCount;
if (dstCount > 1 && actor.IsLoop)
@@ -329,6 +351,119 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
private unsafe void AddSplinePoint(SplinePointNode selectedPoint)
{
// Check mouse hit on scene
var spline = (Spline)Actor;
var viewport = Editor.Instance.Windows.EditWin.Viewport;
var mouseRay = viewport.MouseRay;
var viewRay = viewport.ViewRay;
var flags = RayCastData.FlagTypes.SkipColliders | RayCastData.FlagTypes.SkipEditorPrimitives;
var hit = Editor.Instance.Scene.Root.RayCast(ref mouseRay, ref viewRay, out var closest, out var normal, flags);
if (hit == null)
return;
// Undo data
var oldSpline = spline.SplineKeyframes;
var editAction = new EditSplineAction(spline, oldSpline);
Root.Undo.AddAction(editAction);
// Get spline point to duplicate
var hitPoint = mouseRay.Position + mouseRay.Direction * closest;
var lastPointIndex = selectedPoint.Index;
var newPointIndex = lastPointIndex > 0 ? lastPointIndex + 1 : 0;
var lastKeyframe = spline.GetSplineKeyframe(lastPointIndex);
var isLastPoint = lastPointIndex == spline.SplinePointsCount - 1;
var isFirstPoint = lastPointIndex == 0;
// Get data to create new point
var lastPointTime = spline.GetSplineTime(lastPointIndex);
var nextPointTime = isLastPoint ? lastPointTime : spline.GetSplineTime(newPointIndex);
var newTime = isLastPoint ? lastPointTime + 1.0f : (lastPointTime + nextPointTime) * 0.5f;
var distanceFromLastPoint = Vector3.Distance(hitPoint, spline.GetSplinePoint(lastPointIndex));
var newPointDirection = spline.GetSplineTangent(lastPointIndex, false).Translation - hitPoint;
// Set correctly keyframe direction on spawn point
if (isFirstPoint)
newPointDirection = hitPoint - spline.GetSplineTangent(lastPointIndex, true).Translation;
else if (isLastPoint)
newPointDirection = spline.GetSplineTangent(lastPointIndex, false).Translation - hitPoint;
var newPointLocalPosition = spline.Transform.WorldToLocal(hitPoint);
var newPointLocalOrientation = Quaternion.LookRotation(newPointDirection);
// Add new point
spline.InsertSplinePoint(newPointIndex, newTime, Transform.Identity, false);
var newKeyframe = lastKeyframe.DeepClone();
var newKeyframeTransform = newKeyframe.Value;
newKeyframeTransform.Translation = newPointLocalPosition;
newKeyframeTransform.Orientation = newPointLocalOrientation;
newKeyframe.Value = newKeyframeTransform;
// Set new point keyframe
var newKeyframeTangentIn = Transform.Identity;
var newKeyframeTangentOut = Transform.Identity;
newKeyframeTangentIn.Translation = (Vector3.Forward * newPointLocalOrientation) * distanceFromLastPoint;
newKeyframeTangentOut.Translation = (Vector3.Backward * newPointLocalOrientation) * distanceFromLastPoint;
newKeyframe.TangentIn = newKeyframeTangentIn;
newKeyframe.TangentOut = newKeyframeTangentOut;
spline.SetSplineKeyframe(newPointIndex, newKeyframe);
for (int i = 1; i < spline.SplinePointsCount; i++)
{
// check all elements to don't left keyframe has invalid time
// because points can be added on start or on middle of spline
// conflicting with time of another keyframes
spline.SetSplinePointTime(i, i, false);
}
// Select new point node
SyncSplineKeyframeWithNodes();
Editor.Instance.SceneEditing.Select(ChildNodes[newPointIndex]);
spline.UpdateSpline();
}
private void EditSplineWithSnap(SplinePointNode selectedPoint)
{
var spline = (Spline)Actor;
var selectedPointBounds = new BoundingSphere(selectedPoint.Transform.Translation, 1f);
var allSplinesInView = GetSplinesInView();
allSplinesInView.Remove(spline);
if (allSplinesInView.Count == 0)
return;
var snappedOnSplinePoint = false;
for (int i = 0; i < allSplinesInView.Count; i++)
{
for (int x = 0; x < allSplinesInView[i].SplineKeyframes.Length; x++)
{
var keyframePosition = allSplinesInView[i].GetSplinePoint(x);
var pointIndicatorSize = NodeSizeByDistance(keyframePosition, SnapPointIndicatorSize);
var keyframeBounds = new BoundingSphere(keyframePosition, pointIndicatorSize);
DebugDraw.DrawSphere(keyframeBounds, Color.Red, 0, false);
if (keyframeBounds.Intersects(selectedPointBounds))
{
spline.SetSplinePoint(selectedPoint.Index, keyframeBounds.Center);
snappedOnSplinePoint = true;
break;
}
}
}
if (!snappedOnSplinePoint)
{
var nearSplineSnapPoint = GetNearSplineSnapPosition(selectedPoint.Transform.Translation, allSplinesInView);
var snapIndicatorSize = NodeSizeByDistance(nearSplineSnapPoint, SnapIndicatorSize);
var snapBounds = new BoundingSphere(nearSplineSnapPoint, snapIndicatorSize);
if (snapBounds.Intersects(selectedPointBounds))
{
spline.SetSplinePoint(selectedPoint.Index, snapBounds.Center);
}
DebugDraw.DrawSphere(snapBounds, Color.Yellow, 0, true);
}
}
/// <inheritdoc />
public override void PostSpawn()
{
@@ -343,14 +478,12 @@ namespace FlaxEditor.SceneGraph.Actors
var spline = (Spline)Actor;
spline.AddSplineLocalPoint(Vector3.Zero, false);
spline.AddSplineLocalPoint(new Vector3(0, 0, 100.0f));
spline.SetSplineKeyframe(0, new BezierCurve<Transform>.Keyframe()
{
Value = new Transform(Vector3.Zero, Quaternion.Identity, Vector3.One),
TangentIn = new Transform(Vector3.Backward * 100, Quaternion.Identity, Vector3.One),
TangentOut = new Transform(Vector3.Forward * 100, Quaternion.Identity, Vector3.One),
});
spline.SetSplineKeyframe(1, new BezierCurve<Transform>.Keyframe()
{
Value = new Transform(Vector3.Forward * 100, Quaternion.Identity, Vector3.One),
@@ -413,6 +546,39 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
private static List<Spline> GetSplinesInView()
{
var splines = Level.GetActors<Spline>(true);
var result = new List<Spline>();
var viewBounds = Editor.Instance.Windows.EditWin.Viewport.ViewFrustum;
foreach (var s in splines)
{
var contains = viewBounds.Contains(s.EditorBox);
if (contains == ContainmentType.Contains || contains == ContainmentType.Intersects)
result.Add(s);
}
return result;
}
private static Vector3 GetNearSplineSnapPosition(Vector3 position, List<Spline> splines)
{
var nearPoint = splines[0].GetSplinePointClosestToPoint(position);
var nearDistance = Vector3.Distance(nearPoint, position);
for (int i = 1; i < splines.Count; i++)
{
var point = splines[i].GetSplinePointClosestToPoint(position);
var distance = Vector3.Distance(point, position);
if (distance < nearDistance)
{
nearPoint = point;
nearDistance = distance;
}
}
return nearPoint;
}
internal static Real NodeSizeByDistance(Vector3 nodePosition, Real nodeSize)
{
var cameraTransform = Editor.Instance.Windows.EditWin.Viewport.ViewportCamera.Viewport.ViewTransform;

View File

@@ -23,7 +23,7 @@ namespace FlaxEditor.SceneGraph.Actors
base.OnDebugDraw(data);
var transform = Actor.Transform;
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 0.3f, Color.Red, 0.0f, false);
DebugDraw.DrawWireArrow(transform.Translation, transform.Orientation, 0.3f, 0.15f, Color.Red, 0.0f, false);
}
}
}

View File

@@ -115,19 +115,34 @@ namespace FlaxEditor.Surface
internal AnimGraphTraceEvent[] LastTraceEvents;
internal bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent)
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;
}

View File

@@ -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;
@@ -484,7 +482,7 @@ namespace FlaxEditor.Surface.Archetypes
var startPos = PointToParent(ref center);
targetState.GetConnectionEndPoint(ref startPos, out var endPos);
var color = style.Foreground;
StateMachineState.DrawConnection(ref startPos, ref endPos, ref color);
SurfaceStyle.DrawStraightConnection(startPos, endPos, color);
}
}
@@ -514,7 +512,7 @@ namespace FlaxEditor.Surface.Archetypes
/// <inheritdoc />
public void DrawConnectingLine(ref Float2 startPos, ref Float2 endPos, ref Color color)
{
StateMachineState.DrawConnection(ref startPos, ref endPos, ref color);
SurfaceStyle.DrawStraightConnection(startPos, endPos, color);
}
/// <inheritdoc />
@@ -655,6 +653,7 @@ namespace FlaxEditor.Surface.Archetypes
protected Rectangle _renameButtonRect;
private bool _cursorChanged = false;
private bool _textRectHovered = false;
private bool _debugActive;
/// <summary>
/// The transitions list from this state to the others.
@@ -677,38 +676,6 @@ namespace FlaxEditor.Surface.Archetypes
{
}
/// <summary>
/// Draws the connection between two state machine nodes.
/// </summary>
/// <param name="startPos">The start position.</param>
/// <param name="endPos">The end position.</param>
/// <param name="color">The line color.</param>
public static void DrawConnection(ref Float2 startPos, ref Float2 endPos, ref Color color)
{
var sub = endPos - startPos;
var length = sub.Length;
if (length > Mathf.Epsilon)
{
var dir = sub / length;
var arrowRect = new Rectangle(0, 0, 16.0f, 16.0f);
float rotation = Float2.Dot(dir, Float2.UnitY);
if (endPos.X < startPos.X)
rotation = 2 - rotation;
var sprite = Editor.Instance.Icons.VisjectArrowClosed32;
var arrowTransform =
Matrix3x3.Translation2D(-6.5f, -8) *
Matrix3x3.RotationZ(rotation * Mathf.PiOverTwo) *
Matrix3x3.Translation2D(endPos - dir * 8);
Render2D.PushTransform(ref arrowTransform);
Render2D.DrawSprite(sprite, arrowRect, color);
Render2D.PopTransform();
endPos -= dir * 4.0f;
}
Render2D.DrawLine(startPos, endPos, color);
}
/// <summary>
/// Gets the connection end point for the given input position. Puts the end point near the edge of the node bounds.
/// </summary>
@@ -1092,6 +1059,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;
}
}
/// <inheritdoc />
@@ -1132,6 +1109,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);
}
/// <inheritdoc />
@@ -1295,7 +1276,7 @@ namespace FlaxEditor.Surface.Archetypes
isMouseOver = Float2.DistanceSquared(ref mousePosition, ref point) < 25.0f;
}
var color = isMouseOver ? Color.Wheat : t.LineColor;
DrawConnection(ref t.StartPos, ref t.EndPos, ref color);
SurfaceStyle.DrawStraightConnection(t.StartPos, t.EndPos, color);
}
}
@@ -1324,7 +1305,7 @@ namespace FlaxEditor.Surface.Archetypes
/// <inheritdoc />
public void DrawConnectingLine(ref Float2 startPos, ref Float2 endPos, ref Color color)
{
DrawConnection(ref startPos, ref endPos, ref color);
SurfaceStyle.DrawStraightConnection(startPos, endPos, color);
}
/// <inheritdoc />

View File

@@ -31,7 +31,7 @@ namespace FlaxEditor.Surface
var editor = Editor.Instance;
var style = SurfaceStyle.CreateStyleHandler(editor);
style.DrawBox = DrawBox;
style.DrawConnection = DrawConnection;
style.DrawConnection = SurfaceStyle.DrawStraightConnection;
return style;
}
@@ -49,11 +49,6 @@ namespace FlaxEditor.Surface
Render2D.FillRectangle(rect, color);
}
private static void DrawConnection(Float2 start, Float2 end, Color color, float thickness)
{
Archetypes.Animation.StateMachineStateBase.DrawConnection(ref start, ref end, ref color);
}
private void OnActiveContextMenuVisibleChanged(Control activeCM)
{
_nodesCache.Wait();

View File

@@ -492,7 +492,6 @@ namespace FlaxEditor.Surface.ContextMenu
// If no item is selected (or it's not visible anymore), select the top one
Profiler.BeginEvent("VisjectCM.Layout");
if (SelectedItem == null || !SelectedItem.VisibleInHierarchy)
SelectedItem = _groups.Find(g => g.Visible)?.Children.Find(c => c.Visible && c is VisjectCMItem) as VisjectCMItem;
PerformLayout();
if (SelectedItem != null)
@@ -704,7 +703,7 @@ namespace FlaxEditor.Surface.ContextMenu
Hide();
return true;
}
else if (key == KeyboardKeys.Return)
else if (key == KeyboardKeys.Return || key == KeyboardKeys.Tab)
{
if (SelectedItem != null)
OnClickItem(SelectedItem);

View File

@@ -73,8 +73,6 @@ namespace FlaxEditor.Surface.ContextMenu
{
SortScore = 0;
if (!(_highlights?.Count > 0))
return;
if (!Visible)
return;
@@ -83,6 +81,8 @@ namespace FlaxEditor.Surface.ContextMenu
SortScore += 1;
if (Data != null)
SortScore += 1;
if (_highlights is { Count: > 0 })
SortScore += 1;
if (_isStartsWithMatch)
SortScore += 2;
if (_isFullMatch)
@@ -169,6 +169,7 @@ namespace FlaxEditor.Surface.ContextMenu
/// <param name="groupHeaderMatches">True if item's group header got a filter match and item should stay visible.</param>
public void UpdateFilter(string filterText, Box selectedBox, bool groupHeaderMatches = false)
{
// When dragging connection out of a box, validate if the box is compatible with this item's type
if (selectedBox != null)
{
Visible = CanConnectTo(selectedBox);
@@ -185,10 +186,12 @@ namespace FlaxEditor.Surface.ContextMenu
// Clear filter
_highlights?.Clear();
Visible = true;
return;
}
else
{
GetTextRectangle(out var textRect);
// Check archetype title
if (QueryFilterHelper.Match(filterText, _archetype.Title, out var ranges))
{
// Update highlights
@@ -212,46 +215,59 @@ namespace FlaxEditor.Surface.ContextMenu
}
}
Visible = true;
return;
}
else if (_archetype.AlternativeTitles?.Any(altTitle => string.Equals(filterText, altTitle, StringComparison.CurrentCultureIgnoreCase)) == true)
{
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(1);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
var start = font.GetCharPosition(_archetype.Title, 0);
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
_isFullMatch = true;
Visible = true;
}
else if (NodeArchetype.TryParseText != null && NodeArchetype.TryParseText(filterText, out var data))
{
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(1);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
var start = font.GetCharPosition(_archetype.Title, 0);
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
Visible = true;
Data = data;
}
else if (!groupHeaderMatches)
// Check archetype synonyms
if (_archetype.AlternativeTitles!= null && _archetype.AlternativeTitles.Any(altTitle => QueryFilterHelper.Match(filterText, altTitle, out ranges)))
{
// Hide
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(1);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
var start = font.GetCharPosition(_archetype.Title, 0);
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
for (int i = 0; i < ranges.Length; i++)
{
if (ranges[i].StartIndex <= 0)
{
_isStartsWithMatch = true;
}
}
Visible = true;
return;
}
// Check archetype data (if it exists)
if (NodeArchetype.TryParseText != null && NodeArchetype.TryParseText(filterText, out var data))
{
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(1);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
var start = font.GetCharPosition(_archetype.Title, 0);
var end = font.GetCharPosition(_archetype.Title, _archetype.Title.Length - 1);
_highlights.Add(new Rectangle(start.X + textRect.X, 0, end.X - start.X, Height));
Visible = true;
Data = data;
return;
}
_highlights?.Clear();
// Hide
if (!groupHeaderMatches)
Visible = false;
}
}
}
/// <inheritdoc />
public override void Draw()

View File

@@ -295,5 +295,38 @@ namespace FlaxEditor.Surface
Background = editor.UI.VisjectSurfaceBackground,
};
}
/// <summary>
/// Draws a simple straight connection between two locations.
/// </summary>
/// <param name="startPos">The start position.</param>
/// <param name="endPos">The end position.</param>
/// <param name="color">The line color.</param>
/// <param name="thickness">The line thickness.</param>
public static void DrawStraightConnection(Float2 startPos, Float2 endPos, Color color, float thickness = 1.0f)
{
var sub = endPos - startPos;
var length = sub.Length;
if (length > Mathf.Epsilon)
{
var dir = sub / length;
var arrowRect = new Rectangle(0, 0, 16.0f, 16.0f);
float rotation = Float2.Dot(dir, Float2.UnitY);
if (endPos.X < startPos.X)
rotation = 2 - rotation;
var sprite = Editor.Instance.Icons.VisjectArrowClosed32;
var arrowTransform =
Matrix3x3.Translation2D(-6.5f, -8) *
Matrix3x3.RotationZ(rotation * Mathf.PiOverTwo) *
Matrix3x3.Translation2D(endPos - dir * 8);
Render2D.PushTransform(ref arrowTransform);
Render2D.DrawSprite(sprite, arrowRect, color);
Render2D.PopTransform();
endPos -= dir * 4.0f;
}
Render2D.DrawLine(startPos, endPos, color);
}
}
}

View File

@@ -33,6 +33,30 @@ namespace FlaxEditor.Surface
/// </summary>
public event Action<VisjectSurfaceContext> ContextChanged;
/// <summary>
/// Finds the surface context with the given owning nodes IDs path.
/// </summary>
/// <param name="nodePath">The node ids path.</param>
/// <returns>Found context or null if cannot.</returns>
public VisjectSurfaceContext FindContext(Span<uint> 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;
}
/// <summary>
/// Creates the Visject surface context for the given surface data source context.
/// </summary>
@@ -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);

View File

@@ -20,8 +20,15 @@ namespace FlaxEditor.Surface
/// <summary>
/// Utility for easy nodes archetypes generation for Visject Surface based on scripting types.
/// </summary>
internal class NodesCache
[HideInEditor]
public class NodesCache
{
/// <summary>
/// Delegate for scripting types filtering into cache.
/// </summary>
/// <param name="scriptType">The input type to process.</param>
/// <param name="cache">Node groups cache that can be used for reusing groups for different nodes.</param>
/// <param name="version">The cache version number. Can be used to reject any cached data after <see cref="NodesCache"/> rebuilt.</param>
public delegate void IterateType(ScriptType scriptType, Dictionary<KeyValuePair<string, ushort>, GroupArchetype> cache, int version);
internal static readonly List<NodesCache> Caches = new List<NodesCache>(8);
@@ -33,11 +40,18 @@ namespace FlaxEditor.Surface
private VisjectCM _taskContextMenu;
private Dictionary<KeyValuePair<string, ushort>, GroupArchetype> _cache;
/// <summary>
/// Initializes a new instance of the <see cref="NodesCache"/> class.
/// </summary>
/// <param name="iterator">The iterator callback to build node types from Scripting.</param>
public NodesCache(IterateType iterator)
{
_iterator = iterator;
}
/// <summary>
/// Waits for the async caching job to finish.
/// </summary>
public void Wait()
{
if (_task != null)
@@ -48,6 +62,9 @@ namespace FlaxEditor.Surface
}
}
/// <summary>
/// Clears cache.
/// </summary>
public void Clear()
{
Wait();
@@ -62,6 +79,10 @@ namespace FlaxEditor.Surface
}
}
/// <summary>
/// Updates the Visject Context Menu to contain current nodes.
/// </summary>
/// <param name="contextMenu">The output context menu to setup.</param>
public void Get(VisjectCM contextMenu)
{
Profiler.BeginEvent("Setup Context Menu");

View File

@@ -156,6 +156,11 @@ namespace FlaxEditor.Surface
/// </summary>
public event Action<SurfaceControl> ControlDeleted;
/// <summary>
/// Identifier of the node that 'owns' this context (eg. State Machine which created this graph of state nodes).
/// </summary>
public uint OwnerNodeID;
/// <summary>
/// Initializes a new instance of the <see cref="VisjectSurfaceContext"/> class.
/// </summary>

View File

@@ -25,7 +25,7 @@ namespace FlaxEditor.Surface
/// The base interface for editor windows that use <see cref="FlaxEditor.Surface.VisjectSurface"/> for content editing.
/// </summary>
/// <seealso cref="FlaxEditor.Surface.IVisjectSurfaceOwner" />
interface IVisjectSurfaceWindow : IVisjectSurfaceOwner
public interface IVisjectSurfaceWindow : IVisjectSurfaceOwner
{
/// <summary>
/// Gets the asset edited by the window.

View File

@@ -43,6 +43,19 @@ namespace FlaxEngine.Tools
/// Enables continuous painting, otherwise single paint on click.
/// </summary>
public bool ContinuousPaint;
/// <summary>
/// Enables drawing cloth paint debugging with Depth Test enabled (skips occluded vertices).
/// </summary>
public bool DebugDrawDepthTest
{
get => Gizmo.Cloth?.DebugDrawDepthTest ?? true;
set
{
if (Gizmo.Cloth != null)
Gizmo.Cloth.DebugDrawDepthTest = value;
}
}
#pragma warning restore CS0649
public override void Init(IGizmoOwner owner)
@@ -62,6 +75,7 @@ namespace FlaxEngine.Tools
public override void Dispose()
{
Owner.Gizmos.Remove(Gizmo);
Gizmo = null;
base.Dispose();
}
@@ -83,6 +97,7 @@ namespace FlaxEngine.Tools
private EditClothPaintAction _undoAction;
public bool IsPainting => _isPainting;
public Cloth Cloth => _cloth;
public ClothPaintingGizmo(IGizmoOwner owner, ClothPaintingGizmoMode mode)
: base(owner)

View File

@@ -45,9 +45,6 @@ namespace FlaxEditor.Tools.Terrain
[EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")]
public MaterialBase Material;
[EditorOrder(200), EditorDisplay("Collision"), DefaultValue(null), AssetReference(typeof(PhysicalMaterial), true), Tooltip("Terrain default physical material used to define the collider physical properties.")]
public JsonAsset PhysicalMaterial;
[EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")]
public int CollisionLOD = -1;
@@ -152,7 +149,6 @@ namespace FlaxEditor.Tools.Terrain
terrain.Setup(_options.LODCount, (int)_options.ChunkSize);
terrain.Transform = new Transform(_options.Position, _options.Orientation, _options.Scale);
terrain.Material = _options.Material;
terrain.PhysicalMaterial = _options.PhysicalMaterial;
terrain.CollisionLOD = _options.CollisionLOD;
if (_options.Heightmap)
terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0);

View File

@@ -67,11 +67,13 @@ namespace FlaxEditor.Tools.Terrain.Paint
// Prepare
var splatmapIndex = ActiveSplatmapIndex;
var splatmapIndexOther = (splatmapIndex + 1) % 2;
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
var heightmapLength = heightmapSize * heightmapSize;
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes).ToPointer();
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, splatmapIndex).ToPointer();
var tempBufferOther = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, (splatmapIndex + 1) % 2).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
ApplyParams p = new ApplyParams
{
@@ -81,8 +83,10 @@ namespace FlaxEditor.Tools.Terrain.Paint
Options = options,
Strength = strength,
SplatmapIndex = splatmapIndex,
SplatmapIndexOther = splatmapIndexOther,
HeightmapSize = heightmapSize,
TempBuffer = tempBuffer,
TempBufferOther = tempBufferOther,
};
// Get brush bounds in terrain local space
@@ -132,10 +136,15 @@ namespace FlaxEditor.Tools.Terrain.Paint
if (sourceData == null)
throw new Exception("Cannot modify terrain. Loading splatmap failed. See log for more info.");
var sourceDataOther = TerrainTools.GetSplatMapData(terrain, ref patch.PatchCoord, splatmapIndexOther);
if (sourceDataOther == null)
throw new Exception("Cannot modify terrain. Loading splatmap failed. See log for more info.");
// Record patch data before editing it
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
{
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndex);
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord, splatmapIndexOther);
}
// Apply modification
@@ -144,6 +153,7 @@ namespace FlaxEditor.Tools.Terrain.Paint
p.PatchCoord = patch.PatchCoord;
p.PatchPositionLocal = patchPositionLocal;
p.SourceData = sourceData;
p.SourceDataOther = sourceDataOther;
Apply(ref p);
}
}
@@ -198,16 +208,32 @@ namespace FlaxEditor.Tools.Terrain.Paint
/// </summary>
public int SplatmapIndex;
/// <summary>
/// The splatmap texture index. If <see cref="SplatmapIndex"/> is 0, this will be 1. If <see cref="SplatmapIndex"/> is 1, this will be 0.
/// </summary>
public int SplatmapIndexOther;
/// <summary>
/// The temporary data buffer (for modified data).
/// </summary>
public Color32* TempBuffer;
/// <summary>
/// The 'other' temporary data buffer (for modified data). If <see cref="TempBuffer"/> refersto the splatmap with index 0, this one will refer to the one with index 1.
/// </summary>
public Color32* TempBufferOther;
/// <summary>
/// The source data buffer.
/// </summary>
public Color32* SourceData;
/// <summary>
/// The 'other' source data buffer. If <see cref="SourceData"/> refers
/// to the splatmap with index 0, this one will refer to the one with index 1.
/// </summary>
public Color32* SourceDataOther;
/// <summary>
/// The heightmap size (edge).
/// </summary>

View File

@@ -1,6 +1,5 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Paint
@@ -73,53 +72,53 @@ namespace FlaxEditor.Tools.Terrain.Paint
var strength = p.Strength;
var layer = (int)Layer;
var brushPosition = p.Gizmo.CursorPosition;
var layerComponent = layer % 4;
var c = layer % 4;
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
bool otherModified = false;
for (int z = 0; z < p.ModifiedSize.Y; z++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
{
var xx = x + p.ModifiedOffset.X;
var src = p.SourceData[zz * p.HeightmapSize + xx];
var src = (Color)p.SourceData[zz * p.HeightmapSize + xx];
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, 0, zz * FlaxEngine.Terrain.UnitsPerVertex);
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
var sample = Mathf.Saturate(p.Brush.Sample(ref brushPosition, ref samplePositionWorld));
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
var paintAmount = sample * strength;
if (paintAmount < 0.0f)
continue; // Skip when pixel won't be affected
// Extract layer weight
byte* srcPtr = &src.R;
var srcWeight = *(srcPtr + layerComponent) / 255.0f;
// Accumulate weight
float dstWeight = srcWeight + paintAmount;
// Check for solid layer case
if (dstWeight >= 1.0f)
{
// Erase other layers
// TODO: maybe erase only the higher layers?
// TODO: need to erase also weights form the other splatmaps
src = Color32.Transparent;
// Use limit value
dstWeight = 1.0f;
}
// Modify packed weight
*(srcPtr + layerComponent) = (byte)(dstWeight * 255.0f);
// Write back
// Paint on the active splatmap texture
src[c] = Mathf.Saturate(src[c] + paintAmount);
src[(c + 1) % 4] = Mathf.Saturate(src[(c + 1) % 4] - paintAmount);
src[(c + 2) % 4] = Mathf.Saturate(src[(c + 2) % 4] - paintAmount);
src[(c + 3) % 4] = Mathf.Saturate(src[(c + 3) % 4] - paintAmount);
p.TempBuffer[z * p.ModifiedSize.X + x] = src;
var other = (Color)p.SourceDataOther[zz * p.HeightmapSize + xx];
//if (other.ValuesSum > 0.0f) // Skip editing the other splatmap if it's empty
{
// Remove 'paint' from the other splatmap texture
other[c] = Mathf.Saturate(other[c] - paintAmount);
other[(c + 1) % 4] = Mathf.Saturate(other[(c + 1) % 4] - paintAmount);
other[(c + 2) % 4] = Mathf.Saturate(other[(c + 2) % 4] - paintAmount);
other[(c + 3) % 4] = Mathf.Saturate(other[(c + 3) % 4] - paintAmount);
p.TempBufferOther[z * p.ModifiedSize.X + x] = other;
otherModified = true;
}
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifySplatMap(p.Terrain, ref p.PatchCoord, p.SplatmapIndex, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
if (otherModified)
TerrainTools.ModifySplatMap(p.Terrain, ref p.PatchCoord, p.SplatmapIndexOther, p.TempBufferOther, ref p.ModifiedOffset, ref p.ModifiedSize);
}
}
}

View File

@@ -36,9 +36,35 @@ namespace FlaxEditor.Tools.Terrain
"Layer 7",
};
private IntPtr _cachedSplatmapData;
private int _cachedSplatmapDataSize;
private struct SplatmapData
{
public IntPtr DataPtr;
public int Size;
public void EnsureCapacity(int size)
{
if (Size < size)
{
if (DataPtr != IntPtr.Zero)
Marshal.FreeHGlobal(DataPtr);
DataPtr = Marshal.AllocHGlobal(size);
Utils.MemoryClear(DataPtr, (ulong)size);
Size = size;
}
}
public void Free()
{
if (DataPtr == IntPtr.Zero)
return;
Marshal.FreeHGlobal(DataPtr);
DataPtr = IntPtr.Zero;
Size = 0;
}
}
private EditTerrainMapAction _activeAction;
private SplatmapData[] _cachedSplatmapData = new SplatmapData[2];
/// <summary>
/// The terrain painting gizmo.
@@ -230,20 +256,13 @@ namespace FlaxEditor.Tools.Terrain
/// Gets the splatmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
/// </summary>
/// <param name="size">The minimum buffer size (in bytes).</param>
/// <param name="splatmapIndex">The splatmap index for which to return/create the temp buffer.</param>
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
public IntPtr GetSplatmapTempBuffer(int size)
public IntPtr GetSplatmapTempBuffer(int size, int splatmapIndex)
{
if (_cachedSplatmapDataSize < size)
{
if (_cachedSplatmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedSplatmapData);
}
_cachedSplatmapData = Marshal.AllocHGlobal(size);
_cachedSplatmapDataSize = size;
}
return _cachedSplatmapData;
ref var splatmapData = ref _cachedSplatmapData[splatmapIndex];
splatmapData.EnsureCapacity(size);
return splatmapData.DataPtr;
}
/// <summary>
@@ -276,12 +295,8 @@ namespace FlaxEditor.Tools.Terrain
base.OnDeactivated();
// Free temporary memory buffer
if (_cachedSplatmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedSplatmapData);
_cachedSplatmapData = IntPtr.Zero;
_cachedSplatmapDataSize = 0;
}
foreach (ref var splatmapData in _cachedSplatmapData.AsSpan())
splatmapData.Free();
}
/// <summary>

View File

@@ -20,7 +20,7 @@ bool TerrainTools::TryGetPatchCoordToAdd(Terrain* terrain, const Ray& ray, Int2&
{
CHECK_RETURN(terrain, true);
result = Int2::Zero;
const float patchSize = terrain->GetChunkSize() * TERRAIN_UNITS_PER_VERTEX * TerrainPatch::CHUNKS_COUNT_EDGE;
const float patchSize = terrain->GetChunkSize() * TERRAIN_UNITS_PER_VERTEX * Terrain::ChunksCountEdge;
// Try to pick any of the patch edges
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
@@ -179,7 +179,7 @@ bool TerrainTools::GenerateTerrain(Terrain* terrain, const Int2& numberOfPatches
terrain->AddPatches(numberOfPatches);
// Prepare data
const auto heightmapSize = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
const auto heightmapSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
Array<float> heightmapData;
heightmapData.Resize(heightmapSize * heightmapSize);
@@ -380,7 +380,7 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
const auto firstPatch = terrain->GetPatch(0);
// Calculate texture size
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1;
const int32 patchEdgeVertexCount = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
const int32 patchVertexCount = patchEdgeVertexCount * patchEdgeVertexCount;
// Find size of heightmap in patches

View File

@@ -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/Core/Config/BuildSettings.h"
#include "Engine/Content/Content.h"
@@ -19,498 +18,6 @@
#if PLATFORM_MAC
#include "Engine/Platform/Apple/ApplePlatformSettings.h"
#endif
#include <fstream>
#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
/// <summary>
/// MS-DOS header found at the beginning in a PE format file.
/// </summary>
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;
};
/// <summary>
/// COFF header found in a PE format file.
/// </summary>
struct COFFHeader
{
uint16 machine;
uint16 numSections;
uint32 timeDateStamp;
uint32 ptrSymbolTable;
uint32 numSymbols;
uint16 sizeOptHeader;
uint16 characteristics;
};
/// <summary>
/// Contains address and size of data areas in a PE image.
/// </summary>
struct PEDataDirectory
{
uint32 virtualAddress;
uint32 size;
};
/// <summary>
/// Optional header in a 32-bit PE format file.
/// </summary>
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];
};
/// <summary>
/// Optional header in a 64-bit PE format file.
/// </summary>
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];
};
/// <summary>
/// A section header in a PE format file.
/// </summary>
struct PESectionHeader
{
char name[8];
uint32 virtualSize;
uint32 relativeVirtualAddress;
uint32 physicalSize;
uint32 physicalAddress;
uint8 deprecated[12];
uint32 flags;
};
/// <summary>
/// A resource table header within a .rsrc section in a PE format file.
/// </summary>
struct PEImageResourceDirectory
{
uint32 flags;
uint32 timeDateStamp;
uint16 majorVersion;
uint16 minorVersion;
uint16 numNamedEntries;
uint16 numIdEntries;
};
/// <summary>
/// A single entry in a resource table within a .rsrc section in a PE format file.
/// </summary>
struct PEImageResourceEntry
{
uint32 type;
uint32 offsetDirectory : 31;
uint32 isDirectory : 1;
};
/// <summary>
/// An entry in a resource table referencing resource data. Found within a .rsrc section in a PE format file.
/// </summary>
struct PEImageResourceEntryData
{
uint32 offsetData;
uint32 size;
uint32 codePage;
uint32 resourceHandle;
};
/// <summary>
/// Header used in icon file format.
/// </summary>
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<PESectionHeader> 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<uint8> 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;
}
String EditorUtilities::GetOutputName()
{

View File

@@ -22,14 +22,6 @@ public:
SplashScreen,
};
/// <summary>
/// Updates the Win32 executable file icon.
/// </summary>
/// <param name="path">The exe path.</param>
/// <param name="icon">The icon image data.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool UpdateExeIcon(const String& path, const TextureData& icon);
static String GetOutputName();
static bool FormatAppPackageName(String& packageName);
static bool GetApplicationImage(const Guid& imageId, TextureData& imageData, ApplicationImageType type = ApplicationImageType::Icon);

View File

@@ -140,8 +140,7 @@ namespace FlaxEditor.Utilities
// Check if start the matching sequence
if (matchStartPos == -1)
{
if (ranges == null)
ranges = new List<Range>();
ranges ??= new List<Range>();
matchStartPos = textPos;
}
}
@@ -152,7 +151,7 @@ namespace FlaxEditor.Utilities
{
var length = textPos - matchStartPos;
if (length >= MinLength)
ranges.Add(new Range(matchStartPos, length));
ranges!.Add(new Range(matchStartPos, length));
textPos = matchStartPos + length;
matchStartPos = -1;
}
@@ -165,13 +164,13 @@ namespace FlaxEditor.Utilities
{
var length = endPos - matchStartPos;
if (length >= MinLength)
ranges.Add(new Range(matchStartPos, length));
ranges!.Add(new Range(matchStartPos, length));
textPos = matchStartPos + length;
}
}
// Check if has any range
if (ranges != null && ranges.Count > 0)
if (ranges is { Count: > 0 })
{
matches = ranges.ToArray();
return true;

View File

@@ -174,7 +174,10 @@ namespace FlaxEditor.Viewport.Cameras
}
else
{
position = sphere.Center - Vector3.Forward * orientation * (sphere.Radius * 2.5f);
// calculate the min. distance so that the sphere fits roughly 70% in FOV
// clip to far plane as a disappearing big object might be confusing
var distance = Mathf.Min(1.4f * sphere.Radius / Mathf.Tan(Mathf.DegreesToRadians * Viewport.FieldOfView / 2), Viewport.FarPlane);
position = sphere.Center - Vector3.Forward * orientation * distance;
}
TargetPoint = sphere.Center;
MoveViewport(position, orientation);

View File

@@ -334,6 +334,22 @@ namespace FlaxEditor.Viewport
}
}
/// <summary>
/// Gets the bounding frustum of the current viewport camera.
/// </summary>
public BoundingFrustum ViewFrustum
{
get
{
Vector3 viewOrigin = Task.View.Origin;
Float3 position = ViewPosition - viewOrigin;
CreateViewMatrix(position, out var view);
CreateProjectionMatrix(out var projection);
Matrix.Multiply(ref view, ref projection, out var viewProjection);
return new BoundingFrustum(ref viewProjection);
}
}
/// <summary>
/// Gets or sets the yaw angle (in degrees).
/// </summary>

View File

@@ -234,7 +234,6 @@ namespace FlaxEditor.Viewport
LocalOrientation = RootNode.RaycastNormalRotation(ref hitNormal),
Name = item.ShortName
};
DebugDraw.DrawWireArrow(PostProcessSpawnedActorLocation(actor, ref hitNormal), actor.LocalOrientation, 1.0f, Color.Red, 1000000);
Spawn(actor, ref hitLocation, ref hitNormal);
}
else if (hit is StaticModelNode staticModelNode)

View File

@@ -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,7 +396,7 @@ namespace FlaxEditor.Windows.Assets
}
/// <inheritdoc />
public override void OnUpdate()
public override unsafe void OnUpdate()
{
// Extract animations playback state from the events tracing
var debugActor = _debugPicker.Value as AnimatedModel;
@@ -413,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<uint>(debugFlow.NodePath, 8));
var node = context?.FindNode(debugFlow.NodeId);
var box = node?.GetBox(debugFlow.BoxId);
box?.HighlightConnections();
}

View File

@@ -48,7 +48,7 @@ namespace FlaxEditor.Windows.Assets
// Use virtual animation graph to playback the animation
_animGraph = FlaxEngine.Content.CreateVirtualAsset<AnimationGraph>();
_animGraph.InitAsAnimation(model, _window.Asset);
_animGraph.InitAsAnimation(model, _window.Asset, true, true);
PreviewActor.AnimationGraph = _animGraph;
}

View File

@@ -144,7 +144,7 @@ namespace FlaxEditor.Windows.Assets
protected override void OnAssetLinked()
{
Asset.WaitForLoaded();
_textPreview.Font = new FontReference(Asset.CreateFont(30));
_textPreview.Font = new FontReference(Asset, 30);
_inputText.Text = string.Format("This is a sample text using font {0}.", Asset.FamilyName);
var options = Asset.Options;
_proxy.Set(ref options);

View File

@@ -79,6 +79,13 @@ namespace FlaxEditor.Windows
if (item.HasDefaultThumbnail == false)
{
if (_view.SelectedCount > 1)
cm.AddButton("Refresh thumbnails", () =>
{
foreach (var e in _view.Selection)
e.RefreshThumbnail();
});
else
cm.AddButton("Refresh thumbnail", item.RefreshThumbnail);
}

View File

@@ -263,6 +263,8 @@ namespace FlaxEditor.Windows
/// <inheritdoc />
public override void OnDestroy()
{
if (IsDisposing)
return;
OnExit();
// Unregister

View File

@@ -485,6 +485,16 @@ namespace FlaxEditor.Windows
{
base.OnShowContextMenu(menu);
// Focus on play
{
var focus = menu.AddButton("Start Focused");
focus.CloseMenuOnClick = false;
var checkbox = new CheckBox(140, 2, FocusOnPlay) { Parent = focus };
checkbox.StateChanged += state => FocusOnPlay = state.Checked;
}
menu.AddSeparator();
// Viewport Brightness
{
var brightness = menu.AddButton("Viewport Brightness");

View File

@@ -20,24 +20,20 @@ namespace FlaxEngine
get
{
fixed (short* name = Name0)
{
return new string((char*)name);
}
}
}
internal unsafe bool NameStartsWith(string prefix)
{
fixed (short* name = Name0)
{
fixed (char* p = prefix)
{
return Utils.MemoryCompare(new IntPtr(name), new IntPtr(p), (ulong)(prefix.Length * 2)) == 0;
}
}
}
}
}
}
namespace FlaxEditor.Windows.Profiler

View File

@@ -132,10 +132,13 @@ namespace FlaxEditor.Windows
b = contextMenu.AddButton("Cut", inputOptions.Cut, Editor.SceneEditing.Cut);
b.Enabled = canEditScene;
// Prefab options
// Create option
contextMenu.AddSeparator();
b = contextMenu.AddButton("Create parent for selected actors", Editor.SceneEditing.CreateParentForSelectedActors);
b.Enabled = canEditScene && hasSthSelected;
b = contextMenu.AddButton("Create Prefab", Editor.Prefabs.CreatePrefab);
b.Enabled = isSingleActorSelected &&
((ActorNode)Editor.SceneEditing.Selection[0]).CanCreatePrefab &&

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Gizmo;
using FlaxEditor.Content;
using FlaxEditor.GUI.Tree;
@@ -14,7 +13,6 @@ using FlaxEditor.Scripting;
using FlaxEditor.States;
using FlaxEngine;
using FlaxEngine.GUI;
using static FlaxEditor.GUI.ItemsListContextMenu;
namespace FlaxEditor.Windows
{
@@ -35,6 +33,11 @@ namespace FlaxEditor.Windows
private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
/// <summary>
/// Scene tree panel.
/// </summary>
public Panel SceneTreePanel => _sceneTreePanel;
/// <summary>
/// Initializes a new instance of the <see cref="SceneTreeWindow"/> class.
/// </summary>

View File

@@ -146,7 +146,7 @@ namespace FlaxEngine
public string Path;
/// <summary>
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelectorAny"/> structure.
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelector{T}"/> structure.
/// </summary>
/// <param name="path">The selector path.</param>
public BehaviorKnowledgeSelector(string path)
@@ -155,7 +155,7 @@ namespace FlaxEngine
}
/// <summary>
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelectorAny"/> structure.
/// Initializes a new instance of the <see cref="BehaviorKnowledgeSelector{T}"/> structure.
/// </summary>
/// <param name="other">The other selector.</param>
public BehaviorKnowledgeSelector(BehaviorKnowledgeSelectorAny other)

View File

@@ -91,6 +91,13 @@ API_STRUCT(InBuild, Template, MarshalAs=StringAnsi) struct FLAXENGINE_API Behavi
return false;
}
BehaviorKnowledgeSelector() = default;
BehaviorKnowledgeSelector(const StringAnsi& other)
{
Path = other;
}
BehaviorKnowledgeSelector& operator=(const StringAnsiView& other) noexcept
{
Path = other;

View File

@@ -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);
}

View File

@@ -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"
/// <summary>
/// Single node animation data container.
@@ -50,19 +50,7 @@ public:
/// <param name="time">The time to evaluate the curves at.</param>
/// <param name="result">The interpolated value from the curve at provided time.</param>
/// <param name="loop">If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.</param>
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;
/// <summary>
/// Evaluates the animation transformation at the specified time.
@@ -70,29 +58,37 @@ public:
/// <param name="time">The time to evaluate the curves at.</param>
/// <param name="result">The interpolated value from the curve at provided time.</param>
/// <param name="loop">If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.</param>
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;
/// <summary>
/// Gets the total amount of keyframes in the animation curves.
/// </summary>
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;
};
/// <summary>
/// Root Motion modes that can be applied by the animation. Used as flags for selective behavior.
/// </summary>
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);
/// <summary>
/// Skeleton nodes animation data container. Includes metadata about animation sampling, duration and node animations curves.
/// </summary>
@@ -111,7 +107,7 @@ struct AnimationData
/// <summary>
/// Enables root motion extraction support from this animation.
/// </summary>
bool EnableRootMotion = false;
AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::None;
/// <summary>
/// The animation name.
@@ -140,49 +136,23 @@ public:
return static_cast<float>(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;
/// <summary>
/// Gets the total amount of keyframes in the all animation channels.
/// </summary>
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);
/// <summary>
/// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange.
/// </summary>
/// <param name="other">The other object.</param>
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);
/// <summary>
/// Releases data.
/// </summary>
void Dispose()
{
Name.Clear();
Duration = 0.0;
FramesPerSecond = 0.0;
RootNodeName.Clear();
EnableRootMotion = false;
Channels.Resize(0);
}
void Dispose();
};

View File

@@ -52,7 +52,7 @@ namespace
AnimationsService AnimationManagerInstance;
TaskGraphSystem* Animations::System = nullptr;
#if USE_EDITOR
Delegate<Asset*, ScriptingObject*, uint32, uint32> Animations::DebugFlow;
Delegate<Animations::DebugFlowInfo> 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

View File

@@ -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<Asset*, ScriptingObject*, uint32, uint32> 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<DebugFlowInfo> DebugFlow;
#endif
/// <summary>

View File

@@ -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

View File

@@ -157,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

View File

@@ -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;

View File

@@ -9,7 +9,7 @@
extern void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 i);
ThreadLocal<AnimGraphContext> AnimGraphExecutor::Context;
ThreadLocal<AnimGraphContext*> AnimGraphExecutor::Context;
Transform AnimGraphImpulse::GetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex) const
{
@@ -104,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();
@@ -204,19 +204,24 @@ 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<AnimGraphContext>();
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.StackOverFlow = false;
context.CurrentFrameIndex = ++data.CurrentFrame;
context.CallStack.Clear();
context.Functions.Clear();
@@ -377,12 +382,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;
@@ -404,12 +409,15 @@ 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.StackOverFlow)
return Value::Zero;
if (context.CallStack.Count() >= ANIM_GRAPH_MAX_CALL_STACK)
{
OnError(caller, box, TEXT("Graph is looped or too deep!"));
context.StackOverFlow = true;
return Value::Zero;
}
#if !BUILD_RELEASE
@@ -424,7 +432,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<Node>()->ID, box->ID);
Animations::DebugFlowInfo flowInfo;
flowInfo.Asset = _graph._owner;
flowInfo.Instance = context.Data->Object;
flowInfo.NodeId = box->GetParent<Node>()->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
@@ -441,6 +457,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();
}

View File

@@ -92,9 +92,9 @@ enum class BoneTransformMode
};
/// <summary>
/// The animated model root motion mode.
/// The animated model root motion extraction modes.
/// </summary>
enum class RootMotionMode
enum class RootMotionExtraction
{
/// <summary>
/// Don't extract nor apply the root motion.
@@ -205,7 +205,7 @@ struct FLAXENGINE_API AnimGraphSlot
/// <summary>
/// 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.
/// </summary>
API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
API_STRUCT(NoDefault) struct FLAXENGINE_API AnimGraphTraceEvent
{
DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent);
@@ -215,6 +215,8 @@ API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
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] = {};
};
/// <summary>
@@ -794,12 +796,16 @@ struct AnimGraphContext
AnimGraphInstanceData* Data;
AnimGraphImpulse EmptyNodes;
AnimGraphTransitionData TransitionData;
bool StackOverFlow;
Array<VisjectExecutor::Node*, FixedAllocation<ANIM_GRAPH_MAX_CALL_STACK>> CallStack;
Array<VisjectExecutor::Graph*, FixedAllocation<32>> GraphStack;
Array<uint32, FixedAllocation<ANIM_GRAPH_MAX_CALL_STACK> > NodePath;
Dictionary<VisjectExecutor::Node*, VisjectExecutor::Graph*> Functions;
ChunkedArray<AnimGraphImpulse, 256> PoseCache;
int32 PoseCacheSize;
Dictionary<VisjectExecutor::Box*, Variant> ValueCache;
AnimGraphTraceEvent& AddTraceEvent(const AnimGraphNode* node);
};
/// <summary>
@@ -810,11 +816,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<AnimGraphContext> Context;
static ThreadLocal<AnimGraphContext*> Context;
public:
/// <summary>
@@ -891,7 +897,7 @@ 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);

View File

@@ -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)
@@ -220,13 +231,12 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
// Add to trace
auto& context = Context.Get();
auto& context = *Context.Get();
if (context.Data->EnableTracing)
{
auto& trace = context.Data->TraceEvents.AddOne();
auto& trace = context.AddTraceEvent(node);
trace.Asset = anim;
trace.Value = animPos;
trace.NodeId = node->ID;
}
// Evaluate nested animations
@@ -313,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;
@@ -346,7 +361,9 @@ 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;
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;
}
@@ -354,7 +371,9 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
{
// Simple motion delta
// (now - before)
srcNode.Translation = rootNode.Translation - rootBefore.Translation;
if (motionPosition)
srcNode.Translation = (rootNode.Translation - rootBefore.Translation) * motionPositionMask;
if (motionRotation)
srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation;
}
@@ -369,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;
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;
if (motionPosition)
dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
if (motionRotation)
dstNode.Orientation += srcNode.Orientation * weight;
}
else if (weighted)
{
dstNode.Translation = srcNode.Translation * 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;
}
}
@@ -494,19 +525,30 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
return nodes;
}
Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
Variant AnimGraphExecutor::SampleState(AnimGraphContext& context, const AnimGraphNode* state)
{
auto& data = state->Data.State;
if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr)
return Value::Null;
// Add to trace
if (context.Data->EnableTracing)
{
auto& trace = context.AddTraceEvent(state);
}
ANIM_GRAPH_PROFILE_EVENT("Evaluate State");
context.NodePath.Add(state->ID);
auto rootNode = data.Graph->GetRootNode();
return eatBox((Node*)rootNode, &rootNode->Boxes[0]);
auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]);
context.NodePath.Pop();
return result;
}
void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition)
{
// Reset transiton
// Reset transition
stateMachineBucket.ActiveTransition = transition;
stateMachineBucket.TransitionPosition = 0.0f;
@@ -537,7 +579,7 @@ AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphCon
}
// Evaluate source state transition data (position, length, etc.)
const Value sourceStatePtr = SampleState(state);
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))
{
@@ -634,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
@@ -745,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)
{
@@ -769,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;
@@ -1660,6 +1702,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
bucket.CurrentState = bucket.ActiveTransition->Destination; \
InitStateTransition(context, bucket)
context.NodePath.Push(node->ID);
// Update the active transition
if (bucket.ActiveTransition)
{
@@ -1767,11 +1811,11 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
if (bucket.BaseTransitionState)
{
// Sample the other state (eg. when blending from interrupted state to the another state from the old destination)
value = SampleState(bucket.BaseTransitionState);
value = SampleState(context, bucket.BaseTransitionState);
if (bucket.BaseTransition)
{
// Evaluate the base pose from the time when transition was interrupted
const auto destinationState = SampleState(bucket.BaseTransition->Destination);
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);
}
@@ -1779,14 +1823,14 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
else
{
// Sample the current state
value = SampleState(bucket.CurrentState);
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 = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration;
@@ -1794,6 +1838,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
bucket.LastUpdateFrame = context.CurrentFrameIndex;
context.NodePath.Pop();
#undef END_TRANSITION
break;
}
@@ -2248,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)

View File

@@ -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);

View File

@@ -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];

View File

@@ -37,8 +37,9 @@ public:
/// <param name="baseModel">The base model asset.</param>
/// <param name="anim">The animation to play.</param>
/// <param name="loop">True if play animation in a loop.</param>
/// <param name="rootMotion">True if apply root motion. Otherwise it will be ignored.</param>
/// <returns>True if failed, otherwise false.</returns>
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);
/// <summary>
/// Tries to load surface graph from the asset.

View File

@@ -277,6 +277,8 @@ bool Content::GetAssetInfo(const StringView& path, AssetInfo& info)
// Find asset in registry
if (Cache.FindAsset(path, info))
return true;
if (!FileSystem::FileExists(path))
return false;
PROFILE_CPU();
const auto extension = FileSystem::GetExtension(path).ToLower();

View File

@@ -393,35 +393,58 @@ bool JsonAsset::CreateInstance()
if (typeHandle)
{
auto& type = typeHandle.GetType();
switch (type.Type)
{
case ScriptingTypes::Class:
{
// Ensure that object can deserialized
const ScriptingType::InterfaceImplementation* interface = type.GetInterface(ISerializable::TypeInitializer);
if (!interface)
{
LOG(Warning, "Cannot deserialize {0} from Json Asset because it doesn't implement ISerializable interface.", type.ToString());
break;
return false;
}
auto modifier = Cache::ISerializeModifier.Get();
modifier->EngineBuild = DataEngineBuild;
// Allocate object
// Create object
switch (type.Type)
{
case ScriptingTypes::Class:
case ScriptingTypes::Structure:
{
const auto instance = Allocator::Allocate(type.Size);
if (!instance)
return true;
Instance = instance;
InstanceType = typeHandle;
if (type.Type == ScriptingTypes::Class)
{
_dtor = type.Class.Dtor;
type.Class.Ctor(instance);
}
else
{
_dtor = type.Struct.Dtor;
type.Struct.Ctor(instance);
}
// Deserialize object
auto modifier = Cache::ISerializeModifier.Get();
modifier->EngineBuild = DataEngineBuild;
((ISerializable*)((byte*)instance + interface->VTableOffset))->Deserialize(*Data, modifier.Value);
break;
}
case ScriptingTypes::Script:
{
const ScriptingObjectSpawnParams params(Guid::New(), typeHandle);
const auto instance = type.Script.Spawn(params);
if (!instance)
return true;
Instance = instance;
_dtor = nullptr;
// Deserialize object
ToInterface<ISerializable>(instance)->Deserialize(*Data, modifier.Value);
break;
}
}
InstanceType = typeHandle;
}
return false;
}
@@ -441,13 +464,20 @@ void JsonAsset::DeleteInstance()
}
// C++ instance
if (!Instance || !_dtor)
if (!Instance)
return;
if (_dtor)
{
_dtor(Instance);
InstanceType = ScriptingTypeHandle();
Allocator::Free(Instance);
Instance = nullptr;
_dtor = nullptr;
Allocator::Free(Instance);
}
else
{
Delete((ScriptingObject*)Instance);
}
InstanceType = ScriptingTypeHandle();
Instance = nullptr;
}
#if USE_EDITOR

View File

@@ -139,7 +139,8 @@ public:
T* GetInstance() const
{
const_cast<JsonAsset*>(this)->CreateInstance();
return Instance && InstanceType.IsAssignableFrom(T::TypeInitializer) ? (T*)Instance : nullptr;
const ScriptingTypeHandle& type = T::TypeInitializer;
return Instance && type.IsAssignableFrom(InstanceType) ? (T*)Instance : nullptr;
}
public:

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Runtime.CompilerServices;
namespace FlaxEngine
{
/// <summary>
/// Json asset reference utility. References resource with a typed data type.
/// </summary>
/// <typeparam name="T">Type of the asset instance type.</typeparam>
#if FLAX_EDITOR
[CustomEditor(typeof(FlaxEditor.CustomEditors.Editors.AssetRefEditor))]
#endif
public struct JsonAssetReference<T> : IComparable, IComparable<JsonAssetReference<T>>, IEquatable<JsonAssetReference<T>>
{
/// <summary>
/// Gets or sets the referenced asset.
/// </summary>
public JsonAsset Asset;
/// <summary>
/// Gets the instance of the serialized object from the json asset data. Cached internally.
/// </summary>
public T Instance => (T)Asset?.Instance;
/// <summary>
/// Initializes a new instance of the <see cref="JsonAssetReference{T}"/> structure.
/// </summary>
/// <param name="asset">The Json Asset.</param>
public JsonAssetReference(JsonAsset asset)
{
Asset = asset;
}
/// <summary>
/// Implicit cast operator.
/// </summary>
public static implicit operator JsonAsset(JsonAssetReference<T> value)
{
return value.Asset;
}
/// <summary>
/// Implicit cast operator.
/// </summary>
public static implicit operator IntPtr(JsonAssetReference<T> value)
{
return Object.GetUnmanagedPtr(value.Asset);
}
/// <summary>
/// Implicit cast operator.
/// </summary>
public static implicit operator JsonAssetReference<T>(JsonAsset value)
{
return new JsonAssetReference<T>(value);
}
/// <summary>
/// Implicit cast operator.
/// </summary>
public static implicit operator JsonAssetReference<T>(IntPtr valuePtr)
{
return new JsonAssetReference<T>(Object.FromUnmanagedPtr(valuePtr) as JsonAsset);
}
/// <summary>
/// Checks if the object exists (reference is not null and the unmanaged object pointer is valid).
/// </summary>
/// <param name="obj">The object to check.</param>
/// <returns>True if object is valid, otherwise false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator bool(JsonAssetReference<T> obj)
{
return obj.Asset;
}
/// <summary>
/// Checks whether the two objects are equal.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(JsonAssetReference<T> left, JsonAssetReference<T> right)
{
return left.Asset == right.Asset;
}
/// <summary>
/// Checks whether the two objects are not equal.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(JsonAssetReference<T> left, JsonAssetReference<T> right)
{
return left.Asset != right.Asset;
}
/// <inheritdoc />
public bool Equals(JsonAssetReference<T> other)
{
return Asset == other.Asset;
}
/// <inheritdoc />
public int CompareTo(JsonAssetReference<T> other)
{
return Object.GetUnmanagedPtr(Asset).CompareTo(Object.GetUnmanagedPtr(other.Asset));
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is JsonAssetReference<T> other && Asset == other.Asset;
}
/// <inheritdoc />
public override string ToString()
{
return Asset?.ToString();
}
/// <inheritdoc />
public int CompareTo(object obj)
{
return obj is JsonAssetReference<T> other ? CompareTo(other) : 1;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (Asset != null ? Asset.GetHashCode() : 0);
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Content/JsonAsset.h"
#include "Engine/Content/AssetReference.h"
/// <summary>
/// Json asset reference utility. References resource with a typed data type.
/// </summary>
/// <typeparam name="T">Type of the asset instance type.</typeparam>
template<typename T>
API_STRUCT(NoDefault, Template, MarshalAs=JsonAsset*) struct JsonAssetReference : AssetReference<JsonAsset>
{
JsonAssetReference() = default;
JsonAssetReference(JsonAsset* asset)
{
OnSet(asset);
}
/// <summary>
/// Gets the deserialized native object instance of the given type. Returns null if asset is not loaded or loaded object has different type.
/// </summary>
/// <returns>The asset instance object or null.</returns>
FORCE_INLINE T* GetInstance() const
{
return _asset ? Get()->template GetInstance<T>() : nullptr;
}
JsonAssetReference& operator=(JsonAsset* asset) noexcept
{
OnSet(asset);
return *this;
}
operator JsonAsset*() const
{
return Get();
}
};

View File

@@ -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<FlaxPackage>(path);
Packages.Add(package);
result = package;
storage = package;
}
else
{
auto file = New<FlaxFile>(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);
}
}

View File

@@ -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<FileReadStream*> streams;
_file.GetValues(streams);
for (FileReadStream* stream : streams)
{
if (stream)
Delete(stream);
}
#endif
}
@@ -1266,7 +1272,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)
{
@@ -1276,7 +1281,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;
@@ -1339,7 +1343,14 @@ bool FlaxStorage::CloseFileHandles()
return true; // Failed, someone is still accessing the file
// Close file handles (from all threads)
_file.DeleteAll();
Array<FileReadStream*> streams;
_file.GetValues(streams);
for (FileReadStream* stream : streams)
{
if (stream)
Delete(stream);
}
_file.Clear();
return false;
}

View File

@@ -93,7 +93,7 @@ protected:
CriticalSection _loadLocker;
// Storage
ThreadLocalObject<FileReadStream> _file;
ThreadLocal<FileReadStream*> _file;
Array<FlaxChunk*> _chunks;
// Metadata

View File

@@ -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;
}

View File

@@ -234,7 +234,6 @@ bool AssetsImportingManager::Create(const String& tag, const StringView& outputP
LOG(Warning, "Cannot find asset creator object for tag \'{0}\'.", tag);
return true;
}
return Create(creator->Callback, outputPath, assetId, arg);
}

View File

@@ -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);

View File

@@ -113,7 +113,7 @@ private:
/// <summary>
/// Asset importer entry
/// </summary>
struct AssetImporter
struct FLAXENGINE_API AssetImporter
{
public:
/// <summary>
@@ -135,7 +135,7 @@ public:
/// <summary>
/// Asset creator entry
/// </summary>
struct AssetCreator
struct FLAXENGINE_API AssetCreator
{
public:
/// <summary>

Some files were not shown because too many files have changed in this diff Show More