diff --git a/Content/Editor/IconsAtlas.flax b/Content/Editor/IconsAtlas.flax
index cd1c745a2..01baadfed 100644
--- a/Content/Editor/IconsAtlas.flax
+++ b/Content/Editor/IconsAtlas.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2248c3069f16a3b1eb62aa4660c81427fd6effa364f8f0694ba751be8e60114c
-size 5612622
+oid sha256:c2a7d0c6969a180d59a32fbc908fe432bddb393437e9c5b64ddb25737e4aab94
+size 5609840
diff --git a/Content/Editor/IconsAtlas.psd b/Content/Editor/IconsAtlas.psd
new file mode 100644
index 000000000..b062fc0fa
--- /dev/null
+++ b/Content/Editor/IconsAtlas.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0ee4e46c2b39cf6def6be079a898204e283253bf5841ccf3985fa9d49834b9a0
+size 2766306
diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs
index 5e17d7ecf..eed8db6ba 100644
--- a/Source/Editor/Content/Import/ModelImportEntry.cs
+++ b/Source/Editor/Content/Import/ModelImportEntry.cs
@@ -19,6 +19,7 @@ namespace FlaxEngine.Tools
private bool ShowRootMotion => ShowAnimation && RootMotion != RootMotionMode.None;
private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals;
private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents;
+ private bool ShowGenerateLODs => ShowGeometry && GenerateLODs;
private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom;
private bool ShowSplitting => Type != ModelType.Prefab;
}
diff --git a/Source/Editor/Content/Items/AssetItem.cs b/Source/Editor/Content/Items/AssetItem.cs
index eaa93561a..78519b400 100644
--- a/Source/Editor/Content/Items/AssetItem.cs
+++ b/Source/Editor/Content/Items/AssetItem.cs
@@ -98,12 +98,12 @@ namespace FlaxEditor.Content
}
///
- /// Reloads the asset (if it's loaded).
+ /// Reloads the asset (if it's loaded or failed to load).
///
public void Reload()
{
var asset = FlaxEngine.Content.GetAsset(ID);
- if (asset != null && asset.IsLoaded)
+ if (asset != null && (asset.IsLoaded || asset.LastLoadFailed))
{
asset.Reload();
}
diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
index 58df5f836..a5ff3758b 100644
--- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
+++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
@@ -216,16 +216,17 @@ namespace FlaxEditor.Viewport.Previews
_showFloorButton = ViewWidgetShowMenu.AddButton("Floor", button => ShowFloor = !ShowFloor);
_showFloorButton.IndexInParent = 1;
_showFloorButton.CloseMenuOnClick = false;
- }
- _nodeNameSizeButton = ViewWidgetButtonMenu.AddButton("Skeleton Names Size");
- _nodeNameSizeButton.CloseMenuOnClick = false;
- var nodeNameSizeValue = new IntValueBox(NodeNamesSize, 118, 2, 70.0f, 1, 32)
- {
- Parent = _nodeNameSizeButton
- };
- _nodeNameSizeButton.Enabled = ShowNodesNames;
- nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value;
+ // Skeleton Names Size
+ _nodeNameSizeButton = ViewWidgetButtonMenu.AddButton("Skeleton Names Size");
+ _nodeNameSizeButton.CloseMenuOnClick = false;
+ var nodeNameSizeValue = new IntValueBox(NodeNamesSize, 118, 2, 70.0f, 1, 32)
+ {
+ Parent = _nodeNameSizeButton
+ };
+ _nodeNameSizeButton.Enabled = ShowNodesNames;
+ nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value;
+ }
// Enable shadows
PreviewLight.ShadowsMode = ShadowsCastingMode.All;
diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs
index f4985eb7a..22ef54bad 100644
--- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs
+++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs
@@ -132,7 +132,8 @@ namespace FlaxEditor.Windows
if (item is AssetItem assetItem)
{
- if (assetItem.IsLoaded)
+ var asset = FlaxEngine.Content.GetAsset(assetItem.ID);
+ if (asset != null && (asset.IsLoaded || asset.LastLoadFailed))
cm.AddButton("Reload", assetItem.Reload);
cm.AddButton("Copy asset ID", () => Clipboard.Text = JsonSerializer.GetStringID(assetItem.ID));
cm.AddButton("Select actors using this asset", () => Editor.SceneEditing.SelectActorsUsingAsset(assetItem.ID));
diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp
index da6b7329a..5560c20b9 100644
--- a/Source/Engine/Content/Asset.cpp
+++ b/Source/Engine/Content/Asset.cpp
@@ -272,6 +272,7 @@ String Asset::ToString() const
void Asset::OnDeleteObject()
{
+ PROFILE_CPU_NAMED("Asset.Unload");
ASSERT(IsInMainThread());
// Send event to the gameplay so it can release handle to this asset
diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp
index 04fb3c3cf..c79e011b2 100644
--- a/Source/Engine/Content/BinaryAsset.cpp
+++ b/Source/Engine/Content/BinaryAsset.cpp
@@ -564,10 +564,7 @@ ContentLoadTask* BinaryAsset::createLoadingTask()
loadTask = preLoadChunksTask;
}
- // Before asset loading we have to initialize storage
- // TODO: maybe in build game we could do it in place?
- // This step is only for opening asset files in background and upgrading them
- // In build game we have only a few packages which are ready to use
+ // Before asset loading we have to initialize storage and pull the asset header
auto initTask = New(this);
initTask->ContinueWith(loadTask);
loadTask = initTask;
diff --git a/Source/Engine/Content/Cache/AssetsCache.cpp b/Source/Engine/Content/Cache/AssetsCache.cpp
index 96083509e..96da061f1 100644
--- a/Source/Engine/Content/Cache/AssetsCache.cpp
+++ b/Source/Engine/Content/Cache/AssetsCache.cpp
@@ -113,7 +113,7 @@ void AssetsCache::Init()
}
// Use only valid entries
- if (IsEntryValid(e))
+ if (IsEntryValid(e) != EntryValidation::Invalid)
_registry.Add(e.Info.ID, e);
else
rejectedCount++;
@@ -295,14 +295,23 @@ bool AssetsCache::FindAsset(const StringView& path, AssetInfo& info)
auto& e = i->Value;
if (e.Info.Path == path)
{
- if (!IsEntryValid(e))
+ const auto validation = IsEntryValid(e);
+ if (validation == EntryValidation::Invalid)
{
LOG(Warning, "Missing file from registry: \'{0}\':{1}:{2}", e.Info.Path, e.Info.ID, e.Info.TypeName);
_registry.Remove(i);
}
else
{
- // Found
+#if ENABLE_ASSETS_DISCOVERY
+ if (validation == EntryValidation::Inaccessible && !e.WarnedInaccessible)
+ {
+ e.WarnedInaccessible = true;
+ LOG(Warning, "Asset file locked, keeping cached entry: \'{0}\':{1}:{2}", e.Info.Path, e.Info.ID, e.Info.TypeName);
+ }
+#endif
+
+ // Found valid or inaccessible but return cached info either way
result = true;
info = e.Info;
}
@@ -322,13 +331,22 @@ bool AssetsCache::FindAsset(const Guid& id, AssetInfo& info)
auto e = _registry.TryGet(id);
if (e != nullptr)
{
- if (!IsEntryValid(*e))
+ const auto validation = IsEntryValid(*e);
+ if (validation == EntryValidation::Invalid)
{
LOG(Warning, "Missing file from registry: \'{0}\':{1}:{2}", e->Info.Path, e->Info.ID, e->Info.TypeName);
_registry.Remove(id);
}
else
{
+#if ENABLE_ASSETS_DISCOVERY
+ if (validation == EntryValidation::Inaccessible && !e->WarnedInaccessible)
+ {
+ e->WarnedInaccessible = true;
+ LOG(Warning, "Asset file locked, keeping cached entry: \'{0}\':{1}:{2}", e->Info.Path, e->Info.ID, e->Info.TypeName);
+ }
+#endif
+
// Found
result = true;
info = e->Info;
@@ -360,13 +378,13 @@ void AssetsCache::GetAllByTypeName(const StringView& typeName, Array& resu
void AssetsCache::RegisterAssets(FlaxStorage* storage)
{
PROFILE_CPU();
-
ASSERT(storage);
// Get all entries
Array entries;
storage->GetEntries(entries);
- ASSERT(entries.HasItems());
+ if (entries.IsEmpty())
+ return;
ASSETS_CACHE_LOCK();
auto storagePath = storage->GetPath();
@@ -567,60 +585,70 @@ bool AssetsCache::RenameAsset(const StringView& oldPath, const StringView& newPa
#endif
-bool AssetsCache::IsEntryValid(Entry& e)
+AssetsCache::EntryValidation AssetsCache::IsEntryValid(Entry& e)
{
#if ENABLE_ASSETS_DISCOVERY
// Check if file exists
- if (FileSystem::FileExists(e.Info.Path))
+ if (!FileSystem::FileExists(e.Info.Path))
+ return EntryValidation::Invalid;
+
+ // Check if file hasn't been modified
+ const auto fileModified = FileSystem::GetFileLastEditTime(e.Info.Path);
+ if (fileModified == e.FileModified)
{
- // Check if file hasn't been modified
- const auto fileModified = FileSystem::GetFileLastEditTime(e.Info.Path);
- if (fileModified == e.FileModified)
- return true;
-
- const auto extension = FileSystem::GetExtension(e.Info.Path).ToLower();
-
- // Check if it's a binary asset
- if (ContentStorageManager::IsFlaxStorageExtension(extension))
- {
- // Validate ID within storage container
- const auto storage = ContentStorageManager::GetStorage(e.Info.Path);
- if (storage)
- {
- // Check if storage at given location contains that asset
- const bool isValid = storage->HasAsset(e.Info);
-
- // Update entry and mark cache as dirty
- e.FileModified = fileModified;
- _isDirty = true;
-
- return isValid;
- }
- }
- // Check for json resource
- else if (JsonStorageProxy::IsValidExtension(extension))
- {
- // Check Json storage layer
- Guid jsonId;
- String jsonTypeName;
- if (JsonStorageProxy::GetAssetInfo(e.Info.Path, jsonId, jsonTypeName))
- {
- const bool isValid = e.Info.ID == jsonId && e.Info.TypeName == jsonTypeName;
-
- // Update entry and mark cache as dirty
- e.FileModified = fileModified;
- _isDirty = true;
-
- return isValid;
- }
- }
+ e.WarnedInaccessible = false;
+ return EntryValidation::Valid;
}
- return false;
+ const auto extension = FileSystem::GetExtension(e.Info.Path).ToLower();
+ // Check if it's a binary asset
+ if (ContentStorageManager::IsFlaxStorageExtension(extension))
+ {
+ // Validate ID within storage container
+ const auto storage = ContentStorageManager::GetStorage(e.Info.Path);
+ if (storage)
+ {
+ // Check if storage at given location contains that asset
+ const bool isValid = storage->HasAsset(e.Info);
+
+ // Update entry and mark cache as dirty
+ e.FileModified = fileModified;
+ e.WarnedInaccessible = false;
+ _isDirty = true;
+
+ return isValid ? EntryValidation::Valid : EntryValidation::Invalid;
+ }
+ }
+ // Check for json resource
+ else if (JsonStorageProxy::IsValidExtension(extension))
+ {
+ // Check Json storage layer
+ Guid jsonId;
+ String jsonTypeName;
+ if (JsonStorageProxy::GetAssetInfo(e.Info.Path, jsonId, jsonTypeName))
+ {
+ const bool isValid = e.Info.ID == jsonId && e.Info.TypeName == jsonTypeName;
+
+ // Update entry and mark cache as dirty
+ e.FileModified = fileModified;
+ e.WarnedInaccessible = false;
+ _isDirty = true;
+
+ return isValid ? EntryValidation::Valid : EntryValidation::Invalid;
+ }
+ }
+ else
+ {
+ // Unknown file type
+ return EntryValidation::Invalid;
+ }
+
+ // File exists but cannot be read (likely locked by git or another process)
+ return EntryValidation::Inaccessible;
#else
// In game we don't care about it because all cached asset entries are valid (precached)
// Skip only entries with missing file
- return e.Info.Path.HasChars();
+ return e.Info.Path.HasChars() ? EntryValidation::Valid : EntryValidation::Invalid;
#endif
}
diff --git a/Source/Engine/Content/Cache/AssetsCache.h b/Source/Engine/Content/Cache/AssetsCache.h
index 42f05a2aa..28380b69d 100644
--- a/Source/Engine/Content/Cache/AssetsCache.h
+++ b/Source/Engine/Content/Cache/AssetsCache.h
@@ -58,6 +58,11 @@ public:
/// The file modified date.
///
DateTime FileModified;
+
+ ///
+ /// True if a warning about this entry being inaccessible has already been logged (prevents log spam). Runtime-only, not serialized.
+ ///
+ bool WarnedInaccessible = false;
#endif
Entry()
@@ -73,6 +78,27 @@ public:
}
};
+ ///
+ /// Result of validating an asset cache entry.
+ ///
+ enum class EntryValidation
+ {
+ ///
+ /// File verified, contains this asset.
+ ///
+ Valid,
+
+ ///
+ /// File missing or contains a different asset.
+ ///
+ Invalid,
+
+ ///
+ /// File exists but cannot be opened (locked by another process).
+ ///
+ Inaccessible,
+ };
+
typedef Dictionary Registry;
typedef Dictionary PathsMapping;
@@ -232,6 +258,6 @@ public:
/// Determines whether cached asset entry is valid.
///
/// The asset entry.
- /// True if is valid, otherwise false.
- bool IsEntryValid(Entry& e);
+ /// The validation result.
+ EntryValidation IsEntryValid(Entry& e);
};
diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp
index 8aa36c7be..33a16e031 100644
--- a/Source/Engine/Content/Storage/ContentStorageManager.cpp
+++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp
@@ -54,6 +54,7 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
Locker.Lock();
// Try fast lookup
+ bool wasCached = true;
FlaxStorage* storage;
if (!StorageMap.TryGet(path, storage))
{
@@ -74,6 +75,7 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
// Register storage container
StorageMap.Add(path, storage);
+ wasCached = false;
}
// Build reference (before releasing the lock so ContentStorageSystem::Job won't delete it when running from async thread)
@@ -90,6 +92,8 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b
if (loadFailed)
{
LOG(Error, "Failed to load {0}.", path);
+ if (wasCached)
+ return result;
Locker.Lock();
StorageMap.Remove(path);
if (storage->IsPackage())
diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp
index 8dee911e4..4eaea48f8 100644
--- a/Source/Engine/Content/Storage/FlaxStorage.cpp
+++ b/Source/Engine/Content/Storage/FlaxStorage.cpp
@@ -243,9 +243,9 @@ FlaxStorage::~FlaxStorage()
{
// Validate if has been disposed
ASSERT(IsDisposed());
- CHECK(_chunksLock == 0);
- CHECK(_refCount == 0);
- CHECK(_isUnloadingData == 0);
+ CHECK_NO_RETURN(_chunksLock == 0);
+ CHECK_NO_RETURN(_refCount == 0);
+ CHECK_NO_RETURN(_isUnloadingData == 0);
ASSERT(_chunks.IsEmpty());
#if USE_EDITOR
diff --git a/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp b/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp
index 1c9440b39..6f8ace8d7 100644
--- a/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp
+++ b/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp
@@ -21,6 +21,13 @@ bool ModelInstanceEntries::HasContentLoaded() const
return result;
}
+bool ModelInstanceEntries::ShouldSerialize(const void* otherObj) const
+{
+ if (!otherObj)
+ return true;
+ return !(*this == *(const ModelInstanceEntries*)otherObj);
+}
+
void ModelInstanceEntries::Serialize(SerializeStream& stream, const void* otherObj)
{
SERIALIZE_GET_OTHER_OBJ(ModelInstanceEntries);
@@ -43,12 +50,13 @@ void ModelInstanceEntries::Serialize(SerializeStream& stream, const void* otherO
void ModelInstanceEntries::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
PROFILE_MEM(Graphics);
- const DeserializeStream& entries = stream["Entries"];
- ASSERT(entries.IsArray());
- Resize(entries.Size());
- for (rapidjson::SizeType i = 0; i < entries.Size(); i++)
+ const DeserializeStream& entriesData = stream[DeserializeStream::GenericValue(rapidjson::StringRef("Entries", 7))];
+ CHECK(entriesData.IsArray());
+ Resize(entriesData.Size());
+ ModelInstanceEntry* entries = Get();
+ for (int32 i = 0; i < Count(); i++)
{
- At(i).Deserialize((DeserializeStream&)entries[i], modifier);
+ entries[i].Deserialize((DeserializeStream&)entriesData[i], modifier);
}
}
diff --git a/Source/Engine/Graphics/Models/ModelInstanceEntry.h b/Source/Engine/Graphics/Models/ModelInstanceEntry.h
index 8f8a2ca3c..79403cce1 100644
--- a/Source/Engine/Graphics/Models/ModelInstanceEntry.h
+++ b/Source/Engine/Graphics/Models/ModelInstanceEntry.h
@@ -115,6 +115,7 @@ public:
public:
// [ISerializable]
+ bool ShouldSerialize(const void* otherObj) const override;
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
};
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index ce106dc83..ba3a2025d 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -820,8 +820,7 @@ void AnimatedModel::RunBlendShapeDeformer(const MeshBase* mesh, MeshDeformationD
void AnimatedModel::BeginPlay(SceneBeginData* data)
{
- if (SkinnedModel && SkinnedModel->IsLoaded())
- PreInitSkinningData();
+ PreInitSkinningData();
// Base
ModelInstanceActor::BeginPlay(data);
@@ -1263,9 +1262,7 @@ void AnimatedModel::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE(ShadowsMode);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
SERIALIZE(RootMotionTarget);
-
- stream.JKEY("Buffer");
- stream.Object(&Entries, other ? &other->Entries : nullptr);
+ SERIALIZE_MEMBER(Buffer, Entries);
}
void AnimatedModel::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
@@ -1290,8 +1287,7 @@ void AnimatedModel::Deserialize(DeserializeStream& stream, ISerializeModifier* m
DESERIALIZE(ShadowsMode);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
DESERIALIZE(RootMotionTarget);
-
- Entries.DeserializeIfExists(stream, "Buffer", modifier);
+ DESERIALIZE_MEMBER(Buffer, Entries);
// [Deprecated on 07.02.2022, expires on 07.02.2024]
if (modifier->EngineBuild <= 6330)
diff --git a/Source/Engine/Level/Actors/SplineModel.cpp b/Source/Engine/Level/Actors/SplineModel.cpp
index 0da4a3d70..fdda6941e 100644
--- a/Source/Engine/Level/Actors/SplineModel.cpp
+++ b/Source/Engine/Level/Actors/SplineModel.cpp
@@ -497,9 +497,7 @@ void SplineModel::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_MEMBER(PreTransform, _preTransform)
SERIALIZE(Model);
SERIALIZE(DrawModes);
-
- stream.JKEY("Buffer");
- stream.Object(&Entries, other ? &other->Entries : nullptr);
+ SERIALIZE_MEMBER(Buffer, Entries);
}
void SplineModel::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
@@ -514,8 +512,7 @@ void SplineModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE_MEMBER(PreTransform, _preTransform);
DESERIALIZE(Model);
DESERIALIZE(DrawModes);
-
- Entries.DeserializeIfExists(stream, "Buffer", modifier);
+ DESERIALIZE_MEMBER(Buffer, Entries);
// [Deprecated on 07.02.2022, expires on 07.02.2024]
if (modifier->EngineBuild <= 6330)
diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp
index 1fba1ba2e..82976b54c 100644
--- a/Source/Engine/Level/Actors/StaticModel.cpp
+++ b/Source/Engine/Level/Actors/StaticModel.cpp
@@ -464,8 +464,7 @@ void StaticModel::Serialize(SerializeStream& stream, const void* otherObj)
stream.Rectangle(Lightmap.UVsArea);
}
- stream.JKEY("Buffer");
- stream.Object(&Entries, other ? &other->Entries : nullptr);
+ SERIALIZE_MEMBER(Buffer, Entries);
if (_vertexColorsCount)
{
@@ -504,8 +503,7 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE_MEMBER(DrawModes, _drawModes);
DESERIALIZE_MEMBER(LightmapIndex, Lightmap.TextureIndex);
DESERIALIZE_MEMBER(LightmapArea, Lightmap.UVsArea);
-
- Entries.DeserializeIfExists(stream, "Buffer", modifier);
+ DESERIALIZE_MEMBER(Buffer, Entries);
{
const auto member = stream.FindMember("VertexColors");
diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
index cb6c82b5d..d972ee520 100644
--- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
@@ -24,6 +24,7 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/MainThreadTask.h"
#include "Editor/Editor.h"
+#include "FlaxEngine.Gen.h"
// Apply flow:
// - collect all prefabs using this prefab (load and create default instances)
@@ -772,7 +773,13 @@ bool Prefab::ApplyAll(Actor* targetActor)
if (ApplyAllInternal(targetActor, true, thisPrefabInstancesData))
return true;
- SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData);
+ // Sync nested prefabs
+ if (allPrefabs.HasItems())
+ {
+ LOG(Info, "Updating referencing prefabs");
+ HashSet synced;
+ SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData, synced);
+ }
const auto endTime = DateTime::NowUTC();
LOG(Info, "Prefab updated! {0} ms", (int32)(endTime - startTime).GetTotalMilliseconds());
@@ -1027,8 +1034,14 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
rapidjson_flax::Document targetDataDocument;
if (NestedPrefabs.HasItems())
{
+ // Use initial data buffer (unstripped) but reorder objects to match the sequence (eg. when new object was added to the nested prefab)
targetDataDocument.Parse(dataBuffer.GetString(), dataBuffer.GetSize());
- SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, targetDataDocument, modifier.Value);
+ Array reorderedObjects = *sceneObjects.Value;
+ newPrefabInstanceIdToDataIndexCounter = 0;
+ for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i)
+ reorderedObjects.Insert(i->Value, sceneObjects->At(newPrefabInstanceIdToDataIndexStart + newPrefabInstanceIdToDataIndexCounter++));
+ reorderedObjects.Resize(sceneObjects.Value->Count()); // reorderedObjects matches order in targetDataDocument
+ SceneObjectsFactory::PrefabSyncData prefabSyncData(reorderedObjects, targetDataDocument, modifier.Value);
SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData);
if (context.Instances.HasItems())
@@ -1236,7 +1249,7 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r
{
return Init(TypeName, StringAnsiView(tmpBuffer.GetString(), (int32)tmpBuffer.GetSize()));
}
-#if 1 // Set to 0 to use memory-only reload that does not modifies the source file - useful for testing and debugging prefabs apply
+#if 1 // Set to 0 to use memory-only reload that does not modify the source file - useful for testing and debugging prefabs apply
#if COMPILE_WITH_ASSETS_IMPORTER
Locker.Unlock();
@@ -1295,7 +1308,7 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r
_defaultInstance->DeleteObject();
_defaultInstance = nullptr;
}
- _isLoaded = false;
+ _loadState = 0;
// Update prefab data manually (to prevent updating source asset file - just for testing)
Document.Parse(buffer.GetString(), buffer.GetSize());
@@ -1348,7 +1361,7 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r
NestedPrefabs.Add(prefabId);
}
}
- _isLoaded = true;
+ _loadState = 1;
}
#endif
@@ -1395,34 +1408,31 @@ bool Prefab::SyncChangesInternal(PrefabInstancesData& prefabInstancesData)
return ApplyAllInternal(targetActor, false, prefabInstancesData);
}
-void Prefab::SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData) const
+void Prefab::SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData, HashSet& synced) const
{
PROFILE_CPU();
- LOG(Info, "Updating referencing prefabs");
-
- // TODO: this may not work well for very complex prefab nesting -> loop order matters, maybe build a graph of dependencies?
// Call recursive for all referencing prefab assets to refresh nested prefabs
for (int32 i = 0; i < allPrefabs.Count(); i++)
{
- auto nestedPrefab = allPrefabs[i].Get();
- if (nestedPrefab)
+ Prefab* nestedPrefab = allPrefabs[i].Get();
+ if (!nestedPrefab || synced.Contains(nestedPrefab->GetID()))
+ continue;
+ if (nestedPrefab->WaitForLoaded())
{
- if (nestedPrefab->WaitForLoaded())
- {
- LOG(Warning, "Waiting for prefab asset load failed.");
- continue;
- }
+ LOG(Warning, "Waiting for '{}' load failed.", nestedPrefab->ToString());
+ continue;
+ }
- // Sync only if prefab is used by this prefab (directly) and it has been captured before
- const int32 nestedPrefabIndex = nestedPrefab->NestedPrefabs.Find(GetID());
- if (nestedPrefabIndex != -1)
- {
- if (nestedPrefab->SyncChangesInternal(allPrefabsInstancesData[i]))
- continue;
- nestedPrefab->SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData);
- ObjectsRemovalService::Flush();
- }
+ // Sync only if prefab is used by this prefab (directly) and it has been captured before
+ const int32 nestedPrefabIndex = nestedPrefab->NestedPrefabs.Find(GetID());
+ if (nestedPrefabIndex != -1)
+ {
+ synced.Add(nestedPrefab->GetID());
+ if (nestedPrefab->SyncChangesInternal(allPrefabsInstancesData[i]))
+ continue;
+ nestedPrefab->SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData, synced);
+ ObjectsRemovalService::Flush();
}
}
}
diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp
index d309bf7bb..80120ea87 100644
--- a/Source/Engine/Level/Prefabs/Prefab.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.cpp
@@ -7,6 +7,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Actor.h"
+#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Scripting/Scripting.h"
@@ -22,6 +23,7 @@ Prefab::Prefab(const SpawnParams& params, const AssetInfo* info)
Guid Prefab::GetRootObjectId() const
{
+ PROFILE_CPU();
ASSERT(IsLoaded());
ScopeLock lock(Locker);
@@ -57,6 +59,7 @@ Actor* Prefab::GetDefaultInstance()
// Skip if already created (reuse cached result)
if (_defaultInstance)
return _defaultInstance;
+ PROFILE_CPU();
// Skip if not loaded
if (!IsLoaded())
diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h
index cde99a2cb..b1200ca29 100644
--- a/Source/Engine/Level/Prefabs/Prefab.h
+++ b/Source/Engine/Level/Prefabs/Prefab.h
@@ -104,7 +104,7 @@ private:
bool ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPrefab, PrefabInstancesData& prefabInstancesData);
bool UpdateInternal(const Array& defaultInstanceObjects, rapidjson_flax::StringBuffer& tmpBuffer);
bool SyncChangesInternal(PrefabInstancesData& prefabInstancesData);
- void SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData) const;
+ void SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData, HashSet& synced) const;
#endif
void DeleteDefaultInstance();
diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp
index 59d23a994..e9931eb20 100644
--- a/Source/Engine/Level/SceneObjectsFactory.cpp
+++ b/Source/Engine/Level/SceneObjectsFactory.cpp
@@ -752,7 +752,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn
obj->SetOrderInParent(order);
}
- // Setup hierarchy for the prefab instances (ensure any new objects are connected)
+ // Setup hierarchy for the prefab instances (after adding new objects to ensure they are connected, eg. when reparenting existing prefab into a new root)
for (const auto& instance : context.Instances)
{
const auto& prefabStartData = data.Data[instance.StatIndex];
diff --git a/Source/Engine/Platform/Platform.h b/Source/Engine/Platform/Platform.h
index e2c6808c9..438cb6602 100644
--- a/Source/Engine/Platform/Platform.h
+++ b/Source/Engine/Platform/Platform.h
@@ -75,6 +75,12 @@
Platform::CheckFailed(#expression, __FILE__, __LINE__); \
return returnValue; \
}
+// Performs a soft check of the expression. Logs the expression failure and continues execution.
+#define CHECK_NO_RETURN(expression) \
+ if (!(expression)) \
+ { \
+ Platform::CheckFailed(#expression, __FILE__, __LINE__); \
+ }
#if ENABLE_ASSERTION
// Performs a soft check of the expression. Logs the expression failure and returns from the function call.
diff --git a/Source/Engine/Tests/TestPrefabs.cpp b/Source/Engine/Tests/TestPrefabs.cpp
index 91bce81cc..fb34a7284 100644
--- a/Source/Engine/Tests/TestPrefabs.cpp
+++ b/Source/Engine/Tests/TestPrefabs.cpp
@@ -8,6 +8,7 @@
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/DirectionalLight.h"
#include "Engine/Level/Actors/ExponentialHeightFog.h"
+#include "Engine/Level/Actors/AnimatedModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Scripting/ScriptingObjectReference.h"
@@ -905,4 +906,124 @@ TEST_CASE("Prefabs")
instance1->DeleteObject();
instance2->DeleteObject();
}
+ SECTION("Test Adding Object To Base Prefab")
+ {
+ // https://github.com/LOOPDISK/FlaxEngine/pull/44
+
+ // Create inner prefab with 3 objects in hierarchy
+ AssetReference prefabInner = Content::CreateVirtualAsset();
+ REQUIRE(prefabInner);
+ Guid id;
+ Guid::Parse("15dbe4b0416be0777a6ce59e8788b10f", id);
+ prefabInner->ChangeID(id);
+ auto prefabInnerInit = prefabInner->Init(Prefab::TypeName,
+ "["
+ "{"
+ "\"ID\": \"3de462104f56f681c14650a0171f88fb\","
+ "\"TypeName\" : \"FlaxEngine.SpotLight\","
+ "\"Name\" : \"Inner.Root\""
+ "},"
+ "{"
+ "\"ID\": \"19b181f846b6911635ffacb902c93c6a\","
+ "\"TypeName\" : \"FlaxEngine.StaticModel\","
+ "\"ParentID\" : \"3de462104f56f681c14650a0171f88fb\","
+ "\"Name\" : \"Inner.Cube\""
+ "},"
+ "{"
+ "\"ID\": \"8950889f4a2e752d55165fbf10eaf184\","
+ "\"TypeName\" : \"FlaxEngine.AnimatedModel\","
+ "\"ParentID\" : \"19b181f846b6911635ffacb902c93c6a\","
+ "\"Name\" : \"Inner.Model\""
+ "}"
+ "]");
+ REQUIRE(!prefabInnerInit);
+
+ // Create outer prefab with 2 instances of inner prefab
+ AssetReference prefabOuter = Content::CreateVirtualAsset();
+ REQUIRE(prefabOuter);
+ SCOPE_EXIT{ Content::DeleteAsset(prefabOuter); };
+ Guid::Parse("2ab744714f746e31855f41815612d14b", id);
+ prefabOuter->ChangeID(id);
+ auto prefabOuterInit = prefabOuter->Init(Prefab::TypeName,
+ "["
+ "{"
+ "\"ID\": \"dba7f4bb4acfd62608b9a8bf550f31a5\","
+ "\"TypeName\": \"FlaxEngine.EmptyActor\","
+ "\"Name\": \"Outer.Root\""
+ "},"
+ "{"
+ "\"ID\": \"a3b705284432bed9f043829c04a2bc8f\","
+ "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\","
+ "\"PrefabObjectID\": \"3de462104f56f681c14650a0171f88fb\","
+ "\"ParentID\": \"dba7f4bb4acfd62608b9a8bf550f31a5\","
+ "\"Name\": \"Instance 1\""
+ "},"
+ "{"
+ "\"ID\": \"06a8c15a41b822dd27f3ac9d79b142d3\","
+ "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\","
+ "\"PrefabObjectID\": \"19b181f846b6911635ffacb902c93c6a\","
+ "\"ParentID\": \"a3b705284432bed9f043829c04a2bc8f\""
+ "},"
+ "{"
+ "\"ID\": \"4759fb9e4c4dda3b61ab5ab43949e42f\","
+ "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\","
+ "\"PrefabObjectID\": \"8950889f4a2e752d55165fbf10eaf184\","
+ "\"ParentID\": \"06a8c15a41b822dd27f3ac9d79b142d3\""
+ "},"
+ "{"
+ "\"ID\": \"1225be664c0c081e714bbf93e09b99e4\","
+ "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\","
+ "\"PrefabObjectID\": \"3de462104f56f681c14650a0171f88fb\","
+ "\"ParentID\": \"dba7f4bb4acfd62608b9a8bf550f31a5\","
+ "\"Name\": \"Instance 2\""
+ "},"
+ "{"
+ "\"ID\": \"b397243540322182b806ad8339b7b617\","
+ "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\","
+ "\"PrefabObjectID\": \"19b181f846b6911635ffacb902c93c6a\","
+ "\"ParentID\": \"1225be664c0c081e714bbf93e09b99e4\""
+ "},"
+ "{"
+ "\"ID\": \"2c3b8e824daf038a58df528a238ca2de\","
+ "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\","
+ "\"PrefabObjectID\": \"8950889f4a2e752d55165fbf10eaf184\","
+ "\"ParentID\": \"b397243540322182b806ad8339b7b617\""
+ "}"
+ "]");
+ REQUIRE(!prefabOuterInit);
+
+ // Spawn test instances of both prefabs
+ ScriptingObjectReference instanceInner = PrefabManager::SpawnPrefab(prefabInner);
+ ScriptingObjectReference instanceOuter = PrefabManager::SpawnPrefab(prefabOuter);
+
+ // Add new object to the inner prefab
+ instanceInner->Children[0]->GetOrAddChild();
+
+ // Apply changes
+ bool applyResult = PrefabManager::ApplyAll(instanceInner);
+ REQUIRE(!applyResult);
+
+ // Check state of outer instance to properly reflect hierarchy
+ REQUIRE(instanceOuter);
+ REQUIRE(instanceOuter->Children.Count() == 2);
+ REQUIRE(instanceOuter->Children[0] != nullptr);
+ REQUIRE(instanceOuter->Children[0]->Children.Count() == 1);
+ REQUIRE(instanceOuter->Children[0]->Children[0]);
+ REQUIRE(instanceOuter->Children[0]->Children[0]->Children.Count() == 2);
+ REQUIRE(instanceOuter->Children[0]->Children[0]->Children[0]->Is());
+ REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1]->Is());
+ REQUIRE(instanceOuter->Children[1] != nullptr);
+ REQUIRE(instanceOuter->Children[1]->Children.Count() == 1);
+ REQUIRE(instanceOuter->Children[1]->Children[0]);
+ REQUIRE(instanceOuter->Children[1]->Children[0]->Children.Count() == 2);
+ REQUIRE(instanceOuter->Children[1]->Children[0]->Children[0]->Is());
+ REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1]->Is());
+ REQUIRE(instanceOuter->Children[0]->Children[0] != instanceOuter->Children[1]->Children[0]);
+ REQUIRE(instanceOuter->Children[0]->Children[0]->Children[0] != instanceOuter->Children[1]->Children[0]->Children[0]);
+ REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1] != instanceOuter->Children[1]->Children[0]->Children[1]);
+
+ // Cleanup
+ instanceInner->DeleteObject();
+ instanceOuter->DeleteObject();
+ }
}
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index 119fa9555..5fe834ad0 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -588,6 +588,10 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(TriangleReduction);
SERIALIZE(SloppyOptimization);
SERIALIZE(LODTargetError);
+ SERIALIZE(LODTargetErrorAbsolute);
+ SERIALIZE(LODLockBorder);
+ SERIALIZE(LODPreserveUVs);
+ SERIALIZE(LODPreserveUVsWeight);
SERIALIZE(ImportMaterials);
SERIALIZE(CreateEmptyMaterialSlots);
SERIALIZE(ImportMaterialsAsInstances);
@@ -645,6 +649,10 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(TriangleReduction);
DESERIALIZE(SloppyOptimization);
DESERIALIZE(LODTargetError);
+ DESERIALIZE(LODTargetErrorAbsolute);
+ DESERIALIZE(LODLockBorder);
+ DESERIALIZE(LODPreserveUVs);
+ DESERIALIZE(LODPreserveUVsWeight);
DESERIALIZE(ImportMaterials);
DESERIALIZE(CreateEmptyMaterialSlots);
DESERIALIZE(ImportMaterialsAsInstances);
@@ -1954,6 +1962,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
// Automatic LOD generation
if (options.GenerateLODs && options.LODCount > 1 && data.LODs.HasItems() && options.TriangleReduction < 1.0f - ZeroTolerance)
{
+ PROFILE_CPU_NAMED("GenerateLODs");
auto lodStartTime = DateTime::NowUTC();
meshopt_setAllocator(MeshOptAllocate, MeshOptDeallocate);
float triangleReduction = Math::Saturate(options.TriangleReduction);
@@ -1992,13 +2001,51 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
continue;
indices.Clear();
indices.Resize(srcMeshIndexCount);
- int32 dstMeshIndexCount = {};
+ int32 dstMeshIndexCount = 0;
if (options.SloppyOptimization)
+ {
+ PROFILE_CPU_NAMED("meshopt_simplifySloppy");
dstMeshIndexCount = (int32)meshopt_simplifySloppy(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError);
+ }
else
- dstMeshIndexCount = (int32)meshopt_simplify(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError);
- if (dstMeshIndexCount <= 0 || dstMeshIndexCount > indices.Count())
- continue;
+ {
+ // Build simplification flags
+ unsigned int simplifyOptions = 0;
+ if (options.LODLockBorder)
+ simplifyOptions |= meshopt_SimplifyLockBorder;
+ if (options.LODTargetErrorAbsolute)
+ simplifyOptions |= meshopt_SimplifyErrorAbsolute;
+ if (options.LODPreserveUVs && srcMesh->UVs.HasItems())
+ {
+ // Pack UV channels as attributes for meshopt_simplifyWithAttributes
+ int32 uvChannelCount = srcMesh->UVs.Count();
+ int32 attributeCount = uvChannelCount * 2; // 2 floats (U, V) per channel
+ Array attributes;
+ attributes.Resize(srcMeshVertexCount * attributeCount);
+ Array attributeWeights;
+ attributeWeights.Resize(attributeCount);
+ for (int32 ch = 0; ch < uvChannelCount; ch++)
+ {
+ for (int32 v = 0; v < srcMeshVertexCount; v++)
+ {
+ Float2 uv = srcMesh->UVs[ch][v];
+ attributes[v * attributeCount + ch * 2 + 0] = uv.X;
+ attributes[v * attributeCount + ch * 2 + 1] = uv.Y;
+ }
+ attributeWeights[ch * 2 + 0] = options.LODPreserveUVsWeight;
+ attributeWeights[ch * 2 + 1] = options.LODPreserveUVsWeight;
+ }
+ PROFILE_CPU_NAMED("meshopt_simplifyWithAttributes");
+ dstMeshIndexCount = (int32)meshopt_simplifyWithAttributes(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), attributes.Get(), sizeof(float) * attributeCount, attributeWeights.Get(), attributeCount, nullptr, dstMeshIndexCountTarget, options.LODTargetError, simplifyOptions, nullptr);
+ }
+ else
+ {
+ PROFILE_CPU_NAMED("meshopt_simplify");
+ dstMeshIndexCount = (int32)meshopt_simplify(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError, simplifyOptions, nullptr);
+ }
+ }
+ if (dstMeshIndexCount <= 0 || dstMeshIndexCount >= indices.Count())
+ continue; // Skip if failed to generate LOD or it doesn't have less vertices than source
indices.Resize(dstMeshIndexCount);
// Generate simplified vertex buffer remapping table (use only vertices from LOD index buffer)
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h
index ff47e635d..f104bd83f 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.h
+++ b/Source/Engine/Tools/ModelTool/ModelTool.h
@@ -296,20 +296,32 @@ public:
API_FIELD(Attributes="EditorOrder(1100), EditorDisplay(\"Level Of Detail\", \"Generate LODs\"), VisibleIf(nameof(ShowGeometry))")
bool GenerateLODs = false;
// The index of the LOD from the source model data to use as a reference for following LODs generation.
- API_FIELD(Attributes="EditorOrder(1110), EditorDisplay(\"Level Of Detail\", \"Base LOD\"), VisibleIf(nameof(ShowGeometry)), Limit(0, 5, 0.065f)")
+ API_FIELD(Attributes="EditorOrder(1110), EditorDisplay(\"Level Of Detail\", \"Base LOD\"), VisibleIf(nameof(ShowGenerateLODs)), Limit(0, 5, 0.065f)")
int32 BaseLOD = 0;
// The amount of LODs to include in the model (all remaining ones starting from Base LOD will be generated).
- API_FIELD(Attributes="EditorOrder(1120), EditorDisplay(\"Level Of Detail\", \"LOD Count\"), VisibleIf(nameof(ShowGeometry)), Limit(1, 6, 0.065f)")
+ API_FIELD(Attributes="EditorOrder(1120), EditorDisplay(\"Level Of Detail\", \"LOD Count\"), VisibleIf(nameof(ShowGenerateLODs)), Limit(1, 6, 0.065f)")
int32 LODCount = 4;
// The target amount of triangles for the generated LOD (based on the higher LOD). Normalized to range 0-1. For instance 0.4 cuts the triangle count to 40%.
- API_FIELD(Attributes="EditorOrder(1130), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry)), Limit(0, 1, 0.001f)")
+ API_FIELD(Attributes="EditorOrder(1130), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGenerateLODs)), Limit(0, 1, 0.001f)")
float TriangleReduction = 0.5f;
// Whether to do a sloppy mesh optimization. This is faster but does not follow the topology of the original mesh.
- API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry))")
+ API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGenerateLODs))")
bool SloppyOptimization = false;
// Target error is an approximate measure of the deviation from the original mesh using distance normalized to [0,1] range (e.g. 0.01 means that simplifier will try to maintain the error to be below 1% of the mesh extents). Only used if Sloppy is unchecked.
- API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGeometry)), Limit(0.01f, 1, 0.001f)")
+ API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\", \"LOD Target Error\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs)), Limit(0.01f, 1, 0.001f)")
float LODTargetError = 0.05f;
+ // If checked, vertices on topological borders (edges without a paired triangle) will not be moved during simplification. Useful for meshes that tile or share edges with other meshes.
+ API_FIELD(Attributes="EditorOrder(1170), EditorDisplay(\"Level Of Detail\", \"Lock Border\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs))")
+ bool LODLockBorder = false;
+ // If checked, the target error will be treated as absolute rather than relative to the mesh extents. In that mode, error is defined in absolute units which can be universal across similar mesh types no matter their size.
+ API_FIELD(Attributes="EditorOrder(1160), EditorDisplay(\"Level Of Detail\", \"LOD Target Error Absolute\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs))")
+ bool LODTargetErrorAbsolute = false;
+ // If checked, UV channels will be included in the simplification error metric to preserve UV layout. Essential for trimsheets and atlased textures.
+ API_FIELD(Attributes="EditorOrder(1180), EditorDisplay(\"Level Of Detail\", \"Preserve UVs\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs))")
+ bool LODPreserveUVs = false;
+ // The weight of UV attributes in the simplification error metric. Higher values preserve UVs more aggressively at the cost of geometric quality. Only used when Preserve UVs is enabled.
+ API_FIELD(Attributes="EditorOrder(1190), EditorDisplay(\"Level Of Detail\", \"Preserve UVs Weight\"), VisibleIf(nameof(LODPreserveUVs)), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs)), Limit(0.001f, 1, 0.001f)")
+ float LODPreserveUVsWeight = 0.01f;
public: // Materials