Continue refactoring model tool to support importing multiple objects properly

This commit is contained in:
Wojtek Figat
2023-12-03 10:55:40 +01:00
parent c8dd2c045c
commit 022a69aaf2
3 changed files with 452 additions and 493 deletions

View File

@@ -28,7 +28,6 @@
#include "Engine/Core/Utilities.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Utilities/RectPack.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/ContentImporters/CreateMaterial.h"
@@ -760,6 +759,27 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span<const Char*> par
}
}
String GetAdditionalImportPath(const String& autoImportOutput, Array<String>& importedFileNames, const String& name)
{
String filename = name;
for (int32 j = filename.Length() - 1; j >= 0; j--)
{
if (EditorUtilities::IsInvalidPathChar(filename[j]))
filename[j] = ' ';
}
if (importedFileNames.Contains(filename))
{
int32 counter = 1;
do
{
filename = name + TEXT(" ") + StringUtils::ToString(counter);
counter++;
} while (importedFileNames.Contains(filename));
}
importedFileNames.Add(filename);
return autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT;
}
bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput)
{
LOG(Info, "Importing model from \'{0}\'", path);
@@ -792,22 +812,43 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
return true;
// Validate result data
switch (options.Type)
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
{
case ModelType::Model:
{
// Validate
if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty())
{
errorMsg = TEXT("Imported model has no valid geometry.");
return true;
}
LOG(Info, "Imported model has {0} LODs, {1} meshes (in LOD0) and {2} materials", data.LODs.Count(), data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0, data.Materials.Count());
LOG(Info, "Imported model has {0} LODs, {1} meshes (in LOD0) and {2} materials", data.LODs.Count(), data.LODs[0].Meshes.Count(), data.Materials.Count());
break;
// Process blend shapes
for (auto& lod : data.LODs)
{
for (auto& mesh : lod.Meshes)
{
for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{
auto& blendShape = mesh->BlendShapes[blendShapeIndex];
// Remove blend shape vertices with empty deltas
for (int32 i = blendShape.Vertices.Count() - 1; i >= 0; i--)
{
auto& v = blendShape.Vertices.Get()[i];
if (v.PositionDelta.IsZero() && v.NormalDelta.IsZero())
{
blendShape.Vertices.RemoveAt(i);
}
}
// Remove empty blend shapes
if (blendShape.Vertices.IsEmpty() || blendShape.Name.IsEmpty())
{
LOG(Info, "Removing empty blend shape '{0}' from mesh '{1}'", blendShape.Name, mesh->Name);
mesh->BlendShapes.RemoveAt(blendShapeIndex);
}
}
}
}
}
case ModelType::SkinnedModel:
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
LOG(Info, "Imported skeleton has {0} bones and {1} nodes", data.Skeleton.Bones.Count(), data.Nodes.Count());
// Add single node if imported skeleton is empty
if (data.Skeleton.Nodes.IsEmpty())
{
@@ -843,448 +884,12 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
// Validate
// Check bones limit currently supported by the engine
if (data.Skeleton.Bones.Count() > MAX_BONES_PER_MODEL)
{
errorMsg = String::Format(TEXT("Imported model skeleton has too many bones. Imported: {0}, maximum supported: {1}. Please optimize your asset."), data.Skeleton.Bones.Count(), MAX_BONES_PER_MODEL);
return true;
}
if (data.LODs.Count() > 1)
{
LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported.");
data.LODs.Resize(1);
}
const int32 meshesCount = data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0;
for (int32 i = 0; i < meshesCount; i++)
{
const auto mesh = data.LODs[0].Meshes[i];
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
{
auto indices = Int4::Zero;
auto weights = Float4::UnitX;
// Check if use a single bone for skinning
auto nodeIndex = data.Skeleton.FindNode(mesh->Name);
auto boneIndex = data.Skeleton.FindBone(nodeIndex);
if (boneIndex == -1 && nodeIndex != -1 && data.Skeleton.Bones.Count() < MAX_BONES_PER_MODEL)
{
// Add missing bone to be used by skinned model from animated nodes pose
boneIndex = data.Skeleton.Bones.Count();
auto& bone = data.Skeleton.Bones.AddOne();
bone.ParentIndex = -1;
bone.NodeIndex = nodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(data.Nodes, -1, nodeIndex);
CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex);
LOG(Warning, "Using auto-created bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
indices.X = boneIndex;
}
else if (boneIndex != -1)
{
// Fallback to already added bone
LOG(Warning, "Using auto-detected bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
indices.X = boneIndex;
}
else
{
// No bone
LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name);
}
mesh->BlendIndices.Resize(mesh->Positions.Count());
mesh->BlendWeights.Resize(mesh->Positions.Count());
mesh->BlendIndices.SetAll(indices);
mesh->BlendWeights.SetAll(weights);
}
#if BUILD_DEBUG
else
{
auto& indices = mesh->BlendIndices;
for (int32 j = 0; j < indices.Count(); j++)
{
const int32 min = indices[j].MinValue();
const int32 max = indices[j].MaxValue();
if (min < 0 || max >= data.Skeleton.Bones.Count())
{
LOG(Warning, "Imported mesh \'{0}\' has invalid blend indices. It may result in invalid rendering.", mesh->Name);
}
}
auto& weights = mesh->BlendWeights;
for (int32 j = 0; j < weights.Count(); j++)
{
const float sum = weights[j].SumValues();
if (Math::Abs(sum - 1.0f) > ZeroTolerance)
{
LOG(Warning, "Imported mesh \'{0}\' has invalid blend weights. It may result in invalid rendering.", mesh->Name);
}
}
}
#endif
}
LOG(Info, "Imported skeleton has {0} bones, {3} nodes, {1} meshes and {2} material", data.Skeleton.Bones.Count(), meshesCount, data.Materials.Count(), data.Nodes.Count());
break;
}
case ModelType::Animation:
{
// Validate
if (data.Animations.IsEmpty())
{
errorMsg = TEXT("Imported file has no valid animations.");
return true;
}
for (auto& animation : data.Animations)
{
LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond);
if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance)
{
errorMsg = TEXT("Invalid animation duration.");
return true;
}
}
break;
}
}
// Prepare textures
Array<String> importedFileNames;
for (int32 i = 0; i < data.Textures.Count(); i++)
{
auto& texture = data.Textures[i];
// Auto-import textures
if (autoImportOutput.IsEmpty() || (options.ImportTypes & ImportDataTypes::Textures) == ImportDataTypes::None || texture.FilePath.IsEmpty())
continue;
String filename = StringUtils::GetFileNameWithoutExtension(texture.FilePath);
for (int32 j = filename.Length() - 1; j >= 0; j--)
{
if (EditorUtilities::IsInvalidPathChar(filename[j]))
filename[j] = ' ';
}
if (importedFileNames.Contains(filename))
{
int32 counter = 1;
do
{
filename = String(StringUtils::GetFileNameWithoutExtension(texture.FilePath)) + TEXT(" ") + StringUtils::ToString(counter);
counter++;
} while (importedFileNames.Contains(filename));
}
importedFileNames.Add(filename);
#if COMPILE_WITH_ASSETS_IMPORTER
auto assetPath = autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT;
TextureTool::Options textureOptions;
switch (texture.Type)
{
case TextureEntry::TypeHint::ColorRGB:
textureOptions.Type = TextureFormatType::ColorRGB;
break;
case TextureEntry::TypeHint::ColorRGBA:
textureOptions.Type = TextureFormatType::ColorRGBA;
break;
case TextureEntry::TypeHint::Normals:
textureOptions.Type = TextureFormatType::NormalMap;
break;
}
AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions);
#endif
}
// Prepare material
for (int32 i = 0; i < data.Materials.Count(); i++)
{
auto& material = data.Materials[i];
if (material.Name.IsEmpty())
material.Name = TEXT("Material ") + StringUtils::ToString(i);
// Auto-import materials
if (autoImportOutput.IsEmpty() || (options.ImportTypes & ImportDataTypes::Materials) == ImportDataTypes::None || !material.UsesProperties())
continue;
auto filename = material.Name;
for (int32 j = filename.Length() - 1; j >= 0; j--)
{
if (EditorUtilities::IsInvalidPathChar(filename[j]))
filename[j] = ' ';
}
if (importedFileNames.Contains(filename))
{
int32 counter = 1;
do
{
filename = material.Name + TEXT(" ") + StringUtils::ToString(counter);
counter++;
} while (importedFileNames.Contains(filename));
}
importedFileNames.Add(filename);
#if COMPILE_WITH_ASSETS_IMPORTER
auto assetPath = autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT;
// When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1])
if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1)
{
// Find that asset created previously
AssetInfo info;
if (Content::GetAssetInfo(assetPath, info))
material.AssetID = info.ID;
continue;
}
if (options.ImportMaterialsAsInstances)
{
// Create material instance
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID);
if (auto* materialInstance = Content::Load<MaterialInstance>(assetPath))
{
materialInstance->SetBaseMaterial(options.InstanceToImportAs);
// Customize base material based on imported material (blind guess based on the common names used in materials)
const Char* diffuseColorNames[] = { TEXT("color"), TEXT("col"), TEXT("diffuse"), TEXT("basecolor"), TEXT("base color") };
TrySetupMaterialParameter(materialInstance, ToSpan(diffuseColorNames, ARRAY_COUNT(diffuseColorNames)), material.Diffuse.Color, MaterialParameterType::Color);
const Char* emissiveColorNames[] = { TEXT("emissive"), TEXT("emission"), TEXT("light") };
TrySetupMaterialParameter(materialInstance, ToSpan(emissiveColorNames, ARRAY_COUNT(emissiveColorNames)), material.Emissive.Color, MaterialParameterType::Color);
const Char* opacityValueNames[] = { TEXT("opacity"), TEXT("alpha") };
TrySetupMaterialParameter(materialInstance, ToSpan(opacityValueNames, ARRAY_COUNT(opacityValueNames)), material.Opacity.Value, MaterialParameterType::Float);
materialInstance->Save();
}
else
{
LOG(Error, "Failed to load material instance after creation. ({0})", assetPath);
}
}
else
{
// Create material
CreateMaterial::Options materialOptions;
materialOptions.Diffuse.Color = material.Diffuse.Color;
if (material.Diffuse.TextureIndex != -1)
materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID;
materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask;
materialOptions.Emissive.Color = material.Emissive.Color;
if (material.Emissive.TextureIndex != -1)
materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID;
materialOptions.Opacity.Value = material.Opacity.Value;
if (material.Opacity.TextureIndex != -1)
materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID;
if (material.Normals.TextureIndex != -1)
materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID;
if (material.TwoSided || material.Diffuse.HasAlphaMask)
materialOptions.Info.CullMode = CullMode::TwoSided;
if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1)
materialOptions.Info.BlendMode = MaterialBlendMode::Transparent;
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions);
}
#endif
}
// Prepare import transformation
Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale));
if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
{
importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale;
}
if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
{
// Calculate the bounding box (use LOD0 as a reference)
BoundingBox box = data.LODs[0].GetBox();
auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling;
importTransform.Translation -= center;
}
const bool applyImportTransform = !importTransform.IsIdentity();
// Post-process imported data based on a target asset type
if (options.Type == ModelType::Model)
{
if (data.Nodes.IsEmpty())
{
errorMsg = TEXT("Missing model nodes.");
return true;
}
// Apply the import transformation
if (applyImportTransform)
{
// Transform the root node using the import transformation
auto& root = data.Nodes[0];
root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform);
}
// Perform simple nodes mapping to single node (will transform meshes to model local space)
SkeletonMapping<ModelDataNode> skeletonMapping(data.Nodes, nullptr);
// Refresh skeleton updater with model skeleton
SkeletonUpdater<ModelDataNode> hierarchyUpdater(data.Nodes);
hierarchyUpdater.UpdateMatrices();
// Move meshes in the new nodes
for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++)
{
for (int32 meshIndex = 0; meshIndex < data.LODs[lodIndex].Meshes.Count(); meshIndex++)
{
auto& mesh = *data.LODs[lodIndex].Meshes[meshIndex];
// Check if there was a remap using model skeleton
if (skeletonMapping.SourceToSource[mesh.NodeIndex] != mesh.NodeIndex)
{
// Transform vertices
const auto transformationMatrix = hierarchyUpdater.CombineMatricesFromNodeIndices(skeletonMapping.SourceToSource[mesh.NodeIndex], mesh.NodeIndex);
if (!transformationMatrix.IsIdentity())
mesh.TransformBuffer(transformationMatrix);
}
// Update new node index using real asset skeleton
mesh.NodeIndex = skeletonMapping.SourceToTarget[mesh.NodeIndex];
}
}
// Collision mesh output
if (options.CollisionMeshesPrefix.HasChars())
{
// Extract collision meshes
ModelData collisionModel;
for (auto& lod : data.LODs)
{
for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--)
{
auto mesh = lod.Meshes[i];
if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase))
{
if (collisionModel.LODs.Count() == 0)
collisionModel.LODs.AddOne();
collisionModel.LODs[0].Meshes.Add(mesh);
lod.Meshes.RemoveAtKeepOrder(i);
if (lod.Meshes.IsEmpty())
break;
}
}
}
if (collisionModel.LODs.HasItems())
{
#if COMPILE_WITH_PHYSICS_COOKING
// Create collision
CollisionCooking::Argument arg;
arg.Type = options.CollisionType;
arg.OverrideModelData = &collisionModel;
auto assetPath = autoImportOutput / StringUtils::GetFileNameWithoutExtension(path) + TEXT("Collision") ASSET_FILES_EXTENSION_WITH_DOT;
if (CreateCollisionData::CookMeshCollision(assetPath, arg))
{
LOG(Error, "Failed to create collision mesh.");
}
#endif
}
}
// For generated lightmap UVs coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space
if (options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data.LODs.HasItems() && data.LODs[0].Meshes.Count() > 1)
{
// 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;
}
}
}
}
}
else if (options.Type == ModelType::SkinnedModel)
{
// Process blend shapes
for (auto& lod : data.LODs)
{
for (auto& mesh : lod.Meshes)
{
for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{
auto& blendShape = mesh->BlendShapes[blendShapeIndex];
// Remove blend shape vertices with empty deltas
for (int32 i = blendShape.Vertices.Count() - 1; i >= 0; i--)
{
auto& v = blendShape.Vertices.Get()[i];
if (v.PositionDelta.IsZero() && v.NormalDelta.IsZero())
{
blendShape.Vertices.RemoveAt(i);
}
}
// Remove empty blend shapes
if (blendShape.Vertices.IsEmpty() || blendShape.Name.IsEmpty())
{
LOG(Info, "Removing empty blend shape '{0}' from mesh '{1}'", blendShape.Name, mesh->Name);
mesh->BlendShapes.RemoveAt(blendShapeIndex);
}
}
}
}
// Ensure that root node is at index 0
int32 rootIndex = -1;
@@ -1372,15 +977,245 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
#endif
// Apply the import transformation
if (applyImportTransform)
}
if (EnumHasAllFlags(options.ImportTypes, ImportDataTypes::Geometry | ImportDataTypes::Skeleton))
{
// Validate skeleton bones used by the meshes
const int32 meshesCount = data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0;
for (int32 i = 0; i < meshesCount; i++)
{
// Transform the root node using the import transformation
auto& root = data.Skeleton.RootNode();
Transform meshTransform = root.LocalTransform.WorldToLocal(importTransform).LocalToWorld(root.LocalTransform);
root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform);
const auto mesh = data.LODs[0].Meshes[i];
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
{
auto indices = Int4::Zero;
auto weights = Float4::UnitX;
// Check if use a single bone for skinning
auto nodeIndex = data.Skeleton.FindNode(mesh->Name);
auto boneIndex = data.Skeleton.FindBone(nodeIndex);
if (boneIndex == -1 && nodeIndex != -1 && data.Skeleton.Bones.Count() < MAX_BONES_PER_MODEL)
{
// Add missing bone to be used by skinned model from animated nodes pose
boneIndex = data.Skeleton.Bones.Count();
auto& bone = data.Skeleton.Bones.AddOne();
bone.ParentIndex = -1;
bone.NodeIndex = nodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(data.Nodes, -1, nodeIndex);
CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex);
LOG(Warning, "Using auto-created bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
indices.X = boneIndex;
}
else if (boneIndex != -1)
{
// Fallback to already added bone
LOG(Warning, "Using auto-detected bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
indices.X = boneIndex;
}
else
{
// No bone
LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name);
}
mesh->BlendIndices.Resize(mesh->Positions.Count());
mesh->BlendWeights.Resize(mesh->Positions.Count());
mesh->BlendIndices.SetAll(indices);
mesh->BlendWeights.SetAll(weights);
}
#if BUILD_DEBUG
else
{
auto& indices = mesh->BlendIndices;
for (int32 j = 0; j < indices.Count(); j++)
{
const int32 min = indices[j].MinValue();
const int32 max = indices[j].MaxValue();
if (min < 0 || max >= data.Skeleton.Bones.Count())
{
LOG(Warning, "Imported mesh \'{0}\' has invalid blend indices. It may result in invalid rendering.", mesh->Name);
}
}
auto& weights = mesh->BlendWeights;
for (int32 j = 0; j < weights.Count(); j++)
{
const float sum = weights[j].SumValues();
if (Math::Abs(sum - 1.0f) > ZeroTolerance)
{
LOG(Warning, "Imported mesh \'{0}\' has invalid blend weights. It may result in invalid rendering.", mesh->Name);
}
}
}
#endif
}
}
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
{
for (auto& animation : data.Animations)
{
LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond);
if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance)
{
errorMsg = TEXT("Invalid animation duration.");
return true;
}
}
}
switch (options.Type)
{
case ModelType::Model:
if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty())
{
errorMsg = TEXT("Imported model has no valid geometry.");
return true;
}
if (data.Nodes.IsEmpty())
{
errorMsg = TEXT("Missing model nodes.");
return true;
}
break;
case ModelType::SkinnedModel:
if (data.LODs.Count() > 1)
{
LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported.");
data.LODs.Resize(1);
}
break;
case ModelType::Animation:
if (data.Animations.IsEmpty())
{
errorMsg = TEXT("Imported file has no valid animations.");
return true;
}
break;
}
// Keep additionally imported files well organized
Array<String> importedFileNames;
// Prepare textures
for (int32 i = 0; i < data.Textures.Count(); i++)
{
auto& texture = data.Textures[i];
// Auto-import textures
if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty())
continue;
String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath));
#if COMPILE_WITH_ASSETS_IMPORTER
TextureTool::Options textureOptions;
switch (texture.Type)
{
case TextureEntry::TypeHint::ColorRGB:
textureOptions.Type = TextureFormatType::ColorRGB;
break;
case TextureEntry::TypeHint::ColorRGBA:
textureOptions.Type = TextureFormatType::ColorRGBA;
break;
case TextureEntry::TypeHint::Normals:
textureOptions.Type = TextureFormatType::NormalMap;
break;
}
AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions);
#endif
}
// Prepare materials
for (int32 i = 0; i < data.Materials.Count(); i++)
{
auto& material = data.Materials[i];
if (material.Name.IsEmpty())
material.Name = TEXT("Material ") + StringUtils::ToString(i);
// Auto-import materials
if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Materials) || !material.UsesProperties())
continue;
String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, material.Name);
#if COMPILE_WITH_ASSETS_IMPORTER
// When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1])
if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1)
{
// Find that asset created previously
AssetInfo info;
if (Content::GetAssetInfo(assetPath, info))
material.AssetID = info.ID;
continue;
}
if (options.ImportMaterialsAsInstances)
{
// Create material instance
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID);
if (auto* materialInstance = Content::Load<MaterialInstance>(assetPath))
{
materialInstance->SetBaseMaterial(options.InstanceToImportAs);
// Customize base material based on imported material (blind guess based on the common names used in materials)
const Char* diffuseColorNames[] = { TEXT("color"), TEXT("col"), TEXT("diffuse"), TEXT("basecolor"), TEXT("base color") };
TrySetupMaterialParameter(materialInstance, ToSpan(diffuseColorNames, ARRAY_COUNT(diffuseColorNames)), material.Diffuse.Color, MaterialParameterType::Color);
const Char* emissiveColorNames[] = { TEXT("emissive"), TEXT("emission"), TEXT("light") };
TrySetupMaterialParameter(materialInstance, ToSpan(emissiveColorNames, ARRAY_COUNT(emissiveColorNames)), material.Emissive.Color, MaterialParameterType::Color);
const Char* opacityValueNames[] = { TEXT("opacity"), TEXT("alpha") };
TrySetupMaterialParameter(materialInstance, ToSpan(opacityValueNames, ARRAY_COUNT(opacityValueNames)), material.Opacity.Value, MaterialParameterType::Float);
materialInstance->Save();
}
else
{
LOG(Error, "Failed to load material instance after creation. ({0})", assetPath);
}
}
else
{
// Create material
CreateMaterial::Options materialOptions;
materialOptions.Diffuse.Color = material.Diffuse.Color;
if (material.Diffuse.TextureIndex != -1)
materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID;
materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask;
materialOptions.Emissive.Color = material.Emissive.Color;
if (material.Emissive.TextureIndex != -1)
materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID;
materialOptions.Opacity.Value = material.Opacity.Value;
if (material.Opacity.TextureIndex != -1)
materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID;
if (material.Normals.TextureIndex != -1)
materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID;
if (material.TwoSided || material.Diffuse.HasAlphaMask)
materialOptions.Info.CullMode = CullMode::TwoSided;
if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1)
materialOptions.Info.BlendMode = MaterialBlendMode::Transparent;
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions);
}
#endif
}
// Prepare import transformation
Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale));
if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
{
importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale;
}
if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
{
// Calculate the bounding box (use LOD0 as a reference)
BoundingBox box = data.LODs[0].GetBox();
auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling;
importTransform.Translation -= center;
}
// Apply the import transformation
if (!importTransform.IsIdentity())
{
// Transform the root node using the import transformation
auto& root = data.Skeleton.RootNode();
Transform meshTransform = root.LocalTransform.WorldToLocal(importTransform).LocalToWorld(root.LocalTransform);
root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform);
if (options.Type == ModelType::SkinnedModel)
{
// Apply import transform on meshes
Matrix meshTransformMatrix;
meshTransform.GetWorld(meshTransformMatrix);
@@ -1400,20 +1235,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
for (SkeletonBone& bone : data.Skeleton.Bones)
{
if (bone.ParentIndex == -1)
{
bone.LocalTransform = importTransform.LocalToWorld(bone.LocalTransform);
}
bone.OffsetMatrix = importMatrixInv * bone.OffsetMatrix;
}
}
}
// Perform simple nodes mapping to single node (will transform meshes to model local space)
SkeletonMapping<ModelDataNode> skeletonMapping(data.Nodes, nullptr);
// Refresh skeleton updater with model skeleton
SkeletonUpdater<ModelDataNode> hierarchyUpdater(data.Nodes);
hierarchyUpdater.UpdateMatrices();
// Post-process imported data
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
if (options.CalculateBoneOffsetMatrices)
{
// Calculate offset matrix (inverse bind pose transform) for every bone manually
@@ -1423,27 +1253,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
// Move meshes in the new nodes
for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++)
{
for (int32 meshIndex = 0; meshIndex < data.LODs[lodIndex].Meshes.Count(); meshIndex++)
{
auto& mesh = *data.LODs[lodIndex].Meshes[meshIndex];
// Check if there was a remap using model skeleton
if (skeletonMapping.SourceToSource[mesh.NodeIndex] != mesh.NodeIndex)
{
// Transform vertices
const auto transformationMatrix = hierarchyUpdater.CombineMatricesFromNodeIndices(skeletonMapping.SourceToSource[mesh.NodeIndex], mesh.NodeIndex);
if (!transformationMatrix.IsIdentity())
mesh.TransformBuffer(transformationMatrix);
}
// Update new node index using real asset skeleton
mesh.NodeIndex = skeletonMapping.SourceToTarget[mesh.NodeIndex];
}
}
#if USE_SKELETON_NODES_SORTING
// Sort skeleton nodes and bones hierarchy (parents first)
// Then it can be used with a simple linear loop update
@@ -1467,7 +1276,37 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
!
#endif
}
else if (options.Type == ModelType::Animation)
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
{
// Perform simple nodes mapping to single node (will transform meshes to model local space)
SkeletonMapping<ModelDataNode> skeletonMapping(data.Nodes, nullptr);
// Refresh skeleton updater with model skeleton
SkeletonUpdater<ModelDataNode> hierarchyUpdater(data.Nodes);
hierarchyUpdater.UpdateMatrices();
// Move meshes in the new nodes
for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++)
{
for (int32 meshIndex = 0; meshIndex < data.LODs[lodIndex].Meshes.Count(); meshIndex++)
{
auto& mesh = *data.LODs[lodIndex].Meshes[meshIndex];
// Check if there was a remap using model skeleton
if (skeletonMapping.SourceToSource[mesh.NodeIndex] != mesh.NodeIndex)
{
// Transform vertices
const auto transformationMatrix = hierarchyUpdater.CombineMatricesFromNodeIndices(skeletonMapping.SourceToSource[mesh.NodeIndex], mesh.NodeIndex);
if (!transformationMatrix.IsIdentity())
mesh.TransformBuffer(transformationMatrix);
}
// Update new node index using real asset skeleton
mesh.NodeIndex = skeletonMapping.SourceToTarget[mesh.NodeIndex];
}
}
}
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
{
for (auto& animation : data.Animations)
{
@@ -1532,11 +1371,47 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
// Collision mesh output
if (options.CollisionMeshesPrefix.HasChars())
{
// Extract collision meshes from the model
ModelData collisionModel;
for (auto& lod : data.LODs)
{
for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--)
{
auto mesh = lod.Meshes[i];
if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase))
{
if (collisionModel.LODs.Count() == 0)
collisionModel.LODs.AddOne();
collisionModel.LODs[0].Meshes.Add(mesh);
lod.Meshes.RemoveAtKeepOrder(i);
if (lod.Meshes.IsEmpty())
break;
}
}
}
#if COMPILE_WITH_PHYSICS_COOKING
if (collisionModel.LODs.HasItems() && options.CollisionType != CollisionDataType::None)
{
// Cook collision
String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, TEXT("Collision"));
CollisionCooking::Argument arg;
arg.Type = options.CollisionType;
arg.OverrideModelData = &collisionModel;
if (CreateCollisionData::CookMeshCollision(assetPath, arg))
{
LOG(Error, "Failed to create collision mesh.");
}
}
#endif
}
// Merge meshes with the same parent nodes, material and skinning
if (options.MergeMeshes)
{
int32 meshesMerged = 0;
for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++)
{
auto& meshes = data.LODs[lodIndex].Meshes;
@@ -1568,11 +1443,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
}
if (meshesMerged)
{
LOG(Info, "Merged {0} meshes", meshesMerged);
}
}
// Automatic LOD generation