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/Assets/Animation.h"
|
||||||
#include "Engine/Content/Content.h"
|
#include "Engine/Content/Content.h"
|
||||||
#include "Engine/Platform/FileSystem.h"
|
#include "Engine/Platform/FileSystem.h"
|
||||||
|
#include "Engine/Utilities/RectPack.h"
|
||||||
#include "AssetsImportingManager.h"
|
#include "AssetsImportingManager.h"
|
||||||
|
|
||||||
bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
|
bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
|
||||||
@@ -48,6 +49,85 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
|
|||||||
return false;
|
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)
|
void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
|
||||||
{
|
{
|
||||||
// Skip if file is missing
|
// Skip if file is missing
|
||||||
@@ -295,6 +375,13 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
|
|||||||
TryRestoreMaterials(context, *data);
|
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
|
// Create destination asset type
|
||||||
CreateAssetResult result = CreateAssetResult::InvalidTypeID;
|
CreateAssetResult result = CreateAssetResult::InvalidTypeID;
|
||||||
switch (options.Type)
|
switch (options.Type)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public:
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourceSkeleton">The source model skeleton.</param>
|
/// <param name="sourceSkeleton">The source model skeleton.</param>
|
||||||
/// <param name="targetSkeleton">The target skeleton. May be null to disable nodes mapping.</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();
|
Size = sourceSkeleton.Count();
|
||||||
SourceToTarget.Resize(Size); // model => skeleton mapping
|
SourceToTarget.Resize(Size); // model => skeleton mapping
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
#include "Engine/Core/Utilities.h"
|
#include "Engine/Core/Utilities.h"
|
||||||
#include "Engine/Core/Types/StringView.h"
|
#include "Engine/Core/Types/StringView.h"
|
||||||
#include "Engine/Platform/FileSystem.h"
|
#include "Engine/Platform/FileSystem.h"
|
||||||
#include "Engine/Utilities/RectPack.h"
|
|
||||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||||
#include "Engine/ContentImporters/AssetsImportingManager.h"
|
#include "Engine/ContentImporters/AssetsImportingManager.h"
|
||||||
#include "Engine/ContentImporters/CreateMaterial.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)
|
bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput)
|
||||||
{
|
{
|
||||||
LOG(Info, "Importing model from \'{0}\'", path);
|
LOG(Info, "Importing model from \'{0}\'", path);
|
||||||
@@ -792,22 +812,43 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Validate result data
|
// Validate result data
|
||||||
switch (options.Type)
|
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
|
||||||
{
|
{
|
||||||
case ModelType::Model:
|
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());
|
||||||
{
|
|
||||||
// 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[0].Meshes.Count(), data.Materials.Count());
|
// Process blend shapes
|
||||||
break;
|
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
|
// Add single node if imported skeleton is empty
|
||||||
if (data.Skeleton.Nodes.IsEmpty())
|
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)
|
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);
|
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;
|
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
|
// Ensure that root node is at index 0
|
||||||
int32 rootIndex = -1;
|
int32 rootIndex = -1;
|
||||||
@@ -1372,15 +977,245 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
// Apply the import transformation
|
if (EnumHasAllFlags(options.ImportTypes, ImportDataTypes::Geometry | ImportDataTypes::Skeleton))
|
||||||
if (applyImportTransform)
|
{
|
||||||
|
// 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
|
const auto mesh = data.LODs[0].Meshes[i];
|
||||||
auto& root = data.Skeleton.RootNode();
|
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
|
||||||
Transform meshTransform = root.LocalTransform.WorldToLocal(importTransform).LocalToWorld(root.LocalTransform);
|
{
|
||||||
root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform);
|
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
|
// Apply import transform on meshes
|
||||||
Matrix meshTransformMatrix;
|
Matrix meshTransformMatrix;
|
||||||
meshTransform.GetWorld(meshTransformMatrix);
|
meshTransform.GetWorld(meshTransformMatrix);
|
||||||
@@ -1400,20 +1235,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
|
|||||||
for (SkeletonBone& bone : data.Skeleton.Bones)
|
for (SkeletonBone& bone : data.Skeleton.Bones)
|
||||||
{
|
{
|
||||||
if (bone.ParentIndex == -1)
|
if (bone.ParentIndex == -1)
|
||||||
{
|
|
||||||
bone.LocalTransform = importTransform.LocalToWorld(bone.LocalTransform);
|
bone.LocalTransform = importTransform.LocalToWorld(bone.LocalTransform);
|
||||||
}
|
|
||||||
bone.OffsetMatrix = importMatrixInv * bone.OffsetMatrix;
|
bone.OffsetMatrix = importMatrixInv * bone.OffsetMatrix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform simple nodes mapping to single node (will transform meshes to model local space)
|
// Post-process imported data
|
||||||
SkeletonMapping<ModelDataNode> skeletonMapping(data.Nodes, nullptr);
|
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
|
||||||
|
{
|
||||||
// Refresh skeleton updater with model skeleton
|
|
||||||
SkeletonUpdater<ModelDataNode> hierarchyUpdater(data.Nodes);
|
|
||||||
hierarchyUpdater.UpdateMatrices();
|
|
||||||
|
|
||||||
if (options.CalculateBoneOffsetMatrices)
|
if (options.CalculateBoneOffsetMatrices)
|
||||||
{
|
{
|
||||||
// Calculate offset matrix (inverse bind pose transform) for every bone manually
|
// 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
|
#if USE_SKELETON_NODES_SORTING
|
||||||
// Sort skeleton nodes and bones hierarchy (parents first)
|
// Sort skeleton nodes and bones hierarchy (parents first)
|
||||||
// Then it can be used with a simple linear loop update
|
// 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
|
#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)
|
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
|
// Merge meshes with the same parent nodes, material and skinning
|
||||||
if (options.MergeMeshes)
|
if (options.MergeMeshes)
|
||||||
{
|
{
|
||||||
int32 meshesMerged = 0;
|
int32 meshesMerged = 0;
|
||||||
|
|
||||||
for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++)
|
for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++)
|
||||||
{
|
{
|
||||||
auto& meshes = data.LODs[lodIndex].Meshes;
|
auto& meshes = data.LODs[lodIndex].Meshes;
|
||||||
@@ -1568,11 +1443,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meshesMerged)
|
if (meshesMerged)
|
||||||
{
|
|
||||||
LOG(Info, "Merged {0} meshes", meshesMerged);
|
LOG(Info, "Merged {0} meshes", meshesMerged);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic LOD generation
|
// Automatic LOD generation
|
||||||
|
|||||||
Reference in New Issue
Block a user