Merge branch 'master' into ImprovementSlider

This commit is contained in:
Phantom
2026-06-02 18:50:05 +02:00
25 changed files with 392 additions and 131 deletions
Binary file not shown.
Binary file not shown.
@@ -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;
}
+2 -2
View File
@@ -98,12 +98,12 @@ namespace FlaxEditor.Content
}
/// <summary>
/// Reloads the asset (if it's loaded).
/// Reloads the asset (if it's loaded or failed to load).
/// </summary>
public void Reload()
{
var asset = FlaxEngine.Content.GetAsset(ID);
if (asset != null && asset.IsLoaded)
if (asset != null && (asset.IsLoaded || asset.LastLoadFailed))
{
asset.Reload();
}
@@ -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;
@@ -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));
+1
View File
@@ -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
+1 -4
View File
@@ -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<InitAssetTask>(this);
initTask->ContinueWith(loadTask);
loadTask = initTask;
+79 -51
View File
@@ -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<Guid>& resu
void AssetsCache::RegisterAssets(FlaxStorage* storage)
{
PROFILE_CPU();
ASSERT(storage);
// Get all entries
Array<FlaxStorage::Entry> 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
}
+28 -2
View File
@@ -58,6 +58,11 @@ public:
/// The file modified date.
/// </summary>
DateTime FileModified;
/// <summary>
/// True if a warning about this entry being inaccessible has already been logged (prevents log spam). Runtime-only, not serialized.
/// </summary>
bool WarnedInaccessible = false;
#endif
Entry()
@@ -73,6 +78,27 @@ public:
}
};
/// <summary>
/// Result of validating an asset cache entry.
/// </summary>
enum class EntryValidation
{
/// <summary>
/// File verified, contains this asset.
/// </summary>
Valid,
/// <summary>
/// File missing or contains a different asset.
/// </summary>
Invalid,
/// <summary>
/// File exists but cannot be opened (locked by another process).
/// </summary>
Inaccessible,
};
typedef Dictionary<Guid, Entry> Registry;
typedef Dictionary<String, Guid> PathsMapping;
@@ -232,6 +258,6 @@ public:
/// Determines whether cached asset entry is valid.
/// </summary>
/// <param name="e">The asset entry.</param>
/// <returns>True if is valid, otherwise false.</returns>
bool IsEntryValid(Entry& e);
/// <returns>The validation result.</returns>
EntryValidation IsEntryValid(Entry& e);
};
@@ -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())
@@ -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
@@ -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);
}
}
@@ -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;
};
+3 -7
View File
@@ -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)
+2 -5
View File
@@ -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)
+2 -4
View File
@@ -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");
+35 -25
View File
@@ -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<Guid> 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<SceneObject*> 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<SceneObject*>& 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<SceneObject*>& 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<SceneObject*>& 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<PrefabInstancesData>& allPrefabsInstancesData) const
void Prefab::SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array<PrefabInstancesData>& allPrefabsInstancesData, HashSet<Guid>& 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();
}
}
}
+3
View File
@@ -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())
+1 -1
View File
@@ -104,7 +104,7 @@ private:
bool ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPrefab, PrefabInstancesData& prefabInstancesData);
bool UpdateInternal(const Array<SceneObject*>& defaultInstanceObjects, rapidjson_flax::StringBuffer& tmpBuffer);
bool SyncChangesInternal(PrefabInstancesData& prefabInstancesData);
void SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array<PrefabInstancesData>& allPrefabsInstancesData) const;
void SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array<PrefabInstancesData>& allPrefabsInstancesData, HashSet<Guid, HeapAllocation>& synced) const;
#endif
void DeleteDefaultInstance();
+1 -1
View File
@@ -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];
+6
View File
@@ -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.
+121
View File
@@ -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<Prefab> prefabInner = Content::CreateVirtualAsset<Prefab>();
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<Prefab> prefabOuter = Content::CreateVirtualAsset<Prefab>();
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<Actor> instanceInner = PrefabManager::SpawnPrefab(prefabInner);
ScriptingObjectReference<Actor> instanceOuter = PrefabManager::SpawnPrefab(prefabOuter);
// Add new object to the inner prefab
instanceInner->Children[0]->GetOrAddChild<DirectionalLight>();
// 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<AnimatedModel>());
REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1]->Is<DirectionalLight>());
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<AnimatedModel>());
REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1]->Is<DirectionalLight>());
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();
}
}
+51 -4
View File
@@ -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<float> attributes;
attributes.Resize(srcMeshVertexCount * attributeCount);
Array<float> 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)
+17 -5
View File
@@ -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