Fix object ids remapping inside nested prefabs

#3255
This commit is contained in:
Wojtek Figat
2025-09-02 21:41:36 +02:00
parent 8fdda1a71a
commit 1042ad4e7d
6 changed files with 313 additions and 22 deletions

View File

@@ -568,6 +568,10 @@ namespace FlaxEditor.Modules
return;
}
// Skip if already added
if (SceneGraphFactory.Nodes.ContainsKey(actor.ID))
return;
var node = SceneGraphFactory.BuildActorNode(actor);
if (node != null)
{

View File

@@ -304,6 +304,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
{
for (int32 instanceIndex = 0; instanceIndex < prefabInstancesData.Count(); instanceIndex++)
{
PROFILE_CPU_NAMED("Instance");
auto& instance = prefabInstancesData[instanceIndex];
ISerializeModifierCacheType modifier = Cache::ISerializeModifier.Get();
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
@@ -377,6 +378,10 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
sceneObjects->Add(obj);
}
// Generate nested prefab instances to properly handle Ids Mapping within each nested prefab
SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, instance.Data, modifier.Value);
SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData);
// Apply modifications
for (int32 i = existingObjectsCount - 1; i >= 0; i--)
{
@@ -388,6 +393,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
if (prefabObjectIdToDiffData.TryGet(obj->GetPrefabObjectID(), data))
{
// Apply prefab changes
context.SetupIdsMapping(obj, modifier.Value);
obj->Deserialize(*(ISerializable::DeserializeStream*)data, modifier.Value);
}
else
@@ -424,7 +430,6 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
for (int32 i = 0; i < sceneObjects->Count(); i++)
{
SceneObject* obj = sceneObjects->At(i);
int32 dataIndex;
if (instance.PrefabInstanceIdToDataIndex.TryGet(obj->GetSceneObjectId(), dataIndex))
{
@@ -440,6 +445,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
data.RemoveMember("ParentID");
#endif
context.SetupIdsMapping(obj, modifier.Value);
obj->Deserialize(data, modifier.Value);
// Preserve order in parent (values from prefab are used)
@@ -527,6 +533,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
{
if (prefabInstancesData.IsEmpty())
return false;
PROFILE_CPU();
// Fully serialize default instance scene objects (accumulate all prefab and nested prefabs changes into a single linear list of objects)
rapidjson_flax::Document defaultInstanceData;
@@ -926,7 +933,6 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
// TODO: what if user applied prefab with references to the other objects from scene? clear them or what?
JsonTools::ChangeIds(diffDataDocument, objectInstanceIdToPrefabObjectId);
}
dataBuffer.Clear();
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
// Destroy default instance and some cache data in Prefab
@@ -1002,6 +1008,32 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
obj->RegisterObject();
}
// Generate nested prefab instances to properly handle Ids Mapping within each nested prefab
rapidjson_flax::Document targetDataDocument;
if (NestedPrefabs.HasItems())
{
targetDataDocument.Parse(dataBuffer.GetString(), dataBuffer.GetSize());
SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, targetDataDocument, modifier.Value);
SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData);
if (context.Instances.HasItems())
{
// Only main prefab instance is allowed (in case nested prefab was added to this prefab)
for (auto i = context.ObjectToInstance.Begin(); i.IsNotEnd(); ++i)
{
if (i->Value != 0)
context.ObjectToInstance.Remove(i);
}
context.Instances.Resize(1);
// Trash object mapping to prevent messing up prefab structure when applying hierarchy changes (only nested instances are used)
context.Instances[0].IdsMapping.Clear();
}
}
dataBuffer.Clear();
auto originalIdsMapping = modifier.Value->IdsMapping;
// Deserialize prefab objects and apply modifications
for (int32 i = 0; i < ObjectsCount; i++)
{
@@ -1037,6 +1069,9 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
}
}
// Use the initial Ids Mapping (SetupIdsMapping overrides it for instanced prefabs)
modifier.Value->IdsMapping = originalIdsMapping;
// Deserialize new prefab objects
newPrefabInstanceIdToDataIndexCounter = 0;
for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i)
@@ -1283,6 +1318,10 @@ bool Prefab::UpdateInternal(const Array<SceneObject*>& defaultInstanceObjects, r
ObjectsDataCache.Add(objectId, &objData);
ObjectsCount++;
Guid parentID;
if (JsonTools::GetGuidIfValid(parentID, objData, "ParentID"))
ObjectsHierarchyCache[parentID].Add(objectId);
Guid prefabId = JsonTools::GetGuid(objData, "PrefabID");
if (prefabId.IsValid() && !NestedPrefabs.Contains(prefabId))
{

View File

@@ -172,7 +172,9 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const SpawnOptions& options)
SceneObjectsFactory::HandleObjectDeserializationError(stream);
}
SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, data, modifier.Value);
if (options.WithSync)
bool withSync = options.WithSync || prefab->NestedPrefabs.HasItems(); // Nested prefabs needs prefab instances generation for correct IdsMapping if the same prefab exists multiple times
// TODO: let prefab check if has multiple nested prefabs at cook time?
if (withSync)
{
// Synchronize new prefab instances (prefab may have new objects added so deserialized instances need to synchronize with it)
// TODO: resave and force sync prefabs during game cooking so this step could be skipped in game
@@ -187,14 +189,14 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const SpawnOptions& options)
if (obj)
SceneObjectsFactory::Deserialize(context, obj, stream);
}
Scripting::ObjectsLookupIdMapping.Set(prevIdMapping);
// Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it)
if (options.WithSync)
if (withSync)
{
// TODO: resave and force sync scenes during game cooking so this step could be skipped in game
SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData);
}
Scripting::ObjectsLookupIdMapping.Set(prevIdMapping);
// Pick prefab root object
Actor* root = nullptr;

