Files
FlaxEngine/Source/Engine/ContentImporters/ImportModel.cpp
2024-04-22 15:29:25 +02:00

817 lines
32 KiB
C++

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#include "ImportModel.h"
#if COMPILE_WITH_ASSETS_IMPORTER
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Core/Collections/ArrayExtensions.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Content/Assets/SkinnedModel.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Content/Content.h"
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Scripts/ModelPrefab.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Utilities/RectPack.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "AssetsImportingManager.h"
bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
{
if (FileSystem::FileExists(path))
{
// Try to load asset file and asset info
auto tmpFile = ContentStorageManager::GetStorage(path);
AssetInitData data;
if (tmpFile
&& tmpFile->GetEntriesCount() == 1
&& (
(tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4)
||
(tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
||
(tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
))
{
// Check import meta
rapidjson_flax::Document metadata;
metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length());
if (metadata.HasParseError() == false)
{
options.Deserialize(metadata, nullptr);
return true;
}
}
}
return false;
}
struct PrefabObject
{
int32 NodeIndex;
String Name;
String AssetPath;
};
void RepackMeshLightmapUVs(ModelData& data)
{
// Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart
int32 lodIndex = 0;
auto& lod = data.LODs[lodIndex];
// Build list of meshes with their area
struct LightmapUVsPack : RectPack<LightmapUVsPack, float>
{
LightmapUVsPack(float x, float y, float width, float height)
: RectPack<LightmapUVsPack, float>(x, y, width, height)
{
}
void OnInsert()
{
}
};
struct MeshEntry
{
MeshData* Mesh;
float Area;
float Size;
LightmapUVsPack* Slot;
};
Array<MeshEntry> entries;
entries.Resize(lod.Meshes.Count());
float areaSum = 0;
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
{
auto& entry = entries[meshIndex];
entry.Mesh = lod.Meshes[meshIndex];
entry.Area = entry.Mesh->CalculateTrianglesArea();
entry.Size = Math::Sqrt(entry.Area);
areaSum += entry.Area;
}
if (areaSum > ZeroTolerance)
{
// Pack all surfaces into atlas
float atlasSize = Math::Sqrt(areaSum) * 1.02f;
int32 triesLeft = 10;
while (triesLeft--)
{
bool failed = false;
const float chartsPadding = (4.0f / 256.0f) * atlasSize;
LightmapUVsPack root(chartsPadding, chartsPadding, atlasSize - chartsPadding, atlasSize - chartsPadding);
for (auto& entry : entries)
{
entry.Slot = root.Insert(entry.Size, entry.Size, chartsPadding);
if (entry.Slot == nullptr)
{
// Failed to insert surface, increase atlas size and try again
atlasSize *= 1.5f;
failed = true;
break;
}
}
if (!failed)
{
// Transform meshes lightmap UVs into the slots in the whole atlas
const float atlasSizeInv = 1.0f / atlasSize;
for (const auto& entry : entries)
{
Float2 uvOffset(entry.Slot->X * atlasSizeInv, entry.Slot->Y * atlasSizeInv);
Float2 uvScale((entry.Slot->Width - chartsPadding) * atlasSizeInv, (entry.Slot->Height - chartsPadding) * atlasSizeInv);
// TODO: SIMD
for (auto& uv : entry.Mesh->LightmapUVs)
{
uv = uv * uvScale + uvOffset;
}
}
break;
}
}
}
}
void SetupMaterialSlots(ModelData& data, const Array<MaterialSlotEntry>& materials)
{
Array<int32> materialSlotsTable;
materialSlotsTable.Resize(materials.Count());
materialSlotsTable.SetAll(-1);
for (auto& lod : data.LODs)
{
for (MeshData* mesh : lod.Meshes)
{
int32 newSlotIndex = materialSlotsTable[mesh->MaterialSlotIndex];
if (newSlotIndex == -1)
{
newSlotIndex = data.Materials.Count();
data.Materials.AddOne() = materials[mesh->MaterialSlotIndex];
}
mesh->MaterialSlotIndex = newSlotIndex;
}
}
}
bool SortMeshGroups(IGrouping<StringView, MeshData*> const& i1, IGrouping<StringView, MeshData*> const& i2)
{
return i1.GetKey().Compare(i2.GetKey()) < 0;
}
CreateAssetResult ImportModel::Import(CreateAssetContext& context)
{
// Get import options
Options options;
if (context.CustomArg != nullptr)
{
// Copy import options from argument
options = *static_cast<Options*>(context.CustomArg);
}
else
{
// Restore the previous settings or use default ones
if (!TryGetImportOptions(context.TargetAssetPath, options))
{
LOG(Warning, "Missing model import options. Using default values.");
}
}
// Import model file
ModelData* data = options.Cached ? options.Cached->Data : nullptr;
ModelData dataThis;
Array<IGrouping<StringView, MeshData*>>* meshesByNamePtr = options.Cached ? (Array<IGrouping<StringView, MeshData*>>*)options.Cached->MeshesByName : nullptr;
Array<IGrouping<StringView, MeshData*>> meshesByNameThis;
String autoImportOutput;
if (!data)
{
String errorMsg;
autoImportOutput = StringUtils::GetDirectoryName(context.TargetAssetPath);
autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath));
if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput))
{
LOG(Error, "Cannot import model file. {0}", errorMsg);
return CreateAssetResult::Error;
}
data = &dataThis;
// Group meshes by the name (the same mesh name can be used by multiple meshes that use different materials)
if (data->LODs.Count() != 0)
{
const Function<StringView(MeshData* const&)> f = [](MeshData* const& x) -> StringView
{
return x->Name;
};
ArrayExtensions::GroupBy(data->LODs[0].Meshes, f, meshesByNameThis);
Sorting::QuickSort(meshesByNameThis.Get(), meshesByNameThis.Count(), &SortMeshGroups);
}
meshesByNamePtr = &meshesByNameThis;
}
Array<IGrouping<StringView, MeshData*>>& meshesByName = *meshesByNamePtr;
// Import objects from file separately
ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr };
Array<PrefabObject> prefabObjects;
if (options.Type == ModelTool::ModelType::Prefab)
{
// Normalize options
options.SplitObjects = false;
options.ObjectIndex = -1;
// Import all of the objects recursive but use current model data to skip loading file again
options.Cached = &cached;
Function<bool(Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData)> splitImport = [&context, &autoImportOutput](Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData)
{
// Recursive importing of the split object
String postFix = objectName;
const int32 splitPos = postFix.FindLast(TEXT('|'));
if (splitPos != -1)
postFix = postFix.Substring(splitPos + 1);
// TODO: check for name collisions with material/texture assets
outputPath = autoImportOutput / String(StringUtils::GetFileNameWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
splitOptions.SubAssetFolder = TEXT(" "); // Use the same folder as asset as they all are imported to the subdir for the prefab (see SubAssetFolder usage above)
if (splitOptions.Type == ModelTool::ModelType::Model && meshData)
{
// These settings interfere with submesh reimporting
splitOptions.CenterGeometry = false;
splitOptions.UseLocalOrigin = false;
// This properly sets the transformation of the mesh during reimport
auto* nodes = &splitOptions.Cached->Data->Nodes;
Vector3 scale = Vector3::One;
// TODO: Improve this hack.
// This is the same hack as in ImportModel::CreatePrefab(), and it is documented further there
auto* currentNode = &(*nodes)[meshData->NodeIndex];
while (true)
{
if (currentNode->ParentIndex == -1)
{
scale *= currentNode->LocalTransform.Scale;
break;
}
currentNode = &(*nodes)[currentNode->ParentIndex];
}
splitOptions.Translation = meshData->OriginTranslation * scale * -1.0f;
}
return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
};
auto splitOptions = options;
LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
PrefabObject prefabObject;
for (int32 groupIndex = 0; groupIndex < meshesByName.Count(); groupIndex++)
{
auto& group = meshesByName[groupIndex];
// Cache object options (nested sub-object import removes the meshes)
prefabObject.NodeIndex = group.First()->NodeIndex;
prefabObject.Name = group.First()->Name;
splitOptions.Type = ModelTool::ModelType::Model;
splitOptions.ObjectIndex = groupIndex;
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, group.First()))
{
prefabObjects.Add(prefabObject);
}
}
LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
for (int32 i = 0; i < data->Animations.Count(); i++)
{
auto& animation = data->Animations[i];
splitOptions.Type = ModelTool::ModelType::Animation;
splitOptions.ObjectIndex = i;
splitImport(splitOptions, animation.Name, prefabObject.AssetPath, nullptr);
}
}
else if (options.SplitObjects)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
// Import rest of the objects recursive but use current model data to skip loading file again
options.Cached = &cached;
Function<bool(Options& splitOptions, const StringView& objectName)> splitImport;
splitImport.Bind([&context](Options& splitOptions, const StringView& objectName)
{
// Recursive importing of the split object
String postFix = objectName;
const int32 splitPos = postFix.FindLast(TEXT('|'));
if (splitPos != -1)
postFix = postFix.Substring(splitPos + 1);
const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
});
auto splitOptions = options;
switch (options.Type)
{
case ModelTool::ModelType::Model:
case ModelTool::ModelType::SkinnedModel:
LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
for (int32 groupIndex = 1; groupIndex < meshesByName.Count(); groupIndex++)
{
auto& group = meshesByName[groupIndex];
splitOptions.ObjectIndex = groupIndex;
splitImport(splitOptions, group.GetKey());
}
break;
case ModelTool::ModelType::Animation:
LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
for (int32 i = 1; i < data->Animations.Count(); i++)
{
auto& animation = data->Animations[i];
splitOptions.ObjectIndex = i;
splitImport(splitOptions, animation.Name);
}
break;
}
}
// When importing a single object as model asset then select a specific mesh group
Array<MeshData*> meshesToDelete;
if (options.ObjectIndex >= 0 &&
options.ObjectIndex < meshesByName.Count() &&
(options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel))
{
auto& group = meshesByName[options.ObjectIndex];
if (&dataThis == data)
{
// Use meshes only from the grouping (others will be removed manually)
{
auto& lod = dataThis.LODs[0];
meshesToDelete.Add(lod.Meshes);
lod.Meshes.Clear();
for (MeshData* mesh : group)
{
lod.Meshes.Add(mesh);
meshesToDelete.Remove(mesh);
}
}
for (int32 lodIndex = 1; lodIndex < dataThis.LODs.Count(); lodIndex++)
{
auto& lod = dataThis.LODs[lodIndex];
Array<MeshData*> lodMeshes = lod.Meshes;
lod.Meshes.Clear();
for (MeshData* lodMesh : lodMeshes)
{
if (lodMesh->Name == group.GetKey())
lod.Meshes.Add(lodMesh);
else
meshesToDelete.Add(lodMesh);
}
}
// Use only materials references by meshes from the first grouping
{
auto materials = dataThis.Materials;
dataThis.Materials.Clear();
SetupMaterialSlots(dataThis, materials);
}
}
else
{
// Copy data from others data
dataThis.Skeleton = data->Skeleton;
dataThis.Nodes = data->Nodes;
// Move meshes from this group (including any LODs of them)
{
auto& lod = dataThis.LODs.AddOne();
lod.ScreenSize = data->LODs[0].ScreenSize;
lod.Meshes.Add(group);
for (MeshData* mesh : group)
data->LODs[0].Meshes.Remove(mesh);
}
for (int32 lodIndex = 1; lodIndex < data->LODs.Count(); lodIndex++)
{
Array<MeshData*> lodMeshes = data->LODs[lodIndex].Meshes;
for (int32 i = lodMeshes.Count() - 1; i >= 0; i--)
{
MeshData* lodMesh = lodMeshes[i];
if (lodMesh->Name == group.GetKey())
data->LODs[lodIndex].Meshes.Remove(lodMesh);
else
lodMeshes.RemoveAtKeepOrder(i);
}
if (lodMeshes.Count() == 0)
break; // No meshes of that name in this LOD so skip further ones
auto& lod = dataThis.LODs.AddOne();
lod.ScreenSize = data->LODs[lodIndex].ScreenSize;
lod.Meshes.Add(lodMeshes);
}
// Copy materials used by the meshes
SetupMaterialSlots(dataThis, data->Materials);
}
data = &dataThis;
}
// Check if restore local changes on asset reimport
constexpr bool RestoreAnimEventsOnReimport = true;
const bool restoreMaterials = options.RestoreMaterialsOnReimport && data->Materials.HasItems();
const bool restoreAnimEvents = RestoreAnimEventsOnReimport && options.Type == ModelTool::ModelType::Animation && data->Animations.HasItems();
if ((restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath))
{
AssetReference<Asset> asset = Content::LoadAsync<Asset>(context.TargetAssetPath);
if (asset && !asset->WaitForLoaded())
{
auto* model = ScriptingObject::Cast<ModelBase>(asset);
auto* animation = ScriptingObject::Cast<Animation>(asset);
if (restoreMaterials && model)
{
// Copy material settings
for (int32 i = 0; i < data->Materials.Count(); i++)
{
auto& dstSlot = data->Materials[i];
if (model->MaterialSlots.Count() > i)
{
auto& srcSlot = model->MaterialSlots[i];
dstSlot.Name = srcSlot.Name;
dstSlot.ShadowsMode = srcSlot.ShadowsMode;
dstSlot.AssetID = srcSlot.Material.GetID();
}
}
}
if (restoreAnimEvents && animation)
{
// Copy anim event tracks
for (const auto& e : animation->Events)
{
auto& clone = data->Animations[0].Events.AddOne();
clone.First = e.First;
const auto& eKeys = e.Second.GetKeyframes();
auto& cloneKeys = clone.Second.GetKeyframes();
clone.Second.Resize(eKeys.Count());
for (int32 i = 0; i < eKeys.Count(); i++)
{
const auto& eKey = eKeys[i];
auto& cloneKey = cloneKeys[i];
cloneKey.Time = eKey.Time;
cloneKey.Value.Duration = eKey.Value.Duration;
if (eKey.Value.Instance)
{
cloneKey.Value.TypeName = eKey.Value.Instance->GetType().Fullname;
rapidjson_flax::StringBuffer buffer;
CompactJsonWriter writer(buffer);
writer.StartObject();
eKey.Value.Instance->Serialize(writer, nullptr);
writer.EndObject();
cloneKey.Value.JsonData.Set(buffer.GetString(), (int32)buffer.GetSize());
}
}
}
}
}
}
// When using generated lightmap UVs those coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space
// Model importer generates UVs in [0-1] space for each mesh so now we need to pack them inside the whole model (only when using multiple meshes)
if (options.Type == ModelTool::ModelType::Model && options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data->LODs.HasItems() && data->LODs[0].Meshes.Count() > 1)
{
RepackMeshLightmapUVs(*data);
}
// Create destination asset type
CreateAssetResult result = CreateAssetResult::InvalidTypeID;
switch (options.Type)
{
case ModelTool::ModelType::Model:
result = CreateModel(context, *data, &options);
break;
case ModelTool::ModelType::SkinnedModel:
result = CreateSkinnedModel(context, *data, &options);
break;
case ModelTool::ModelType::Animation:
result = CreateAnimation(context, *data, &options);
break;
case ModelTool::ModelType::Prefab:
result = CreatePrefab(context, *data, options, prefabObjects);
break;
}
for (auto mesh : meshesToDelete)
Delete(mesh);
if (result != CreateAssetResult::Ok)
return result;
// Create json with import context
rapidjson_flax::StringBuffer importOptionsMetaBuffer;
importOptionsMetaBuffer.Reserve(256);
CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer);
JsonWriter& importOptionsMeta = importOptionsMetaObj;
importOptionsMeta.StartObject();
{
context.AddMeta(importOptionsMeta);
options.Serialize(importOptionsMeta, nullptr);
}
importOptionsMeta.EndObject();
context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize());
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::Create(CreateAssetContext& context)
{
ASSERT(context.CustomArg != nullptr);
auto& modelData = *(ModelData*)context.CustomArg;
// Ensure model has any meshes
if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty()))
{
LOG(Warning, "Models has no valid meshes");
return CreateAssetResult::Error;
}
// Auto calculate LODs transition settings
modelData.CalculateLODsScreenSizes();
return CreateModel(context, modelData);
}
CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
PROFILE_CPU();
IMPORT_SETUP(Model, Model::SerializedVersion);
// Save model header
MemoryWriteStream stream(4096);
if (modelData.Pack2ModelHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
// Pack model LODs data
const auto lodCount = modelData.GetLODsCount();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
stream.SetPosition(0);
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
if (meshes[meshIndex]->Pack2Model(&stream))
{
LOG(Warning, "Cannot pack mesh.");
return CreateAssetResult::Error;
}
}
const int32 chunkIndex = lodIndex + 1;
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
// Generate SDF
if (options && options->GenerateSDF)
{
stream.SetPosition(0);
if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath))
{
if (context.AllocateChunk(15))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
}
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
PROFILE_CPU();
IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
// Save skinned model header
MemoryWriteStream stream(4096);
if (modelData.Pack2SkinnedModelHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
// Pack model LODs data
const auto lodCount = modelData.GetLODsCount();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
stream.SetPosition(0);
// Mesh Data Version
stream.WriteByte(1);
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
if (meshes[meshIndex]->Pack2SkinnedModel(&stream))
{
LOG(Warning, "Cannot pack mesh.");
return CreateAssetResult::Error;
}
}
const int32 chunkIndex = lodIndex + 1;
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
PROFILE_CPU();
IMPORT_SETUP(Animation, Animation::SerializedVersion);
// Save animation data
MemoryWriteStream stream(8182);
const int32 animIndex = options && options->ObjectIndex != -1 ? options->ObjectIndex : 0; // Single animation per asset
if (modelData.Pack2AnimationHeader(&stream, animIndex))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array<PrefabObject>& prefabObjects)
{
PROFILE_CPU();
if (data.Nodes.Count() == 0)
return CreateAssetResult::Error;
// If that prefab already exists then we need to use it as base to preserve object IDs and local changes applied by user
const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + DEFAULT_PREFAB_EXTENSION_DOT;
auto* prefab = FileSystem::FileExists(outputPath) ? Content::Load<Prefab>(outputPath) : nullptr;
if (prefab)
{
// Ensure that prefab has Default Instance so ObjectsCache is valid (used below)
prefab->GetDefaultInstance();
}
// Create prefab structure
Dictionary<int32, Actor*> nodeToActor;
Array<Actor*> nodeActors;
Actor* rootActor = nullptr;
for (int32 nodeIndex = 0; nodeIndex < data.Nodes.Count(); nodeIndex++)
{
const auto& node = data.Nodes[nodeIndex];
// Create actor(s) for this node
nodeActors.Clear();
for (const PrefabObject& e : prefabObjects)
{
if (e.NodeIndex == nodeIndex)
{
auto* actor = New<StaticModel>();
actor->SetName(e.Name);
if (auto* model = Content::LoadAsync<Model>(e.AssetPath))
{
actor->Model = model;
}
nodeActors.Add(actor);
}
}
Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New<EmptyActor>();
if (nodeActors.Count() > 1)
{
for (Actor* e : nodeActors)
{
e->SetParent(nodeActor);
}
}
if (nodeActors.Count() != 1)
{
// Include default actor to iterate over it properly in code below
nodeActors.Add(nodeActor);
}
// Setup node in hierarchy
nodeToActor.Add(nodeIndex, nodeActor);
nodeActor->SetName(node.Name);
// When use local origin is checked, it shifts everything over the same amount, including the root. This tries to work around that.
if (!(nodeIndex == 0 && options.UseLocalOrigin))
{
// TODO: Improve this hack.
// Assimp importer has the meter -> centimeter conversion scale applied to the local transform of
// the root node, and only the root node. The OpenFBX importer has the same scale applied
// to each node, *except* the root node. This difference makes it hard to calculate the
// global scale properly. Position offsets are not calculated properly from Assimp without summing up
// the global scale because translations from Assimp don't get scaled with the global scaler option,
// but the OpenFBX importer does scale them. So this hack will end up only applying the global scale
// change if its using Assimp due to the difference in where the nodes' local transform scales are set.
auto* currentNode = &node;
Vector3 scale = Vector3::One;
while (true)
{
if (currentNode->ParentIndex == -1)
{
scale *= currentNode->LocalTransform.Scale;
break;
}
currentNode = &data.Nodes[currentNode->ParentIndex];
}
// Only set translation, since scale and rotation is applied earlier.
Transform positionOffset = Transform::Identity;
positionOffset.Translation = node.LocalTransform.Translation * scale;
if (options.UseLocalOrigin)
{
positionOffset.Translation += data.Nodes[0].LocalTransform.Translation;
}
nodeActor->SetLocalTransform(positionOffset);
}
if (nodeIndex == 0)
{
// Special case for root actor to link any unlinked nodes
nodeToActor.Add(-1, nodeActor);
rootActor = nodeActor;
}
else
{
Actor* parentActor;
if (nodeToActor.TryGet(node.ParentIndex, parentActor))
nodeActor->SetParent(parentActor);
}
// Link with object from prefab (if reimporting)
if (prefab)
{
for (Actor* a : nodeActors)
{
for (const auto& i : prefab->ObjectsCache)
{
if (i.Value->GetTypeHandle() != a->GetTypeHandle()) // Type match
continue;
auto* o = (Actor*)i.Value;
if (o->GetName() != a->GetName()) // Name match
continue;
// Mark as this object already exists in prefab so will be preserved when updating it
a->LinkPrefab(o->GetPrefabID(), o->GetPrefabObjectID());
break;
}
}
}
}
ASSERT_LOW_LAYER(rootActor);
{
// Add script with import options
auto* modelPrefabScript = New<ModelPrefab>();
modelPrefabScript->SetParent(rootActor);
modelPrefabScript->ImportPath = AssetsImportingManager::GetImportPath(context.InputPath);
modelPrefabScript->ImportOptions = options;
// Link with existing prefab instance
if (prefab)
{
for (const auto& i : prefab->ObjectsCache)
{
if (i.Value->GetTypeHandle() == modelPrefabScript->GetTypeHandle())
{
modelPrefabScript->LinkPrefab(i.Value->GetPrefabID(), i.Value->GetPrefabObjectID());
break;
}
}
}
}
// Create prefab instead of native asset
bool failed;
if (prefab)
{
failed = prefab->ApplyAll(rootActor);
}
else
{
failed = PrefabManager::CreatePrefab(rootActor, outputPath, false);
}
// Cleanup objects from memory
rootActor->DeleteObjectNow();
if (failed)
return CreateAssetResult::Error;
return CreateAssetResult::Skip;
}
#endif