Merge branch 'master' into ImprovementSlider
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user