1 //
2 // Copyright (c) 2008-2017 the Urho3D project.
3 //
4 // Permission is hereby granted, free of charge, to any person obtaining a copy
5 // of this software and associated documentation files (the "Software"), to deal
6 // in the Software without restriction, including without limitation the rights
7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 // copies of the Software, and to permit persons to whom the Software is
9 // furnished to do so, subject to the following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included in
12 // all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 // THE SOFTWARE.
21 //
22
23 #include <Urho3D/Core/Context.h>
24 #include <Urho3D/Core/ProcessUtils.h>
25 #include <Urho3D/Core/StringUtils.h>
26 #include <Urho3D/Core/WorkQueue.h>
27 #include <Urho3D/Graphics/AnimatedModel.h>
28 #include <Urho3D/Graphics/Animation.h>
29 #include <Urho3D/Graphics/DebugRenderer.h>
30 #include <Urho3D/Graphics/Geometry.h>
31 #include <Urho3D/Graphics/Graphics.h>
32 #include <Urho3D/Graphics/IndexBuffer.h>
33 #include <Urho3D/Graphics/Light.h>
34 #include <Urho3D/Graphics/Material.h>
35 #include <Urho3D/Graphics/Octree.h>
36 #include <Urho3D/Graphics/VertexBuffer.h>
37 #include <Urho3D/Graphics/Zone.h>
38 #include <Urho3D/IO/File.h>
39 #include <Urho3D/IO/FileSystem.h>
40 #ifdef URHO3D_PHYSICS
41 #include <Urho3D/Physics/PhysicsWorld.h>
42 #endif
43 #include <Urho3D/Resource/ResourceCache.h>
44 #include <Urho3D/Resource/XMLFile.h>
45 #include <Urho3D/Scene/Scene.h>
46
47 #ifdef WIN32
48 #include <windows.h>
49 #endif
50
51 #include <assimp/config.h>
52 #include <assimp/cimport.h>
53 #include <assimp/scene.h>
54 #include <assimp/postprocess.h>
55 #include <assimp/DefaultLogger.hpp>
56
57 #include <Urho3D/DebugNew.h>
58
59 using namespace Urho3D;
60
61 struct OutModel
62 {
OutModelOutModel63 OutModel() :
64 rootBone_(0),
65 totalVertices_(0),
66 totalIndices_(0)
67 {
68 }
69
70 String outName_;
71 aiNode* rootNode_;
72 HashSet<unsigned> meshIndices_;
73 PODVector<aiMesh*> meshes_;
74 PODVector<aiNode*> meshNodes_;
75 PODVector<aiNode*> bones_;
76 PODVector<aiNode*> pivotlessBones_;
77 PODVector<aiAnimation*> animations_;
78 PODVector<float> boneRadii_;
79 PODVector<BoundingBox> boneHitboxes_;
80 aiNode* rootBone_;
81 unsigned totalVertices_;
82 unsigned totalIndices_;
83 };
84
85 struct OutScene
86 {
87 String outName_;
88 aiNode* rootNode_;
89 Vector<OutModel> models_;
90 PODVector<aiNode*> nodes_;
91 PODVector<unsigned> nodeModelIndices_;
92 };
93
94 // FBX transform chain
95 enum TransformationComp
96 {
97 TransformationComp_Translation = 0,
98 TransformationComp_RotationOffset,
99 TransformationComp_RotationPivot,
100 TransformationComp_PreRotation,
101 TransformationComp_Rotation,
102 TransformationComp_PostRotation,
103 TransformationComp_RotationPivotInverse,
104
105 TransformationComp_ScalingOffset,
106 TransformationComp_ScalingPivot,
107 TransformationComp_Scaling,
108
109 // Not checking these
110 // They are typically flushed out in the fbxconverter, but there
111 // might be cases where they're not, hence, leaving them.
112 #ifdef EXT_TRANSFORMATION_CHECK
113 TransformationComp_ScalingPivotInverse,
114 TransformationComp_GeometricTranslation,
115 TransformationComp_GeometricRotation,
116 TransformationComp_GeometricScaling,
117 #endif
118
119 TransformationComp_MAXIMUM
120 };
121
122 const char *transformSuffix[TransformationComp_MAXIMUM] =
123 {
124 "Translation", // TransformationComp_Translation = 0,
125 "RotationOffset", // TransformationComp_RotationOffset,
126 "RotationPivot", // TransformationComp_RotationPivot,
127 "PreRotation", // TransformationComp_PreRotation,
128 "Rotation", // TransformationComp_Rotation,
129 "PostRotation", // TransformationComp_PostRotation,
130 "RotationPivotInverse", // TransformationComp_RotationPivotInverse,
131
132 "ScalingOffset", // TransformationComp_ScalingOffset,
133 "ScalingPivot", // TransformationComp_ScalingPivot,
134 "Scaling", // TransformationComp_Scaling,
135
136 #ifdef EXT_TRANSFORMATION_CHECK
137 "ScalingPivotInverse", // TransformationComp_ScalingPivotInverse,
138 "GeometricTranslation", // TransformationComp_GeometricTranslation,
139 "GeometricRotation", // TransformationComp_GeometricRotation,
140 "GeometricScaling", // TransformationComp_GeometricScaling,
141 #endif
142 };
143
144 static const unsigned MAX_CHANNELS = 4;
145
146 SharedPtr<Context> context_(new Context());
147 const aiScene* scene_ = 0;
148 aiNode* rootNode_ = 0;
149 String inputName_;
150 String resourcePath_;
151 String outPath_;
152 String outName_;
153 bool useSubdirs_ = true;
154 bool localIDs_ = false;
155 bool saveBinary_ = false;
156 bool saveJson_ = false;
157 bool createZone_ = true;
158 bool noAnimations_ = false;
159 bool noHierarchy_ = false;
160 bool noMaterials_ = false;
161 bool noTextures_ = false;
162 bool noMaterialDiffuseColor_ = false;
163 bool noEmptyNodes_ = false;
164 bool saveMaterialList_ = false;
165 bool includeNonSkinningBones_ = false;
166 bool verboseLog_ = false;
167 bool emissiveAO_ = false;
168 bool noOverwriteMaterial_ = false;
169 bool noOverwriteTexture_ = false;
170 bool noOverwriteNewerTexture_ = false;
171 bool checkUniqueModel_ = true;
172 bool moveToBindPose_ = false;
173 unsigned maxBones_ = 64;
174 Vector<String> nonSkinningBoneIncludes_;
175 Vector<String> nonSkinningBoneExcludes_;
176
177 HashSet<aiAnimation*> allAnimations_;
178 PODVector<aiAnimation*> sceneAnimations_;
179
180 float defaultTicksPerSecond_ = 4800.0f;
181 // For subset animation import usage
182 float importStartTime_ = 0.0f;
183 float importEndTime_ = 0.0f;
184 bool suppressFbxPivotNodes_ = true;
185
186 int main(int argc, char** argv);
187 void Run(const Vector<String>& arguments);
188 void DumpNodes(aiNode* rootNode, unsigned level);
189
190 void ExportModel(const String& outName, bool animationOnly);
191 void ExportAnimation(const String& outName, bool animationOnly);
192 void CollectMeshes(OutModel& model, aiNode* node);
193 void CollectBones(OutModel& model, bool animationOnly = false);
194 void CollectBonesFinal(PODVector<aiNode*>& dest, const HashSet<aiNode*>& necessary, aiNode* node);
195 void MoveToBindPose(OutModel& model, aiNode* current);
196 void CollectAnimations(OutModel* model = 0);
197 void BuildBoneCollisionInfo(OutModel& model);
198 void BuildAndSaveModel(OutModel& model);
199 void BuildAndSaveAnimations(OutModel* model = 0);
200
201 void ExportScene(const String& outName, bool asPrefab);
202 void CollectSceneModels(OutScene& scene, aiNode* node);
203 void CreateHierarchy(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping);
204 Node* CreateSceneNode(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping);
205 void BuildAndSaveScene(OutScene& scene, bool asPrefab);
206
207 void ExportMaterials(HashSet<String>& usedTextures);
208 void BuildAndSaveMaterial(aiMaterial* material, HashSet<String>& usedTextures);
209 void CopyTextures(const HashSet<String>& usedTextures, const String& sourcePath);
210
211 void CombineLods(const PODVector<float>& lodDistances, const Vector<String>& modelNames, const String& outName);
212
213 void GetMeshesUnderNode(Vector<Pair<aiNode*, aiMesh*> >& meshes, aiNode* node);
214 unsigned GetMeshIndex(aiMesh* mesh);
215 unsigned GetBoneIndex(OutModel& model, const String& boneName);
216 aiBone* GetMeshBone(OutModel& model, const String& boneName);
217 Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName);
218 void GetBlendData(OutModel& model, aiMesh* mesh, aiNode* meshNode, PODVector<unsigned>& boneMappings, Vector<PODVector<unsigned char> >&
219 blendIndices, Vector<PODVector<float> >& blendWeights);
220 String GetMeshMaterialName(aiMesh* mesh);
221 String GetMaterialTextureName(const String& nameIn);
222 String GenerateMaterialName(aiMaterial* material);
223 String GenerateTextureName(unsigned texIndex);
224 unsigned GetNumValidFaces(aiMesh* mesh);
225
226 void WriteShortIndices(unsigned short*& dest, aiMesh* mesh, unsigned index, unsigned offset);
227 void WriteLargeIndices(unsigned*& dest, aiMesh* mesh, unsigned index, unsigned offset);
228 void WriteVertex(float*& dest, aiMesh* mesh, unsigned index, bool isSkinned, BoundingBox& box,
229 const Matrix3x4& vertexTransform, const Matrix3& normalTransform, Vector<PODVector<unsigned char> >& blendIndices,
230 Vector<PODVector<float> >& blendWeights);
231 PODVector<VertexElement> GetVertexElements(aiMesh* mesh, bool isSkinned);
232
233 aiNode* GetNode(const String& name, aiNode* rootNode, bool caseSensitive = true);
234 aiMatrix4x4 GetDerivedTransform(aiNode* node, aiNode* rootNode, bool rootInclusive = true);
235 aiMatrix4x4 GetDerivedTransform(aiMatrix4x4 transform, aiNode* node, aiNode* rootNode, bool rootInclusive = true);
236 aiMatrix4x4 GetMeshBakingTransform(aiNode* meshNode, aiNode* modelRootNode);
237 void GetPosRotScale(const aiMatrix4x4& transform, Vector3& pos, Quaternion& rot, Vector3& scale);
238
239 String FromAIString(const aiString& str);
240 Vector3 ToVector3(const aiVector3D& vec);
241 Vector2 ToVector2(const aiVector2D& vec);
242 Quaternion ToQuaternion(const aiQuaternion& quat);
243 Matrix3x4 ToMatrix3x4(const aiMatrix4x4& mat);
244 aiMatrix4x4 ToAIMatrix4x4(const Matrix3x4& mat);
245 String SanitateAssetName(const String& name);
246
247 unsigned GetPivotlessBoneIndex(OutModel& model, const String& boneName);
248 void ExtrapolatePivotlessAnimation(OutModel* model);
249 void CollectSceneNodesAsBones(OutModel &model, aiNode* rootNode);
250
main(int argc,char ** argv)251 int main(int argc, char** argv)
252 {
253 Vector<String> arguments;
254
255 #ifdef WIN32
256 arguments = ParseArguments(GetCommandLineW());
257 #else
258 arguments = ParseArguments(argc, argv);
259 #endif
260
261 Run(arguments);
262 return 0;
263 }
264
Run(const Vector<String> & arguments)265 void Run(const Vector<String>& arguments)
266 {
267 if (arguments.Size() < 2)
268 {
269 ErrorExit(
270 "Usage: AssetImporter <command> <input file> <output file> [options]\n"
271 "See http://assimp.sourceforge.net/main_features_formats.html for input formats\n\n"
272 "Commands:\n"
273 "model Output a model\n"
274 "anim Output animation(s)\n"
275 "scene Output a scene\n"
276 "node Output a node and its children (prefab)\n"
277 "dump Dump scene node structure. No output file is generated\n"
278 "lod Combine several Urho3D models as LOD levels of the output model\n"
279 " Syntax: lod <dist0> <mdl0> <dist1 <mdl1> ... <output file>\n"
280 "\n"
281 "Options:\n"
282 "-b Save scene in binary format, default format is XML\n"
283 "-j Save scene in JSON format, default format is XML\n"
284 "-h Generate hard instead of smooth normals if input has no normals\n"
285 "-i Use local ID's for scene nodes\n"
286 "-l Output a material list file for models\n"
287 "-na Do not output animations\n"
288 "-nm Do not output materials\n"
289 "-nt Do not output material textures\n"
290 "-nc Do not use material diffuse color value, instead output white\n"
291 "-nh Do not save full node hierarchy (scene mode only)\n"
292 "-ns Do not create subdirectories for resources\n"
293 "-nz Do not create a zone and a directional light (scene mode only)\n"
294 "-nf Do not fix infacing normals\n"
295 "-ne Do not save empty nodes (scene mode only)\n"
296 "-mb <x> Maximum number of bones per submesh. Default 64\n"
297 "-p <path> Set path for scene resources. Default is output file path\n"
298 "-r <name> Use the named scene node as root node\n"
299 "-f <freq> Animation tick frequency to use if unspecified. Default 4800\n"
300 "-o Optimize redundant submeshes. Loses scene hierarchy and animations\n"
301 "-s <filter> Include non-skinning bones in the model's skeleton. Can be given a\n"
302 " case-insensitive semicolon separated filter list. Bone is included\n"
303 " if its name contains any of the filters. Prefix filter with minus\n"
304 " sign to use as an exclude. For example -s \"Bip01;-Dummy;-Helper\"\n"
305 "-t Generate tangents\n"
306 "-v Enable verbose Assimp library logging\n"
307 "-eao Interpret material emissive texture as ambient occlusion\n"
308 "-cm Check and do not overwrite if material exists\n"
309 "-ct Check and do not overwrite if texture exists\n"
310 "-ctn Check and do not overwrite if texture has newer timestamp\n"
311 "-am Export all meshes even if identical (scene mode only)\n"
312 "-bp Move bones to bind pose before saving model\n"
313 "-split <start> <end> (animation model only)\n"
314 " Split animation, will only import from start frame to end frame\n"
315 "-np Do not suppress $fbx pivot nodes (FBX files only)\n"
316 );
317 }
318
319 context_->RegisterSubsystem(new FileSystem(context_));
320 context_->RegisterSubsystem(new ResourceCache(context_));
321 context_->RegisterSubsystem(new WorkQueue(context_));
322 RegisterSceneLibrary(context_);
323 RegisterGraphicsLibrary(context_);
324 #ifdef URHO3D_PHYSICS
325 RegisterPhysicsLibrary(context_);
326 #endif
327
328 String command = arguments[0].ToLower();
329 String rootNodeName;
330
331 unsigned flags =
332 aiProcess_ConvertToLeftHanded |
333 aiProcess_JoinIdenticalVertices |
334 aiProcess_Triangulate |
335 aiProcess_GenSmoothNormals |
336 aiProcess_LimitBoneWeights |
337 aiProcess_ImproveCacheLocality |
338 aiProcess_RemoveRedundantMaterials |
339 aiProcess_FixInfacingNormals |
340 aiProcess_FindInvalidData |
341 aiProcess_GenUVCoords |
342 aiProcess_FindInstances |
343 aiProcess_OptimizeMeshes;
344
345 for (unsigned i = 2; i < arguments.Size(); ++i)
346 {
347 if (arguments[i].Length() > 1 && arguments[i][0] == '-')
348 {
349 String argument = arguments[i].Substring(1).ToLower();
350 String value = i + 1 < arguments.Size() ? arguments[i + 1] : String::EMPTY;
351
352 if (argument == "b")
353 saveBinary_ = true;
354 else if(argument == "j")
355 saveJson_ = true;
356 else if (argument == "h")
357 {
358 flags &= ~aiProcess_GenSmoothNormals;
359 flags |= aiProcess_GenNormals;
360 }
361 else if (argument == "i")
362 localIDs_ = true;
363 else if (argument == "l")
364 saveMaterialList_ = true;
365 else if (argument == "t")
366 flags |= aiProcess_CalcTangentSpace;
367 else if (argument == "o")
368 flags |= aiProcess_PreTransformVertices;
369 else if (argument.Length() == 2 && argument[0] == 'n')
370 {
371 switch (tolower(argument[1]))
372 {
373 case 'a':
374 noAnimations_ = true;
375 break;
376
377 case 'c':
378 noMaterialDiffuseColor_ = true;
379 break;
380
381 case 'm':
382 noMaterials_ = true;
383 break;
384
385 case 'h':
386 noHierarchy_ = true;
387 break;
388
389 case 'e':
390 noEmptyNodes_ = true;
391 break;
392
393 case 's':
394 useSubdirs_ = false;
395 break;
396
397 case 't':
398 noTextures_ = true;
399 break;
400
401 case 'z':
402 createZone_ = false;
403 break;
404
405 case 'f':
406 flags &= ~aiProcess_FixInfacingNormals;
407 break;
408
409 case 'p':
410 suppressFbxPivotNodes_ = false;
411 break;
412
413 }
414 }
415 else if (argument == "mb" && !value.Empty())
416 {
417 maxBones_ = ToUInt(value);
418 if (maxBones_ < 1)
419 maxBones_ = 1;
420 ++i;
421 }
422 else if (argument == "p" && !value.Empty())
423 {
424 resourcePath_ = AddTrailingSlash(value);
425 ++i;
426 }
427 else if (argument == "r" && !value.Empty())
428 {
429 rootNodeName = value;
430 ++i;
431 }
432 else if (argument == "f" && !value.Empty())
433 {
434 defaultTicksPerSecond_ = ToFloat(value);
435 ++i;
436 }
437 else if (argument == "s")
438 {
439 includeNonSkinningBones_ = true;
440 if (value.Length() && (value[0] != '-' || value.Length() > 3))
441 {
442 Vector<String> filters = value.Split(';');
443 for (unsigned i = 0; i < filters.Size(); ++i)
444 {
445 if (filters[i][0] == '-')
446 nonSkinningBoneExcludes_.Push(filters[i].Substring(1));
447 else
448 nonSkinningBoneIncludes_.Push(filters[i]);
449 }
450 }
451 }
452 else if (argument == "v")
453 verboseLog_ = true;
454 else if (argument == "eao")
455 emissiveAO_ = true;
456 else if (argument == "cm")
457 noOverwriteMaterial_ = true;
458 else if (argument == "ct")
459 noOverwriteTexture_ = true;
460 else if (argument == "ctn")
461 noOverwriteNewerTexture_ = true;
462 else if (argument == "am")
463 checkUniqueModel_ = false;
464 else if (argument == "bp")
465 moveToBindPose_ = true;
466 else if (argument == "split")
467 {
468 String value2 = i + 2 < arguments.Size() ? arguments[i + 2] : String::EMPTY;
469 if (value.Length() && value2.Length() && (value[0] != '-') && (value2[0] != '-'))
470 {
471 importStartTime_ = ToFloat(value);
472 importEndTime_ = ToFloat(value2);
473 }
474 }
475 }
476 }
477
478 if (command == "model" || command == "scene" || command == "anim" || command == "node" || command == "dump")
479 {
480 String inFile = arguments[1];
481 String outFile;
482 if (arguments.Size() > 2 && arguments[2][0] != '-')
483 outFile = GetInternalPath(arguments[2]);
484
485 inputName_ = GetFileName(inFile);
486 outName_ = outFile;
487 outPath_ = GetPath(outFile);
488
489 if (resourcePath_.Empty())
490 {
491 resourcePath_ = outPath_;
492 // If output file already has the Models/ path (model mode), do not take it into the resource path
493 if (command == "model")
494 {
495 if (resourcePath_.EndsWith("Models/", false))
496 resourcePath_ = resourcePath_.Substring(0, resourcePath_.Length() - 7);
497 }
498 if (resourcePath_.Empty())
499 resourcePath_ = "./";
500 }
501
502 resourcePath_ = AddTrailingSlash(resourcePath_);
503
504 if (command != "dump" && outFile.Empty())
505 ErrorExit("No output file defined");
506
507 if (verboseLog_)
508 Assimp::DefaultLogger::create("", Assimp::Logger::VERBOSE, aiDefaultLogStream_STDOUT);
509
510 PrintLine("Reading file " + inFile);
511
512 if (!inFile.EndsWith(".fbx", false))
513 suppressFbxPivotNodes_ = false;
514
515 // Only do this for the "model" command. "anim" command extrapolates animation from the original bone definition
516 if (suppressFbxPivotNodes_ && command == "model")
517 {
518 PrintLine("Suppressing $fbx nodes");
519 aiPropertyStore *aiprops = aiCreatePropertyStore();
520 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, 1); //default = true;
521 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS, 0); //default = false;
522 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_MATERIALS, 1); //default = true;
523 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_CAMERAS, 1); //default = true;
524 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_LIGHTS, 1); //default = true;
525 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, 1); //default = true;
526 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_STRICT_MODE, 0); //default = false;
527 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, 0); //**false, default = true;
528 aiSetImportPropertyInteger(aiprops, AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, 1);//default = true;
529
530 scene_ = aiImportFileExWithProperties(GetNativePath(inFile).CString(), flags, NULL, aiprops);
531
532 // prevent processing animation suppression, both cannot work simultaneously
533 suppressFbxPivotNodes_ = false;
534 }
535 else
536 scene_ = aiImportFile(GetNativePath(inFile).CString(), flags);
537
538 if (!scene_)
539 ErrorExit("Could not open or parse input file " + inFile + ": " + String(aiGetErrorString()));
540
541 if (verboseLog_)
542 Assimp::DefaultLogger::kill();
543
544 rootNode_ = scene_->mRootNode;
545 if (!rootNodeName.Empty())
546 {
547 rootNode_ = GetNode(rootNodeName, rootNode_, false);
548 if (!rootNode_)
549 ErrorExit("Could not find scene node " + rootNodeName);
550 }
551
552 if (command == "dump")
553 {
554 DumpNodes(rootNode_, 0);
555 return;
556 }
557
558 if (command == "model")
559 ExportModel(outFile, scene_->mFlags & AI_SCENE_FLAGS_INCOMPLETE);
560
561 if (command == "anim")
562 {
563 noMaterials_ = true;
564 ExportAnimation(outFile, scene_->mFlags & AI_SCENE_FLAGS_INCOMPLETE);
565 }
566 if (command == "scene" || command == "node")
567 {
568 bool asPrefab = command == "node";
569 // Saving as prefab requires the hierarchy, especially the root node
570 if (asPrefab)
571 noHierarchy_ = false;
572 ExportScene(outFile, asPrefab);
573 }
574
575 if (!noMaterials_)
576 {
577 HashSet<String> usedTextures;
578 ExportMaterials(usedTextures);
579 if (!noTextures_)
580 CopyTextures(usedTextures, GetPath(inFile));
581 }
582 }
583 else if (command == "lod")
584 {
585 PODVector<float> lodDistances;
586 Vector<String> modelNames;
587 String outFile;
588
589 unsigned numLodArguments = 0;
590 for (unsigned i = 1; i < arguments.Size(); ++i)
591 {
592 if (arguments[i][0] == '-')
593 break;
594 ++numLodArguments;
595 }
596 if (numLodArguments < 4)
597 ErrorExit("Must define at least 2 LOD levels");
598 if (!(numLodArguments & 1))
599 ErrorExit("No output file defined");
600
601 for (unsigned i = 1; i < numLodArguments + 1; ++i)
602 {
603 if (i == numLodArguments)
604 outFile = GetInternalPath(arguments[i]);
605 else
606 {
607 if (i & 1)
608 lodDistances.Push(Max(ToFloat(arguments[i]), 0.0f));
609 else
610 modelNames.Push(GetInternalPath(arguments[i]));
611 }
612 }
613
614 if (lodDistances[0] != 0.0f)
615 {
616 PrintLine("Warning: first LOD distance forced to 0");
617 lodDistances[0] = 0.0f;
618 }
619
620 CombineLods(lodDistances, modelNames, outFile);
621 }
622 else
623 ErrorExit("Unrecognized command " + command);
624 }
625
DumpNodes(aiNode * rootNode,unsigned level)626 void DumpNodes(aiNode* rootNode, unsigned level)
627 {
628 if (!rootNode)
629 return;
630
631 String indent(' ', level * 2);
632 Vector3 pos, scale;
633 Quaternion rot;
634 aiMatrix4x4 transform = GetDerivedTransform(rootNode, rootNode_);
635 GetPosRotScale(transform, pos, rot, scale);
636
637 PrintLine(indent + "Node " + FromAIString(rootNode->mName) + " pos " + String(pos));
638
639 if (rootNode->mNumMeshes == 1)
640 PrintLine(indent + " " + String(rootNode->mNumMeshes) + " geometry");
641 if (rootNode->mNumMeshes > 1)
642 PrintLine(indent + " " + String(rootNode->mNumMeshes) + " geometries");
643
644 for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
645 DumpNodes(rootNode->mChildren[i], level + 1);
646 }
647
ExportModel(const String & outName,bool animationOnly)648 void ExportModel(const String& outName, bool animationOnly)
649 {
650 if (outName.Empty())
651 ErrorExit("No output file defined");
652
653 OutModel model;
654 model.rootNode_ = rootNode_;
655 model.outName_ = outName;
656
657 CollectMeshes(model, model.rootNode_);
658 CollectBones(model, animationOnly);
659 BuildBoneCollisionInfo(model);
660 BuildAndSaveModel(model);
661 if (!noAnimations_)
662 {
663 CollectAnimations(&model);
664 BuildAndSaveAnimations(&model);
665
666 // Save scene-global animations
667 CollectAnimations();
668 BuildAndSaveAnimations();
669 }
670 }
671
ExportAnimation(const String & outName,bool animationOnly)672 void ExportAnimation(const String& outName, bool animationOnly)
673 {
674 if (outName.Empty())
675 ErrorExit("No output file defined");
676
677 OutModel model;
678 model.rootNode_ = rootNode_;
679 model.outName_ = outName;
680
681 CollectMeshes(model, model.rootNode_);
682 CollectBones(model, animationOnly);
683 BuildBoneCollisionInfo(model);
684 // BuildAndSaveModel(model);
685 if (!noAnimations_)
686 {
687 // Most fbx animation files contain only a skeleton and no skinned mesh.
688 // Assume the scene node contains the model's bone definition and,
689 // transfer the info to the model.
690 if (suppressFbxPivotNodes_ && model.bones_.Size() == 0)
691 CollectSceneNodesAsBones(model, rootNode_);
692
693 CollectAnimations(&model);
694 BuildAndSaveAnimations(&model);
695
696 // Save scene-global animations
697 CollectAnimations();
698 BuildAndSaveAnimations();
699 }
700 }
701
CollectMeshes(OutModel & model,aiNode * node)702 void CollectMeshes(OutModel& model, aiNode* node)
703 {
704 for (unsigned i = 0; i < node->mNumMeshes; ++i)
705 {
706 aiMesh* mesh = scene_->mMeshes[node->mMeshes[i]];
707 for (unsigned j = 0; j < model.meshes_.Size(); ++j)
708 {
709 if (mesh == model.meshes_[j])
710 {
711 PrintLine("Warning: same mesh found multiple times");
712 break;
713 }
714 }
715
716 model.meshIndices_.Insert(node->mMeshes[i]);
717 model.meshes_.Push(mesh);
718 model.meshNodes_.Push(node);
719 model.totalVertices_ += mesh->mNumVertices;
720 model.totalIndices_ += GetNumValidFaces(mesh) * 3;
721 }
722
723 for (unsigned i = 0; i < node->mNumChildren; ++i)
724 CollectMeshes(model, node->mChildren[i]);
725 }
726
CollectBones(OutModel & model,bool animationOnly)727 void CollectBones(OutModel& model, bool animationOnly)
728 {
729 HashSet<aiNode*> necessary;
730 HashSet<aiNode*> rootNodes;
731
732 bool haveSkinnedMeshes = false;
733 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
734 {
735 if (model.meshes_[i]->HasBones())
736 {
737 haveSkinnedMeshes = true;
738 break;
739 }
740 }
741
742 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
743 {
744 aiMesh* mesh = model.meshes_[i];
745 aiNode* meshNode = model.meshNodes_[i];
746 aiNode* meshParentNode = meshNode->mParent;
747 aiNode* rootNode = 0;
748
749 for (unsigned j = 0; j < mesh->mNumBones; ++j)
750 {
751 aiBone* bone = mesh->mBones[j];
752 String boneName(FromAIString(bone->mName));
753 aiNode* boneNode = GetNode(boneName, scene_->mRootNode, true);
754 if (!boneNode)
755 ErrorExit("Could not find scene node for bone " + boneName);
756 necessary.Insert(boneNode);
757 rootNode = boneNode;
758
759 for (;;)
760 {
761 boneNode = boneNode->mParent;
762 if (!boneNode || ((boneNode == meshNode || boneNode == meshParentNode) && !animationOnly))
763 break;
764 rootNode = boneNode;
765 necessary.Insert(boneNode);
766 }
767
768 if (rootNodes.Find(rootNode) == rootNodes.End())
769 rootNodes.Insert(rootNode);
770 }
771
772 // When model is partially skinned, include the attachment nodes of the rigid meshes in the skeleton
773 if (haveSkinnedMeshes && !mesh->mNumBones)
774 {
775 aiNode* boneNode = meshNode;
776 necessary.Insert(boneNode);
777 rootNode = boneNode;
778
779 for (;;)
780 {
781 boneNode = boneNode->mParent;
782 if (!boneNode || ((boneNode == meshNode || boneNode == meshParentNode) && !animationOnly))
783 break;
784 rootNode = boneNode;
785 necessary.Insert(boneNode);
786 }
787
788 if (rootNodes.Find(rootNode) == rootNodes.End())
789 rootNodes.Insert(rootNode);
790 }
791 }
792
793
794 // If we find multiple root nodes, try to remedy by going back in the parent chain and finding a common parent
795 if (rootNodes.Size() > 1)
796 {
797 for (HashSet<aiNode*>::Iterator i = rootNodes.Begin(); i != rootNodes.End(); ++i)
798 {
799 aiNode* commonParent = (*i);
800
801 while (commonParent)
802 {
803 unsigned found = 0;
804 for (HashSet<aiNode*>::Iterator j = rootNodes.Begin(); j != rootNodes.End(); ++j)
805 {
806 if (i == j)
807 continue;
808 aiNode* parent = *j;
809 while (parent)
810 {
811 if (parent == commonParent)
812 {
813 ++found;
814 break;
815 }
816 parent = parent->mParent;
817 }
818 }
819
820 if (found >= rootNodes.Size() - 1)
821 {
822 PrintLine("Multiple roots initially found, using new root node " + FromAIString(commonParent->mName));
823 rootNodes.Clear();
824 rootNodes.Insert(commonParent);
825 necessary.Insert(commonParent);
826 break;
827 }
828
829 commonParent = commonParent->mParent;
830 }
831
832 if (rootNodes.Size() == 1)
833 break; // Succeeded
834 }
835 if (rootNodes.Size() > 1)
836 ErrorExit("Skeleton with multiple root nodes found, not supported");
837 }
838
839 if (rootNodes.Empty())
840 return;
841
842 model.rootBone_ = *rootNodes.Begin();
843
844 // Move the model to bind pose now if requested
845 if (moveToBindPose_)
846 {
847 PrintLine("Moving bones to bind pose");
848 MoveToBindPose(model, model.rootBone_);
849 }
850
851 CollectBonesFinal(model.bones_, necessary, model.rootBone_);
852 // Initialize the bone collision info
853 model.boneRadii_.Resize(model.bones_.Size());
854 model.boneHitboxes_.Resize(model.bones_.Size());
855 for (unsigned i = 0; i < model.bones_.Size(); ++i)
856 {
857 model.boneRadii_[i] = 0.0f;
858 model.boneHitboxes_[i] = BoundingBox(0.0f, 0.0f);
859 }
860 }
861
MoveToBindPose(OutModel & model,aiNode * current)862 void MoveToBindPose(OutModel& model, aiNode* current)
863 {
864 String nodeName(FromAIString(current->mName));
865 Matrix3x4 bindWorldTransform = GetOffsetMatrix(model, nodeName).Inverse();
866 // Skip if we get an identity offset matrix (bone lookup failed)
867 if (!bindWorldTransform.Equals(Matrix3x4::IDENTITY))
868 {
869 if (current->mParent && current != model.rootNode_)
870 {
871 aiMatrix4x4 parentWorldTransform = GetDerivedTransform(current->mParent, model.rootNode_, true);
872 Matrix3x4 parentInverse = ToMatrix3x4(parentWorldTransform).Inverse();
873 current->mTransformation = ToAIMatrix4x4(parentInverse * bindWorldTransform);
874 }
875 else
876 current->mTransformation = ToAIMatrix4x4(bindWorldTransform);
877 }
878
879 for (unsigned i = 0; i < current->mNumChildren; ++i)
880 MoveToBindPose(model, current->mChildren[i]);
881 }
882
CollectBonesFinal(PODVector<aiNode * > & dest,const HashSet<aiNode * > & necessary,aiNode * node)883 void CollectBonesFinal(PODVector<aiNode*>& dest, const HashSet<aiNode*>& necessary, aiNode* node)
884 {
885 bool includeBone = necessary.Find(node) != necessary.End();
886 String boneName = FromAIString(node->mName);
887
888 // Check include/exclude filters for non-skinned bones
889 if (!includeBone && includeNonSkinningBones_)
890 {
891 // If no includes specified, include by default but check for excludes
892 if (nonSkinningBoneIncludes_.Empty())
893 includeBone = true;
894
895 // Check against includes/excludes
896 for (unsigned i = 0; i < nonSkinningBoneIncludes_.Size(); ++i)
897 {
898 if (boneName.Contains(nonSkinningBoneIncludes_[i], false))
899 {
900 includeBone = true;
901 break;
902 }
903 }
904 for (unsigned i = 0; i < nonSkinningBoneExcludes_.Size(); ++i)
905 {
906 if (boneName.Contains(nonSkinningBoneExcludes_[i], false))
907 {
908 includeBone = false;
909 break;
910 }
911 }
912
913 if (includeBone)
914 PrintLine("Including non-skinning bone " + boneName);
915 }
916
917 if (includeBone)
918 dest.Push(node);
919
920 for (unsigned i = 0; i < node->mNumChildren; ++i)
921 CollectBonesFinal(dest, necessary, node->mChildren[i]);
922 }
923
CollectAnimations(OutModel * model)924 void CollectAnimations(OutModel* model)
925 {
926 const aiScene* scene = scene_;
927 for (unsigned i = 0; i < scene->mNumAnimations; ++i)
928 {
929 aiAnimation* anim = scene->mAnimations[i];
930 if (allAnimations_.Contains(anim))
931 continue;
932
933 if (model)
934 {
935 bool modelBoneFound = false;
936 for (unsigned j = 0; j < anim->mNumChannels; ++j)
937 {
938 aiNodeAnim* channel = anim->mChannels[j];
939 String channelName = FromAIString(channel->mNodeName);
940 if (GetBoneIndex(*model, channelName) != M_MAX_UNSIGNED)
941 {
942 modelBoneFound = true;
943 break;
944 }
945 }
946 if (modelBoneFound)
947 {
948 model->animations_.Push(anim);
949 allAnimations_.Insert(anim);
950 }
951 }
952 else
953 {
954 sceneAnimations_.Push(anim);
955 allAnimations_.Insert(anim);
956 }
957 }
958
959 /// \todo Vertex morphs are ignored for now
960 }
961
BuildBoneCollisionInfo(OutModel & model)962 void BuildBoneCollisionInfo(OutModel& model)
963 {
964 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
965 {
966 aiMesh* mesh = model.meshes_[i];
967 for (unsigned j = 0; j < mesh->mNumBones; ++j)
968 {
969 aiBone* bone = mesh->mBones[j];
970 String boneName = FromAIString(bone->mName);
971 unsigned boneIndex = GetBoneIndex(model, boneName);
972 if (boneIndex == M_MAX_UNSIGNED)
973 continue;
974 for (unsigned k = 0; k < bone->mNumWeights; ++k)
975 {
976 float weight = bone->mWeights[k].mWeight;
977 // Require skinning weight to be sufficiently large before vertex contributes to bone hitbox
978 if (weight > 0.33f)
979 {
980 aiVector3D vertexBoneSpace = bone->mOffsetMatrix * mesh->mVertices[bone->mWeights[k].mVertexId];
981 Vector3 vertex = ToVector3(vertexBoneSpace);
982 float radius = vertex.Length();
983 if (radius > model.boneRadii_[boneIndex])
984 model.boneRadii_[boneIndex] = radius;
985 model.boneHitboxes_[boneIndex].Merge(vertex);
986 }
987 }
988 }
989 }
990 }
991
BuildAndSaveModel(OutModel & model)992 void BuildAndSaveModel(OutModel& model)
993 {
994 if (!model.rootNode_)
995 {
996 PrintLine("Null root node for model, skipping model save");
997 return;
998 }
999
1000 String rootNodeName = FromAIString(model.rootNode_->mName);
1001 if (!model.meshes_.Size())
1002 {
1003 PrintLine("No geometries found starting from node " + rootNodeName + ", skipping model save");
1004 return;
1005 }
1006
1007 PrintLine("Writing model " + rootNodeName);
1008
1009 SharedPtr<Model> outModel(new Model(context_));
1010 Vector<PODVector<unsigned> > allBoneMappings;
1011 BoundingBox box;
1012
1013 unsigned numValidGeometries = 0;
1014
1015 bool combineBuffers = true;
1016 // Check if buffers can be combined (same vertex elements, under 65535 vertices)
1017 PODVector<VertexElement> elements = GetVertexElements(model.meshes_[0], model.bones_.Size() > 0);
1018 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
1019 {
1020 if (GetNumValidFaces(model.meshes_[i]))
1021 {
1022 ++numValidGeometries;
1023 if (i > 0 && GetVertexElements(model.meshes_[i], model.bones_.Size() > 0) != elements)
1024 combineBuffers = false;
1025 }
1026 }
1027
1028 // Check if keeping separate buffers allows to avoid 32-bit indices
1029 if (combineBuffers && model.totalVertices_ > 65535)
1030 {
1031 bool allUnder65k = true;
1032 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
1033 {
1034 if (GetNumValidFaces(model.meshes_[i]))
1035 {
1036 if (model.meshes_[i]->mNumVertices > 65535)
1037 allUnder65k = false;
1038 }
1039 }
1040 if (allUnder65k == true)
1041 combineBuffers = false;
1042 }
1043
1044 SharedPtr<IndexBuffer> ib;
1045 SharedPtr<VertexBuffer> vb;
1046 Vector<SharedPtr<VertexBuffer> > vbVector;
1047 Vector<SharedPtr<IndexBuffer> > ibVector;
1048 unsigned startVertexOffset = 0;
1049 unsigned startIndexOffset = 0;
1050 unsigned destGeomIndex = 0;
1051 bool isSkinned = model.bones_.Size() > 0;
1052
1053 outModel->SetNumGeometries(numValidGeometries);
1054
1055 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
1056 {
1057 aiMesh* mesh = model.meshes_[i];
1058 PODVector<VertexElement> elements = GetVertexElements(mesh, isSkinned);
1059 unsigned validFaces = GetNumValidFaces(mesh);
1060 if (!validFaces)
1061 continue;
1062
1063 bool largeIndices;
1064 if (combineBuffers)
1065 largeIndices = model.totalIndices_ > 65535;
1066 else
1067 largeIndices = mesh->mNumVertices > 65535;
1068
1069 // Create new buffers if necessary
1070 if (!combineBuffers || vbVector.Empty())
1071 {
1072 vb = new VertexBuffer(context_);
1073 ib = new IndexBuffer(context_);
1074
1075 if (combineBuffers)
1076 {
1077 ib->SetSize(model.totalIndices_, largeIndices);
1078 vb->SetSize(model.totalVertices_, elements);
1079 }
1080 else
1081 {
1082 ib->SetSize(validFaces * 3, largeIndices);
1083 vb->SetSize(mesh->mNumVertices, elements);
1084 }
1085
1086 vbVector.Push(vb);
1087 ibVector.Push(ib);
1088 startVertexOffset = 0;
1089 startIndexOffset = 0;
1090 }
1091
1092 // Get the world transform of the mesh for baking into the vertices
1093 Matrix3x4 vertexTransform;
1094 Matrix3 normalTransform;
1095 Vector3 pos, scale;
1096 Quaternion rot;
1097 GetPosRotScale(GetMeshBakingTransform(model.meshNodes_[i], model.rootNode_), pos, rot, scale);
1098 vertexTransform = Matrix3x4(pos, rot, scale);
1099 normalTransform = rot.RotationMatrix();
1100
1101 SharedPtr<Geometry> geom(new Geometry(context_));
1102
1103 PrintLine("Writing geometry " + String(i) + " with " + String(mesh->mNumVertices) + " vertices " +
1104 String(validFaces * 3) + " indices");
1105
1106 if (model.bones_.Size() > 0 && !mesh->HasBones())
1107 PrintLine("Warning: model has bones but geometry " + String(i) + " has no skinning information");
1108
1109 unsigned char* vertexData = vb->GetShadowData();
1110 unsigned char* indexData = ib->GetShadowData();
1111
1112 // Build the index data
1113 if (!largeIndices)
1114 {
1115 unsigned short* dest = (unsigned short*)indexData + startIndexOffset;
1116 for (unsigned j = 0; j < mesh->mNumFaces; ++j)
1117 WriteShortIndices(dest, mesh, j, startVertexOffset);
1118 }
1119 else
1120 {
1121 unsigned* dest = (unsigned*)indexData + startIndexOffset;
1122 for (unsigned j = 0; j < mesh->mNumFaces; ++j)
1123 WriteLargeIndices(dest, mesh, j, startVertexOffset);
1124 }
1125
1126 // Build the vertex data
1127 // If there are bones, get blend data
1128 Vector<PODVector<unsigned char> > blendIndices;
1129 Vector<PODVector<float> > blendWeights;
1130 PODVector<unsigned> boneMappings;
1131 if (model.bones_.Size())
1132 GetBlendData(model, mesh, model.meshNodes_[i], boneMappings, blendIndices, blendWeights);
1133
1134 float* dest = (float*)((unsigned char*)vertexData + startVertexOffset * vb->GetVertexSize());
1135 for (unsigned j = 0; j < mesh->mNumVertices; ++j)
1136 WriteVertex(dest, mesh, j, isSkinned, box, vertexTransform, normalTransform, blendIndices, blendWeights);
1137
1138 // Calculate the geometry center
1139 Vector3 center = Vector3::ZERO;
1140 if (validFaces)
1141 {
1142 for (unsigned j = 0; j < mesh->mNumFaces; ++j)
1143 {
1144 if (mesh->mFaces[j].mNumIndices == 3)
1145 {
1146 center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[0]]);
1147 center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[1]]);
1148 center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[2]]);
1149 }
1150 }
1151
1152 center /= (float)validFaces * 3;
1153 }
1154
1155 // Define the geometry
1156 geom->SetIndexBuffer(ib);
1157 geom->SetVertexBuffer(0, vb);
1158 geom->SetDrawRange(TRIANGLE_LIST, startIndexOffset, validFaces * 3, true);
1159 outModel->SetNumGeometryLodLevels(destGeomIndex, 1);
1160 outModel->SetGeometry(destGeomIndex, 0, geom);
1161 outModel->SetGeometryCenter(destGeomIndex, center);
1162 if (model.bones_.Size() > maxBones_)
1163 allBoneMappings.Push(boneMappings);
1164
1165 startVertexOffset += mesh->mNumVertices;
1166 startIndexOffset += validFaces * 3;
1167 ++destGeomIndex;
1168 }
1169
1170 // Define the model buffers and bounding box
1171 PODVector<unsigned> emptyMorphRange;
1172 outModel->SetVertexBuffers(vbVector, emptyMorphRange, emptyMorphRange);
1173 outModel->SetIndexBuffers(ibVector);
1174 outModel->SetBoundingBox(box);
1175
1176 // Build skeleton if necessary
1177 if (model.bones_.Size() && model.rootBone_)
1178 {
1179 PrintLine("Writing skeleton with " + String(model.bones_.Size()) + " bones, rootbone " +
1180 FromAIString(model.rootBone_->mName));
1181
1182 Skeleton skeleton;
1183 Vector<Bone>& bones = skeleton.GetModifiableBones();
1184
1185 for (unsigned i = 0; i < model.bones_.Size(); ++i)
1186 {
1187 aiNode* boneNode = model.bones_[i];
1188 String boneName(FromAIString(boneNode->mName));
1189
1190 Bone newBone;
1191 newBone.name_ = boneName;
1192
1193 aiMatrix4x4 transform = boneNode->mTransformation;
1194 // Make the root bone transform relative to the model's root node, if it is not already
1195 // (in case there are nodes between that are not accounted for otherwise)
1196 if (boneNode == model.rootBone_)
1197 transform = GetDerivedTransform(boneNode, model.rootNode_, false);
1198
1199 GetPosRotScale(transform, newBone.initialPosition_, newBone.initialRotation_, newBone.initialScale_);
1200
1201 // Get offset information if exists
1202 newBone.offsetMatrix_ = GetOffsetMatrix(model, boneName);
1203 newBone.radius_ = model.boneRadii_[i];
1204 newBone.boundingBox_ = model.boneHitboxes_[i];
1205 newBone.collisionMask_ = BONECOLLISION_SPHERE | BONECOLLISION_BOX;
1206 newBone.parentIndex_ = i;
1207 bones.Push(newBone);
1208 }
1209 // Set the bone hierarchy
1210 for (unsigned i = 1; i < model.bones_.Size(); ++i)
1211 {
1212 String parentName = FromAIString(model.bones_[i]->mParent->mName);
1213 for (unsigned j = 0; j < bones.Size(); ++j)
1214 {
1215 if (bones[j].name_ == parentName)
1216 {
1217 bones[i].parentIndex_ = j;
1218 break;
1219 }
1220 }
1221 }
1222
1223 outModel->SetSkeleton(skeleton);
1224 if (model.bones_.Size() > maxBones_)
1225 outModel->SetGeometryBoneMappings(allBoneMappings);
1226 }
1227
1228 File outFile(context_);
1229 if (!outFile.Open(model.outName_, FILE_WRITE))
1230 ErrorExit("Could not open output file " + model.outName_);
1231 outModel->Save(outFile);
1232
1233 // If exporting materials, also save material list for use by the editor
1234 if (!noMaterials_ && saveMaterialList_)
1235 {
1236 String materialListName = ReplaceExtension(model.outName_, ".txt");
1237 File listFile(context_);
1238 if (listFile.Open(materialListName, FILE_WRITE))
1239 {
1240 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
1241 listFile.WriteLine(GetMeshMaterialName(model.meshes_[i]));
1242 }
1243 else
1244 PrintLine("Warning: could not write material list file " + materialListName);
1245 }
1246 }
1247
BuildAndSaveAnimations(OutModel * model)1248 void BuildAndSaveAnimations(OutModel* model)
1249 {
1250 // extrapolate anim
1251 ExtrapolatePivotlessAnimation(model);
1252
1253 // build and save anim
1254 const PODVector<aiAnimation*>& animations = model ? model->animations_ : sceneAnimations_;
1255
1256 for (unsigned i = 0; i < animations.Size(); ++i)
1257 {
1258 aiAnimation* anim = animations[i];
1259
1260 float duration = (float)anim->mDuration;
1261 String animName = FromAIString(anim->mName);
1262 String animOutName;
1263
1264 float thisImportEndTime = importEndTime_;
1265 float thisImportStartTime = importStartTime_;
1266
1267 // If no animation split specified, set the end time to duration
1268 if (thisImportEndTime == 0.0f)
1269 thisImportEndTime = duration;
1270
1271 if (animName.Empty())
1272 animName = "Anim" + String(i + 1);
1273 if (model)
1274 animOutName = GetPath(model->outName_) + GetFileName(model->outName_) + "_" + SanitateAssetName(animName) + ".ani";
1275 else
1276 animOutName = outPath_ + GetFileName(outName_) + "_" + SanitateAssetName(animName) + ".ani";
1277
1278 float ticksPerSecond = (float)anim->mTicksPerSecond;
1279 // If ticks per second not specified, it's probably a .X file. In this case use the default tick rate
1280 if (ticksPerSecond < M_EPSILON)
1281 ticksPerSecond = defaultTicksPerSecond_;
1282 float tickConversion = 1.0f / ticksPerSecond;
1283
1284 // Find out the start time of animation from each channel's first keyframe for adjusting the keyframe times
1285 // to start from zero
1286 float startTime = duration;
1287 for (unsigned j = 0; j < anim->mNumChannels; ++j)
1288 {
1289 aiNodeAnim* channel = anim->mChannels[j];
1290 if (channel->mNumPositionKeys > 0)
1291 startTime = Min(startTime, (float)channel->mPositionKeys[0].mTime);
1292 if (channel->mNumRotationKeys > 0)
1293 startTime = Min(startTime, (float)channel->mRotationKeys[0].mTime);
1294 if (channel->mNumScalingKeys > 0)
1295 startTime = Min(startTime, (float)channel->mScalingKeys[0].mTime);
1296 }
1297 if (startTime > thisImportStartTime)
1298 thisImportStartTime = startTime;
1299 duration = thisImportEndTime - thisImportStartTime;
1300
1301 SharedPtr<Animation> outAnim(new Animation(context_));
1302 outAnim->SetAnimationName(animName);
1303 outAnim->SetLength(duration * tickConversion);
1304
1305 PrintLine("Writing animation " + animName + " length " + String(outAnim->GetLength()));
1306 for (unsigned j = 0; j < anim->mNumChannels; ++j)
1307 {
1308 aiNodeAnim* channel = anim->mChannels[j];
1309 String channelName = FromAIString(channel->mNodeName);
1310 aiNode* boneNode = 0;
1311
1312 if (model)
1313 {
1314 unsigned boneIndex;
1315 unsigned pos = channelName.Find("_$AssimpFbx$");
1316
1317 if (!suppressFbxPivotNodes_ || pos == String::NPOS)
1318 {
1319 boneIndex = GetBoneIndex(*model, channelName);
1320 if (boneIndex == M_MAX_UNSIGNED)
1321 {
1322 PrintLine("Warning: skipping animation track " + channelName + " not found in model skeleton");
1323 outAnim->RemoveTrack(channelName);
1324 continue;
1325 }
1326 boneNode = model->bones_[boneIndex];
1327 }
1328 else
1329 {
1330 channelName = channelName.Substring(0, pos);
1331
1332 // every first $fbx animation channel for a bone will consolidate other $fbx animation to a single channel
1333 // skip subsequent $fbx animation channel for the same bone
1334 if (outAnim->GetTrack(channelName) != NULL)
1335 continue;
1336
1337 boneIndex = GetPivotlessBoneIndex(*model, channelName);
1338 if (boneIndex == M_MAX_UNSIGNED)
1339 {
1340 PrintLine("Warning: skipping animation track " + channelName + " not found in model skeleton");
1341 outAnim->RemoveTrack(channelName);
1342 continue;
1343 }
1344
1345 boneNode = model->pivotlessBones_[boneIndex];
1346 }
1347 }
1348 else
1349 {
1350 boneNode = GetNode(channelName, scene_->mRootNode);
1351 if (!boneNode)
1352 {
1353 PrintLine("Warning: skipping animation track " + channelName + " whose scene node was not found");
1354 outAnim->RemoveTrack(channelName);
1355 continue;
1356 }
1357 }
1358
1359 // To export single frame animation, check if first key frame is identical to bone transformation
1360 aiVector3D bonePos, boneScale;
1361 aiQuaternion boneRot;
1362 boneNode->mTransformation.Decompose(boneScale, boneRot, bonePos);
1363
1364 bool posEqual = true;
1365 bool scaleEqual = true;
1366 bool rotEqual = true;
1367
1368 if (channel->mNumPositionKeys > 0 && !ToVector3(bonePos).Equals(ToVector3(channel->mPositionKeys[0].mValue)))
1369 posEqual = false;
1370 if (channel->mNumScalingKeys > 0 && !ToVector3(boneScale).Equals(ToVector3(channel->mScalingKeys[0].mValue)))
1371 scaleEqual = false;
1372 if (channel->mNumRotationKeys > 0 && !ToQuaternion(boneRot).Equals(ToQuaternion(channel->mRotationKeys[0].mValue)))
1373 rotEqual = false;
1374
1375 AnimationTrack* track = outAnim->CreateTrack(channelName);
1376
1377 // Check which channels are used
1378 track->channelMask_ = 0;
1379 if (channel->mNumPositionKeys > 1 || !posEqual)
1380 track->channelMask_ |= CHANNEL_POSITION;
1381 if (channel->mNumRotationKeys > 1 || !rotEqual)
1382 track->channelMask_ |= CHANNEL_ROTATION;
1383 if (channel->mNumScalingKeys > 1 || !scaleEqual)
1384 track->channelMask_ |= CHANNEL_SCALE;
1385 // Check for redundant identity scale in all keyframes and remove in that case
1386 if (track->channelMask_ & CHANNEL_SCALE)
1387 {
1388 bool redundantScale = true;
1389 for (unsigned k = 0; k < channel->mNumScalingKeys; ++k)
1390 {
1391 float SCALE_EPSILON = 0.000001f;
1392 Vector3 scaleVec = ToVector3(channel->mScalingKeys[k].mValue);
1393 if (fabsf(scaleVec.x_ - 1.0f) >= SCALE_EPSILON || fabsf(scaleVec.y_ - 1.0f) >= SCALE_EPSILON ||
1394 fabsf(scaleVec.z_ - 1.0f) >= SCALE_EPSILON)
1395 {
1396 redundantScale = false;
1397 break;
1398 }
1399 }
1400 if (redundantScale)
1401 track->channelMask_ &= ~CHANNEL_SCALE;
1402 }
1403
1404 if (!track->channelMask_)
1405 {
1406 PrintLine("Warning: skipping animation track " + channelName + " with no keyframes");
1407 outAnim->RemoveTrack(channelName);
1408 continue;
1409 }
1410
1411 // Currently only same amount of keyframes is supported
1412 // Note: should also check the times of individual keyframes for match
1413 if ((channel->mNumPositionKeys > 1 && channel->mNumRotationKeys > 1 && channel->mNumPositionKeys != channel->mNumRotationKeys) ||
1414 (channel->mNumPositionKeys > 1 && channel->mNumScalingKeys > 1 && channel->mNumPositionKeys != channel->mNumScalingKeys) ||
1415 (channel->mNumRotationKeys > 1 && channel->mNumScalingKeys > 1 && channel->mNumRotationKeys != channel->mNumScalingKeys))
1416 {
1417 PrintLine("Warning: differing amounts of channel keyframes, skipping animation track " + channelName);
1418 outAnim->RemoveTrack(channelName);
1419 continue;
1420 }
1421
1422 unsigned keyFrames = channel->mNumPositionKeys;
1423 if (channel->mNumRotationKeys > keyFrames)
1424 keyFrames = channel->mNumRotationKeys;
1425 if (channel->mNumScalingKeys > keyFrames)
1426 keyFrames = channel->mNumScalingKeys;
1427
1428 for (unsigned k = 0; k < keyFrames; ++k)
1429 {
1430 AnimationKeyFrame kf;
1431 kf.time_ = 0.0f;
1432 kf.position_ = Vector3::ZERO;
1433 kf.rotation_ = Quaternion::IDENTITY;
1434 kf.scale_ = Vector3::ONE;
1435
1436 // Get time for the keyframe. Adjust with animation's start time
1437 if (track->channelMask_ & CHANNEL_POSITION && k < channel->mNumPositionKeys)
1438 kf.time_ = ((float)channel->mPositionKeys[k].mTime - startTime);
1439 else if (track->channelMask_ & CHANNEL_ROTATION && k < channel->mNumRotationKeys)
1440 kf.time_ = ((float)channel->mRotationKeys[k].mTime - startTime);
1441 else if (track->channelMask_ & CHANNEL_SCALE && k < channel->mNumScalingKeys)
1442 kf.time_ = ((float)channel->mScalingKeys[k].mTime - startTime);
1443
1444 // Make sure time stays positive
1445 kf.time_ = Max(kf.time_, 0.0f);
1446
1447 // Start with the bone's base transform
1448 aiMatrix4x4 boneTransform = boneNode->mTransformation;
1449 aiVector3D pos, scale;
1450 aiQuaternion rot;
1451 boneTransform.Decompose(scale, rot, pos);
1452 // Then apply the active channels
1453 if (track->channelMask_ & CHANNEL_POSITION && k < channel->mNumPositionKeys)
1454 pos = channel->mPositionKeys[k].mValue;
1455 if (track->channelMask_ & CHANNEL_ROTATION && k < channel->mNumRotationKeys)
1456 rot = channel->mRotationKeys[k].mValue;
1457 if (track->channelMask_ & CHANNEL_SCALE && k < channel->mNumScalingKeys)
1458 scale = channel->mScalingKeys[k].mValue;
1459
1460 // If root bone, transform with nodes in between model root node (if any)
1461 if (model && boneNode == model->rootBone_)
1462 {
1463 aiMatrix4x4 transMat, scaleMat, rotMat;
1464 aiMatrix4x4::Translation(pos, transMat);
1465 aiMatrix4x4::Scaling(scale, scaleMat);
1466 rotMat = aiMatrix4x4(rot.GetMatrix());
1467 aiMatrix4x4 tform = transMat * rotMat * scaleMat;
1468 aiMatrix4x4 tformOld = tform;
1469 tform = GetDerivedTransform(tform, boneNode, model->rootNode_, false);
1470 // Do not decompose if did not actually change
1471 if (tform != tformOld)
1472 tform.Decompose(scale, rot, pos);
1473 }
1474
1475 if (track->channelMask_ & CHANNEL_POSITION)
1476 kf.position_ = ToVector3(pos);
1477 if (track->channelMask_ & CHANNEL_ROTATION)
1478 kf.rotation_ = ToQuaternion(rot);
1479 if (track->channelMask_ & CHANNEL_SCALE)
1480 kf.scale_ = ToVector3(scale);
1481 if (kf.time_ >= thisImportStartTime && kf.time_ <= thisImportEndTime)
1482 {
1483 kf.time_ = (kf.time_ - thisImportStartTime) * tickConversion;
1484 track->keyFrames_.Push(kf);
1485 }
1486 }
1487 }
1488
1489 File outFile(context_);
1490 if (!outFile.Open(animOutName, FILE_WRITE))
1491 ErrorExit("Could not open output file " + animOutName);
1492 outAnim->Save(outFile);
1493 }
1494 }
1495
ExportScene(const String & outName,bool asPrefab)1496 void ExportScene(const String& outName, bool asPrefab)
1497 {
1498 OutScene outScene;
1499 outScene.outName_ = outName;
1500 outScene.rootNode_ = rootNode_;
1501
1502 if (useSubdirs_)
1503 context_->GetSubsystem<FileSystem>()->CreateDir(resourcePath_ + "Models");
1504
1505 CollectSceneModels(outScene, rootNode_);
1506
1507 // Save models, their material lists and animations
1508 for (unsigned i = 0; i < outScene.models_.Size(); ++i)
1509 BuildAndSaveModel(outScene.models_[i]);
1510
1511 // Save scene-global animations
1512 if (!noAnimations_)
1513 {
1514 CollectAnimations();
1515 BuildAndSaveAnimations();
1516 }
1517
1518 // Save scene
1519 BuildAndSaveScene(outScene, asPrefab);
1520 }
1521
CollectSceneModels(OutScene & scene,aiNode * node)1522 void CollectSceneModels(OutScene& scene, aiNode* node)
1523 {
1524 Vector<Pair<aiNode*, aiMesh*> > meshes;
1525 GetMeshesUnderNode(meshes, node);
1526
1527 if (meshes.Size())
1528 {
1529 OutModel model;
1530 model.rootNode_ = node;
1531 model.outName_ = resourcePath_ + (useSubdirs_ ? "Models/" : "") + SanitateAssetName(FromAIString(node->mName)) + ".mdl";
1532 for (unsigned i = 0; i < meshes.Size(); ++i)
1533 {
1534 aiMesh* mesh = meshes[i].second_;
1535 unsigned meshIndex = GetMeshIndex(mesh);
1536 model.meshIndices_.Insert(meshIndex);
1537 model.meshes_.Push(mesh);
1538 model.meshNodes_.Push(meshes[i].first_);
1539 model.totalVertices_ += mesh->mNumVertices;
1540 model.totalIndices_ += GetNumValidFaces(mesh) * 3;
1541 }
1542
1543 // Check if a model with identical mesh indices already exists. If yes, do not export twice
1544 bool unique = true;
1545 if (checkUniqueModel_)
1546 {
1547 for (unsigned i = 0; i < scene.models_.Size(); ++i)
1548 {
1549 if (scene.models_[i].meshIndices_ == model.meshIndices_)
1550 {
1551 PrintLine("Added node " + FromAIString(node->mName));
1552 scene.nodes_.Push(node);
1553 scene.nodeModelIndices_.Push(i);
1554 unique = false;
1555 break;
1556 }
1557 }
1558 }
1559 if (unique)
1560 {
1561 PrintLine("Added model " + model.outName_);
1562 PrintLine("Added node " + FromAIString(node->mName));
1563 CollectBones(model);
1564 BuildBoneCollisionInfo(model);
1565 if (!noAnimations_)
1566 {
1567 CollectAnimations(&model);
1568 BuildAndSaveAnimations(&model);
1569 }
1570
1571 scene.models_.Push(model);
1572 scene.nodes_.Push(node);
1573 scene.nodeModelIndices_.Push(scene.models_.Size() - 1);
1574 }
1575 }
1576
1577 for (unsigned i = 0; i < node->mNumChildren; ++i)
1578 CollectSceneModels(scene, node->mChildren[i]);
1579 }
1580
CreateHierarchy(Scene * scene,aiNode * srcNode,HashMap<aiNode *,Node * > & nodeMapping)1581 void CreateHierarchy(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping)
1582 {
1583 CreateSceneNode(scene, srcNode, nodeMapping);
1584 for (unsigned i = 0; i < srcNode->mNumChildren; ++i)
1585 CreateHierarchy(scene, srcNode->mChildren[i], nodeMapping);
1586 }
1587
CreateSceneNode(Scene * scene,aiNode * srcNode,HashMap<aiNode *,Node * > & nodeMapping)1588 Node* CreateSceneNode(Scene* scene, aiNode* srcNode, HashMap<aiNode*, Node*>& nodeMapping)
1589 {
1590 if (nodeMapping.Contains(srcNode))
1591 return nodeMapping[srcNode];
1592 // Flatten hierarchy if requested
1593 if (noHierarchy_)
1594 {
1595 Node* outNode = scene->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED);
1596 Vector3 pos, scale;
1597 Quaternion rot;
1598 GetPosRotScale(GetDerivedTransform(srcNode, rootNode_), pos, rot, scale);
1599 outNode->SetTransform(pos, rot, scale);
1600 nodeMapping[srcNode] = outNode;
1601
1602 return outNode;
1603 }
1604
1605 if (srcNode == rootNode_ || !srcNode->mParent)
1606 {
1607 Node* outNode = scene->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED);
1608 Vector3 pos, scale;
1609 Quaternion rot;
1610 GetPosRotScale(srcNode->mTransformation, pos, rot, scale);
1611 outNode->SetTransform(pos, rot, scale);
1612 nodeMapping[srcNode] = outNode;
1613
1614 return outNode;
1615 }
1616 else
1617 {
1618 // Ensure the existence of the parent chain as in the original file
1619 if (!nodeMapping.Contains(srcNode->mParent))
1620 CreateSceneNode(scene, srcNode->mParent, nodeMapping);
1621
1622 Node* parent = nodeMapping[srcNode->mParent];
1623 Node* outNode = parent->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED);
1624 Vector3 pos, scale;
1625 Quaternion rot;
1626 GetPosRotScale(srcNode->mTransformation, pos, rot, scale);
1627 outNode->SetTransform(pos, rot, scale);
1628 nodeMapping[srcNode] = outNode;
1629
1630 return outNode;
1631 }
1632 }
1633
BuildAndSaveScene(OutScene & scene,bool asPrefab)1634 void BuildAndSaveScene(OutScene& scene, bool asPrefab)
1635 {
1636 if (!asPrefab)
1637 PrintLine("Writing scene");
1638 else
1639 PrintLine("Writing node hierarchy");
1640
1641 SharedPtr<Scene> outScene(new Scene(context_));
1642
1643 if (!asPrefab)
1644 {
1645 #ifdef URHO3D_PHYSICS
1646 /// \todo Make the physics properties configurable
1647 outScene->CreateComponent<PhysicsWorld>();
1648 #endif
1649
1650 /// \todo Make the octree properties configurable, or detect from the scene contents
1651 outScene->CreateComponent<Octree>();
1652
1653 outScene->CreateComponent<DebugRenderer>();
1654
1655 if (createZone_)
1656 {
1657 Node* zoneNode = outScene->CreateChild("Zone", localIDs_ ? LOCAL : REPLICATED);
1658 Zone* zone = zoneNode->CreateComponent<Zone>();
1659 zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.f));
1660 zone->SetAmbientColor(Color(0.25f, 0.25f, 0.25f));
1661
1662 // Create default light only if scene does not define them
1663 if (!scene_->HasLights())
1664 {
1665 Node* lightNode = outScene->CreateChild("GlobalLight", localIDs_ ? LOCAL : REPLICATED);
1666 Light* light = lightNode->CreateComponent<Light>();
1667 light->SetLightType(LIGHT_DIRECTIONAL);
1668 lightNode->SetRotation(Quaternion(60.0f, 30.0f, 0.0f));
1669 }
1670 }
1671 }
1672
1673 ResourceCache* cache = context_->GetSubsystem<ResourceCache>();
1674
1675 HashMap<aiNode*, Node*> nodeMapping;
1676
1677 Node* outRootNode = 0;
1678 if (asPrefab)
1679 outRootNode = CreateSceneNode(outScene, rootNode_, nodeMapping);
1680 else
1681 {
1682 // If not saving as a prefab, associate the root node with the scene first to prevent unnecessary creation of a root
1683 // However do not do that if the root node does not have an identity matrix, or itself contains a model
1684 // (models at the Urho scene root are not preferable)
1685 if (ToMatrix3x4(rootNode_->mTransformation).Equals(Matrix3x4::IDENTITY) && !scene.nodes_.Contains(rootNode_))
1686 nodeMapping[rootNode_] = outScene;
1687 }
1688
1689 // If is allowed to export empty nodes, export the full Assimp node hierarchy first
1690 if (!noHierarchy_ && !noEmptyNodes_)
1691 CreateHierarchy(outScene, rootNode_, nodeMapping);
1692
1693 // Create geometry nodes
1694 for (unsigned i = 0; i < scene.nodes_.Size(); ++i)
1695 {
1696 const OutModel& model = scene.models_[scene.nodeModelIndices_[i]];
1697 Node* modelNode = CreateSceneNode(outScene, scene.nodes_[i], nodeMapping);
1698 StaticModel* staticModel = model.bones_.Empty() ? modelNode->CreateComponent<StaticModel>() : modelNode->CreateComponent<AnimatedModel>();
1699
1700 // Create a dummy model so that the reference can be stored
1701 String modelName = (useSubdirs_ ? "Models/" : "") + GetFileNameAndExtension(model.outName_);
1702 if (!cache->Exists(modelName))
1703 {
1704 Model* dummyModel = new Model(context_);
1705 dummyModel->SetName(modelName);
1706 dummyModel->SetNumGeometries(model.meshes_.Size());
1707 cache->AddManualResource(dummyModel);
1708 }
1709 staticModel->SetModel(cache->GetResource<Model>(modelName));
1710
1711 // Set materials if they are known
1712 for (unsigned j = 0; j < model.meshes_.Size(); ++j)
1713 {
1714 String matName = GetMeshMaterialName(model.meshes_[j]);
1715 // Create a dummy material so that the reference can be stored
1716 if (!cache->Exists(matName))
1717 {
1718 Material* dummyMat = new Material(context_);
1719 dummyMat->SetName(matName);
1720 cache->AddManualResource(dummyMat);
1721 }
1722 staticModel->SetMaterial(j, cache->GetResource<Material>(matName));
1723 }
1724 }
1725
1726 // Create lights
1727 if (!asPrefab)
1728 {
1729 for (unsigned i = 0; i < scene_->mNumLights; ++i)
1730 {
1731 aiLight* light = scene_->mLights[i];
1732 aiNode* lightNode = GetNode(FromAIString(light->mName), rootNode_, true);
1733 if (!lightNode)
1734 continue;
1735 Node* outNode = CreateSceneNode(outScene, lightNode, nodeMapping);
1736
1737 Vector3 lightAdjustPosition = ToVector3(light->mPosition);
1738 Vector3 lightAdjustDirection = ToVector3(light->mDirection);
1739 // If light is not aligned at the scene node, an adjustment node needs to be created
1740 if (!lightAdjustPosition.Equals(Vector3::ZERO) || (light->mType != aiLightSource_POINT &&
1741 !lightAdjustDirection.Equals(Vector3::FORWARD)))
1742 {
1743 outNode = outNode->CreateChild("LightAdjust");
1744 outNode->SetPosition(lightAdjustPosition);
1745 outNode->SetDirection(lightAdjustDirection);
1746 }
1747
1748 Light* outLight = outNode->CreateComponent<Light>();
1749 outLight->SetColor(Color(light->mColorDiffuse.r, light->mColorDiffuse.g, light->mColorDiffuse.b));
1750
1751 switch (light->mType)
1752 {
1753 case aiLightSource_DIRECTIONAL:
1754 outLight->SetLightType(LIGHT_DIRECTIONAL);
1755 break;
1756 case aiLightSource_SPOT:
1757 outLight->SetLightType(LIGHT_SPOT);
1758 outLight->SetFov(light->mAngleOuterCone * 0.5f * M_RADTODEG);
1759 break;
1760 case aiLightSource_POINT:
1761 outLight->SetLightType(LIGHT_POINT);
1762 break;
1763 default:
1764 break;
1765 }
1766
1767 // Calculate range from attenuation parameters so that light intensity has been reduced to 10% at that distance
1768 if (light->mType != aiLightSource_DIRECTIONAL)
1769 {
1770 float a = light->mAttenuationQuadratic;
1771 float b = light->mAttenuationLinear;
1772 float c = -10.0f;
1773 if (!Equals(a, 0.0f))
1774 {
1775 float root1 = (-b + sqrtf(b * b - 4.0f * a * c)) / (2.0f * a);
1776 float root2 = (-b - sqrtf(b * b - 4.0f * a * c)) / (2.0f * a);
1777 outLight->SetRange(Max(root1, root2));
1778 }
1779 else if (!Equals(b, 0.0f))
1780 outLight->SetRange(-c / b);
1781 }
1782 }
1783 }
1784
1785 File file(context_);
1786 if (!file.Open(scene.outName_, FILE_WRITE))
1787 ErrorExit("Could not open output file " + scene.outName_);
1788 if (!asPrefab)
1789 {
1790 if (saveBinary_)
1791 outScene->Save(file);
1792 else if (saveJson_)
1793 outScene->SaveJSON(file);
1794 else
1795 outScene->SaveXML(file);
1796 }
1797 else
1798 {
1799 if (saveBinary_)
1800 outRootNode->Save(file);
1801 else if (saveJson_)
1802 outRootNode->SaveJSON(file);
1803 else
1804 outRootNode->SaveXML(file);
1805 }
1806 }
1807
ExportMaterials(HashSet<String> & usedTextures)1808 void ExportMaterials(HashSet<String>& usedTextures)
1809 {
1810 if (useSubdirs_)
1811 context_->GetSubsystem<FileSystem>()->CreateDir(resourcePath_ + "Materials");
1812
1813 for (unsigned i = 0; i < scene_->mNumMaterials; ++i)
1814 BuildAndSaveMaterial(scene_->mMaterials[i], usedTextures);
1815 }
1816
BuildAndSaveMaterial(aiMaterial * material,HashSet<String> & usedTextures)1817 void BuildAndSaveMaterial(aiMaterial* material, HashSet<String>& usedTextures)
1818 {
1819 aiString matNameStr;
1820 material->Get(AI_MATKEY_NAME, matNameStr);
1821 String matName = SanitateAssetName(FromAIString(matNameStr));
1822 if (matName.Trimmed().Empty())
1823 matName = GenerateMaterialName(material);
1824
1825 // Do not actually create a material instance, but instead craft an xml file manually
1826 XMLFile outMaterial(context_);
1827 XMLElement materialElem = outMaterial.CreateRoot("material");
1828
1829 String diffuseTexName;
1830 String normalTexName;
1831 String specularTexName;
1832 String lightmapTexName;
1833 String emissiveTexName;
1834 Color diffuseColor = Color::WHITE;
1835 Color specularColor;
1836 Color emissiveColor = Color::BLACK;
1837 bool hasAlpha = false;
1838 bool twoSided = false;
1839 float specPower = 1.0f;
1840
1841 aiString stringVal;
1842 float floatVal;
1843 int intVal;
1844 aiColor3D colorVal;
1845
1846 if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), stringVal) == AI_SUCCESS)
1847 diffuseTexName = GetFileNameAndExtension(FromAIString(stringVal));
1848 if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_NORMALS, 0), stringVal) == AI_SUCCESS)
1849 normalTexName = GetFileNameAndExtension(FromAIString(stringVal));
1850 if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_SPECULAR, 0), stringVal) == AI_SUCCESS)
1851 specularTexName = GetFileNameAndExtension(FromAIString(stringVal));
1852 if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_LIGHTMAP, 0), stringVal) == AI_SUCCESS)
1853 lightmapTexName = GetFileNameAndExtension(FromAIString(stringVal));
1854 if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_EMISSIVE, 0), stringVal) == AI_SUCCESS)
1855 emissiveTexName = GetFileNameAndExtension(FromAIString(stringVal));
1856 if (!noMaterialDiffuseColor_)
1857 {
1858 if (material->Get(AI_MATKEY_COLOR_DIFFUSE, colorVal) == AI_SUCCESS)
1859 diffuseColor = Color(colorVal.r, colorVal.g, colorVal.b);
1860 }
1861 if (material->Get(AI_MATKEY_COLOR_SPECULAR, colorVal) == AI_SUCCESS)
1862 specularColor = Color(colorVal.r, colorVal.g, colorVal.b);
1863 if (!emissiveAO_)
1864 {
1865 if (material->Get(AI_MATKEY_COLOR_EMISSIVE, colorVal) == AI_SUCCESS)
1866 emissiveColor = Color(colorVal.r, colorVal.g, colorVal.b);
1867 }
1868 if (material->Get(AI_MATKEY_OPACITY, floatVal) == AI_SUCCESS)
1869 {
1870 /// \hack New Assimp behavior - some materials may return 0 opacity, which is invisible.
1871 /// Revert to full opacity in that case
1872 if (floatVal < M_EPSILON)
1873 floatVal = 1.0f;
1874
1875 if (floatVal < 1.0f)
1876 hasAlpha = true;
1877 diffuseColor.a_ = floatVal;
1878 }
1879 if (material->Get(AI_MATKEY_SHININESS, floatVal) == AI_SUCCESS)
1880 specPower = floatVal;
1881 if (material->Get(AI_MATKEY_TWOSIDED, intVal) == AI_SUCCESS)
1882 twoSided = (intVal != 0);
1883
1884 String techniqueName = "Techniques/NoTexture";
1885 if (!diffuseTexName.Empty())
1886 {
1887 techniqueName = "Techniques/Diff";
1888 if (!normalTexName.Empty())
1889 techniqueName += "Normal";
1890 if (!specularTexName.Empty())
1891 techniqueName += "Spec";
1892 // For now lightmap does not coexist with normal & specular
1893 if (normalTexName.Empty() && specularTexName.Empty() && !lightmapTexName.Empty())
1894 techniqueName += "LightMap";
1895 if (lightmapTexName.Empty() && !emissiveTexName.Empty())
1896 techniqueName += emissiveAO_ ? "AO" : "Emissive";
1897 }
1898 if (hasAlpha)
1899 techniqueName += "Alpha";
1900
1901 XMLElement techniqueElem = materialElem.CreateChild("technique");
1902 techniqueElem.SetString("name", techniqueName + ".xml");
1903
1904 if (!diffuseTexName.Empty())
1905 {
1906 XMLElement diffuseElem = materialElem.CreateChild("texture");
1907 diffuseElem.SetString("unit", "diffuse");
1908 diffuseElem.SetString("name", GetMaterialTextureName(diffuseTexName));
1909 usedTextures.Insert(diffuseTexName);
1910 }
1911 if (!normalTexName.Empty())
1912 {
1913 XMLElement normalElem = materialElem.CreateChild("texture");
1914 normalElem.SetString("unit", "normal");
1915 normalElem.SetString("name", GetMaterialTextureName(normalTexName));
1916 usedTextures.Insert(normalTexName);
1917 }
1918 if (!specularTexName.Empty())
1919 {
1920 XMLElement specularElem = materialElem.CreateChild("texture");
1921 specularElem.SetString("unit", "specular");
1922 specularElem.SetString("name", GetMaterialTextureName(specularTexName));
1923 usedTextures.Insert(specularTexName);
1924 }
1925 if (!lightmapTexName.Empty())
1926 {
1927 XMLElement lightmapElem = materialElem.CreateChild("texture");
1928 lightmapElem.SetString("unit", "emissive");
1929 lightmapElem.SetString("name", GetMaterialTextureName(lightmapTexName));
1930 usedTextures.Insert(lightmapTexName);
1931 }
1932 if (!emissiveTexName.Empty())
1933 {
1934 XMLElement emissiveElem = materialElem.CreateChild("texture");
1935 emissiveElem.SetString("unit", "emissive");
1936 emissiveElem.SetString("name", GetMaterialTextureName(emissiveTexName));
1937 usedTextures.Insert(emissiveTexName);
1938 }
1939
1940 XMLElement diffuseColorElem = materialElem.CreateChild("parameter");
1941 diffuseColorElem.SetString("name", "MatDiffColor");
1942 diffuseColorElem.SetColor("value", diffuseColor);
1943 XMLElement specularElem = materialElem.CreateChild("parameter");
1944 specularElem.SetString("name", "MatSpecColor");
1945 specularElem.SetVector4("value", Vector4(specularColor.r_, specularColor.g_, specularColor.b_, specPower));
1946 XMLElement emissiveColorElem = materialElem.CreateChild("parameter");
1947 emissiveColorElem.SetString("name", "MatEmissiveColor");
1948 emissiveColorElem.SetColor("value", emissiveColor);
1949
1950 if (twoSided)
1951 {
1952 XMLElement cullElem = materialElem.CreateChild("cull");
1953 XMLElement shadowCullElem = materialElem.CreateChild("shadowcull");
1954 cullElem.SetString("value", "none");
1955 shadowCullElem.SetString("value", "none");
1956 }
1957
1958 FileSystem* fileSystem = context_->GetSubsystem<FileSystem>();
1959
1960 String outFileName = resourcePath_ + (useSubdirs_ ? "Materials/" : "" ) + matName + ".xml";
1961 if (noOverwriteMaterial_ && fileSystem->FileExists(outFileName))
1962 {
1963 PrintLine("Skipping save of existing material " + matName);
1964 return;
1965 }
1966
1967 PrintLine("Writing material " + matName);
1968
1969 File outFile(context_);
1970 if (!outFile.Open(outFileName, FILE_WRITE))
1971 ErrorExit("Could not open output file " + outFileName);
1972 outMaterial.Save(outFile);
1973 }
1974
CopyTextures(const HashSet<String> & usedTextures,const String & sourcePath)1975 void CopyTextures(const HashSet<String>& usedTextures, const String& sourcePath)
1976 {
1977 FileSystem* fileSystem = context_->GetSubsystem<FileSystem>();
1978
1979 if (useSubdirs_)
1980 fileSystem->CreateDir(resourcePath_ + "Textures");
1981
1982 for (HashSet<String>::ConstIterator i = usedTextures.Begin(); i != usedTextures.End(); ++i)
1983 {
1984 // Handle assimp embedded textures
1985 if (i->Length() && i->At(0) == '*')
1986 {
1987 unsigned texIndex = ToInt(i->Substring(1));
1988 if (texIndex >= scene_->mNumTextures)
1989 PrintLine("Skipping out of range texture index " + String(texIndex));
1990 else
1991 {
1992 aiTexture* tex = scene_->mTextures[texIndex];
1993 String fullDestName = resourcePath_ + GenerateTextureName(texIndex);
1994 bool destExists = fileSystem->FileExists(fullDestName);
1995 if (destExists && noOverwriteTexture_)
1996 {
1997 PrintLine("Skipping copy of existing embedded texture " + GetFileNameAndExtension(fullDestName));
1998 continue;
1999 }
2000 // Encoded texture
2001 if (!tex->mHeight)
2002 {
2003 PrintLine("Saving embedded texture " + GetFileNameAndExtension(fullDestName));
2004 File dest(context_, fullDestName, FILE_WRITE);
2005 dest.Write((const void*)tex->pcData, tex->mWidth);
2006 }
2007 // RGBA8 texture
2008 else
2009 {
2010 PrintLine("Saving embedded RGBA texture " + GetFileNameAndExtension(fullDestName));
2011 Image image(context_);
2012 image.SetSize(tex->mWidth, tex->mHeight, 4);
2013 memcpy(image.GetData(), (const void*)tex->pcData, tex->mWidth * tex->mHeight * 4);
2014 image.SavePNG(fullDestName);
2015 }
2016 }
2017 }
2018 else
2019 {
2020 String fullSourceName = sourcePath + *i;
2021 String fullDestName = resourcePath_ + (useSubdirs_ ? "Textures/" : "") + *i;
2022
2023 if (!fileSystem->FileExists(fullSourceName))
2024 {
2025 PrintLine("Skipping copy of nonexisting material texture " + *i);
2026 continue;
2027 }
2028 {
2029 File test(context_, fullSourceName);
2030 if (!test.GetSize())
2031 {
2032 PrintLine("Skipping copy of zero-size material texture " + *i);
2033 continue;
2034 }
2035 }
2036
2037 bool destExists = fileSystem->FileExists(fullDestName);
2038 if (destExists && noOverwriteTexture_)
2039 {
2040 PrintLine("Skipping copy of existing texture " + *i);
2041 continue;
2042 }
2043 if (destExists && noOverwriteNewerTexture_ && fileSystem->GetLastModifiedTime(fullDestName) >
2044 fileSystem->GetLastModifiedTime(fullSourceName))
2045 {
2046 PrintLine("Skipping copying of material texture " + *i + ", destination is newer");
2047 continue;
2048 }
2049
2050 PrintLine("Copying material texture " + *i);
2051 fileSystem->Copy(fullSourceName, fullDestName);
2052 }
2053 }
2054 }
2055
CombineLods(const PODVector<float> & lodDistances,const Vector<String> & modelNames,const String & outName)2056 void CombineLods(const PODVector<float>& lodDistances, const Vector<String>& modelNames, const String& outName)
2057 {
2058 // Load models
2059 Vector<SharedPtr<Model> > srcModels;
2060 for (unsigned i = 0; i < modelNames.Size(); ++i)
2061 {
2062 PrintLine("Reading LOD level " + String(i) + ": model " + modelNames[i] + " distance " + String(lodDistances[i]));
2063 File srcFile(context_);
2064 srcFile.Open(modelNames[i]);
2065 SharedPtr<Model> srcModel(new Model(context_));
2066 if (!srcModel->Load(srcFile))
2067 ErrorExit("Could not load input model " + modelNames[i]);
2068 srcModels.Push(srcModel);
2069 }
2070
2071 // Check that none of the models already has LOD levels
2072 for (unsigned i = 0; i < srcModels.Size(); ++i)
2073 {
2074 for (unsigned j = 0; j < srcModels[i]->GetNumGeometries(); ++j)
2075 {
2076 if (srcModels[i]->GetNumGeometryLodLevels(j) > 1)
2077 ErrorExit(modelNames[i] + " already has multiple LOD levels defined");
2078 }
2079 }
2080
2081 // Check for number of geometries (need to have same amount for now)
2082 for (unsigned i = 1; i < srcModels.Size(); ++i)
2083 {
2084 if (srcModels[i]->GetNumGeometries() != srcModels[0]->GetNumGeometries())
2085 ErrorExit(modelNames[i] + " has different amount of geometries than " + modelNames[0]);
2086 }
2087
2088 // If there are bones, check for compatibility (need to have exact match for now)
2089 for (unsigned i = 1; i < srcModels.Size(); ++i)
2090 {
2091 if (srcModels[i]->GetSkeleton().GetNumBones() != srcModels[0]->GetSkeleton().GetNumBones())
2092 ErrorExit(modelNames[i] + " has different amount of bones than " + modelNames[0]);
2093 for (unsigned j = 0; j < srcModels[0]->GetSkeleton().GetNumBones(); ++j)
2094 {
2095 if (srcModels[i]->GetSkeleton().GetBone(j)->name_ != srcModels[0]->GetSkeleton().GetBone(j)->name_)
2096 ErrorExit(modelNames[i] + " has different bones than " + modelNames[0]);
2097 }
2098 if (srcModels[i]->GetGeometryBoneMappings() != srcModels[0]->GetGeometryBoneMappings())
2099 ErrorExit(modelNames[i] + " has different per-geometry bone mappings than " + modelNames[0]);
2100 }
2101
2102 Vector<SharedPtr<VertexBuffer> > vbVector;
2103 Vector<SharedPtr<IndexBuffer> > ibVector;
2104 PODVector<unsigned> emptyMorphRange;
2105
2106 // Create the final model
2107 SharedPtr<Model> outModel(new Model(context_));
2108 outModel->SetNumGeometries(srcModels[0]->GetNumGeometries());
2109 for (unsigned i = 0; i < srcModels[0]->GetNumGeometries(); ++i)
2110 {
2111 outModel->SetNumGeometryLodLevels(i, srcModels.Size());
2112 for (unsigned j = 0; j < srcModels.Size(); ++j)
2113 {
2114 Geometry* geometry = srcModels[j]->GetGeometry(i, 0);
2115 geometry->SetLodDistance(lodDistances[j]);
2116 outModel->SetGeometry(i, j, geometry);
2117
2118 for (unsigned k = 0; k < geometry->GetNumVertexBuffers(); ++k)
2119 {
2120 SharedPtr<VertexBuffer> vb(geometry->GetVertexBuffer(k));
2121 if (!vbVector.Contains(vb))
2122 vbVector.Push(vb);
2123 }
2124
2125 SharedPtr<IndexBuffer> ib(geometry->GetIndexBuffer());
2126 if (!ibVector.Contains(ib))
2127 ibVector.Push(ib);
2128 }
2129 }
2130
2131 outModel->SetVertexBuffers(vbVector, emptyMorphRange, emptyMorphRange);
2132 outModel->SetIndexBuffers(ibVector);
2133 outModel->SetSkeleton(srcModels[0]->GetSkeleton());
2134 outModel->SetGeometryBoneMappings(srcModels[0]->GetGeometryBoneMappings());
2135 outModel->SetBoundingBox(srcModels[0]->GetBoundingBox());
2136 /// \todo Vertex morphs are ignored for now
2137
2138 // Save the final model
2139 PrintLine("Writing output model");
2140 File outFile(context_);
2141 if (!outFile.Open(outName, FILE_WRITE))
2142 ErrorExit("Could not open output file " + outName);
2143 outModel->Save(outFile);
2144 }
2145
GetMeshesUnderNode(Vector<Pair<aiNode *,aiMesh * >> & dest,aiNode * node)2146 void GetMeshesUnderNode(Vector<Pair<aiNode*, aiMesh*> >& dest, aiNode* node)
2147 {
2148 for (unsigned i = 0; i < node->mNumMeshes; ++i)
2149 dest.Push(MakePair(node, scene_->mMeshes[node->mMeshes[i]]));
2150 }
2151
GetMeshIndex(aiMesh * mesh)2152 unsigned GetMeshIndex(aiMesh* mesh)
2153 {
2154 for (unsigned i = 0; i < scene_->mNumMeshes; ++i)
2155 {
2156 if (scene_->mMeshes[i] == mesh)
2157 return i;
2158 }
2159 return M_MAX_UNSIGNED;
2160 }
2161
GetBoneIndex(OutModel & model,const String & boneName)2162 unsigned GetBoneIndex(OutModel& model, const String& boneName)
2163 {
2164 for (unsigned i = 0; i < model.bones_.Size(); ++i)
2165 {
2166 if (boneName == model.bones_[i]->mName.data)
2167 return i;
2168 }
2169 return M_MAX_UNSIGNED;
2170 }
2171
GetMeshBone(OutModel & model,const String & boneName)2172 aiBone* GetMeshBone(OutModel& model, const String& boneName)
2173 {
2174 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
2175 {
2176 aiMesh* mesh = model.meshes_[i];
2177 for (unsigned j = 0; j < mesh->mNumBones; ++j)
2178 {
2179 aiBone* bone = mesh->mBones[j];
2180 if (boneName == bone->mName.data)
2181 return bone;
2182 }
2183 }
2184 return 0;
2185 }
2186
GetOffsetMatrix(OutModel & model,const String & boneName)2187 Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName)
2188 {
2189 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
2190 {
2191 aiMesh* mesh = model.meshes_[i];
2192 aiNode* node = model.meshNodes_[i];
2193 for (unsigned j = 0; j < mesh->mNumBones; ++j)
2194 {
2195 aiBone* bone = mesh->mBones[j];
2196 if (boneName == bone->mName.data)
2197 {
2198 aiMatrix4x4 offset = bone->mOffsetMatrix;
2199 aiMatrix4x4 nodeDerivedInverse = GetMeshBakingTransform(node, model.rootNode_);
2200 nodeDerivedInverse.Inverse();
2201 offset *= nodeDerivedInverse;
2202 return ToMatrix3x4(offset);
2203 }
2204 }
2205 }
2206
2207 // Fallback for rigid skinning for which actual offset matrix information doesn't exist
2208 for (unsigned i = 0; i < model.meshes_.Size(); ++i)
2209 {
2210 aiMesh* mesh = model.meshes_[i];
2211 aiNode* node = model.meshNodes_[i];
2212 if (!mesh->HasBones() && boneName == node->mName.data)
2213 {
2214 aiMatrix4x4 nodeDerivedInverse = GetMeshBakingTransform(node, model.rootNode_);
2215 nodeDerivedInverse.Inverse();
2216 return ToMatrix3x4(nodeDerivedInverse);
2217 }
2218 }
2219
2220 return Matrix3x4::IDENTITY;
2221 }
2222
GetBlendData(OutModel & model,aiMesh * mesh,aiNode * meshNode,PODVector<unsigned> & boneMappings,Vector<PODVector<unsigned char>> & blendIndices,Vector<PODVector<float>> & blendWeights)2223 void GetBlendData(OutModel& model, aiMesh* mesh, aiNode* meshNode, PODVector<unsigned>& boneMappings, Vector<PODVector<unsigned char> >&
2224 blendIndices, Vector<PODVector<float> >& blendWeights)
2225 {
2226 blendIndices.Resize(mesh->mNumVertices);
2227 blendWeights.Resize(mesh->mNumVertices);
2228 boneMappings.Clear();
2229
2230 // If model has more bones than can fit vertex shader parameters, write the per-geometry mappings
2231 if (model.bones_.Size() > maxBones_)
2232 {
2233 if (mesh->mNumBones > maxBones_)
2234 {
2235 ErrorExit(
2236 "Geometry (submesh) has over " + String(maxBones_) + " bone influences. Try splitting to more submeshes\n"
2237 "that each stay at " + String(maxBones_) + " bones or below."
2238 );
2239 }
2240 if (mesh->mNumBones > 0)
2241 {
2242 boneMappings.Resize(mesh->mNumBones);
2243 for (unsigned i = 0; i < mesh->mNumBones; ++i)
2244 {
2245 aiBone* bone = mesh->mBones[i];
2246 String boneName = FromAIString(bone->mName);
2247 unsigned globalIndex = GetBoneIndex(model, boneName);
2248 if (globalIndex == M_MAX_UNSIGNED)
2249 ErrorExit("Bone " + boneName + " not found");
2250 boneMappings[i] = globalIndex;
2251 for (unsigned j = 0; j < bone->mNumWeights; ++j)
2252 {
2253 unsigned vertex = bone->mWeights[j].mVertexId;
2254 blendIndices[vertex].Push(i);
2255 blendWeights[vertex].Push(bone->mWeights[j].mWeight);
2256 }
2257 }
2258 }
2259 else
2260 {
2261 // If mesh does not have skinning information, implement rigid skinning so that it stays compatible with AnimatedModel
2262 String boneName = FromAIString(meshNode->mName);
2263 unsigned globalIndex = GetBoneIndex(model, boneName);
2264 if (globalIndex == M_MAX_UNSIGNED)
2265 PrintLine("Warning: bone " + boneName + " not found, skipping rigid skinning");
2266 else
2267 {
2268 boneMappings.Push(globalIndex);
2269 for (unsigned i = 0; i < mesh->mNumVertices; ++i)
2270 {
2271 blendIndices[i].Push(0);
2272 blendWeights[i].Push(1.0f);
2273 }
2274 }
2275 }
2276 }
2277 else
2278 {
2279 if (mesh->mNumBones > 0)
2280 {
2281 for (unsigned i = 0; i < mesh->mNumBones; ++i)
2282 {
2283 aiBone* bone = mesh->mBones[i];
2284 String boneName = FromAIString(bone->mName);
2285 unsigned globalIndex = GetBoneIndex(model, boneName);
2286 if (globalIndex == M_MAX_UNSIGNED)
2287 ErrorExit("Bone " + boneName + " not found");
2288 for (unsigned j = 0; j < bone->mNumWeights; ++j)
2289 {
2290 unsigned vertex = bone->mWeights[j].mVertexId;
2291 blendIndices[vertex].Push(globalIndex);
2292 blendWeights[vertex].Push(bone->mWeights[j].mWeight);
2293 }
2294 }
2295 }
2296 else
2297 {
2298 String boneName = FromAIString(meshNode->mName);
2299 unsigned globalIndex = GetBoneIndex(model, boneName);
2300 if (globalIndex == M_MAX_UNSIGNED)
2301 PrintLine("Warning: bone " + boneName + " not found, skipping rigid skinning");
2302 else
2303 {
2304 for (unsigned i = 0; i < mesh->mNumVertices; ++i)
2305 {
2306 blendIndices[i].Push(globalIndex);
2307 blendWeights[i].Push(1.0f);
2308 }
2309 }
2310 }
2311 }
2312
2313 // Normalize weights now if necessary, also remove too many influences
2314 for (unsigned i = 0; i < blendWeights.Size(); ++i)
2315 {
2316 if (blendWeights[i].Size() > 4)
2317 {
2318 PrintLine("Warning: more than 4 bone influences in vertex " + String(i));
2319
2320 while (blendWeights[i].Size() > 4)
2321 {
2322 unsigned lowestIndex = 0;
2323 float lowest = M_INFINITY;
2324 for (unsigned j = 0; j < blendWeights[i].Size(); ++j)
2325 {
2326 if (blendWeights[i][j] < lowest)
2327 {
2328 lowest = blendWeights[i][j];
2329 lowestIndex = j;
2330 }
2331 }
2332 blendWeights[i].Erase(lowestIndex);
2333 blendIndices[i].Erase(lowestIndex);
2334 }
2335 }
2336
2337 float sum = 0.0f;
2338 for (unsigned j = 0; j < blendWeights[i].Size(); ++j)
2339 sum += blendWeights[i][j];
2340 if (sum != 1.0f && sum != 0.0f)
2341 {
2342 for (unsigned j = 0; j < blendWeights[i].Size(); ++j)
2343 blendWeights[i][j] /= sum;
2344 }
2345 }
2346 }
2347
GetMeshMaterialName(aiMesh * mesh)2348 String GetMeshMaterialName(aiMesh* mesh)
2349 {
2350 aiMaterial* material = scene_->mMaterials[mesh->mMaterialIndex];
2351 aiString matNameStr;
2352 material->Get(AI_MATKEY_NAME, matNameStr);
2353 String matName = SanitateAssetName(FromAIString(matNameStr));
2354 if (matName.Trimmed().Empty())
2355 matName = GenerateMaterialName(material);
2356
2357 return (useSubdirs_ ? "Materials/" : "") + matName + ".xml";
2358 }
2359
GenerateMaterialName(aiMaterial * material)2360 String GenerateMaterialName(aiMaterial* material)
2361 {
2362 for (unsigned i = 0; i < scene_->mNumMaterials; ++i)
2363 {
2364 if (scene_->mMaterials[i] == material)
2365 return inputName_ + "_Material" + String(i);
2366 }
2367
2368 // Should not go here
2369 return String::EMPTY;
2370 }
2371
GetMaterialTextureName(const String & nameIn)2372 String GetMaterialTextureName(const String& nameIn)
2373 {
2374 // Detect assimp embedded texture
2375 if (nameIn.Length() && nameIn[0] == '*')
2376 return GenerateTextureName(ToInt(nameIn.Substring(1)));
2377 else
2378 return (useSubdirs_ ? "Textures/" : "") + nameIn;
2379 }
2380
GenerateTextureName(unsigned texIndex)2381 String GenerateTextureName(unsigned texIndex)
2382 {
2383 if (texIndex < scene_->mNumTextures)
2384 {
2385 // If embedded texture contains encoded data, use the format hint for file extension. Else save RGBA8 data as PNG
2386 aiTexture* tex = scene_->mTextures[texIndex];
2387 if (!tex->mHeight)
2388 return (useSubdirs_ ? "Textures/" : "") + inputName_ + "_Texture" + String(texIndex) + "." + tex->achFormatHint;
2389 else
2390 return (useSubdirs_ ? "Textures/" : "") + inputName_ + "_Texture" + String(texIndex) + ".png";
2391 }
2392
2393 // Should not go here
2394 return String::EMPTY;
2395 }
2396
GetNumValidFaces(aiMesh * mesh)2397 unsigned GetNumValidFaces(aiMesh* mesh)
2398 {
2399 unsigned ret = 0;
2400
2401 for (unsigned j = 0; j < mesh->mNumFaces; ++j)
2402 {
2403 if (mesh->mFaces[j].mNumIndices == 3)
2404 ++ret;
2405 }
2406
2407 return ret;
2408 }
2409
WriteShortIndices(unsigned short * & dest,aiMesh * mesh,unsigned index,unsigned offset)2410 void WriteShortIndices(unsigned short*& dest, aiMesh* mesh, unsigned index, unsigned offset)
2411 {
2412 if (mesh->mFaces[index].mNumIndices == 3)
2413 {
2414 *dest++ = mesh->mFaces[index].mIndices[0] + offset;
2415 *dest++ = mesh->mFaces[index].mIndices[1] + offset;
2416 *dest++ = mesh->mFaces[index].mIndices[2] + offset;
2417 }
2418 }
2419
WriteLargeIndices(unsigned * & dest,aiMesh * mesh,unsigned index,unsigned offset)2420 void WriteLargeIndices(unsigned*& dest, aiMesh* mesh, unsigned index, unsigned offset)
2421 {
2422 if (mesh->mFaces[index].mNumIndices == 3)
2423 {
2424 *dest++ = mesh->mFaces[index].mIndices[0] + offset;
2425 *dest++ = mesh->mFaces[index].mIndices[1] + offset;
2426 *dest++ = mesh->mFaces[index].mIndices[2] + offset;
2427 }
2428 }
2429
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)2430 void WriteVertex(float*& dest, aiMesh* mesh, unsigned index, bool isSkinned, BoundingBox& box,
2431 const Matrix3x4& vertexTransform, const Matrix3& normalTransform, Vector<PODVector<unsigned char> >& blendIndices,
2432 Vector<PODVector<float> >& blendWeights)
2433 {
2434 Vector3 vertex = vertexTransform * ToVector3(mesh->mVertices[index]);
2435 box.Merge(vertex);
2436 *dest++ = vertex.x_;
2437 *dest++ = vertex.y_;
2438 *dest++ = vertex.z_;
2439
2440 if (mesh->HasNormals())
2441 {
2442 Vector3 normal = normalTransform * ToVector3(mesh->mNormals[index]);
2443 *dest++ = normal.x_;
2444 *dest++ = normal.y_;
2445 *dest++ = normal.z_;
2446 }
2447
2448 for (unsigned i = 0; i < mesh->GetNumColorChannels() && i < MAX_CHANNELS; ++i)
2449 {
2450 *((unsigned*)dest) = Color(mesh->mColors[i][index].r, mesh->mColors[i][index].g, mesh->mColors[i][index].b,
2451 mesh->mColors[i][index].a).ToUInt();
2452 ++dest;
2453 }
2454
2455 for (unsigned i = 0; i < mesh->GetNumUVChannels() && i < MAX_CHANNELS; ++i)
2456 {
2457 Vector3 texCoord = ToVector3(mesh->mTextureCoords[i][index]);
2458 *dest++ = texCoord.x_;
2459 *dest++ = texCoord.y_;
2460 }
2461
2462 if (mesh->HasTangentsAndBitangents())
2463 {
2464 Vector3 tangent = normalTransform * ToVector3(mesh->mTangents[index]);
2465 Vector3 normal = normalTransform * ToVector3(mesh->mNormals[index]);
2466 Vector3 bitangent = normalTransform * ToVector3(mesh->mBitangents[index]);
2467 // Check handedness
2468 float w = 1.0f;
2469 if ((tangent.CrossProduct(normal)).DotProduct(bitangent) < 0.5f)
2470 w = -1.0f;
2471
2472 *dest++ = tangent.x_;
2473 *dest++ = tangent.y_;
2474 *dest++ = tangent.z_;
2475 *dest++ = w;
2476 }
2477
2478 if (isSkinned)
2479 {
2480 for (unsigned i = 0; i < 4; ++i)
2481 {
2482 if (i < blendWeights[index].Size())
2483 *dest++ = blendWeights[index][i];
2484 else
2485 *dest++ = 0.0f;
2486 }
2487
2488 unsigned char* destBytes = (unsigned char*)dest;
2489 ++dest;
2490 for (unsigned i = 0; i < 4; ++i)
2491 {
2492 if (i < blendIndices[index].Size())
2493 *destBytes++ = blendIndices[index][i];
2494 else
2495 *destBytes++ = 0;
2496 }
2497 }
2498 }
2499
GetVertexElements(aiMesh * mesh,bool isSkinned)2500 PODVector<VertexElement> GetVertexElements(aiMesh* mesh, bool isSkinned)
2501 {
2502 PODVector<VertexElement> ret;
2503
2504 // Position must always be first and of type Vector3 for raycasts to work
2505 ret.Push(VertexElement(TYPE_VECTOR3, SEM_POSITION));
2506
2507 if (mesh->HasNormals())
2508 ret.Push(VertexElement(TYPE_VECTOR3, SEM_NORMAL));
2509
2510 for (unsigned i = 0; i < mesh->GetNumColorChannels() && i < MAX_CHANNELS; ++i)
2511 ret.Push(VertexElement(TYPE_UBYTE4_NORM, SEM_COLOR, i));
2512
2513 /// \todo Assimp mesh structure can specify 3D UV-coords. How to determine the difference? For now always treated as 2D.
2514 for (unsigned i = 0; i < mesh->GetNumUVChannels() && i < MAX_CHANNELS; ++i)
2515 ret.Push(VertexElement(TYPE_VECTOR2, SEM_TEXCOORD, i));
2516
2517 if (mesh->HasTangentsAndBitangents())
2518 ret.Push(VertexElement(TYPE_VECTOR4, SEM_TANGENT));
2519
2520 if (isSkinned)
2521 {
2522 ret.Push(VertexElement(TYPE_VECTOR4, SEM_BLENDWEIGHTS));
2523 ret.Push(VertexElement(TYPE_UBYTE4, SEM_BLENDINDICES));
2524 }
2525
2526 return ret;
2527 }
2528
GetNode(const String & name,aiNode * rootNode,bool caseSensitive)2529 aiNode* GetNode(const String& name, aiNode* rootNode, bool caseSensitive)
2530 {
2531 if (!rootNode)
2532 return 0;
2533 if (!name.Compare(rootNode->mName.data, caseSensitive))
2534 return rootNode;
2535 for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
2536 {
2537 aiNode* found = GetNode(name, rootNode->mChildren[i], caseSensitive);
2538 if (found)
2539 return found;
2540 }
2541 return 0;
2542 }
2543
GetDerivedTransform(aiNode * node,aiNode * rootNode,bool rootInclusive)2544 aiMatrix4x4 GetDerivedTransform(aiNode* node, aiNode* rootNode, bool rootInclusive)
2545 {
2546 return GetDerivedTransform(node->mTransformation, node, rootNode, rootInclusive);
2547 }
2548
GetDerivedTransform(aiMatrix4x4 transform,aiNode * node,aiNode * rootNode,bool rootInclusive)2549 aiMatrix4x4 GetDerivedTransform(aiMatrix4x4 transform, aiNode* node, aiNode* rootNode, bool rootInclusive)
2550 {
2551 // If basenode is defined, go only up to it in the parent chain
2552 while (node && node != rootNode)
2553 {
2554 node = node->mParent;
2555 if (!rootInclusive && node == rootNode)
2556 break;
2557 if (node)
2558 transform = node->mTransformation * transform;
2559 }
2560 return transform;
2561 }
2562
GetMeshBakingTransform(aiNode * meshNode,aiNode * modelRootNode)2563 aiMatrix4x4 GetMeshBakingTransform(aiNode* meshNode, aiNode* modelRootNode)
2564 {
2565 if (meshNode == modelRootNode)
2566 return aiMatrix4x4();
2567 else
2568 return GetDerivedTransform(meshNode, modelRootNode);
2569 }
2570
GetPosRotScale(const aiMatrix4x4 & transform,Vector3 & pos,Quaternion & rot,Vector3 & scale)2571 void GetPosRotScale(const aiMatrix4x4& transform, Vector3& pos, Quaternion& rot, Vector3& scale)
2572 {
2573 aiVector3D aiPos;
2574 aiQuaternion aiRot;
2575 aiVector3D aiScale;
2576 transform.Decompose(aiScale, aiRot, aiPos);
2577 pos = ToVector3(aiPos);
2578 rot = ToQuaternion(aiRot);
2579 scale = ToVector3(aiScale);
2580 }
2581
2582
FromAIString(const aiString & str)2583 String FromAIString(const aiString& str)
2584 {
2585 return String(str.data);
2586 }
2587
ToVector3(const aiVector3D & vec)2588 Vector3 ToVector3(const aiVector3D& vec)
2589 {
2590 return Vector3(vec.x, vec.y, vec.z);
2591 }
2592
ToVector2(const aiVector2D & vec)2593 Vector2 ToVector2(const aiVector2D& vec)
2594 {
2595 return Vector2(vec.x, vec.y);
2596 }
2597
ToQuaternion(const aiQuaternion & quat)2598 Quaternion ToQuaternion(const aiQuaternion& quat)
2599 {
2600 return Quaternion(quat.w, quat.x, quat.y, quat.z);
2601 }
2602
ToMatrix3x4(const aiMatrix4x4 & mat)2603 Matrix3x4 ToMatrix3x4(const aiMatrix4x4& mat)
2604 {
2605 Matrix3x4 ret;
2606 memcpy(&ret.m00_, &mat.a1, sizeof(Matrix3x4));
2607 return ret;
2608 }
2609
ToAIMatrix4x4(const Matrix3x4 & mat)2610 aiMatrix4x4 ToAIMatrix4x4(const Matrix3x4& mat)
2611 {
2612 aiMatrix4x4 ret;
2613 memcpy(&ret.a1, &mat.m00_, sizeof(Matrix3x4));
2614 return ret;
2615 }
2616
SanitateAssetName(const String & name)2617 String SanitateAssetName(const String& name)
2618 {
2619 String fixedName = name;
2620 fixedName.Replace("<", "");
2621 fixedName.Replace(">", "");
2622 fixedName.Replace("?", "");
2623 fixedName.Replace("*", "");
2624 fixedName.Replace(":", "");
2625 fixedName.Replace("\"", "");
2626 fixedName.Replace("/", "");
2627 fixedName.Replace("\\", "");
2628 fixedName.Replace("|", "");
2629
2630 return fixedName;
2631 }
2632
GetPivotlessBoneIndex(OutModel & model,const String & boneName)2633 unsigned GetPivotlessBoneIndex(OutModel& model, const String& boneName)
2634 {
2635 for (unsigned i = 0; i < model.pivotlessBones_.Size(); ++i)
2636 {
2637 if (boneName == model.pivotlessBones_[i]->mName.data)
2638 return i;
2639 }
2640 return M_MAX_UNSIGNED;
2641 }
2642
FillChainTransforms(OutModel & model,aiMatrix4x4 * chain,const String & mainBoneName)2643 void FillChainTransforms(OutModel &model, aiMatrix4x4 *chain, const String& mainBoneName)
2644 {
2645 for (unsigned j = 0; j < TransformationComp_MAXIMUM; ++j)
2646 {
2647 String transfBoneName = mainBoneName + "_$AssimpFbx$_" + String(transformSuffix[j]);
2648
2649 for (unsigned k = 0; k < model.bones_.Size(); ++k)
2650 {
2651 String boneName = String(model.bones_[k]->mName.data);
2652
2653 if (boneName == transfBoneName)
2654 {
2655 chain[j] = model.bones_[k]->mTransformation;
2656 break;
2657 }
2658 }
2659 }
2660 }
2661
ExpandAnimatedChannelKeys(aiAnimation * anim,unsigned mainChannel,int * channelIndices)2662 void ExpandAnimatedChannelKeys(aiAnimation* anim, unsigned mainChannel, int *channelIndices)
2663 {
2664 aiNodeAnim* channel = anim->mChannels[mainChannel];
2665 unsigned int poskeyFrames = channel->mNumPositionKeys;
2666 unsigned int rotkeyFrames = channel->mNumRotationKeys;
2667 unsigned int scalekeyFrames = channel->mNumScalingKeys;
2668
2669 // Get max key frames
2670 for (unsigned i = 0; i < TransformationComp_MAXIMUM; ++i)
2671 {
2672 if (channelIndices[i] != -1 && channelIndices[i] != mainChannel)
2673 {
2674 aiNodeAnim* channel2 = anim->mChannels[channelIndices[i]];
2675
2676 if (channel2->mNumPositionKeys > poskeyFrames)
2677 poskeyFrames = channel2->mNumPositionKeys;
2678 if (channel2->mNumRotationKeys > rotkeyFrames)
2679 rotkeyFrames = channel2->mNumRotationKeys;
2680 if (channel2->mNumScalingKeys > scalekeyFrames)
2681 scalekeyFrames = channel2->mNumScalingKeys;
2682 }
2683 }
2684
2685 // Resize and init vector key array
2686 if (poskeyFrames > channel->mNumPositionKeys)
2687 {
2688 aiVectorKey* newKeys = new aiVectorKey[poskeyFrames];
2689 for (unsigned i = 0; i < poskeyFrames; ++i)
2690 {
2691 if (i < channel->mNumPositionKeys )
2692 newKeys[i] = aiVectorKey(channel->mPositionKeys[i].mTime, channel->mPositionKeys[i].mValue);
2693 else
2694 newKeys[i].mValue = aiVector3D(0.0f, 0.0f, 0.0f);
2695 }
2696 delete[] channel->mPositionKeys;
2697 channel->mPositionKeys = newKeys;
2698 channel->mNumPositionKeys = poskeyFrames;
2699 }
2700 if (rotkeyFrames > channel->mNumRotationKeys)
2701 {
2702 aiQuatKey* newKeys = new aiQuatKey[rotkeyFrames];
2703 for (unsigned i = 0; i < rotkeyFrames; ++i)
2704 {
2705 if (i < channel->mNumRotationKeys)
2706 newKeys[i] = aiQuatKey(channel->mRotationKeys[i].mTime, channel->mRotationKeys[i].mValue);
2707 else
2708 newKeys[i].mValue = aiQuaternion();
2709 }
2710 delete[] channel->mRotationKeys;
2711 channel->mRotationKeys = newKeys;
2712 channel->mNumRotationKeys = rotkeyFrames;
2713 }
2714 if (scalekeyFrames > channel->mNumScalingKeys)
2715 {
2716 aiVectorKey* newKeys = new aiVectorKey[scalekeyFrames];
2717 for (unsigned i = 0; i < scalekeyFrames; ++i)
2718 {
2719 if ( i < channel->mNumScalingKeys)
2720 newKeys[i] = aiVectorKey(channel->mScalingKeys[i].mTime, channel->mScalingKeys[i].mValue);
2721 else
2722 newKeys[i].mValue = aiVector3D(1.0f, 1.0f, 1.0f);
2723 }
2724 delete[] channel->mScalingKeys;
2725 channel->mScalingKeys = newKeys;
2726 channel->mNumScalingKeys = scalekeyFrames;
2727 }
2728 }
2729
InitAnimatedChainTransformIndices(aiAnimation * anim,unsigned mainChannel,const String & mainBoneName,int * channelIndices)2730 void InitAnimatedChainTransformIndices(aiAnimation* anim, unsigned mainChannel, const String& mainBoneName, int *channelIndices)
2731 {
2732 int numTransforms = 0;
2733
2734 for (unsigned j = 0; j < TransformationComp_MAXIMUM; ++j)
2735 {
2736 String transfBoneName = mainBoneName + "_$AssimpFbx$_" + String(transformSuffix[j]);
2737 channelIndices[j] = -1;
2738
2739 for (unsigned k = 0; k < anim->mNumChannels; ++k)
2740 {
2741 aiNodeAnim* channel = anim->mChannels[k];
2742 String channelName = FromAIString(channel->mNodeName);
2743
2744 if (channelName == transfBoneName)
2745 {
2746 ++numTransforms;
2747 channelIndices[j] = k;
2748 break;
2749 }
2750 }
2751 }
2752
2753 // resize animated channel key size
2754 if (numTransforms > 1)
2755 ExpandAnimatedChannelKeys(anim, mainChannel, channelIndices);
2756 }
2757
CreatePivotlessFbxBoneStruct(OutModel & model)2758 void CreatePivotlessFbxBoneStruct(OutModel &model)
2759 {
2760 // Init
2761 model.pivotlessBones_.Clear();
2762 aiMatrix4x4 chain[TransformationComp_MAXIMUM];
2763
2764 for (unsigned i = 0; i < model.bones_.Size(); ++i)
2765 {
2766 String mainBoneName = String(model.bones_[i]->mName.data);
2767
2768 // Skip $fbx nodes
2769 if (mainBoneName.Find("$AssimpFbx$") != String::NPOS)
2770 continue;
2771
2772 std::fill_n(chain, static_cast<unsigned int>(TransformationComp_MAXIMUM), aiMatrix4x4());
2773 FillChainTransforms(model, &chain[0], mainBoneName);
2774
2775 // Calculate chained transform
2776 aiMatrix4x4 finalTransform;
2777 for (unsigned j = 0; j < TransformationComp_MAXIMUM; ++j)
2778 finalTransform = finalTransform * chain[j];
2779
2780 // New bone node
2781 aiNode *pnode = new aiNode;
2782 pnode->mName = model.bones_[i]->mName;
2783 pnode->mTransformation = finalTransform * model.bones_[i]->mTransformation;
2784
2785 model.pivotlessBones_.Push(pnode);
2786 }
2787 }
2788
ExtrapolatePivotlessAnimation(OutModel * model)2789 void ExtrapolatePivotlessAnimation(OutModel* model)
2790 {
2791 if (suppressFbxPivotNodes_ && model)
2792 {
2793 PrintLine("Suppressing $fbx nodes");
2794
2795 // Construct new bone structure from suppressed $fbx pivot nodes
2796 CreatePivotlessFbxBoneStruct(*model);
2797
2798 // Extrapolate anim
2799 const PODVector<aiAnimation *> &animations = model->animations_;
2800 for (unsigned i = 0; i < animations.Size(); ++i)
2801 {
2802 aiAnimation* anim = animations[i];
2803 Vector<String> mainBoneCompleteList;
2804 mainBoneCompleteList.Clear();
2805
2806 for (unsigned j = 0; j < anim->mNumChannels; ++j)
2807 {
2808 aiNodeAnim* channel = anim->mChannels[j];
2809 String channelName = FromAIString(channel->mNodeName);
2810 unsigned pos = channelName.Find("_$AssimpFbx$");
2811
2812 if (pos != String::NPOS)
2813 {
2814 // Every first $fbx animation channel for a bone will consolidate other $fbx animation to a single channel
2815 // skip subsequent $fbx animation channel for the same bone
2816 String mainBoneName = channelName.Substring(0, pos);
2817
2818 if (mainBoneCompleteList.Find(mainBoneName) != mainBoneCompleteList.End())
2819 continue;
2820
2821 mainBoneCompleteList.Push(mainBoneName);
2822 unsigned boneIdx = GetBoneIndex(*model, mainBoneName);
2823
2824 // This condition exists if a geometry, not a bone, has a key animation
2825 if (boneIdx == M_MAX_UNSIGNED)
2826 continue;
2827
2828 // Init chain indices and fill transforms
2829 aiMatrix4x4 mainboneTransform = model->bones_[boneIdx]->mTransformation;
2830 aiMatrix4x4 chain[TransformationComp_MAXIMUM];
2831 int channelIndices[TransformationComp_MAXIMUM];
2832
2833 InitAnimatedChainTransformIndices(anim, j, mainBoneName, &channelIndices[0]);
2834 std::fill_n(chain, static_cast<unsigned int>(TransformationComp_MAXIMUM), aiMatrix4x4());
2835 FillChainTransforms(*model, &chain[0], mainBoneName);
2836
2837 unsigned keyFrames = channel->mNumPositionKeys;
2838 if (channel->mNumRotationKeys > keyFrames)
2839 keyFrames = channel->mNumRotationKeys;
2840 if (channel->mNumScalingKeys > keyFrames)
2841 keyFrames = channel->mNumScalingKeys;
2842
2843 for (unsigned k = 0; k < keyFrames; ++k)
2844 {
2845 double frameTime = 0.0;
2846 aiMatrix4x4 finalTransform;
2847
2848 // Chain transform animated values
2849 for (unsigned l = 0; l < TransformationComp_MAXIMUM; ++l)
2850 {
2851 // It's either the chain transform or animation channel transform
2852 if (channelIndices[l] != -1)
2853 {
2854 aiMatrix4x4 animtform, tempMat;
2855 aiNodeAnim* animchannel = anim->mChannels[channelIndices[l]];
2856
2857 if (k < animchannel->mNumPositionKeys)
2858 {
2859 aiMatrix4x4::Translation(animchannel->mPositionKeys[k].mValue, tempMat);
2860 animtform = animtform * tempMat;
2861 frameTime = Max(animchannel->mPositionKeys[k].mTime, frameTime);
2862 }
2863 if (k < animchannel->mNumRotationKeys)
2864 {
2865 tempMat = aiMatrix4x4(animchannel->mRotationKeys[k].mValue.GetMatrix());
2866 animtform = animtform * tempMat;
2867 frameTime = Max(animchannel->mRotationKeys[k].mTime, frameTime);
2868 }
2869 if (k < animchannel->mNumScalingKeys)
2870 {
2871 aiMatrix4x4::Scaling(animchannel->mScalingKeys[k].mValue, tempMat);
2872 animtform = animtform * tempMat;
2873 frameTime = Max(animchannel->mScalingKeys[k].mTime, frameTime);
2874 }
2875
2876 finalTransform = finalTransform * animtform;
2877 }
2878 else
2879 finalTransform = finalTransform * chain[l];
2880 }
2881
2882 aiVector3D animPos, animScale;
2883 aiQuaternion animRot;
2884 finalTransform = finalTransform * mainboneTransform;
2885 finalTransform.Decompose(animScale, animRot, animPos);
2886
2887 // New values
2888 if (k < channel->mNumPositionKeys)
2889 {
2890 channel->mPositionKeys[k].mValue = animPos;
2891 channel->mPositionKeys[k].mTime = frameTime;
2892 }
2893
2894 if (k < channel->mNumRotationKeys)
2895 {
2896 channel->mRotationKeys[k].mValue = animRot;
2897 channel->mRotationKeys[k].mTime = frameTime;
2898 }
2899
2900 if (k < channel->mNumScalingKeys)
2901 {
2902 channel->mScalingKeys[k].mValue = animScale;
2903 channel->mScalingKeys[k].mTime = frameTime;
2904 }
2905 }
2906 }
2907 }
2908 }
2909 }
2910 }
2911
CollectSceneNodesAsBones(OutModel & model,aiNode * rootNode)2912 void CollectSceneNodesAsBones(OutModel &model, aiNode* rootNode)
2913 {
2914 if (!rootNode)
2915 return;
2916
2917 model.bones_.Push(rootNode);
2918
2919 for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
2920 {
2921 CollectSceneNodesAsBones(model, rootNode->mChildren[i]);
2922 }
2923 }
2924
2925