Continue refactoring model tool to support importing multiple objects properly
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include "Engine/Content/Assets/Animation.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "Engine/Utilities/RectPack.h"
|
||||
#include "AssetsImportingManager.h"
|
||||
|
||||
bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
|
||||
@@ -48,6 +49,85 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
|
||||
return false;
|
||||
}
|
||||
|
||||
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 TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
|
||||
{
|
||||
// Skip if file is missing
|
||||
@@ -295,6 +375,13 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
|
||||
TryRestoreMaterials(context, *data);
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
/// </summary>
|
||||
/// <param name="sourceSkeleton">The source model skeleton.</param>
|
||||
/// <param name="targetSkeleton">The target skeleton. May be null to disable nodes mapping.</param>
|
||||
SkeletonMapping(Items& sourceSkeleton, Items* targetSkeleton)
|
||||
SkeletonMapping(const Items& sourceSkeleton, const Items* targetSkeleton)
|
||||
{
|
||||
Size = sourceSkeleton.Count();
|
||||
SourceToTarget.Resize(Size); // model => skeleton mapping
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user