Urho3D/Source/Tools/AssetImporter/AssetImporter.cpp
2020-01-05 06:21:40 +00:00

2920 lines
110 KiB
C++

//
// Copyright (c) 2008-2020 the Urho3D project.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#include <Urho3D/Core/Context.h>
#include <Urho3D/Core/ProcessUtils.h>
#include <Urho3D/Core/StringUtils.h>
#include <Urho3D/Core/WorkQueue.h>
#include <Urho3D/Graphics/AnimatedModel.h>
#include <Urho3D/Graphics/Animation.h>
#include <Urho3D/Graphics/DebugRenderer.h>
#include <Urho3D/Graphics/Geometry.h>
#include <Urho3D/Graphics/Graphics.h>
#include <Urho3D/Graphics/IndexBuffer.h>
#include <Urho3D/Graphics/Light.h>
#include <Urho3D/Graphics/Material.h>
#include <Urho3D/Graphics/Octree.h>
#include <Urho3D/Graphics/VertexBuffer.h>
#include <Urho3D/Graphics/Zone.h>
#include <Urho3D/IO/File.h>
#include <Urho3D/IO/FileSystem.h>
#ifdef URHO3D_PHYSICS
#include <Urho3D/Physics/PhysicsWorld.h>
#endif
#include <Urho3D/Resource/ResourceCache.h>
#include <Urho3D/Resource/XMLFile.h>
#include <Urho3D/Scene/Scene.h>
#ifdef WIN32
#include <windows.h>
#endif
#include <assimp/config.h>
#include <assimp/cimport.h>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <assimp/DefaultLogger.hpp>
#include <Urho3D/DebugNew.h>
using namespace Urho3D;
struct OutModel
{
String outName_;
aiNode* rootNode_{};
HashSet<unsigned> meshIndices_;
PODVector<aiMesh*> meshes_;
PODVector<aiNode*> meshNodes_;
PODVector<aiNode*> bones_;
PODVector<aiNode*> pivotlessBones_;
PODVector<aiAnimation*> animations_;
PODVector<float> boneRadii_;
PODVector<BoundingBox> boneHitboxes_;
aiNode* rootBone_{};
unsigned totalVertices_{};
unsigned totalIndices_{};
};
struct OutScene
{
String outName_;
aiNode* rootNode_{};
Vector<OutModel> models_;
PODVector<aiNode*> nodes_;
PODVector<unsigned> nodeModelIndices_;
};
// FBX transform chain
enum TransformationComp
{
TransformationComp_Translation = 0,
TransformationComp_RotationOffset,
TransformationComp_RotationPivot,
TransformationComp_PreRotation,
TransformationComp_Rotation,
TransformationComp_PostRotation,
TransformationComp_RotationPivotInverse,
TransformationComp_ScalingOffset,
TransformationComp_ScalingPivot,
TransformationComp_Scaling,
// Not checking these
// They are typically flushed out in the fbxconverter, but there
// might be cases where they're not, hence, leaving them.
#ifdef EXT_TRANSFORMATION_CHECK
TransformationComp_ScalingPivotInverse,
TransformationComp_GeometricTranslation,
TransformationComp_GeometricRotation,
TransformationComp_GeometricScaling,
#endif
TransformationComp_MAXIMUM
};
const char *transformSuffix[TransformationComp_MAXIMUM] =
{
"Translation", // TransformationComp_Translation = 0,
"RotationOffset", // TransformationComp_RotationOffset,
"RotationPivot", // TransformationComp_RotationPivot,
"PreRotation", // TransformationComp_PreRotation,
"Rotation", // TransformationComp_Rotation,
"PostRotation", // TransformationComp_PostRotation,
"RotationPivotInverse", // TransformationComp_RotationPivotInverse,
"ScalingOffset", // TransformationComp_ScalingOffset,
"ScalingPivot", // TransformationComp_ScalingPivot,
"Scaling", // TransformationComp_Scaling,
#ifdef EXT_TRANSFORMATION_CHECK
"ScalingPivotInverse", // TransformationComp_ScalingPivotInverse,
"GeometricTranslation", // TransformationComp_GeometricTranslation,
"GeometricRotation", // TransformationComp_GeometricRotation,
"GeometricScaling", // TransformationComp_GeometricScaling,
#endif
};
static const unsigned MAX_CHANNELS = 4;
SharedPtr<Context> context_(new Context());
const aiScene* scene_ = nullptr;
aiNode* rootNode_ = nullptr;
String inputName_;
String resourcePath_;
String outPath_;
String outName_;
bool useSubdirs_ = true;
bool localIDs_ = false;
bool saveBinary_ = false;
bool saveJson_ = false;
bool createZone_ = true;
bool noAnimations_ = false;
bool noHierarchy_ = false;
bool noMaterials_ = false;
bool noTextures_ = false;
bool noMaterialDiffuseColor_ = false;
bool noEmptyNodes_ = false;
bool saveMaterialList_ = false;
bool includeNonSkinningBones_ = false;
bool verboseLog_ = false;
bool emissiveAO_ = false;
bool noOverwriteMaterial_ = false;
bool noOverwriteTexture_ = false;
bool noOverwriteNewerTexture_ = false;
bool checkUniqueModel_ = true;
bool moveToBindPose_ = false;
unsigned maxBones_ = 64;
Vector<String> nonSkinningBoneIncludes_;
Vector<String> nonSkinningBoneExcludes_;
HashSet<aiAnimation*> allAnimations_;
PODVector<aiAnimation*> sceneAnimations_;
float defaultTicksPerSecond_ = 4800.0f;
// For subset animation import usage
float importStartTime_ = 0.0f;
float importEndTime_ = 0.0f;
bool suppressFbxPivotNodes_ = true;
int main(int argc, char** argv);
void Run(const Vector<String>& arguments);
void DumpNodes(aiNode* rootNode, unsigned level);
void ExportModel(const String& outName, bool animationOnly);
void ExportAnimation(const String& outName, bool animationOnly);
void CollectMeshes(OutModel& model, aiNode* node);
void CollectBones(OutModel& model, bool animationOnly = false);
void CollectBonesFinal(PODVector<aiNode*>& dest, const HashSet<aiNode*>& necessary, aiNode* node);
void MoveToBindPose(OutModel& model, aiNode* current);
void CollectAnimations(OutModel* model = nullptr);
void BuildBoneCollisionInfo(OutModel& model);
void BuildAndSaveModel(OutModel& model);
void BuildAndSaveAnimations(OutModel* model = nullptr);
void ExportScene(const String& outName, bool asPrefab);
void CollectSceneModels(OutScene& scene, aiNode* node);
void CreateHierarchy(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping);
Node* CreateSceneNode(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping);
void BuildAndSaveScene(OutScene& scene, bool asPrefab);
void ExportMaterials(HashSet<String>& usedTextures);
void BuildAndSaveMaterial(aiMaterial* material, HashSet<String>& usedTextures);
void CopyTextures(const HashSet<String>& usedTextures, const String& sourcePath);
void CombineLods(const PODVector<float>& lodDistances, const Vector<String>& modelNames, const String& outName);
void GetMeshesUnderNode(Vector<Pair<aiNode*, aiMesh*> >& dest, aiNode* node);
unsigned GetMeshIndex(aiMesh* mesh);
unsigned GetBoneIndex(OutModel& model, const String& boneName);
aiBone* GetMeshBone(OutModel& model, const String& boneName);
Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName);
void GetBlendData(OutModel& model, aiMesh* mesh, aiNode* meshNode, PODVector<unsigned>& boneMappings, Vector<PODVector<unsigned char> >&
blendIndices, Vector<PODVector<float> >& blendWeights);
String GetMeshMaterialName(aiMesh* mesh);
String GetMaterialTextureName(const String& nameIn);
String GenerateMaterialName(aiMaterial* material);
String GenerateTextureName(unsigned texIndex);
unsigned GetNumValidFaces(aiMesh* mesh);
void WriteShortIndices(unsigned short*& dest, aiMesh* mesh, unsigned index, unsigned offset);
void WriteLargeIndices(unsigned*& dest, aiMesh* mesh, unsigned index, unsigned offset);
void WriteVertex(float*& dest, aiMesh* mesh, unsigned index, bool isSkinned, BoundingBox& box,
const Matrix3x4& vertexTransform, const Matrix3& normalTransform, Vector<PODVector<unsigned char> >& blendIndices,
Vector<PODVector<float> >& blendWeights);
PODVector<VertexElement> GetVertexElements(aiMesh* mesh, bool isSkinned);
aiNode* GetNode(const String& name, aiNode* rootNode, bool caseSensitive = true);
aiMatrix4x4 GetDerivedTransform(aiNode* node, aiNode* rootNode, bool rootInclusive = true);
aiMatrix4x4 GetDerivedTransform(aiMatrix4x4 transform, aiNode* node, aiNode* rootNode, bool rootInclusive = true);
aiMatrix4x4 GetMeshBakingTransform(aiNode* meshNode, aiNode* modelRootNode);
void GetPosRotScale(const aiMatrix4x4& transform, Vector3& pos, Quaternion& rot, Vector3& scale);
String FromAIString(const aiString& str);
Vector3 ToVector3(const aiVector3D& vec);
Vector2 ToVector2(const aiVector2D& vec);
Quaternion ToQuaternion(const aiQuaternion& quat);
Matrix3x4 ToMatrix3x4(const aiMatrix4x4& mat);
aiMatrix4x4 ToAIMatrix4x4(const Matrix3x4& mat);
String SanitateAssetName(const String& name);
unsigned GetPivotlessBoneIndex(OutModel& model, const String& boneName);
void ExtrapolatePivotlessAnimation(OutModel* model);
void CollectSceneNodesAsBones(OutModel &model, aiNode* rootNode);
int main(int argc, char** argv)
{
Vector<String> arguments;
#ifdef WIN32
arguments = ParseArguments(GetCommandLineW());
#else
arguments = ParseArguments(argc, argv);
#endif
Run(arguments);
return 0;
}
void Run(const Vector<String>& arguments)
{
if (arguments.Size() < 2)
{
ErrorExit(
"Usage: AssetImporter <command> <input file> <output file> [options]\n"
"See http://assimp.sourceforge.net/main_features_formats.html for input formats\n\n"
"Commands:\n"
"model Output a model\n"
"anim Output animation(s)\n"
"scene Output a scene\n"
"node Output a node and its children (prefab)\n"
"dump Dump scene node structure. No output file is generated\n"
"lod Combine several Urho3D models as LOD levels of the output model\n"
" Syntax: lod <dist0> <mdl0> <dist1 <mdl1> ... <output file>\n"
"\n"
"Options:\n"
"-b Save scene in binary format, default format is XML\n"
"-j Save scene in JSON format, default format is XML\n"
"-h Generate hard instead of smooth normals if input has no normals\n"
"-i Use local ID's for scene nodes\n"
"-l Output a material list file for models\n"
"-na Do not output animations\n"
"-nm Do not output materials\n"
"-nt Do not output material textures\n"
"-nc Do not use material diffuse color value, instead output white\n"
"-nh Do not save full node hierarchy (scene mode only)\n"
"-ns Do not create subdirectories for resources\n"
"-nz Do not create a zone and a directional light (scene mode only)\n"
"-nf Do not fix infacing normals\n"
"-ne Do not save empty nodes (scene mode only)\n"
"-mb <x> Maximum number of bones per submesh. Default 64\n"
"-p <path> Set path for scene resources. Default is output file path\n"
"-r <name> Use the named scene node as root node\n"
"-f <freq> Animation tick frequency to use if unspecified. Default 4800\n"
"-o Optimize redundant submeshes. Loses scene hierarchy and animations\n"
"-s <filter> Include non-skinning bones in the model's skeleton. Can be given a\n"
" case-insensitive semicolon separated filter list. Bone is included\n"
" if its name contains any of the filters. Prefix filter with minus\n"
" sign to use as an exclude. For example -s \"Bip01;-Dummy;-Helper\"\n"
"-t Generate tangents\n"
"-v Enable verbose Assimp library logging\n"
"-eao Interpret material emissive texture as ambient occlusion\n"
"-cm Check and do not overwrite if material exists\n"
"-ct Check and do not overwrite if texture exists\n"
"-ctn Check and do not overwrite if texture has newer timestamp\n"
"-am Export all meshes even if identical (scene mode only)\n"
"-bp Move bones to bind pose before saving model\n"
"-split <start> <end> (animation model only)\n"
" Split animation, will only import from start frame to end frame\n"
"-np Do not suppress $fbx pivot nodes (FBX files only)\n"
);
}
context_->RegisterSubsystem(new FileSystem(context_));
context_->RegisterSubsystem(new ResourceCache(context_));
context_->RegisterSubsystem(new WorkQueue(context_));
RegisterSceneLibrary(context_);
RegisterGraphicsLibrary(context_);
#ifdef URHO3D_PHYSICS
RegisterPhysicsLibrary(context_);
#endif
String command = arguments[0].ToLower();
String rootNodeName;
unsigned flags =
aiProcess_ConvertToLeftHanded |
aiProcess_JoinIdenticalVertices |
aiProcess_Triangulate |
aiProcess_GenSmoothNormals |
aiProcess_LimitBoneWeights |
aiProcess_ImproveCacheLocality |
aiProcess_RemoveRedundantMaterials |
aiProcess_FixInfacingNormals |
aiProcess_FindInvalidData |
aiProcess_GenUVCoords |
aiProcess_FindInstances |
aiProcess_OptimizeMeshes;
for (unsigned i = 2; i < arguments.Size(); ++i)
{
if (arguments[i].Length() > 1 && arguments[i][0] == '-')
{
String argument = arguments[i].Substring(1).ToLower();
String value = i + 1 < arguments.Size() ? arguments[i + 1] : String::EMPTY;
if (argument == "b")
saveBinary_ = true;
else if(argument == "j")
saveJson_ = true;
else if (argument == "h")
{
flags &= ~aiProcess_GenSmoothNormals;
flags |= aiProcess_GenNormals;
}
else if (argument == "i")
localIDs_ = true;
else if (argument == "l")
saveMaterialList_ = true;
else if (argument == "t")
flags |= aiProcess_CalcTangentSpace;
else if (argument == "o")
flags |= aiProcess_PreTransformVertices;
else if (argument.Length() == 2 && argument[0] == 'n')
{
switch (tolower(argument[1]))
{
case 'a':
noAnimations_ = true;
break;
case 'c':
noMaterialDiffuseColor_ = true;
break;
case 'm':
noMaterials_ = true;
break;
case 'h':
noHierarchy_ = true;
break;
case 'e':
noEmptyNodes_ = true;
break;
case 's':
useSubdirs_ = false;
break;
case 't':
noTextures_ = true;
break;
case 'z':
createZone_ = false;
break;
case 'f':
flags &= ~aiProcess_FixInfacingNormals;
break;
case 'p':
suppressFbxPivotNodes_ = false;
break;
}
}
else if (argument == "mb" && !value.Empty())
{
maxBones_ = ToUInt(value);
if (maxBones_ < 1)
maxBones_ = 1;
++i;
}
else if (argument == "p" && !value.Empty())
{
resourcePath_ = AddTrailingSlash(value);
++i;
}
else if (argument == "r" && !value.Empty())
{
rootNodeName = value;
++i;
}
else if (argument == "f" && !value.Empty())
{
defaultTicksPerSecond_ = ToFloat(value);
++i;
}
else if (argument == "s")
{
includeNonSkinningBones_ = true;
if (value.Length() && (value[0] != '-' || value.Length() > 3))
{
Vector<String> filters = value.Split(';');
for (unsigned i = 0; i < filters.Size(); ++i)
{
if (filters[i][0] == '-')
nonSkinningBoneExcludes_.Push(filters[i].Substring(1));
else
nonSkinningBoneIncludes_.Push(filters[i]);
}
}
}
else if (argument == "v")
verboseLog_ = true;
else if (argument == "eao")
emissiveAO_ = true;
else if (argument == "cm")
noOverwriteMaterial_ = true;
else if (argument == "ct")
noOverwriteTexture_ = true;
else if (argument == "ctn")
noOverwriteNewerTexture_ = true;
else if (argument == "am")
checkUniqueModel_ = false;
else if (argument == "bp")
moveToBindPose_ = true;
else if (argument == "split")
{
String value2 = i + 2 < arguments.Size() ? arguments[i + 2] : String::EMPTY;
if (value.Length() && value2.Length() && (value[0] != '-') && (value2[0] != '-'))
{
importStartTime_ = ToFloat(value);
importEndTime_ = ToFloat(value2);
}
}
}
}
if (command == "model" || command == "scene" || command == "anim" || command == "node" || command == "dump")
{
String inFile = arguments[1];
String outFile;
if (arguments.Size() > 2 && arguments[2][0] != '-')
outFile = GetInternalPath(arguments[2]);
inputName_ = GetFileName(inFile);
outName_ = outFile;
outPath_ = GetPath(outFile);
if (resourcePath_.Empty())
{
resourcePath_ = outPath_;
// If output file already has the Models/ path (model mode), do not take it into the resource path
if (command == "model")
{
if (resourcePath_.EndsWith("Models/", false))
resourcePath_ = resourcePath_.Substring(0, resourcePath_.Length() - 7);
}
if (resourcePath_.Empty())
resourcePath_ = "./";
}
resourcePath_ = AddTrailingSlash(resourcePath_);
if (command != "dump" && outFile.Empty())
ErrorExit("No output file defined");
if (verboseLog_)
Assimp::DefaultLogger::create("", Assimp::Logger::VERBOSE, aiDefaultLogStream_STDOUT);
PrintLine("Reading file " + inFile);
if (!inFile.EndsWith(".fbx", false))
suppressFbxPivotNodes_ = false;
// Only do this for the "model" command. "anim" command extrapolates animation from the original bone definition
if (suppressFbxPivotNodes_ && command == "model")
{
PrintLine("Suppressing $fbx nodes");
aiPropertyStore *aiprops = aiCreatePropertyStore();
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, 1); //default = true;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS, 0); //default = false;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_MATERIALS, 1); //default = true;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_CAMERAS, 1); //default = true;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_LIGHTS, 1); //default = true;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, 1); //default = true;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_STRICT_MODE, 0); //default = false;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, 0); //**false, default = true;
aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, 1);//default = true;
scene_ = aiImportFileExWithProperties(GetNativePath(inFile).CString(), flags, nullptr, aiprops);
// prevent processing animation suppression, both cannot work simultaneously
suppressFbxPivotNodes_ = false;
}
else
scene_ = aiImportFile(GetNativePath(inFile).CString(), flags);
if (!scene_)
ErrorExit("Could not open or parse input file " + inFile + ": " + String(aiGetErrorString()));
if (verboseLog_)
Assimp::DefaultLogger::kill();
rootNode_ = scene_->mRootNode;
if (!rootNodeName.Empty())
{
rootNode_ = GetNode(rootNodeName, rootNode_, false);
if (!rootNode_)
ErrorExit("Could not find scene node " + rootNodeName);
}
if (command == "dump")
{
DumpNodes(rootNode_, 0);
return;
}
if (command == "model")
ExportModel(outFile, scene_->mFlags & AI_SCENE_FLAGS_INCOMPLETE);
if (command == "anim")
{
noMaterials_ = true;
ExportAnimation(outFile, scene_->mFlags & AI_SCENE_FLAGS_INCOMPLETE);
}
if (command == "scene" || command == "node")
{
bool asPrefab = command == "node";
// Saving as prefab requires the hierarchy, especially the root node
if (asPrefab)
noHierarchy_ = false;
ExportScene(outFile, asPrefab);
}
if (!noMaterials_)
{
HashSet<String> usedTextures;
ExportMaterials(usedTextures);
if (!noTextures_)
CopyTextures(usedTextures, GetPath(inFile));
}
}
else if (command == "lod")
{
PODVector<float> lodDistances;
Vector<String> modelNames;
String outFile;
unsigned numLodArguments = 0;
for (unsigned i = 1; i < arguments.Size(); ++i)
{
if (arguments[i][0] == '-')
break;
++numLodArguments;
}
if (numLodArguments < 4)
ErrorExit("Must define at least 2 LOD levels");
if (!(numLodArguments & 1u))
ErrorExit("No output file defined");
for (unsigned i = 1; i < numLodArguments + 1; ++i)
{
if (i == numLodArguments)
outFile = GetInternalPath(arguments[i]);
else
{
if (i & 1u)
lodDistances.Push(Max(ToFloat(arguments[i]), 0.0f));
else
modelNames.Push(GetInternalPath(arguments[i]));
}
}
if (lodDistances[0] != 0.0f)
{
PrintLine("Warning: first LOD distance forced to 0");
lodDistances[0] = 0.0f;
}
CombineLods(lodDistances, modelNames, outFile);
}
else
ErrorExit("Unrecognized command " + command);
}
void DumpNodes(aiNode* rootNode, unsigned level)
{
if (!rootNode)
return;
String indent(' ', level * 2);
Vector3 pos, scale;
Quaternion rot;
aiMatrix4x4 transform = GetDerivedTransform(rootNode, rootNode_);
GetPosRotScale(transform, pos, rot, scale);
PrintLine(indent + "Node " + FromAIString(rootNode->mName) + " pos " + String(pos));
if (rootNode->mNumMeshes == 1)
PrintLine(indent + " " + String(rootNode->mNumMeshes) + " geometry");
if (rootNode->mNumMeshes > 1)
PrintLine(indent + " " + String(rootNode->mNumMeshes) + " geometries");
for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
DumpNodes(rootNode->mChildren[i], level + 1);
}
void ExportModel(const String& outName, bool animationOnly)
{
if (outName.Empty())
ErrorExit("No output file defined");
OutModel model;
model.rootNode_ = rootNode_;
model.outName_ = outName;
CollectMeshes(model, model.rootNode_);
CollectBones(model, animationOnly);
BuildBoneCollisionInfo(model);
BuildAndSaveModel(model);
if (!noAnimations_)
{
CollectAnimations(&model);
BuildAndSaveAnimations(&model);
// Save scene-global animations
CollectAnimations();
BuildAndSaveAnimations();
}
}
void ExportAnimation(const String& outName, bool animationOnly)
{
if (outName.Empty())
ErrorExit("No output file defined");
OutModel model;
model.rootNode_ = rootNode_;
model.outName_ = outName;
CollectMeshes(model, model.rootNode_);
CollectBones(model, animationOnly);
BuildBoneCollisionInfo(model);
// BuildAndSaveModel(model);
if (!noAnimations_)
{
// Most fbx animation files contain only a skeleton and no skinned mesh.
// Assume the scene node contains the model's bone definition and,
// transfer the info to the model.
if (suppressFbxPivotNodes_ && model.bones_.Size() == 0)
CollectSceneNodesAsBones(model, rootNode_);
CollectAnimations(&model);
BuildAndSaveAnimations(&model);
// Save scene-global animations
CollectAnimations();
BuildAndSaveAnimations();
}
}
void CollectMeshes(OutModel& model, aiNode* node)
{
for (unsigned i = 0; i < node->mNumMeshes; ++i)
{
aiMesh* mesh = scene_->mMeshes[node->mMeshes[i]];
for (unsigned j = 0; j < model.meshes_.Size(); ++j)
{
if (mesh == model.meshes_[j])
{
PrintLine("Warning: same mesh found multiple times");
break;
}
}
model.meshIndices_.Insert(node->mMeshes[i]);
model.meshes_.Push(mesh);
model.meshNodes_.Push(node);
model.totalVertices_ += mesh->mNumVertices;
model.totalIndices_ += GetNumValidFaces(mesh) * 3;
}
for (unsigned i = 0; i < node->mNumChildren; ++i)
CollectMeshes(model, node->mChildren[i]);
}
void CollectBones(OutModel& model, bool animationOnly)
{
HashSet<aiNode*> necessary;
HashSet<aiNode*> rootNodes;
bool haveSkinnedMeshes = false;
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
if (model.meshes_[i]->HasBones())
{
haveSkinnedMeshes = true;
break;
}
}
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
aiMesh* mesh = model.meshes_[i];
aiNode* meshNode = model.meshNodes_[i];
aiNode* meshParentNode = meshNode->mParent;
aiNode* rootNode = nullptr;
for (unsigned j = 0; j < mesh->mNumBones; ++j)
{
aiBone* bone = mesh->mBones[j];
String boneName(FromAIString(bone->mName));
aiNode* boneNode = GetNode(boneName, scene_->mRootNode, true);
if (!boneNode)
ErrorExit("Could not find scene node for bone " + boneName);
necessary.Insert(boneNode);
rootNode = boneNode;
for (;;)
{
boneNode = boneNode->mParent;
if (!boneNode || ((boneNode == meshNode || boneNode == meshParentNode) && !animationOnly))
break;
rootNode = boneNode;
necessary.Insert(boneNode);
}
if (rootNodes.Find(rootNode) == rootNodes.End())
rootNodes.Insert(rootNode);
}
// When model is partially skinned, include the attachment nodes of the rigid meshes in the skeleton
if (haveSkinnedMeshes && !mesh->mNumBones)
{
aiNode* boneNode = meshNode;
necessary.Insert(boneNode);
rootNode = boneNode;
for (;;)
{
boneNode = boneNode->mParent;
if (!boneNode || ((boneNode == meshNode || boneNode == meshParentNode) && !animationOnly))
break;
rootNode = boneNode;
necessary.Insert(boneNode);
}
if (rootNodes.Find(rootNode) == rootNodes.End())
rootNodes.Insert(rootNode);
}
}
// If we find multiple root nodes, try to remedy by going back in the parent chain and finding a common parent
if (rootNodes.Size() > 1)
{
for (HashSet<aiNode*>::Iterator i = rootNodes.Begin(); i != rootNodes.End(); ++i)
{
aiNode* commonParent = (*i);
while (commonParent)
{
unsigned found = 0;
for (HashSet<aiNode*>::Iterator j = rootNodes.Begin(); j != rootNodes.End(); ++j)
{
if (i == j)
continue;
aiNode* parent = *j;
while (parent)
{
if (parent == commonParent)
{
++found;
break;
}
parent = parent->mParent;
}
}
if (found >= rootNodes.Size() - 1)
{
PrintLine("Multiple roots initially found, using new root node " + FromAIString(commonParent->mName));
rootNodes.Clear();
rootNodes.Insert(commonParent);
necessary.Insert(commonParent);
break;
}
commonParent = commonParent->mParent;
}
if (rootNodes.Size() == 1)
break; // Succeeded
}
if (rootNodes.Size() > 1)
ErrorExit("Skeleton with multiple root nodes found, not supported");
}
if (rootNodes.Empty())
return;
model.rootBone_ = *rootNodes.Begin();
// Move the model to bind pose now if requested
if (moveToBindPose_)
{
PrintLine("Moving bones to bind pose");
MoveToBindPose(model, model.rootBone_);
}
CollectBonesFinal(model.bones_, necessary, model.rootBone_);
// Initialize the bone collision info
model.boneRadii_.Resize(model.bones_.Size());
model.boneHitboxes_.Resize(model.bones_.Size());
for (unsigned i = 0; i < model.bones_.Size(); ++i)
{
model.boneRadii_[i] = 0.0f;
model.boneHitboxes_[i] = BoundingBox(0.0f, 0.0f);
}
}
void MoveToBindPose(OutModel& model, aiNode* current)
{
String nodeName(FromAIString(current->mName));
Matrix3x4 bindWorldTransform = GetOffsetMatrix(model, nodeName).Inverse();
// Skip if we get an identity offset matrix (bone lookup failed)
if (!bindWorldTransform.Equals(Matrix3x4::IDENTITY))
{
if (current->mParent && current != model.rootNode_)
{
aiMatrix4x4 parentWorldTransform = GetDerivedTransform(current->mParent, model.rootNode_, true);
Matrix3x4 parentInverse = ToMatrix3x4(parentWorldTransform).Inverse();
current->mTransformation = ToAIMatrix4x4(parentInverse * bindWorldTransform);
}
else
current->mTransformation = ToAIMatrix4x4(bindWorldTransform);
}
for (unsigned i = 0; i < current->mNumChildren; ++i)
MoveToBindPose(model, current->mChildren[i]);
}
void CollectBonesFinal(PODVector<aiNode*>& dest, const HashSet<aiNode*>& necessary, aiNode* node)
{
bool includeBone = necessary.Find(node) != necessary.End();
String boneName = FromAIString(node->mName);
// Check include/exclude filters for non-skinned bones
if (!includeBone && includeNonSkinningBones_)
{
// If no includes specified, include by default but check for excludes
if (nonSkinningBoneIncludes_.Empty())
includeBone = true;
// Check against includes/excludes
for (unsigned i = 0; i < nonSkinningBoneIncludes_.Size(); ++i)
{
if (boneName.Contains(nonSkinningBoneIncludes_[i], false))
{
includeBone = true;
break;
}
}
for (unsigned i = 0; i < nonSkinningBoneExcludes_.Size(); ++i)
{
if (boneName.Contains(nonSkinningBoneExcludes_[i], false))
{
includeBone = false;
break;
}
}
if (includeBone)
PrintLine("Including non-skinning bone " + boneName);
}
if (includeBone)
dest.Push(node);
for (unsigned i = 0; i < node->mNumChildren; ++i)
CollectBonesFinal(dest, necessary, node->mChildren[i]);
}
void CollectAnimations(OutModel* model)
{
const aiScene* scene = scene_;
for (unsigned i = 0; i < scene->mNumAnimations; ++i)
{
aiAnimation* anim = scene->mAnimations[i];
if (allAnimations_.Contains(anim))
continue;
if (model)
{
bool modelBoneFound = false;
for (unsigned j = 0; j < anim->mNumChannels; ++j)
{
aiNodeAnim* channel = anim->mChannels[j];
String channelName = FromAIString(channel->mNodeName);
if (GetBoneIndex(*model, channelName) != M_MAX_UNSIGNED)
{
modelBoneFound = true;
break;
}
}
if (modelBoneFound)
{
model->animations_.Push(anim);
allAnimations_.Insert(anim);
}
}
else
{
sceneAnimations_.Push(anim);
allAnimations_.Insert(anim);
}
}
/// \todo Vertex morphs are ignored for now
}
void BuildBoneCollisionInfo(OutModel& model)
{
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
aiMesh* mesh = model.meshes_[i];
for (unsigned j = 0; j < mesh->mNumBones; ++j)
{
aiBone* bone = mesh->mBones[j];
String boneName = FromAIString(bone->mName);
unsigned boneIndex = GetBoneIndex(model, boneName);
if (boneIndex == M_MAX_UNSIGNED)
continue;
for (unsigned k = 0; k < bone->mNumWeights; ++k)
{
float weight = bone->mWeights[k].mWeight;
// Require skinning weight to be sufficiently large before vertex contributes to bone hitbox
if (weight > 0.33f)
{
aiVector3D vertexBoneSpace = bone->mOffsetMatrix * mesh->mVertices[bone->mWeights[k].mVertexId];
Vector3 vertex = ToVector3(vertexBoneSpace);
float radius = vertex.Length();
if (radius > model.boneRadii_[boneIndex])
model.boneRadii_[boneIndex] = radius;
model.boneHitboxes_[boneIndex].Merge(vertex);
}
}
}
}
}
void BuildAndSaveModel(OutModel& model)
{
if (!model.rootNode_)
{
PrintLine("Null root node for model, skipping model save");
return;
}
String rootNodeName = FromAIString(model.rootNode_->mName);
if (!model.meshes_.Size())
{
PrintLine("No geometries found starting from node " + rootNodeName + ", skipping model save");
return;
}
PrintLine("Writing model " + rootNodeName);
SharedPtr<Model> outModel(new Model(context_));
Vector<PODVector<unsigned> > allBoneMappings;
BoundingBox box;
unsigned numValidGeometries = 0;
bool combineBuffers = true;
// Check if buffers can be combined (same vertex elements, under 65535 vertices)
PODVector<VertexElement> elements = GetVertexElements(model.meshes_[0], model.bones_.Size() > 0);
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
if (GetNumValidFaces(model.meshes_[i]))
{
++numValidGeometries;
if (i > 0 && GetVertexElements(model.meshes_[i], model.bones_.Size() > 0) != elements)
combineBuffers = false;
}
}
// Check if keeping separate buffers allows to avoid 32-bit indices
if (combineBuffers && model.totalVertices_ > 65535)
{
bool allUnder65k = true;
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
if (GetNumValidFaces(model.meshes_[i]))
{
if (model.meshes_[i]->mNumVertices > 65535)
allUnder65k = false;
}
}
if (allUnder65k == true)
combineBuffers = false;
}
SharedPtr<IndexBuffer> ib;
SharedPtr<VertexBuffer> vb;
Vector<SharedPtr<VertexBuffer> > vbVector;
Vector<SharedPtr<IndexBuffer> > ibVector;
unsigned startVertexOffset = 0;
unsigned startIndexOffset = 0;
unsigned destGeomIndex = 0;
bool isSkinned = model.bones_.Size() > 0;
outModel->SetNumGeometries(numValidGeometries);
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
aiMesh* mesh = model.meshes_[i];
PODVector<VertexElement> elements = GetVertexElements(mesh, isSkinned);
unsigned validFaces = GetNumValidFaces(mesh);
if (!validFaces)
continue;
bool largeIndices;
if (combineBuffers)
largeIndices = model.totalIndices_ > 65535;
else
largeIndices = mesh->mNumVertices > 65535;
// Create new buffers if necessary
if (!combineBuffers || vbVector.Empty())
{
vb = new VertexBuffer(context_);
ib = new IndexBuffer(context_);
if (combineBuffers)
{
ib->SetSize(model.totalIndices_, largeIndices);
vb->SetSize(model.totalVertices_, elements);
}
else
{
ib->SetSize(validFaces * 3, largeIndices);
vb->SetSize(mesh->mNumVertices, elements);
}
vbVector.Push(vb);
ibVector.Push(ib);
startVertexOffset = 0;
startIndexOffset = 0;
}
// Get the world transform of the mesh for baking into the vertices
Matrix3x4 vertexTransform;
Matrix3 normalTransform;
Vector3 pos, scale;
Quaternion rot;
GetPosRotScale(GetMeshBakingTransform(model.meshNodes_[i], model.rootNode_), pos, rot, scale);
vertexTransform = Matrix3x4(pos, rot, scale);
normalTransform = rot.RotationMatrix();
SharedPtr<Geometry> geom(new Geometry(context_));
PrintLine("Writing geometry " + String(i) + " with " + String(mesh->mNumVertices) + " vertices " +
String(validFaces * 3) + " indices");
if (model.bones_.Size() > 0 && !mesh->HasBones())
PrintLine("Warning: model has bones but geometry " + String(i) + " has no skinning information");
unsigned char* vertexData = vb->GetShadowData();
unsigned char* indexData = ib->GetShadowData();
// Build the index data
if (!largeIndices)
{
unsigned short* dest = (unsigned short*)indexData + startIndexOffset;
for (unsigned j = 0; j < mesh->mNumFaces; ++j)
WriteShortIndices(dest, mesh, j, startVertexOffset);
}
else
{
unsigned* dest = (unsigned*)indexData + startIndexOffset;
for (unsigned j = 0; j < mesh->mNumFaces; ++j)
WriteLargeIndices(dest, mesh, j, startVertexOffset);
}
// Build the vertex data
// If there are bones, get blend data
Vector<PODVector<unsigned char> > blendIndices;
Vector<PODVector<float> > blendWeights;
PODVector<unsigned> boneMappings;
if (model.bones_.Size())
GetBlendData(model, mesh, model.meshNodes_[i], boneMappings, blendIndices, blendWeights);
auto* dest = (float*)((unsigned char*)vertexData + startVertexOffset * vb->GetVertexSize());
for (unsigned j = 0; j < mesh->mNumVertices; ++j)
WriteVertex(dest, mesh, j, isSkinned, box, vertexTransform, normalTransform, blendIndices, blendWeights);
// Calculate the geometry center
Vector3 center = Vector3::ZERO;
if (validFaces)
{
for (unsigned j = 0; j < mesh->mNumFaces; ++j)
{
if (mesh->mFaces[j].mNumIndices == 3)
{
center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[0]]);
center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[1]]);
center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[2]]);
}
}
center /= (float)validFaces * 3;
}
// Define the geometry
geom->SetIndexBuffer(ib);
geom->SetVertexBuffer(0, vb);
geom->SetDrawRange(TRIANGLE_LIST, startIndexOffset, validFaces * 3, true);
outModel->SetNumGeometryLodLevels(destGeomIndex, 1);
outModel->SetGeometry(destGeomIndex, 0, geom);
outModel->SetGeometryCenter(destGeomIndex, center);
if (model.bones_.Size() > maxBones_)
allBoneMappings.Push(boneMappings);
startVertexOffset += mesh->mNumVertices;
startIndexOffset += validFaces * 3;
++destGeomIndex;
}
// Define the model buffers and bounding box
PODVector<unsigned> emptyMorphRange;
outModel->SetVertexBuffers(vbVector, emptyMorphRange, emptyMorphRange);
outModel->SetIndexBuffers(ibVector);
outModel->SetBoundingBox(box);
// Build skeleton if necessary
if (model.bones_.Size() && model.rootBone_)
{
PrintLine("Writing skeleton with " + String(model.bones_.Size()) + " bones, rootbone " +
FromAIString(model.rootBone_->mName));
Skeleton skeleton;
Vector<Bone>& bones = skeleton.GetModifiableBones();
for (unsigned i = 0; i < model.bones_.Size(); ++i)
{
aiNode* boneNode = model.bones_[i];
String boneName(FromAIString(boneNode->mName));
Bone newBone;
newBone.name_ = boneName;
aiMatrix4x4 transform = boneNode->mTransformation;
// Make the root bone transform relative to the model's root node, if it is not already
// (in case there are nodes between that are not accounted for otherwise)
if (boneNode == model.rootBone_)
transform = GetDerivedTransform(boneNode, model.rootNode_, false);
GetPosRotScale(transform, newBone.initialPosition_, newBone.initialRotation_, newBone.initialScale_);
// Get offset information if exists
newBone.offsetMatrix_ = GetOffsetMatrix(model, boneName);
newBone.radius_ = model.boneRadii_[i];
newBone.boundingBox_ = model.boneHitboxes_[i];
newBone.collisionMask_ = BONECOLLISION_SPHERE | BONECOLLISION_BOX;
newBone.parentIndex_ = i;
bones.Push(newBone);
}
// Set the bone hierarchy
for (unsigned i = 1; i < model.bones_.Size(); ++i)
{
String parentName = FromAIString(model.bones_[i]->mParent->mName);
for (unsigned j = 0; j < bones.Size(); ++j)
{
if (bones[j].name_ == parentName)
{
bones[i].parentIndex_ = j;
break;
}
}
}
outModel->SetSkeleton(skeleton);
if (model.bones_.Size() > maxBones_)
outModel->SetGeometryBoneMappings(allBoneMappings);
}
File outFile(context_);
if (!outFile.Open(model.outName_, FILE_WRITE))
ErrorExit("Could not open output file " + model.outName_);
outModel->Save(outFile);
// If exporting materials, also save material list for use by the editor
if (!noMaterials_ && saveMaterialList_)
{
String materialListName = ReplaceExtension(model.outName_, ".txt");
File listFile(context_);
if (listFile.Open(materialListName, FILE_WRITE))
{
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
listFile.WriteLine(GetMeshMaterialName(model.meshes_[i]));
}
else
PrintLine("Warning: could not write material list file " + materialListName);
}
}
void BuildAndSaveAnimations(OutModel* model)
{
// extrapolate anim
ExtrapolatePivotlessAnimation(model);
// build and save anim
const PODVector<aiAnimation*>& animations = model ? model->animations_ : sceneAnimations_;
for (unsigned i = 0; i < animations.Size(); ++i)
{
aiAnimation* anim = animations[i];
auto duration = (float)anim->mDuration;
String animName = FromAIString(anim->mName);
String animOutName;
float thisImportEndTime = importEndTime_;
float thisImportStartTime = importStartTime_;
// If no animation split specified, set the end time to duration
if (thisImportEndTime == 0.0f)
thisImportEndTime = duration;
if (animName.Empty())
animName = "Anim" + String(i + 1);
if (model)
animOutName = GetPath(model->outName_) + GetFileName(model->outName_) + "_" + SanitateAssetName(animName) + ".ani";
else
animOutName = outPath_ + GetFileName(outName_) + "_" + SanitateAssetName(animName) + ".ani";
auto ticksPerSecond = (float)anim->mTicksPerSecond;
// If ticks per second not specified, it's probably a .X file. In this case use the default tick rate
if (ticksPerSecond < M_EPSILON)
ticksPerSecond = defaultTicksPerSecond_;
float tickConversion = 1.0f / ticksPerSecond;
// Find out the start time of animation from each channel's first keyframe for adjusting the keyframe times
// to start from zero
float startTime = duration;
for (unsigned j = 0; j < anim->mNumChannels; ++j)
{
aiNodeAnim* channel = anim->mChannels[j];
if (channel->mNumPositionKeys > 0)
startTime = Min(startTime, (float)channel->mPositionKeys[0].mTime);
if (channel->mNumRotationKeys > 0)
startTime = Min(startTime, (float)channel->mRotationKeys[0].mTime);
if (channel->mNumScalingKeys > 0)
startTime = Min(startTime, (float)channel->mScalingKeys[0].mTime);
}
if (startTime > thisImportStartTime)
thisImportStartTime = startTime;
duration = thisImportEndTime - thisImportStartTime;
SharedPtr<Animation> outAnim(new Animation(context_));
outAnim->SetAnimationName(animName);
outAnim->SetLength(duration * tickConversion);
PrintLine("Writing animation " + animName + " length " + String(outAnim->GetLength()));
for (unsigned j = 0; j < anim->mNumChannels; ++j)
{
aiNodeAnim* channel = anim->mChannels[j];
String channelName = FromAIString(channel->mNodeName);
aiNode* boneNode = nullptr;
if (model)
{
unsigned boneIndex;
unsigned pos = channelName.Find("_$AssimpFbx$");
if (!suppressFbxPivotNodes_ || pos == String::NPOS)
{
boneIndex = GetBoneIndex(*model, channelName);
if (boneIndex == M_MAX_UNSIGNED)
{
PrintLine("Warning: skipping animation track " + channelName + " not found in model skeleton");
outAnim->RemoveTrack(channelName);
continue;
}
boneNode = model->bones_[boneIndex];
}
else
{
channelName = channelName.Substring(0, pos);
// every first $fbx animation channel for a bone will consolidate other $fbx animation to a single channel
// skip subsequent $fbx animation channel for the same bone
if (outAnim->GetTrack(channelName) != nullptr)
continue;
boneIndex = GetPivotlessBoneIndex(*model, channelName);
if (boneIndex == M_MAX_UNSIGNED)
{
PrintLine("Warning: skipping animation track " + channelName + " not found in model skeleton");
outAnim->RemoveTrack(channelName);
continue;
}
boneNode = model->pivotlessBones_[boneIndex];
}
}
else
{
boneNode = GetNode(channelName, scene_->mRootNode);
if (!boneNode)
{
PrintLine("Warning: skipping animation track " + channelName + " whose scene node was not found");
outAnim->RemoveTrack(channelName);
continue;
}
}
// To export single frame animation, check if first key frame is identical to bone transformation
aiVector3D bonePos, boneScale;
aiQuaternion boneRot;
boneNode->mTransformation.Decompose(boneScale, boneRot, bonePos);
bool posEqual = true;
bool scaleEqual = true;
bool rotEqual = true;
if (channel->mNumPositionKeys > 0 && !ToVector3(bonePos).Equals(ToVector3(channel->mPositionKeys[0].mValue)))
posEqual = false;
if (channel->mNumScalingKeys > 0 && !ToVector3(boneScale).Equals(ToVector3(channel->mScalingKeys[0].mValue)))
scaleEqual = false;
if (channel->mNumRotationKeys > 0 && !ToQuaternion(boneRot).Equals(ToQuaternion(channel->mRotationKeys[0].mValue)))
rotEqual = false;
AnimationTrack* track = outAnim->CreateTrack(channelName);
// Check which channels are used
track->channelMask_ = CHANNEL_NONE;
if (channel->mNumPositionKeys > 1 || !posEqual)
track->channelMask_ |= CHANNEL_POSITION;
if (channel->mNumRotationKeys > 1 || !rotEqual)
track->channelMask_ |= CHANNEL_ROTATION;
if (channel->mNumScalingKeys > 1 || !scaleEqual)
track->channelMask_ |= CHANNEL_SCALE;
// Check for redundant identity scale in all keyframes and remove in that case
if (track->channelMask_ & CHANNEL_SCALE)
{
bool redundantScale = true;
for (unsigned k = 0; k < channel->mNumScalingKeys; ++k)
{
float SCALE_EPSILON = 0.000001f;
Vector3 scaleVec = ToVector3(channel->mScalingKeys[k].mValue);
if (fabsf(scaleVec.x_ - 1.0f) >= SCALE_EPSILON || fabsf(scaleVec.y_ - 1.0f) >= SCALE_EPSILON ||
fabsf(scaleVec.z_ - 1.0f) >= SCALE_EPSILON)
{
redundantScale = false;
break;
}
}
if (redundantScale)
track->channelMask_ &= ~CHANNEL_SCALE;
}
if (!track->channelMask_)
{
PrintLine("Warning: skipping animation track " + channelName + " with no keyframes");
outAnim->RemoveTrack(channelName);
continue;
}
// Currently only same amount of keyframes is supported
// Note: should also check the times of individual keyframes for match
if ((channel->mNumPositionKeys > 1 && channel->mNumRotationKeys > 1 && channel->mNumPositionKeys != channel->mNumRotationKeys) ||
(channel->mNumPositionKeys > 1 && channel->mNumScalingKeys > 1 && channel->mNumPositionKeys != channel->mNumScalingKeys) ||
(channel->mNumRotationKeys > 1 && channel->mNumScalingKeys > 1 && channel->mNumRotationKeys != channel->mNumScalingKeys))
{
PrintLine("Warning: differing amounts of channel keyframes, skipping animation track " + channelName);
outAnim->RemoveTrack(channelName);
continue;
}
unsigned keyFrames = channel->mNumPositionKeys;
if (channel->mNumRotationKeys > keyFrames)
keyFrames = channel->mNumRotationKeys;
if (channel->mNumScalingKeys > keyFrames)
keyFrames = channel->mNumScalingKeys;
for (unsigned k = 0; k < keyFrames; ++k)
{
AnimationKeyFrame kf;
kf.time_ = 0.0f;
kf.position_ = Vector3::ZERO;
kf.rotation_ = Quaternion::IDENTITY;
kf.scale_ = Vector3::ONE;
// Get time for the keyframe. Adjust with animation's start time
if (track->channelMask_ & CHANNEL_POSITION && k < channel->mNumPositionKeys)
kf.time_ = ((float)channel->mPositionKeys[k].mTime - startTime);
else if (track->channelMask_ & CHANNEL_ROTATION && k < channel->mNumRotationKeys)
kf.time_ = ((float)channel->mRotationKeys[k].mTime - startTime);
else if (track->channelMask_ & CHANNEL_SCALE && k < channel->mNumScalingKeys)
kf.time_ = ((float)channel->mScalingKeys[k].mTime - startTime);
// Make sure time stays positive
kf.time_ = Max(kf.time_, 0.0f);
// Start with the bone's base transform
aiMatrix4x4 boneTransform = boneNode->mTransformation;
aiVector3D pos, scale;
aiQuaternion rot;
boneTransform.Decompose(scale, rot, pos);
// Then apply the active channels
if (track->channelMask_ & CHANNEL_POSITION && k < channel->mNumPositionKeys)
pos = channel->mPositionKeys[k].mValue;
if (track->channelMask_ & CHANNEL_ROTATION && k < channel->mNumRotationKeys)
rot = channel->mRotationKeys[k].mValue;
if (track->channelMask_ & CHANNEL_SCALE && k < channel->mNumScalingKeys)
scale = channel->mScalingKeys[k].mValue;
// If root bone, transform with nodes in between model root node (if any)
if (model && boneNode == model->rootBone_)
{
aiMatrix4x4 transMat, scaleMat, rotMat;
aiMatrix4x4::Translation(pos, transMat);
aiMatrix4x4::Scaling(scale, scaleMat);
rotMat = aiMatrix4x4(rot.GetMatrix());
aiMatrix4x4 tform = transMat * rotMat * scaleMat;
aiMatrix4x4 tformOld = tform;
tform = GetDerivedTransform(tform, boneNode, model->rootNode_, false);
// Do not decompose if did not actually change
if (tform != tformOld)
tform.Decompose(scale, rot, pos);
}
if (track->channelMask_ & CHANNEL_POSITION)
kf.position_ = ToVector3(pos);
if (track->channelMask_ & CHANNEL_ROTATION)
kf.rotation_ = ToQuaternion(rot);
if (track->channelMask_ & CHANNEL_SCALE)
kf.scale_ = ToVector3(scale);
if (kf.time_ >= thisImportStartTime && kf.time_ <= thisImportEndTime)
{
kf.time_ = (kf.time_ - thisImportStartTime) * tickConversion;
track->keyFrames_.Push(kf);
}
}
}
File outFile(context_);
if (!outFile.Open(animOutName, FILE_WRITE))
ErrorExit("Could not open output file " + animOutName);
outAnim->Save(outFile);
}
}
void ExportScene(const String& outName, bool asPrefab)
{
OutScene outScene;
outScene.outName_ = outName;
outScene.rootNode_ = rootNode_;
if (useSubdirs_)
context_->GetSubsystem<FileSystem>()->CreateDir(resourcePath_ + "Models");
CollectSceneModels(outScene, rootNode_);
// Save models, their material lists and animations
for (unsigned i = 0; i < outScene.models_.Size(); ++i)
BuildAndSaveModel(outScene.models_[i]);
// Save scene-global animations
if (!noAnimations_)
{
CollectAnimations();
BuildAndSaveAnimations();
}
// Save scene
BuildAndSaveScene(outScene, asPrefab);
}
void CollectSceneModels(OutScene& scene, aiNode* node)
{
Vector<Pair<aiNode*, aiMesh*> > meshes;
GetMeshesUnderNode(meshes, node);
if (meshes.Size())
{
OutModel model;
model.rootNode_ = node;
model.outName_ = resourcePath_ + (useSubdirs_ ? "Models/" : "") + SanitateAssetName(FromAIString(node->mName)) + ".mdl";
for (unsigned i = 0; i < meshes.Size(); ++i)
{
aiMesh* mesh = meshes[i].second_;
unsigned meshIndex = GetMeshIndex(mesh);
model.meshIndices_.Insert(meshIndex);
model.meshes_.Push(mesh);
model.meshNodes_.Push(meshes[i].first_);
model.totalVertices_ += mesh->mNumVertices;
model.totalIndices_ += GetNumValidFaces(mesh) * 3;
}
// Check if a model with identical mesh indices already exists. If yes, do not export twice
bool unique = true;
if (checkUniqueModel_)
{
for (unsigned i = 0; i < scene.models_.Size(); ++i)
{
if (scene.models_[i].meshIndices_ == model.meshIndices_)
{
PrintLine("Added node " + FromAIString(node->mName));
scene.nodes_.Push(node);
scene.nodeModelIndices_.Push(i);
unique = false;
break;
}
}
}
if (unique)
{
PrintLine("Added model " + model.outName_);
PrintLine("Added node " + FromAIString(node->mName));
CollectBones(model);
BuildBoneCollisionInfo(model);
if (!noAnimations_)
{
CollectAnimations(&model);
BuildAndSaveAnimations(&model);
}
scene.models_.Push(model);
scene.nodes_.Push(node);
scene.nodeModelIndices_.Push(scene.models_.Size() - 1);
}
}
for (unsigned i = 0; i < node->mNumChildren; ++i)
CollectSceneModels(scene, node->mChildren[i]);
}
void CreateHierarchy(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping)
{
CreateSceneNode(scene, srcNode, nodeMapping);
for (unsigned i = 0; i < srcNode->mNumChildren; ++i)
CreateHierarchy(scene, srcNode->mChildren[i], nodeMapping);
}
Node* CreateSceneNode(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping)
{
if (nodeMapping.Contains(srcNode))
return nodeMapping[srcNode];
// Flatten hierarchy if requested
if (noHierarchy_)
{
Node* outNode = scene->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED);
Vector3 pos, scale;
Quaternion rot;
GetPosRotScale(GetDerivedTransform(srcNode, rootNode_), pos, rot, scale);
outNode->SetTransform(pos, rot, scale);
nodeMapping[srcNode] = outNode;
return outNode;
}
if (srcNode == rootNode_ || !srcNode->mParent)
{
Node* outNode = scene->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED);
Vector3 pos, scale;
Quaternion rot;
GetPosRotScale(srcNode->mTransformation, pos, rot, scale);
outNode->SetTransform(pos, rot, scale);
nodeMapping[srcNode] = outNode;
return outNode;
}
else
{
// Ensure the existence of the parent chain as in the original file
if (!nodeMapping.Contains(srcNode->mParent))
CreateSceneNode(scene, srcNode->mParent, nodeMapping);
Node* parent = nodeMapping[srcNode->mParent];
Node* outNode = parent->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED);
Vector3 pos, scale;
Quaternion rot;
GetPosRotScale(srcNode->mTransformation, pos, rot, scale);
outNode->SetTransform(pos, rot, scale);
nodeMapping[srcNode] = outNode;
return outNode;
}
}
void BuildAndSaveScene(OutScene& scene, bool asPrefab)
{
if (!asPrefab)
PrintLine("Writing scene");
else
PrintLine("Writing node hierarchy");
SharedPtr<Scene> outScene(new Scene(context_));
if (!asPrefab)
{
#ifdef URHO3D_PHYSICS
/// \todo Make the physics properties configurable
outScene->CreateComponent<PhysicsWorld>();
#endif
/// \todo Make the octree properties configurable, or detect from the scene contents
outScene->CreateComponent<Octree>();
outScene->CreateComponent<DebugRenderer>();
if (createZone_)
{
Node* zoneNode = outScene->CreateChild("Zone", localIDs_ ? LOCAL : REPLICATED);
auto* zone = zoneNode->CreateComponent<Zone>();
zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.f));
zone->SetAmbientColor(Color(0.25f, 0.25f, 0.25f));
// Create default light only if scene does not define them
if (!scene_->HasLights())
{
Node* lightNode = outScene->CreateChild("GlobalLight", localIDs_ ? LOCAL : REPLICATED);
auto* light = lightNode->CreateComponent<Light>();
light->SetLightType(LIGHT_DIRECTIONAL);
lightNode->SetRotation(Quaternion(60.0f, 30.0f, 0.0f));
}
}
}
auto* cache = context_->GetSubsystem<ResourceCache>();
HashMap<aiNode*, Node*> nodeMapping;
Node* outRootNode = nullptr;
if (asPrefab)
outRootNode = CreateSceneNode(outScene, rootNode_, nodeMapping);
else
{
// If not saving as a prefab, associate the root node with the scene first to prevent unnecessary creation of a root
// However do not do that if the root node does not have an identity matrix, or itself contains a model
// (models at the Urho scene root are not preferable)
if (ToMatrix3x4(rootNode_->mTransformation).Equals(Matrix3x4::IDENTITY) && !scene.nodes_.Contains(rootNode_))
nodeMapping[rootNode_] = outScene;
}
// If is allowed to export empty nodes, export the full Assimp node hierarchy first
if (!noHierarchy_ && !noEmptyNodes_)
CreateHierarchy(outScene, rootNode_, nodeMapping);
// Create geometry nodes
for (unsigned i = 0; i < scene.nodes_.Size(); ++i)
{
const OutModel& model = scene.models_[scene.nodeModelIndices_[i]];
Node* modelNode = CreateSceneNode(outScene, scene.nodes_[i], nodeMapping);
auto* staticModel =
static_cast<StaticModel*>(
model.bones_.Empty() ? modelNode->CreateComponent<StaticModel>() : modelNode->CreateComponent<AnimatedModel>());
// Create a dummy model so that the reference can be stored
String modelName = (useSubdirs_ ? "Models/" : "") + GetFileNameAndExtension(model.outName_);
if (!cache->Exists(modelName))
{
auto* dummyModel = new Model(context_);
dummyModel->SetName(modelName);
dummyModel->SetNumGeometries(model.meshes_.Size());
cache->AddManualResource(dummyModel);
}
staticModel->SetModel(cache->GetResource<Model>(modelName));
// Set materials if they are known
for (unsigned j = 0; j < model.meshes_.Size(); ++j)
{
String matName = GetMeshMaterialName(model.meshes_[j]);
// Create a dummy material so that the reference can be stored
if (!cache->Exists(matName))
{
auto* dummyMat = new Material(context_);
dummyMat->SetName(matName);
cache->AddManualResource(dummyMat);
}
staticModel->SetMaterial(j, cache->GetResource<Material>(matName));
}
}
// Create lights
if (!asPrefab)
{
for (unsigned i = 0; i < scene_->mNumLights; ++i)
{
aiLight* light = scene_->mLights[i];
aiNode* lightNode = GetNode(FromAIString(light->mName), rootNode_, true);
if (!lightNode)
continue;
Node* outNode = CreateSceneNode(outScene, lightNode, nodeMapping);
Vector3 lightAdjustPosition = ToVector3(light->mPosition);
Vector3 lightAdjustDirection = ToVector3(light->mDirection);
// If light is not aligned at the scene node, an adjustment node needs to be created
if (!lightAdjustPosition.Equals(Vector3::ZERO) || (light->mType != aiLightSource_POINT &&
!lightAdjustDirection.Equals(Vector3::FORWARD)))
{
outNode = outNode->CreateChild("LightAdjust");
outNode->SetPosition(lightAdjustPosition);
outNode->SetDirection(lightAdjustDirection);
}
auto* outLight = outNode->CreateComponent<Light>();
outLight->SetColor(Color(light->mColorDiffuse.r, light->mColorDiffuse.g, light->mColorDiffuse.b));
switch (light->mType)
{
case aiLightSource_DIRECTIONAL:
outLight->SetLightType(LIGHT_DIRECTIONAL);
break;
case aiLightSource_SPOT:
outLight->SetLightType(LIGHT_SPOT);
outLight->SetFov(light->mAngleOuterCone * 0.5f * M_RADTODEG);
break;
case aiLightSource_POINT:
outLight->SetLightType(LIGHT_POINT);
break;
default:
break;
}
// Calculate range from attenuation parameters so that light intensity has been reduced to 10% at that distance
if (light->mType != aiLightSource_DIRECTIONAL)
{
float a = light->mAttenuationQuadratic;
float b = light->mAttenuationLinear;
float c = -10.0f;
if (!Equals(a, 0.0f))
{
float root1 = (-b + sqrtf(b * b - 4.0f * a * c)) / (2.0f * a);
float root2 = (-b - sqrtf(b * b - 4.0f * a * c)) / (2.0f * a);
outLight->SetRange(Max(root1, root2));
}
else if (!Equals(b, 0.0f))
outLight->SetRange(-c / b);
}
}
}
File file(context_);
if (!file.Open(scene.outName_, FILE_WRITE))
ErrorExit("Could not open output file " + scene.outName_);
if (!asPrefab)
{
if (saveBinary_)
outScene->Save(file);
else if (saveJson_)
outScene->SaveJSON(file);
else
outScene->SaveXML(file);
}
else
{
if (saveBinary_)
outRootNode->Save(file);
else if (saveJson_)
outRootNode->SaveJSON(file);
else
outRootNode->SaveXML(file);
}
}
void ExportMaterials(HashSet<String>& usedTextures)
{
if (useSubdirs_)
context_->GetSubsystem<FileSystem>()->CreateDir(resourcePath_ + "Materials");
for (unsigned i = 0; i < scene_->mNumMaterials; ++i)
BuildAndSaveMaterial(scene_->mMaterials[i], usedTextures);
}
void BuildAndSaveMaterial(aiMaterial* material, HashSet<String>& usedTextures)
{
aiString matNameStr;
material->Get(AI_MATKEY_NAME, matNameStr);
String matName = SanitateAssetName(FromAIString(matNameStr));
if (matName.Trimmed().Empty())
matName = GenerateMaterialName(material);
// Do not actually create a material instance, but instead craft an xml file manually
XMLFile outMaterial(context_);
XMLElement materialElem = outMaterial.CreateRoot("material");
String diffuseTexName;
String normalTexName;
String specularTexName;
String lightmapTexName;
String emissiveTexName;
Color diffuseColor = Color::WHITE;
Color specularColor;
Color emissiveColor = Color::BLACK;
bool hasAlpha = false;
bool twoSided = false;
float specPower = 1.0f;
aiString stringVal;
float floatVal;
int intVal;
aiColor3D colorVal;
if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), stringVal) == AI_SUCCESS)
diffuseTexName = GetFileNameAndExtension(FromAIString(stringVal));
if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_NORMALS, 0), stringVal) == AI_SUCCESS)
normalTexName = GetFileNameAndExtension(FromAIString(stringVal));
if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_SPECULAR, 0), stringVal) == AI_SUCCESS)
specularTexName = GetFileNameAndExtension(FromAIString(stringVal));
if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_LIGHTMAP, 0), stringVal) == AI_SUCCESS)
lightmapTexName = GetFileNameAndExtension(FromAIString(stringVal));
if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_EMISSIVE, 0), stringVal) == AI_SUCCESS)
emissiveTexName = GetFileNameAndExtension(FromAIString(stringVal));
if (!noMaterialDiffuseColor_)
{
if (material->Get(AI_MATKEY_COLOR_DIFFUSE, colorVal) == AI_SUCCESS)
diffuseColor = Color(colorVal.r, colorVal.g, colorVal.b);
}
if (material->Get(AI_MATKEY_COLOR_SPECULAR, colorVal) == AI_SUCCESS)
specularColor = Color(colorVal.r, colorVal.g, colorVal.b);
if (!emissiveAO_)
{
if (material->Get(AI_MATKEY_COLOR_EMISSIVE, colorVal) == AI_SUCCESS)
emissiveColor = Color(colorVal.r, colorVal.g, colorVal.b);
}
if (material->Get(AI_MATKEY_OPACITY, floatVal) == AI_SUCCESS)
{
/// \hack New Assimp behavior - some materials may return 0 opacity, which is invisible.
/// Revert to full opacity in that case
if (floatVal < M_EPSILON)
floatVal = 1.0f;
if (floatVal < 1.0f)
hasAlpha = true;
diffuseColor.a_ = floatVal;
}
if (material->Get(AI_MATKEY_SHININESS, floatVal) == AI_SUCCESS)
specPower = floatVal;
if (material->Get(AI_MATKEY_TWOSIDED, intVal) == AI_SUCCESS)
twoSided = (intVal != 0);
String techniqueName = "Techniques/NoTexture";
if (!diffuseTexName.Empty())
{
techniqueName = "Techniques/Diff";
if (!normalTexName.Empty())
techniqueName += "Normal";
if (!specularTexName.Empty())
techniqueName += "Spec";
// For now lightmap does not coexist with normal & specular
if (normalTexName.Empty() && specularTexName.Empty() && !lightmapTexName.Empty())
techniqueName += "LightMap";
if (lightmapTexName.Empty() && !emissiveTexName.Empty())
techniqueName += emissiveAO_ ? "AO" : "Emissive";
}
if (hasAlpha)
techniqueName += "Alpha";
XMLElement techniqueElem = materialElem.CreateChild("technique");
techniqueElem.SetString("name", techniqueName + ".xml");
if (!diffuseTexName.Empty())
{
XMLElement diffuseElem = materialElem.CreateChild("texture");
diffuseElem.SetString("unit", "diffuse");
diffuseElem.SetString("name", GetMaterialTextureName(diffuseTexName));
usedTextures.Insert(diffuseTexName);
}
if (!normalTexName.Empty())
{
XMLElement normalElem = materialElem.CreateChild("texture");
normalElem.SetString("unit", "normal");
normalElem.SetString("name", GetMaterialTextureName(normalTexName));
usedTextures.Insert(normalTexName);
}
if (!specularTexName.Empty())
{
XMLElement specularElem = materialElem.CreateChild("texture");
specularElem.SetString("unit", "specular");
specularElem.SetString("name", GetMaterialTextureName(specularTexName));
usedTextures.Insert(specularTexName);
}
if (!lightmapTexName.Empty())
{
XMLElement lightmapElem = materialElem.CreateChild("texture");
lightmapElem.SetString("unit", "emissive");
lightmapElem.SetString("name", GetMaterialTextureName(lightmapTexName));
usedTextures.Insert(lightmapTexName);
}
if (!emissiveTexName.Empty())
{
XMLElement emissiveElem = materialElem.CreateChild("texture");
emissiveElem.SetString("unit", "emissive");
emissiveElem.SetString("name", GetMaterialTextureName(emissiveTexName));
usedTextures.Insert(emissiveTexName);
}
XMLElement diffuseColorElem = materialElem.CreateChild("parameter");
diffuseColorElem.SetString("name", "MatDiffColor");
diffuseColorElem.SetColor("value", diffuseColor);
XMLElement specularElem = materialElem.CreateChild("parameter");
specularElem.SetString("name", "MatSpecColor");
specularElem.SetVector4("value", Vector4(specularColor.r_, specularColor.g_, specularColor.b_, specPower));
XMLElement emissiveColorElem = materialElem.CreateChild("parameter");
emissiveColorElem.SetString("name", "MatEmissiveColor");
emissiveColorElem.SetColor("value", emissiveColor);
if (twoSided)
{
XMLElement cullElem = materialElem.CreateChild("cull");
XMLElement shadowCullElem = materialElem.CreateChild("shadowcull");
cullElem.SetString("value", "none");
shadowCullElem.SetString("value", "none");
}
auto* fileSystem = context_->GetSubsystem<FileSystem>();
String outFileName = resourcePath_ + (useSubdirs_ ? "Materials/" : "" ) + matName + ".xml";
if (noOverwriteMaterial_ && fileSystem->FileExists(outFileName))
{
PrintLine("Skipping save of existing material " + matName);
return;
}
PrintLine("Writing material " + matName);
File outFile(context_);
if (!outFile.Open(outFileName, FILE_WRITE))
ErrorExit("Could not open output file " + outFileName);
outMaterial.Save(outFile);
}
void CopyTextures(const HashSet<String>& usedTextures, const String& sourcePath)
{
auto* fileSystem = context_->GetSubsystem<FileSystem>();
if (useSubdirs_)
fileSystem->CreateDir(resourcePath_ + "Textures");
for (HashSet<String>::ConstIterator i = usedTextures.Begin(); i != usedTextures.End(); ++i)
{
// Handle assimp embedded textures
if (i->Length() && i->At(0) == '*')
{
unsigned texIndex = ToInt(i->Substring(1));
if (texIndex >= scene_->mNumTextures)
PrintLine("Skipping out of range texture index " + String(texIndex));
else
{
aiTexture* tex = scene_->mTextures[texIndex];
String fullDestName = resourcePath_ + GenerateTextureName(texIndex);
bool destExists = fileSystem->FileExists(fullDestName);
if (destExists && noOverwriteTexture_)
{
PrintLine("Skipping copy of existing embedded texture " + GetFileNameAndExtension(fullDestName));
continue;
}
// Encoded texture
if (!tex->mHeight)
{
PrintLine("Saving embedded texture " + GetFileNameAndExtension(fullDestName));
File dest(context_, fullDestName, FILE_WRITE);
dest.Write((const void*)tex->pcData, tex->mWidth);
}
// RGBA8 texture
else
{
PrintLine("Saving embedded RGBA texture " + GetFileNameAndExtension(fullDestName));
Image image(context_);
image.SetSize(tex->mWidth, tex->mHeight, 4);
memcpy(image.GetData(), (const void*)tex->pcData, (size_t)tex->mWidth * tex->mHeight * 4);
image.SavePNG(fullDestName);
}
}
}
else
{
String fullSourceName = sourcePath + *i;
String fullDestName = resourcePath_ + (useSubdirs_ ? "Textures/" : "") + *i;
if (!fileSystem->FileExists(fullSourceName))
{
PrintLine("Skipping copy of nonexisting material texture " + *i);
continue;
}
{
File test(context_, fullSourceName);
if (!test.GetSize())
{
PrintLine("Skipping copy of zero-size material texture " + *i);
continue;
}
}
bool destExists = fileSystem->FileExists(fullDestName);
if (destExists && noOverwriteTexture_)
{
PrintLine("Skipping copy of existing texture " + *i);
continue;
}
if (destExists && noOverwriteNewerTexture_ && fileSystem->GetLastModifiedTime(fullDestName) >
fileSystem->GetLastModifiedTime(fullSourceName))
{
PrintLine("Skipping copying of material texture " + *i + ", destination is newer");
continue;
}
PrintLine("Copying material texture " + *i);
fileSystem->Copy(fullSourceName, fullDestName);
}
}
}
void CombineLods(const PODVector<float>& lodDistances, const Vector<String>& modelNames, const String& outName)
{
// Load models
Vector<SharedPtr<Model> > srcModels;
for (unsigned i = 0; i < modelNames.Size(); ++i)
{
PrintLine("Reading LOD level " + String(i) + ": model " + modelNames[i] + " distance " + String(lodDistances[i]));
File srcFile(context_);
srcFile.Open(modelNames[i]);
SharedPtr<Model> srcModel(new Model(context_));
if (!srcModel->Load(srcFile))
ErrorExit("Could not load input model " + modelNames[i]);
srcModels.Push(srcModel);
}
// Check that none of the models already has LOD levels
for (unsigned i = 0; i < srcModels.Size(); ++i)
{
for (unsigned j = 0; j < srcModels[i]->GetNumGeometries(); ++j)
{
if (srcModels[i]->GetNumGeometryLodLevels(j) > 1)
ErrorExit(modelNames[i] + " already has multiple LOD levels defined");
}
}
// Check for number of geometries (need to have same amount for now)
for (unsigned i = 1; i < srcModels.Size(); ++i)
{
if (srcModels[i]->GetNumGeometries() != srcModels[0]->GetNumGeometries())
ErrorExit(modelNames[i] + " has different amount of geometries than " + modelNames[0]);
}
// If there are bones, check for compatibility (need to have exact match for now)
for (unsigned i = 1; i < srcModels.Size(); ++i)
{
if (srcModels[i]->GetSkeleton().GetNumBones() != srcModels[0]->GetSkeleton().GetNumBones())
ErrorExit(modelNames[i] + " has different amount of bones than " + modelNames[0]);
for (unsigned j = 0; j < srcModels[0]->GetSkeleton().GetNumBones(); ++j)
{
if (srcModels[i]->GetSkeleton().GetBone(j)->name_ != srcModels[0]->GetSkeleton().GetBone(j)->name_)
ErrorExit(modelNames[i] + " has different bones than " + modelNames[0]);
}
if (srcModels[i]->GetGeometryBoneMappings() != srcModels[0]->GetGeometryBoneMappings())
ErrorExit(modelNames[i] + " has different per-geometry bone mappings than " + modelNames[0]);
}
Vector<SharedPtr<VertexBuffer> > vbVector;
Vector<SharedPtr<IndexBuffer> > ibVector;
PODVector<unsigned> emptyMorphRange;
// Create the final model
SharedPtr<Model> outModel(new Model(context_));
outModel->SetNumGeometries(srcModels[0]->GetNumGeometries());
for (unsigned i = 0; i < srcModels[0]->GetNumGeometries(); ++i)
{
outModel->SetNumGeometryLodLevels(i, srcModels.Size());
for (unsigned j = 0; j < srcModels.Size(); ++j)
{
Geometry* geometry = srcModels[j]->GetGeometry(i, 0);
geometry->SetLodDistance(lodDistances[j]);
outModel->SetGeometry(i, j, geometry);
for (unsigned k = 0; k < geometry->GetNumVertexBuffers(); ++k)
{
SharedPtr<VertexBuffer> vb(geometry->GetVertexBuffer(k));
if (!vbVector.Contains(vb))
vbVector.Push(vb);
}
SharedPtr<IndexBuffer> ib(geometry->GetIndexBuffer());
if (!ibVector.Contains(ib))
ibVector.Push(ib);
}
}
outModel->SetVertexBuffers(vbVector, emptyMorphRange, emptyMorphRange);
outModel->SetIndexBuffers(ibVector);
outModel->SetSkeleton(srcModels[0]->GetSkeleton());
outModel->SetGeometryBoneMappings(srcModels[0]->GetGeometryBoneMappings());
outModel->SetBoundingBox(srcModels[0]->GetBoundingBox());
/// \todo Vertex morphs are ignored for now
// Save the final model
PrintLine("Writing output model");
File outFile(context_);
if (!outFile.Open(outName, FILE_WRITE))
ErrorExit("Could not open output file " + outName);
outModel->Save(outFile);
}
void GetMeshesUnderNode(Vector<Pair<aiNode*, aiMesh*> >& dest, aiNode* node)
{
for (unsigned i = 0; i < node->mNumMeshes; ++i)
dest.Push(MakePair(node, scene_->mMeshes[node->mMeshes[i]]));
}
unsigned GetMeshIndex(aiMesh* mesh)
{
for (unsigned i = 0; i < scene_->mNumMeshes; ++i)
{
if (scene_->mMeshes[i] == mesh)
return i;
}
return M_MAX_UNSIGNED;
}
unsigned GetBoneIndex(OutModel& model, const String& boneName)
{
for (unsigned i = 0; i < model.bones_.Size(); ++i)
{
if (boneName == model.bones_[i]->mName.data)
return i;
}
return M_MAX_UNSIGNED;
}
aiBone* GetMeshBone(OutModel& model, const String& boneName)
{
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
aiMesh* mesh = model.meshes_[i];
for (unsigned j = 0; j < mesh->mNumBones; ++j)
{
aiBone* bone = mesh->mBones[j];
if (boneName == bone->mName.data)
return bone;
}
}
return nullptr;
}
Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName)
{
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
aiMesh* mesh = model.meshes_[i];
aiNode* node = model.meshNodes_[i];
for (unsigned j = 0; j < mesh->mNumBones; ++j)
{
aiBone* bone = mesh->mBones[j];
if (boneName == bone->mName.data)
{
aiMatrix4x4 offset = bone->mOffsetMatrix;
aiMatrix4x4 nodeDerivedInverse = GetMeshBakingTransform(node, model.rootNode_);
nodeDerivedInverse.Inverse();
offset *= nodeDerivedInverse;
return ToMatrix3x4(offset);
}
}
}
// Fallback for rigid skinning for which actual offset matrix information doesn't exist
for (unsigned i = 0; i < model.meshes_.Size(); ++i)
{
aiMesh* mesh = model.meshes_[i];
aiNode* node = model.meshNodes_[i];
if (!mesh->HasBones() && boneName == node->mName.data)
{
aiMatrix4x4 nodeDerivedInverse = GetMeshBakingTransform(node, model.rootNode_);
nodeDerivedInverse.Inverse();
return ToMatrix3x4(nodeDerivedInverse);
}
}
return Matrix3x4::IDENTITY;
}
void GetBlendData(OutModel& model, aiMesh* mesh, aiNode* meshNode, PODVector<unsigned>& boneMappings, Vector<PODVector<unsigned char> >&
blendIndices, Vector<PODVector<float> >& blendWeights)
{
blendIndices.Resize(mesh->mNumVertices);
blendWeights.Resize(mesh->mNumVertices);
boneMappings.Clear();
// If model has more bones than can fit vertex shader parameters, write the per-geometry mappings
if (model.bones_.Size() > maxBones_)
{
if (mesh->mNumBones > maxBones_)
{
ErrorExit(
"Geometry (submesh) has over " + String(maxBones_) + " bone influences. Try splitting to more submeshes\n"
"that each stay at " + String(maxBones_) + " bones or below."
);
}
if (mesh->mNumBones > 0)
{
boneMappings.Resize(mesh->mNumBones);
for (unsigned i = 0; i < mesh->mNumBones; ++i)
{
aiBone* bone = mesh->mBones[i];
String boneName = FromAIString(bone->mName);
unsigned globalIndex = GetBoneIndex(model, boneName);
if (globalIndex == M_MAX_UNSIGNED)
ErrorExit("Bone " + boneName + " not found");
boneMappings[i] = globalIndex;
for (unsigned j = 0; j < bone->mNumWeights; ++j)
{
unsigned vertex = bone->mWeights[j].mVertexId;
blendIndices[vertex].Push(i);
blendWeights[vertex].Push(bone->mWeights[j].mWeight);
}
}
}
else
{
// If mesh does not have skinning information, implement rigid skinning so that it stays compatible with AnimatedModel
String boneName = FromAIString(meshNode->mName);
unsigned globalIndex = GetBoneIndex(model, boneName);
if (globalIndex == M_MAX_UNSIGNED)
PrintLine("Warning: bone " + boneName + " not found, skipping rigid skinning");
else
{
boneMappings.Push(globalIndex);
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
{
blendIndices[i].Push(0);
blendWeights[i].Push(1.0f);
}
}
}
}
else
{
if (mesh->mNumBones > 0)
{
for (unsigned i = 0; i < mesh->mNumBones; ++i)
{
aiBone* bone = mesh->mBones[i];
String boneName = FromAIString(bone->mName);
unsigned globalIndex = GetBoneIndex(model, boneName);
if (globalIndex == M_MAX_UNSIGNED)
ErrorExit("Bone " + boneName + " not found");
for (unsigned j = 0; j < bone->mNumWeights; ++j)
{
unsigned vertex = bone->mWeights[j].mVertexId;
blendIndices[vertex].Push(globalIndex);
blendWeights[vertex].Push(bone->mWeights[j].mWeight);
}
}
}
else
{
String boneName = FromAIString(meshNode->mName);
unsigned globalIndex = GetBoneIndex(model, boneName);
if (globalIndex == M_MAX_UNSIGNED)
PrintLine("Warning: bone " + boneName + " not found, skipping rigid skinning");
else
{
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
{
blendIndices[i].Push(globalIndex);
blendWeights[i].Push(1.0f);
}
}
}
}
// Normalize weights now if necessary, also remove too many influences
for (unsigned i = 0; i < blendWeights.Size(); ++i)
{
if (blendWeights[i].Size() > 4)
{
PrintLine("Warning: more than 4 bone influences in vertex " + String(i));
while (blendWeights[i].Size() > 4)
{
unsigned lowestIndex = 0;
float lowest = M_INFINITY;
for (unsigned j = 0; j < blendWeights[i].Size(); ++j)
{
if (blendWeights[i][j] < lowest)
{
lowest = blendWeights[i][j];
lowestIndex = j;
}
}
blendWeights[i].Erase(lowestIndex);
blendIndices[i].Erase(lowestIndex);
}
}
float sum = 0.0f;
for (unsigned j = 0; j < blendWeights[i].Size(); ++j)
sum += blendWeights[i][j];
if (sum != 1.0f && sum != 0.0f)
{
for (unsigned j = 0; j < blendWeights[i].Size(); ++j)
blendWeights[i][j] /= sum;
}
}
}
String GetMeshMaterialName(aiMesh* mesh)
{
aiMaterial* material = scene_->mMaterials[mesh->mMaterialIndex];
aiString matNameStr;
material->Get(AI_MATKEY_NAME, matNameStr);
String matName = SanitateAssetName(FromAIString(matNameStr));
if (matName.Trimmed().Empty())
matName = GenerateMaterialName(material);
return (useSubdirs_ ? "Materials/" : "") + matName + ".xml";
}
String GenerateMaterialName(aiMaterial* material)
{
for (unsigned i = 0; i < scene_->mNumMaterials; ++i)
{
if (scene_->mMaterials[i] == material)
return inputName_ + "_Material" + String(i);
}
// Should not go here
return String::EMPTY;
}
String GetMaterialTextureName(const String& nameIn)
{
// Detect assimp embedded texture
if (nameIn.Length() && nameIn[0] == '*')
return GenerateTextureName(ToInt(nameIn.Substring(1)));
else
return (useSubdirs_ ? "Textures/" : "") + nameIn;
}
String GenerateTextureName(unsigned texIndex)
{
if (texIndex < scene_->mNumTextures)
{
// If embedded texture contains encoded data, use the format hint for file extension. Else save RGBA8 data as PNG
aiTexture* tex = scene_->mTextures[texIndex];
if (!tex->mHeight)
return (useSubdirs_ ? "Textures/" : "") + inputName_ + "_Texture" + String(texIndex) + "." + tex->achFormatHint;
else
return (useSubdirs_ ? "Textures/" : "") + inputName_ + "_Texture" + String(texIndex) + ".png";
}
// Should not go here
return String::EMPTY;
}
unsigned GetNumValidFaces(aiMesh* mesh)
{
unsigned ret = 0;
for (unsigned j = 0; j < mesh->mNumFaces; ++j)
{
if (mesh->mFaces[j].mNumIndices == 3)
++ret;
}
return ret;
}
void WriteShortIndices(unsigned short*& dest, aiMesh* mesh, unsigned index, unsigned offset)
{
if (mesh->mFaces[index].mNumIndices == 3)
{
*dest++ = mesh->mFaces[index].mIndices[0] + offset;
*dest++ = mesh->mFaces[index].mIndices[1] + offset;
*dest++ = mesh->mFaces[index].mIndices[2] + offset;
}
}
void WriteLargeIndices(unsigned*& dest, aiMesh* mesh, unsigned index, unsigned offset)
{
if (mesh->mFaces[index].mNumIndices == 3)
{
*dest++ = mesh->mFaces[index].mIndices[0] + offset;
*dest++ = mesh->mFaces[index].mIndices[1] + offset;
*dest++ = mesh->mFaces[index].mIndices[2] + offset;
}
}
void WriteVertex(float*& dest, aiMesh* mesh, unsigned index, bool isSkinned, BoundingBox& box,
const Matrix3x4& vertexTransform, const Matrix3& normalTransform, Vector<PODVector<unsigned char> >& blendIndices,
Vector<PODVector<float> >& blendWeights)
{
Vector3 vertex = vertexTransform * ToVector3(mesh->mVertices[index]);
box.Merge(vertex);
*dest++ = vertex.x_;
*dest++ = vertex.y_;
*dest++ = vertex.z_;
if (mesh->HasNormals())
{
Vector3 normal = normalTransform * ToVector3(mesh->mNormals[index]);
*dest++ = normal.x_;
*dest++ = normal.y_;
*dest++ = normal.z_;
}
for (unsigned i = 0; i < mesh->GetNumColorChannels() && i < MAX_CHANNELS; ++i)
{
*((unsigned*)dest) = Color(mesh->mColors[i][index].r, mesh->mColors[i][index].g, mesh->mColors[i][index].b,
mesh->mColors[i][index].a).ToUInt();
++dest;
}
for (unsigned i = 0; i < mesh->GetNumUVChannels() && i < MAX_CHANNELS; ++i)
{
Vector3 texCoord = ToVector3(mesh->mTextureCoords[i][index]);
*dest++ = texCoord.x_;
*dest++ = texCoord.y_;
}
if (mesh->HasTangentsAndBitangents())
{
Vector3 tangent = normalTransform * ToVector3(mesh->mTangents[index]);
Vector3 normal = normalTransform * ToVector3(mesh->mNormals[index]);
Vector3 bitangent = normalTransform * ToVector3(mesh->mBitangents[index]);
// Check handedness
float w = 1.0f;
if ((tangent.CrossProduct(normal)).DotProduct(bitangent) < 0.5f)
w = -1.0f;
*dest++ = tangent.x_;
*dest++ = tangent.y_;
*dest++ = tangent.z_;
*dest++ = w;
}
if (isSkinned)
{
for (unsigned i = 0; i < 4; ++i)
{
if (i < blendWeights[index].Size())
*dest++ = blendWeights[index][i];
else
*dest++ = 0.0f;
}
auto* destBytes = (unsigned char*)dest;
++dest;
for (unsigned i = 0; i < 4; ++i)
{
if (i < blendIndices[index].Size())
*destBytes++ = blendIndices[index][i];
else
*destBytes++ = 0;
}
}
}
PODVector<VertexElement> GetVertexElements(aiMesh* mesh, bool isSkinned)
{
PODVector<VertexElement> ret;
// Position must always be first and of type Vector3 for raycasts to work
ret.Push(VertexElement(TYPE_VECTOR3, SEM_POSITION));
if (mesh->HasNormals())
ret.Push(VertexElement(TYPE_VECTOR3, SEM_NORMAL));
for (unsigned i = 0; i < mesh->GetNumColorChannels() && i < MAX_CHANNELS; ++i)
ret.Push(VertexElement(TYPE_UBYTE4_NORM, SEM_COLOR, i));
/// \todo Assimp mesh structure can specify 3D UV-coords. How to determine the difference? For now always treated as 2D.
for (unsigned i = 0; i < mesh->GetNumUVChannels() && i < MAX_CHANNELS; ++i)
ret.Push(VertexElement(TYPE_VECTOR2, SEM_TEXCOORD, i));
if (mesh->HasTangentsAndBitangents())
ret.Push(VertexElement(TYPE_VECTOR4, SEM_TANGENT));
if (isSkinned)
{
ret.Push(VertexElement(TYPE_VECTOR4, SEM_BLENDWEIGHTS));
ret.Push(VertexElement(TYPE_UBYTE4, SEM_BLENDINDICES));
}
return ret;
}
aiNode* GetNode(const String& name, aiNode* rootNode, bool caseSensitive)
{
if (!rootNode)
return nullptr;
if (!name.Compare(rootNode->mName.data, caseSensitive))
return rootNode;
for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
{
aiNode* found = GetNode(name, rootNode->mChildren[i], caseSensitive);
if (found)
return found;
}
return nullptr;
}
aiMatrix4x4 GetDerivedTransform(aiNode* node, aiNode* rootNode, bool rootInclusive)
{
return GetDerivedTransform(node->mTransformation, node, rootNode, rootInclusive);
}
aiMatrix4x4 GetDerivedTransform(aiMatrix4x4 transform, aiNode* node, aiNode* rootNode, bool rootInclusive)
{
// If basenode is defined, go only up to it in the parent chain
while (node && node != rootNode)
{
node = node->mParent;
if (!rootInclusive && node == rootNode)
break;
if (node)
transform = node->mTransformation * transform;
}
return transform;
}
aiMatrix4x4 GetMeshBakingTransform(aiNode* meshNode, aiNode* modelRootNode)
{
if (meshNode == modelRootNode)
return {};
else
return GetDerivedTransform(meshNode, modelRootNode);
}
void GetPosRotScale(const aiMatrix4x4& transform, Vector3& pos, Quaternion& rot, Vector3& scale)
{
aiVector3D aiPos;
aiQuaternion aiRot;
aiVector3D aiScale;
transform.Decompose(aiScale, aiRot, aiPos);
pos = ToVector3(aiPos);
rot = ToQuaternion(aiRot);
scale = ToVector3(aiScale);
}
String FromAIString(const aiString& str)
{
return String(str.data);
}
Vector3 ToVector3(const aiVector3D& vec)
{
return Vector3(vec.x, vec.y, vec.z);
}
Vector2 ToVector2(const aiVector2D& vec)
{
return Vector2(vec.x, vec.y);
}
Quaternion ToQuaternion(const aiQuaternion& quat)
{
return Quaternion(quat.w, quat.x, quat.y, quat.z);
}
Matrix3x4 ToMatrix3x4(const aiMatrix4x4& mat)
{
Matrix3x4 ret;
memcpy(&ret.m00_, &mat.a1, sizeof(Matrix3x4));
return ret;
}
aiMatrix4x4 ToAIMatrix4x4(const Matrix3x4& mat)
{
aiMatrix4x4 ret;
memcpy(&ret.a1, &mat.m00_, sizeof(Matrix3x4));
return ret;
}
String SanitateAssetName(const String& name)
{
String fixedName = name;
fixedName.Replace("<", "");
fixedName.Replace(">", "");
fixedName.Replace("?", "");
fixedName.Replace("*", "");
fixedName.Replace(":", "");
fixedName.Replace("\"", "");
fixedName.Replace("/", "");
fixedName.Replace("\\", "");
fixedName.Replace("|", "");
return fixedName;
}
unsigned GetPivotlessBoneIndex(OutModel& model, const String& boneName)
{
for (unsigned i = 0; i < model.pivotlessBones_.Size(); ++i)
{
if (boneName == model.pivotlessBones_[i]->mName.data)
return i;
}
return M_MAX_UNSIGNED;
}
void FillChainTransforms(OutModel &model, aiMatrix4x4 *chain, const String& mainBoneName)
{
for (unsigned j = 0; j < TransformationComp_MAXIMUM; ++j)
{
String transfBoneName = mainBoneName + "_$AssimpFbx$_" + String(transformSuffix[j]);
for (unsigned k = 0; k < model.bones_.Size(); ++k)
{
String boneName = String(model.bones_[k]->mName.data);
if (boneName == transfBoneName)
{
chain[j] = model.bones_[k]->mTransformation;
break;
}
}
}
}
void ExpandAnimatedChannelKeys(aiAnimation* anim, unsigned mainChannel, const int *channelIndices)
{
aiNodeAnim* channel = anim->mChannels[mainChannel];
unsigned int poskeyFrames = channel->mNumPositionKeys;
unsigned int rotkeyFrames = channel->mNumRotationKeys;
unsigned int scalekeyFrames = channel->mNumScalingKeys;
// Get max key frames
for (unsigned i = 0; i < TransformationComp_MAXIMUM; ++i)
{
if (channelIndices[i] != -1 && channelIndices[i] != mainChannel)
{
aiNodeAnim* channel2 = anim->mChannels[channelIndices[i]];
if (channel2->mNumPositionKeys > poskeyFrames)
poskeyFrames = channel2->mNumPositionKeys;
if (channel2->mNumRotationKeys > rotkeyFrames)
rotkeyFrames = channel2->mNumRotationKeys;
if (channel2->mNumScalingKeys > scalekeyFrames)
scalekeyFrames = channel2->mNumScalingKeys;
}
}
// Resize and init vector key array
if (poskeyFrames > channel->mNumPositionKeys)
{
auto* newKeys = new aiVectorKey[poskeyFrames];
for (unsigned i = 0; i < poskeyFrames; ++i)
{
if (i < channel->mNumPositionKeys )
newKeys[i] = aiVectorKey(channel->mPositionKeys[i].mTime, channel->mPositionKeys[i].mValue);
else
newKeys[i].mValue = aiVector3D(0.0f, 0.0f, 0.0f);
}
delete[] channel->mPositionKeys;
channel->mPositionKeys = newKeys;
channel->mNumPositionKeys = poskeyFrames;
}
if (rotkeyFrames > channel->mNumRotationKeys)
{
auto* newKeys = new aiQuatKey[rotkeyFrames];
for (unsigned i = 0; i < rotkeyFrames; ++i)
{
if (i < channel->mNumRotationKeys)
newKeys[i] = aiQuatKey(channel->mRotationKeys[i].mTime, channel->mRotationKeys[i].mValue);
else
newKeys[i].mValue = aiQuaternion();
}
delete[] channel->mRotationKeys;
channel->mRotationKeys = newKeys;
channel->mNumRotationKeys = rotkeyFrames;
}
if (scalekeyFrames > channel->mNumScalingKeys)
{
auto* newKeys = new aiVectorKey[scalekeyFrames];
for (unsigned i = 0; i < scalekeyFrames; ++i)
{
if ( i < channel->mNumScalingKeys)
newKeys[i] = aiVectorKey(channel->mScalingKeys[i].mTime, channel->mScalingKeys[i].mValue);
else
newKeys[i].mValue = aiVector3D(1.0f, 1.0f, 1.0f);
}
delete[] channel->mScalingKeys;
channel->mScalingKeys = newKeys;
channel->mNumScalingKeys = scalekeyFrames;
}
}
void InitAnimatedChainTransformIndices(aiAnimation* anim, unsigned mainChannel, const String& mainBoneName, int *channelIndices)
{
int numTransforms = 0;
for (unsigned j = 0; j < TransformationComp_MAXIMUM; ++j)
{
String transfBoneName = mainBoneName + "_$AssimpFbx$_" + String(transformSuffix[j]);
channelIndices[j] = -1;
for (unsigned k = 0; k < anim->mNumChannels; ++k)
{
aiNodeAnim* channel = anim->mChannels[k];
String channelName = FromAIString(channel->mNodeName);
if (channelName == transfBoneName)
{
++numTransforms;
channelIndices[j] = k;
break;
}
}
}
// resize animated channel key size
if (numTransforms > 1)
ExpandAnimatedChannelKeys(anim, mainChannel, channelIndices);
}
void CreatePivotlessFbxBoneStruct(OutModel &model)
{
// Init
model.pivotlessBones_.Clear();
aiMatrix4x4 chains[TransformationComp_MAXIMUM];
for (unsigned i = 0; i < model.bones_.Size(); ++i)
{
String mainBoneName = String(model.bones_[i]->mName.data);
// Skip $fbx nodes
if (mainBoneName.Find("$AssimpFbx$") != String::NPOS)
continue;
std::fill_n(chains, static_cast<unsigned int>(TransformationComp_MAXIMUM), aiMatrix4x4());
FillChainTransforms(model, &chains[0], mainBoneName);
// Calculate chained transform
aiMatrix4x4 finalTransform;
for (const auto& chain : chains)
finalTransform = finalTransform * chain;
// New bone node
auto*pnode = new aiNode;
pnode->mName = model.bones_[i]->mName;
pnode->mTransformation = finalTransform * model.bones_[i]->mTransformation;
model.pivotlessBones_.Push(pnode);
}
}
void ExtrapolatePivotlessAnimation(OutModel* model)
{
if (suppressFbxPivotNodes_ && model)
{
PrintLine("Suppressing $fbx nodes");
// Construct new bone structure from suppressed $fbx pivot nodes
CreatePivotlessFbxBoneStruct(*model);
// Extrapolate anim
const PODVector<aiAnimation *> &animations = model->animations_;
for (unsigned i = 0; i < animations.Size(); ++i)
{
aiAnimation* anim = animations[i];
Vector<String> mainBoneCompleteList;
mainBoneCompleteList.Clear();
for (unsigned j = 0; j < anim->mNumChannels; ++j)
{
aiNodeAnim* channel = anim->mChannels[j];
String channelName = FromAIString(channel->mNodeName);
unsigned pos = channelName.Find("_$AssimpFbx$");
if (pos != String::NPOS)
{
// Every first $fbx animation channel for a bone will consolidate other $fbx animation to a single channel
// skip subsequent $fbx animation channel for the same bone
String mainBoneName = channelName.Substring(0, pos);
if (mainBoneCompleteList.Find(mainBoneName) != mainBoneCompleteList.End())
continue;
mainBoneCompleteList.Push(mainBoneName);
unsigned boneIdx = GetBoneIndex(*model, mainBoneName);
// This condition exists if a geometry, not a bone, has a key animation
if (boneIdx == M_MAX_UNSIGNED)
continue;
// Init chain indices and fill transforms
aiMatrix4x4 mainboneTransform = model->bones_[boneIdx]->mTransformation;
aiMatrix4x4 chain[TransformationComp_MAXIMUM];
int channelIndices[TransformationComp_MAXIMUM];
InitAnimatedChainTransformIndices(anim, j, mainBoneName, &channelIndices[0]);
std::fill_n(chain, static_cast<unsigned int>(TransformationComp_MAXIMUM), aiMatrix4x4());
FillChainTransforms(*model, &chain[0], mainBoneName);
unsigned keyFrames = channel->mNumPositionKeys;
if (channel->mNumRotationKeys > keyFrames)
keyFrames = channel->mNumRotationKeys;
if (channel->mNumScalingKeys > keyFrames)
keyFrames = channel->mNumScalingKeys;
for (unsigned k = 0; k < keyFrames; ++k)
{
double frameTime = 0.0;
aiMatrix4x4 finalTransform;
// Chain transform animated values
for (unsigned l = 0; l < TransformationComp_MAXIMUM; ++l)
{
// It's either the chain transform or animation channel transform
if (channelIndices[l] != -1)
{
aiMatrix4x4 animtform, tempMat;
aiNodeAnim* animchannel = anim->mChannels[channelIndices[l]];
if (k < animchannel->mNumPositionKeys)
{
aiMatrix4x4::Translation(animchannel->mPositionKeys[k].mValue, tempMat);
animtform = animtform * tempMat;
frameTime = Max(animchannel->mPositionKeys[k].mTime, frameTime);
}
if (k < animchannel->mNumRotationKeys)
{
tempMat = aiMatrix4x4(animchannel->mRotationKeys[k].mValue.GetMatrix());
animtform = animtform * tempMat;
frameTime = Max(animchannel->mRotationKeys[k].mTime, frameTime);
}
if (k < animchannel->mNumScalingKeys)
{
aiMatrix4x4::Scaling(animchannel->mScalingKeys[k].mValue, tempMat);
animtform = animtform * tempMat;
frameTime = Max(animchannel->mScalingKeys[k].mTime, frameTime);
}
finalTransform = finalTransform * animtform;
}
else
finalTransform = finalTransform * chain[l];
}
aiVector3D animPos, animScale;
aiQuaternion animRot;
finalTransform = finalTransform * mainboneTransform;
finalTransform.Decompose(animScale, animRot, animPos);
// New values
if (k < channel->mNumPositionKeys)
{
channel->mPositionKeys[k].mValue = animPos;
channel->mPositionKeys[k].mTime = frameTime;
}
if (k < channel->mNumRotationKeys)
{
channel->mRotationKeys[k].mValue = animRot;
channel->mRotationKeys[k].mTime = frameTime;
}
if (k < channel->mNumScalingKeys)
{
channel->mScalingKeys[k].mValue = animScale;
channel->mScalingKeys[k].mTime = frameTime;
}
}
}
}
}
}
}
void CollectSceneNodesAsBones(OutModel &model, aiNode* rootNode)
{
if (!rootNode)
return;
model.bones_.Push(rootNode);
for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
{
CollectSceneNodesAsBones(model, rootNode->mChildren[i]);
}
}