View File

@@ -101,13 +101,25 @@ ISerializeModifier* SceneObjectsFactory::Context::GetModifier()
void SceneObjectsFactory::Context::SetupIdsMapping(const SceneObject* obj, ISerializeModifier* modifier) const
{
int32 instanceIndex;
if (ObjectToInstance.TryGet(obj->GetID(), instanceIndex) && instanceIndex != modifier->CurrentInstance)
const Guid id = obj->GetID();
if (ObjectToInstance.TryGet(id, instanceIndex))
{
// Apply the current prefab instance objects ids table to resolve references inside a prefab properly
modifier->CurrentInstance = instanceIndex;
const auto& instance = Instances[instanceIndex];
for (const auto& e : instance.IdsMapping)
modifier->IdsMapping[e.Key] = e.Value;
if (instanceIndex != modifier->CurrentInstance)
{
modifier->CurrentInstance = instanceIndex;
for (const auto& e : instance.IdsMapping)
modifier->IdsMapping[e.Key] = e.Value;
}
int32 nestedIndex;
if (instance.ObjectToNested.TryGet(id, nestedIndex))
{
// Each nested prefab has own object ids mapping that takes precedence
const auto& nested = instance.Nested[nestedIndex];
for (const auto& e : nested.IdsMapping)
modifier->IdsMapping[e.Key] = e.Value;
}
}
}
@@ -499,6 +511,34 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn
prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab && !prefab->WaitForLoaded())
{
// If prefab instance contains multiple nested prefabs, then need to build separate Ids remapping for each of them to prevent collisions
int32 nestedIndex = -1;
if (prefabInstance.Nested.HasItems())
{
// Check only the last instance if the current object belongs to it
for (int32 j = prefabInstance.Nested.Count() - 1; j >= 0; j--)
{
const auto& e = prefabInstance.Nested[j];
if (e.Prefab == prefab && e.RootObjectId != prefabObjectId)
{
nestedIndex = j;
break;
}
}
}
if (nestedIndex == -1)
{
nestedIndex = prefabInstance.Nested.Count();
auto& e = prefabInstance.Nested.AddOne();
e.Prefab = prefab;
e.RootObjectId = prefabObjectId;
}
// Map this object into this instance to inherit all objects from it when looking up via IdsMapping
prefabInstance.ObjectToNested[id] = nestedIndex;
auto& nestedInstance = prefabInstance.Nested[nestedIndex];
nestedInstance.IdsMapping[prefabObjectId] = id;
// Map prefab object ID to the deserialized instance ID
prefabInstance.IdsMapping[prefabObjectId] = id;
goto NESTED_PREFAB_WALK;
@@ -787,23 +827,20 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab
if (spawned)
continue;
// Map prefab object ID to this actor's prefab instance so the new objects gets added to it
// Map prefab object ID to this actor's prefab instance so the new objects get added to it
context.SetupIdsMapping(actor, data.Modifier);
data.Modifier->IdsMapping[actorPrefabObjectId] = actorId;
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
// Create instance (including all children)
SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId);
SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId, actor->GetID());
}
}
void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId)
void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId, const Guid& nestedInstanceId)
{
PROFILE_CPU_NAMED("SynchronizeNewPrefabInstance");
// Missing object found!
LOG(Info, "Actor {0} has missing child object (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", actor->ToString(), prefabObjectId, prefab->GetID(), prefab->GetPath());
// Get prefab object data from the prefab
const ISerializable::DeserializeStream* prefabData;
if (!prefab->ObjectsDataCache.TryGet(prefabObjectId, prefabData))
@@ -823,6 +860,7 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabS
LOG(Warning, "Failed to create object {1} from prefab {0}.", prefab->ToString(), prefabObjectId);
return;
}
LOG(Info, "Actor {0} has missing child object '{4}' (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", actor->ToString(), prefabObjectId, prefab->GetID(), prefab->GetPath(), child->GetType().ToString());
// Register object
child->RegisterObject();
@@ -839,17 +877,51 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabS
context.ObjectToInstance[id] = instanceIndex;
auto& prefabInstance = context.Instances[instanceIndex];
prefabInstance.IdsMapping[prefabObjectId] = id;
// Check if it's a nested prefab
const ISerializable::DeserializeStream* nestedPrefabData;
Guid nestedPrefabObjectId;
if (prefab->ObjectsDataCache.TryGet(prefabObjectId, nestedPrefabData) && JsonTools::GetGuidIfValid(nestedPrefabObjectId, *nestedPrefabData, "PrefabObjectID"))
{
// Try reusing parent nested instance (or make a new one)
int32 nestedIndex = -1;
if (!prefabInstance.ObjectToNested.TryGet(nestedInstanceId, nestedIndex))
{
if (auto* nestedPrefab = Content::LoadAsync<Prefab>(JsonTools::GetGuid(*prefabData, "PrefabID")))
{
if (prefabInstance.Nested.HasItems())
{
// Check only the last instance if the current object belongs to it
const auto& e = prefabInstance.Nested.Last();
if (e.Prefab == nestedPrefab && e.RootObjectId != nestedPrefabObjectId)
nestedIndex = prefabInstance.Nested.Count() - 1;
}
if (nestedIndex == -1)
{
nestedIndex = prefabInstance.Nested.Count();
auto& e = prefabInstance.Nested.AddOne();
e.Prefab = nestedPrefab;
e.RootObjectId = nestedPrefabObjectId;
}
}
}
if (nestedIndex != -1)
{
// Insert into nested instance
prefabInstance.ObjectToNested[id] = nestedIndex;
auto& nestedInstance = prefabInstance.Nested[nestedIndex];
nestedInstance.IdsMapping[nestedPrefabObjectId] = id;
}
}
}
// Use loop to add even more objects to added objects (prefab can have one new object that has another child, we need to add that child)
// TODO: prefab could cache lookup object id -> children ids
for (auto q = prefab->ObjectsDataCache.Begin(); q.IsNotEnd(); ++q)
const auto* hierarchy = prefab->ObjectsHierarchyCache.TryGet(prefabObjectId);
if (hierarchy)
{
Guid qParentId;
if (JsonTools::GetGuidIfValid(qParentId, *q->Value, "ParentID") && qParentId == prefabObjectId)
for (const Guid& e : *hierarchy)
{
const Guid qPrefabObjectId = JsonTools::GetGuid(*q->Value, "ID");
SynchronizeNewPrefabInstance(context, data, prefab, actor, qPrefabObjectId);
SynchronizeNewPrefabInstance(context, data, prefab, actor, e, id);
}
}
}

View File

@@ -13,6 +13,13 @@
class FLAXENGINE_API SceneObjectsFactory
{
public:
struct NestedPrefabInstance
{
Prefab* Prefab;
Guid RootObjectId;
Dictionary<Guid, Guid> IdsMapping;
};
struct PrefabInstance
{
int32 StatIndex;
@@ -21,6 +28,8 @@ public:
Prefab* Prefab;
bool FixRootParent = false;
Dictionary<Guid, Guid> IdsMapping;
Array<NestedPrefabInstance> Nested;
Dictionary<Guid, int32> ObjectToNested;
};
struct Context
@@ -133,5 +142,5 @@ public:
private:
static void SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream);
static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId);
static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId, const Guid& nestedInstanceId);
};

View File

@@ -687,6 +687,171 @@ TEST_CASE("Prefabs")
instanceA->DeleteObject();
instanceB->DeleteObject();
instanceC->DeleteObject();
}
SECTION("Test Loading Nested Prefab With Multiple Instances of Nested Prefab")
{
// https://github.com/FlaxEngine/FlaxEngine/issues/3255
// Create Prefab C with basic cross-object references
AssetReference<Prefab> prefabC = Content::CreateVirtualAsset<Prefab>();
REQUIRE(prefabC);
Guid id;
Guid::Parse("cccbe4b0416be0777a6ce59e8788b10f", id);
prefabC->ChangeID(id);
auto prefabCInit = prefabC->Init(Prefab::TypeName,
"["
"{"
"\"ID\": \"aac6b9644492fbca1a6ab0a7904a557e\","
"\"TypeName\": \"FlaxEngine.ExponentialHeightFog\","
"\"Name\": \"Prefab C.Root\","
"\"DirectionalInscatteringLight\": \"bbb6b9644492fbca1a6ab0a7904a557e\""
"},"
"{"
"\"ID\": \"bbb6b9644492fbca1a6ab0a7904a557e\","
"\"TypeName\": \"FlaxEngine.DirectionalLight\","
"\"ParentID\": \"aac6b9644492fbca1a6ab0a7904a557e\","
"\"Name\": \"Prefab C.Light\""
"}"
"]");
REQUIRE(!prefabCInit);
// Create Prefab B with two nested Prefab C attached to the root
AssetReference<Prefab> prefabB = Content::CreateVirtualAsset<Prefab>();
REQUIRE(prefabB);
SCOPE_EXIT{ Content::DeleteAsset(prefabB); };
Guid::Parse("bbb744714f746e31855f41815612d14b", id);
prefabB->ChangeID(id);
auto prefabBInit = prefabB->Init(Prefab::TypeName,
"["
"{"
"\"ID\": \"244274a04cc60d56a2f024bfeef5772d\","
"\"TypeName\": \"FlaxEngine.SpotLight\","
"\"Name\": \"Prefab B.Root\""
"},"
"{"
"\"ID\": \"1111f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\","
"\"PrefabObjectID\": \"aac6b9644492fbca1a6ab0a7904a557e\","
"\"ParentID\": \"244274a04cc60d56a2f024bfeef5772d\""
"},"
"{"
"\"ID\": \"2221f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\","
"\"PrefabObjectID\": \"bbb6b9644492fbca1a6ab0a7904a557e\","
"\"ParentID\": \"1111f1094f430733333f8280e78dfcc3\""
"},"
"{"
"\"ID\": \"3331f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\","
"\"PrefabObjectID\": \"aac6b9644492fbca1a6ab0a7904a557e\","
"\"ParentID\": \"244274a04cc60d56a2f024bfeef5772d\""
"},"
"{"
"\"ID\": \"4441f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\","
"\"PrefabObjectID\": \"bbb6b9644492fbca1a6ab0a7904a557e\","
"\"ParentID\": \"3331f1094f430733333f8280e78dfcc3\""
"}"
"]");
REQUIRE(!prefabBInit);
// Create Prefab A as variant of Prefab B (no local changes, just object remapped)
AssetReference<Prefab> prefabA = Content::CreateVirtualAsset<Prefab>();
REQUIRE(prefabA);
SCOPE_EXIT{ Content::DeleteAsset(prefabA); };
Guid::Parse("aaa744714f746e31855f41815612d14b", id);
prefabA->ChangeID(id);
auto prefabAInit = prefabA->Init(Prefab::TypeName,
"["
"{"
"\"ID\": \"123274a04cc60d56a2f024bfeef5772d\","
"\"PrefabID\": \"bbb744714f746e31855f41815612d14b\","
"\"PrefabObjectID\": \"244274a04cc60d56a2f024bfeef5772d\","
"\"Name\": \"Prefab A.Root\""
"},"
"{"
"\"ID\": \"1211f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"bbb744714f746e31855f41815612d14b\","
"\"PrefabObjectID\": \"1111f1094f430733333f8280e78dfcc3\","
"\"ParentID\": \"123274a04cc60d56a2f024bfeef5772d\""
"},"
"{"
"\"ID\": \"4221f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"bbb744714f746e31855f41815612d14b\","
"\"PrefabObjectID\": \"2221f1094f430733333f8280e78dfcc3\","
"\"ParentID\": \"1211f1094f430733333f8280e78dfcc3\""
"},"
"{"
"\"ID\": \"3131f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"bbb744714f746e31855f41815612d14b\","
"\"PrefabObjectID\": \"3331f1094f430733333f8280e78dfcc3\","
"\"ParentID\": \"123274a04cc60d56a2f024bfeef5772d\""
"},"
"{"
"\"ID\": \"5441f1094f430733333f8280e78dfcc3\","
"\"PrefabID\": \"bbb744714f746e31855f41815612d14b\","
"\"PrefabObjectID\": \"4441f1094f430733333f8280e78dfcc3\","
"\"ParentID\": \"3131f1094f430733333f8280e78dfcc3\""
"}"
"]");
REQUIRE(!prefabAInit);
// Spawn test instances of both prefabs
ScriptingObjectReference<Actor> instanceA = PrefabManager::SpawnPrefab(prefabA);
ScriptingObjectReference<Actor> instanceB = PrefabManager::SpawnPrefab(prefabB);
ScriptingObjectReference<Actor> instanceC = PrefabManager::SpawnPrefab(prefabC);
// Check state of objects
REQUIRE(instanceC);
REQUIRE(instanceC->Is<ExponentialHeightFog>());
REQUIRE(instanceC->Children.Count() == 1);
CHECK(instanceC.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceC->Children[0]);
REQUIRE(instanceB);
REQUIRE(instanceB->Children.Count() == 2);
ScriptingObjectReference<Actor> instanceB1 = instanceB->Children[0];
ScriptingObjectReference<Actor> instanceB2 = instanceB->Children[1];
REQUIRE(instanceB1->Is<ExponentialHeightFog>());
REQUIRE(instanceB1->Children.Count() == 1);
CHECK(instanceB1.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceB1->Children[0]);
REQUIRE(instanceB2->Is<ExponentialHeightFog>());
REQUIRE(instanceB2->Children.Count() == 1);
CHECK(instanceB2.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceB2->Children[0]);
REQUIRE(instanceA);
REQUIRE(instanceA->Children.Count() == 2);
ScriptingObjectReference<Actor> instanceA1 = instanceA->Children[0];
ScriptingObjectReference<Actor> instanceA2 = instanceA->Children[1];
REQUIRE(instanceA1->Is<ExponentialHeightFog>());
REQUIRE(instanceA1->Children.Count() == 1);
CHECK(instanceA1.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceA1->Children[0]);
REQUIRE(instanceA2->Is<ExponentialHeightFog>());
REQUIRE(instanceA1->Children.Count() == 1);
CHECK(instanceA2.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceA2->Children[0]);
// Add instance of Prefab C to Prefab B
instanceC->SetName(StringView(TEXT("New")));
instanceC->SetParent(instanceB);
bool applyResult = PrefabManager::ApplyAll(instanceB);
REQUIRE(!applyResult);
// Check if Prefab A reflects that change
REQUIRE(instanceA);
REQUIRE(instanceA->Children.Count() == 3);
instanceA1 = instanceA->Children[0];
instanceA2 = instanceA->Children[1];
ScriptingObjectReference<Actor> instanceA3 = instanceA->Children[2];
REQUIRE(instanceA1->Is<ExponentialHeightFog>());
REQUIRE(instanceA1->Children.Count() == 1);
CHECK(instanceA1.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceA1->Children[0]);
REQUIRE(instanceA2->Is<ExponentialHeightFog>());
REQUIRE(instanceA2->Children.Count() == 1);
CHECK(instanceA2.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceA2->Children[0]);
REQUIRE(instanceA3->Is<ExponentialHeightFog>());
REQUIRE(instanceA3->Children.Count() == 1);
CHECK(instanceA3.As<ExponentialHeightFog>()->DirectionalInscatteringLight == instanceA3->Children[0]);
// Cleanup
instanceA->DeleteObject();
instanceB->DeleteObject();
instanceC->DeleteObject();
}
}