1 /*
2 Open Asset Import Library (assimp)
3 ----------------------------------------------------------------------
4
5 Copyright (c) 2006-2021, assimp team
6
7 All rights reserved.
8
9 Redistribution and use of this software in source and binary forms,
10 with or without modification, are permitted provided that the
11 following conditions are met:
12
13 * Redistributions of source code must retain the above
14 copyright notice, this list of conditions and the
15 following disclaimer.
16
17 * Redistributions in binary form must reproduce the above
18 copyright notice, this list of conditions and the
19 following disclaimer in the documentation and/or other
20 materials provided with the distribution.
21
22 * Neither the name of the assimp team, nor the names of its
23 contributors may be used to endorse or promote products
24 derived from this software without specific prior
25 written permission of the assimp team.
26
27 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
39 ----------------------------------------------------------------------
40 */
41 #ifndef ASSIMP_BUILD_NO_EXPORT
42 #ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
43
44 #include "FBXExporter.h"
45 #include "FBXExportNode.h"
46 #include "FBXExportProperty.h"
47 #include "FBXCommon.h"
48 #include "FBXUtil.h"
49
50 #include <assimp/version.h> // aiGetVersion
51 #include <assimp/IOSystem.hpp>
52 #include <assimp/Exporter.hpp>
53 #include <assimp/DefaultLogger.hpp>
54 #include <assimp/StreamWriter.h> // StreamWriterLE
55 #include <assimp/Exceptional.h> // DeadlyExportError
56 #include <assimp/material.h> // aiTextureType
57 #include <assimp/scene.h>
58 #include <assimp/mesh.h>
59
60 // Header files, standard library.
61 #include <memory> // shared_ptr
62 #include <string>
63 #include <sstream> // stringstream
64 #include <ctime> // localtime, tm_*
65 #include <map>
66 #include <set>
67 #include <vector>
68 #include <array>
69 #include <unordered_set>
70 #include <numeric>
71
72 // RESOURCES:
73 // https://code.blender.org/2013/08/fbx-binary-file-format-specification/
74 // https://wiki.blender.org/index.php/User:Mont29/Foundation/FBX_File_Structure
75
76 const ai_real DEG = ai_real( 57.29577951308232087679815481 ); // degrees per radian
77
78 using namespace Assimp;
79 using namespace Assimp::FBX;
80
81 // some constants that we'll use for writing metadata
82 namespace Assimp {
83 namespace FBX {
84 const std::string EXPORT_VERSION_STR = "7.5.0";
85 const uint32_t EXPORT_VERSION_INT = 7500; // 7.5 == 2016+
86 // FBX files have some hashed values that depend on the creation time field,
87 // but for now we don't actually know how to generate these.
88 // what we can do is set them to a known-working version.
89 // this is the data that Blender uses in their FBX export process.
90 const std::string GENERIC_CTIME = "1970-01-01 10:00:00:000";
91 const std::string GENERIC_FILEID =
92 "\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1";
93 const std::string GENERIC_FOOTID =
94 "\xfa\xbc\xab\x09\xd0\xc8\xd4\x66\xb1\x76\xfb\x83\x1c\xf7\x26\x7e";
95 const std::string FOOT_MAGIC =
96 "\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b";
97 const std::string COMMENT_UNDERLINE =
98 ";------------------------------------------------------------------";
99 }
100
101 // ---------------------------------------------------------------------
102 // Worker function for exporting a scene to binary FBX.
103 // Prototyped and registered in Exporter.cpp
ExportSceneFBX(const char * pFile,IOSystem * pIOSystem,const aiScene * pScene,const ExportProperties * pProperties)104 void ExportSceneFBX (
105 const char* pFile,
106 IOSystem* pIOSystem,
107 const aiScene* pScene,
108 const ExportProperties* pProperties
109 ){
110 // initialize the exporter
111 FBXExporter exporter(pScene, pProperties);
112
113 // perform binary export
114 exporter.ExportBinary(pFile, pIOSystem);
115 }
116
117 // ---------------------------------------------------------------------
118 // Worker function for exporting a scene to ASCII FBX.
119 // Prototyped and registered in Exporter.cpp
ExportSceneFBXA(const char * pFile,IOSystem * pIOSystem,const aiScene * pScene,const ExportProperties * pProperties)120 void ExportSceneFBXA (
121 const char* pFile,
122 IOSystem* pIOSystem,
123 const aiScene* pScene,
124 const ExportProperties* pProperties
125
126 ){
127 // initialize the exporter
128 FBXExporter exporter(pScene, pProperties);
129
130 // perform ascii export
131 exporter.ExportAscii(pFile, pIOSystem);
132 }
133
134 } // end of namespace Assimp
135
FBXExporter(const aiScene * pScene,const ExportProperties * pProperties)136 FBXExporter::FBXExporter ( const aiScene* pScene, const ExportProperties* pProperties )
137 : binary(false)
138 , mScene(pScene)
139 , mProperties(pProperties)
140 , outfile()
141 , connections()
142 , mesh_uids()
143 , material_uids()
144 , node_uids() {
145 // will probably need to determine UIDs, connections, etc here.
146 // basically anything that needs to be known
147 // before we start writing sections to the stream.
148 }
149
ExportBinary(const char * pFile,IOSystem * pIOSystem)150 void FBXExporter::ExportBinary (
151 const char* pFile,
152 IOSystem* pIOSystem
153 ){
154 // remember that we're exporting in binary mode
155 binary = true;
156
157 // we're not currently using these preferences,
158 // but clang will cry about it if we never touch it.
159 // TODO: some of these might be relevant to export
160 (void)mProperties;
161
162 // open the indicated file for writing (in binary mode)
163 outfile.reset(pIOSystem->Open(pFile,"wb"));
164 if (!outfile) {
165 throw DeadlyExportError(
166 "could not open output .fbx file: " + std::string(pFile)
167 );
168 }
169
170 // first a binary-specific file header
171 WriteBinaryHeader();
172
173 // the rest of the file is in node entries.
174 // we have to serialize each entry before we write to the output,
175 // as the first thing we write is the byte offset of the _next_ entry.
176 // Either that or we can skip back to write the offset when we finish.
177 WriteAllNodes();
178
179 // finally we have a binary footer to the file
180 WriteBinaryFooter();
181
182 // explicitly release file pointer,
183 // so we don't have to rely on class destruction.
184 outfile.reset();
185 }
186
ExportAscii(const char * pFile,IOSystem * pIOSystem)187 void FBXExporter::ExportAscii (
188 const char* pFile,
189 IOSystem* pIOSystem
190 ){
191 // remember that we're exporting in ascii mode
192 binary = false;
193
194 // open the indicated file for writing in text mode
195 outfile.reset(pIOSystem->Open(pFile,"wt"));
196 if (!outfile) {
197 throw DeadlyExportError(
198 "could not open output .fbx file: " + std::string(pFile)
199 );
200 }
201
202 // write the ascii header
203 WriteAsciiHeader();
204
205 // write all the sections
206 WriteAllNodes();
207
208 // make sure the file ends with a newline.
209 // note: if the file is opened in text mode,
210 // this should do the right cross-platform thing.
211 outfile->Write("\n", 1, 1);
212
213 // explicitly release file pointer,
214 // so we don't have to rely on class destruction.
215 outfile.reset();
216 }
217
WriteAsciiHeader()218 void FBXExporter::WriteAsciiHeader()
219 {
220 // basically just a comment at the top of the file
221 std::stringstream head;
222 head << "; FBX " << EXPORT_VERSION_STR << " project file\n";
223 head << "; Created by the Open Asset Import Library (Assimp)\n";
224 head << "; http://assimp.org\n";
225 head << "; -------------------------------------------------\n";
226 const std::string ascii_header = head.str();
227 outfile->Write(ascii_header.c_str(), ascii_header.size(), 1);
228 }
229
WriteAsciiSectionHeader(const std::string & title)230 void FBXExporter::WriteAsciiSectionHeader(const std::string& title)
231 {
232 StreamWriterLE outstream(outfile);
233 std::stringstream s;
234 s << "\n\n; " << title << '\n';
235 s << FBX::COMMENT_UNDERLINE << "\n";
236 outstream.PutString(s.str());
237 }
238
WriteBinaryHeader()239 void FBXExporter::WriteBinaryHeader()
240 {
241 // first a specific sequence of 23 bytes, always the same
242 const char binary_header[24] = "Kaydara FBX Binary\x20\x20\x00\x1a\x00";
243 outfile->Write(binary_header, 1, 23);
244
245 // then FBX version number, "multiplied" by 1000, as little-endian uint32.
246 // so 7.3 becomes 7300 == 0x841C0000, 7.4 becomes 7400 == 0xE81C0000, etc
247 {
248 StreamWriterLE outstream(outfile);
249 outstream.PutU4(EXPORT_VERSION_INT);
250 } // StreamWriter destructor writes the data to the file
251
252 // after this the node data starts immediately
253 // (probably with the FBXHEaderExtension node)
254 }
255
WriteBinaryFooter()256 void FBXExporter::WriteBinaryFooter()
257 {
258 outfile->Write(NULL_RECORD.c_str(), NULL_RECORD.size(), 1);
259
260 outfile->Write(GENERIC_FOOTID.c_str(), GENERIC_FOOTID.size(), 1);
261
262 // here some padding is added for alignment to 16 bytes.
263 // if already aligned, the full 16 bytes is added.
264 size_t pos = outfile->Tell();
265 size_t pad = 16 - (pos % 16);
266 for (size_t i = 0; i < pad; ++i) {
267 outfile->Write("\x00", 1, 1);
268 }
269
270 // not sure what this is, but it seems to always be 0 in modern files
271 for (size_t i = 0; i < 4; ++i) {
272 outfile->Write("\x00", 1, 1);
273 }
274
275 // now the file version again
276 {
277 StreamWriterLE outstream(outfile);
278 outstream.PutU4(EXPORT_VERSION_INT);
279 } // StreamWriter destructor writes the data to the file
280
281 // and finally some binary footer added to all files
282 for (size_t i = 0; i < 120; ++i) {
283 outfile->Write("\x00", 1, 1);
284 }
285 outfile->Write(FOOT_MAGIC.c_str(), FOOT_MAGIC.size(), 1);
286 }
287
WriteAllNodes()288 void FBXExporter::WriteAllNodes ()
289 {
290 // header
291 // (and fileid, creation time, creator, if binary)
292 WriteHeaderExtension();
293
294 // global settings
295 WriteGlobalSettings();
296
297 // documents
298 WriteDocuments();
299
300 // references
301 WriteReferences();
302
303 // definitions
304 WriteDefinitions();
305
306 // objects
307 WriteObjects();
308
309 // connections
310 WriteConnections();
311
312 // WriteTakes? (deprecated since at least 2015 (fbx 7.4))
313 }
314
315 //FBXHeaderExtension top-level node
WriteHeaderExtension()316 void FBXExporter::WriteHeaderExtension ()
317 {
318 if (!binary) {
319 // no title, follows directly from the top comment
320 }
321 FBX::Node n("FBXHeaderExtension");
322 StreamWriterLE outstream(outfile);
323 int indent = 0;
324
325 // begin node
326 n.Begin(outstream, binary, indent);
327
328 // write properties
329 // (none)
330
331 // finish properties
332 n.EndProperties(outstream, binary, indent, 0);
333
334 // begin children
335 n.BeginChildren(outstream, binary, indent);
336
337 indent = 1;
338
339 // write child nodes
340 FBX::Node::WritePropertyNode(
341 "FBXHeaderVersion", int32_t(1003), outstream, binary, indent
342 );
343 FBX::Node::WritePropertyNode(
344 "FBXVersion", int32_t(EXPORT_VERSION_INT), outstream, binary, indent
345 );
346 if (binary) {
347 FBX::Node::WritePropertyNode(
348 "EncryptionType", int32_t(0), outstream, binary, indent
349 );
350 }
351
352 FBX::Node CreationTimeStamp("CreationTimeStamp");
353 time_t rawtime;
354 time(&rawtime);
355 struct tm * now = localtime(&rawtime);
356 CreationTimeStamp.AddChild("Version", int32_t(1000));
357 CreationTimeStamp.AddChild("Year", int32_t(now->tm_year + 1900));
358 CreationTimeStamp.AddChild("Month", int32_t(now->tm_mon + 1));
359 CreationTimeStamp.AddChild("Day", int32_t(now->tm_mday));
360 CreationTimeStamp.AddChild("Hour", int32_t(now->tm_hour));
361 CreationTimeStamp.AddChild("Minute", int32_t(now->tm_min));
362 CreationTimeStamp.AddChild("Second", int32_t(now->tm_sec));
363 CreationTimeStamp.AddChild("Millisecond", int32_t(0));
364 CreationTimeStamp.Dump(outstream, binary, indent);
365
366 std::stringstream creator;
367 creator << "Open Asset Import Library (Assimp) " << aiGetVersionMajor()
368 << "." << aiGetVersionMinor() << "." << aiGetVersionRevision();
369 FBX::Node::WritePropertyNode(
370 "Creator", creator.str(), outstream, binary, indent
371 );
372
373 //FBX::Node sceneinfo("SceneInfo");
374 //sceneinfo.AddProperty("GlobalInfo" + FBX::SEPARATOR + "SceneInfo");
375 // not sure if any of this is actually needed,
376 // so just write an empty node for now.
377 //sceneinfo.Dump(outstream, binary, indent);
378
379 indent = 0;
380
381 // finish node
382 n.End(outstream, binary, indent, true);
383
384 // that's it for FBXHeaderExtension...
385 if (!binary) { return; }
386
387 // but binary files also need top-level FileID, CreationTime, Creator:
388 std::vector<uint8_t> raw(GENERIC_FILEID.size());
389 for (size_t i = 0; i < GENERIC_FILEID.size(); ++i) {
390 raw[i] = uint8_t(GENERIC_FILEID[i]);
391 }
392 FBX::Node::WritePropertyNode(
393 "FileId", raw, outstream, binary, indent
394 );
395 FBX::Node::WritePropertyNode(
396 "CreationTime", GENERIC_CTIME, outstream, binary, indent
397 );
398 FBX::Node::WritePropertyNode(
399 "Creator", creator.str(), outstream, binary, indent
400 );
401 }
402
403 // WriteGlobalSettings helpers
404
WritePropInt(const aiScene * scene,FBX::Node & p,const std::string & key,int defaultValue)405 void WritePropInt(const aiScene* scene, FBX::Node& p, const std::string& key, int defaultValue)
406 {
407 int value;
408 if (scene->mMetaData != nullptr && scene->mMetaData->Get(key, value)) {
409 p.AddP70int(key, value);
410 } else {
411 p.AddP70int(key, defaultValue);
412 }
413 }
414
WritePropDouble(const aiScene * scene,FBX::Node & p,const std::string & key,double defaultValue)415 void WritePropDouble(const aiScene* scene, FBX::Node& p, const std::string& key, double defaultValue)
416 {
417 double value;
418 if (scene->mMetaData != nullptr && scene->mMetaData->Get(key, value)) {
419 p.AddP70double(key, value);
420 } else {
421 // fallback lookup float instead
422 float floatValue;
423 if (scene->mMetaData != nullptr && scene->mMetaData->Get(key, floatValue)) {
424 p.AddP70double(key, (double)floatValue);
425 } else {
426 p.AddP70double(key, defaultValue);
427 }
428 }
429 }
430
WritePropEnum(const aiScene * scene,FBX::Node & p,const std::string & key,int defaultValue)431 void WritePropEnum(const aiScene* scene, FBX::Node& p, const std::string& key, int defaultValue)
432 {
433 int value;
434 if (scene->mMetaData != nullptr && scene->mMetaData->Get(key, value)) {
435 p.AddP70enum(key, value);
436 } else {
437 p.AddP70enum(key, defaultValue);
438 }
439 }
440
WritePropColor(const aiScene * scene,FBX::Node & p,const std::string & key,const aiVector3D & defaultValue)441 void WritePropColor(const aiScene* scene, FBX::Node& p, const std::string& key, const aiVector3D& defaultValue)
442 {
443 aiVector3D value;
444 if (scene->mMetaData != nullptr && scene->mMetaData->Get(key, value)) {
445 // ai_real can be float or double, cast to avoid warnings
446 p.AddP70color(key, (double)value.x, (double)value.y, (double)value.z);
447 } else {
448 p.AddP70color(key, (double)defaultValue.x, (double)defaultValue.y, (double)defaultValue.z);
449 }
450 }
451
WritePropString(const aiScene * scene,FBX::Node & p,const std::string & key,const std::string & defaultValue)452 void WritePropString(const aiScene* scene, FBX::Node& p, const std::string& key, const std::string& defaultValue)
453 {
454 aiString value; // MetaData doesn't hold std::string
455 if (scene->mMetaData != nullptr && scene->mMetaData->Get(key, value)) {
456 p.AddP70string(key, value.C_Str());
457 } else {
458 p.AddP70string(key, defaultValue);
459 }
460 }
461
WriteGlobalSettings()462 void FBXExporter::WriteGlobalSettings ()
463 {
464 if (!binary) {
465 // no title, follows directly from the header extension
466 }
467 FBX::Node gs("GlobalSettings");
468 gs.AddChild("Version", int32_t(1000));
469
470 FBX::Node p("Properties70");
471 WritePropInt(mScene, p, "UpAxis", 1);
472 WritePropInt(mScene, p, "UpAxisSign", 1);
473 WritePropInt(mScene, p, "FrontAxis", 2);
474 WritePropInt(mScene, p, "FrontAxisSign", 1);
475 WritePropInt(mScene, p, "CoordAxis", 0);
476 WritePropInt(mScene, p, "CoordAxisSign", 1);
477 WritePropInt(mScene, p, "OriginalUpAxis", 1);
478 WritePropInt(mScene, p, "OriginalUpAxisSign", 1);
479 WritePropDouble(mScene, p, "UnitScaleFactor", 1.0);
480 WritePropDouble(mScene, p, "OriginalUnitScaleFactor", 1.0);
481 WritePropColor(mScene, p, "AmbientColor", aiVector3D((ai_real)0.0, (ai_real)0.0, (ai_real)0.0));
482 WritePropString(mScene, p,"DefaultCamera", "Producer Perspective");
483 WritePropEnum(mScene, p, "TimeMode", 11);
484 WritePropEnum(mScene, p, "TimeProtocol", 2);
485 WritePropEnum(mScene, p, "SnapOnFrameMode", 0);
486 p.AddP70time("TimeSpanStart", 0); // TODO: animation support
487 p.AddP70time("TimeSpanStop", FBX::SECOND); // TODO: animation support
488 WritePropDouble(mScene, p, "CustomFrameRate", -1.0);
489 p.AddP70("TimeMarker", "Compound", "", ""); // not sure what this is
490 WritePropInt(mScene, p, "CurrentTimeMarker", -1);
491 gs.AddChild(p);
492
493 gs.Dump(outfile, binary, 0);
494 }
495
WriteDocuments()496 void FBXExporter::WriteDocuments ()
497 {
498 if (!binary) {
499 WriteAsciiSectionHeader("Documents Description");
500 }
501
502 // not sure what the use of multiple documents would be,
503 // or whether any end-application supports it
504 FBX::Node docs("Documents");
505 docs.AddChild("Count", int32_t(1));
506 FBX::Node doc("Document");
507
508 // generate uid
509 int64_t uid = generate_uid();
510 doc.AddProperties(uid, "", "Scene");
511 FBX::Node p("Properties70");
512 p.AddP70("SourceObject", "object", "", ""); // what is this even for?
513 p.AddP70string("ActiveAnimStackName", ""); // should do this properly?
514 doc.AddChild(p);
515
516 // UID for root node in scene hierarchy.
517 // always set to 0 in the case of a single document.
518 // not sure what happens if more than one document exists,
519 // but that won't matter to us as we're exporting a single scene.
520 doc.AddChild("RootNode", int64_t(0));
521
522 docs.AddChild(doc);
523 docs.Dump(outfile, binary, 0);
524 }
525
WriteReferences()526 void FBXExporter::WriteReferences ()
527 {
528 if (!binary) {
529 WriteAsciiSectionHeader("Document References");
530 }
531 // always empty for now.
532 // not really sure what this is for.
533 FBX::Node n("References");
534 n.force_has_children = true;
535 n.Dump(outfile, binary, 0);
536 }
537
538
539 // ---------------------------------------------------------------
540 // some internal helper functions used for writing the definitions
541 // (before any actual data is written)
542 // ---------------------------------------------------------------
543
count_nodes(const aiNode * n,const aiNode * root)544 size_t count_nodes(const aiNode* n, const aiNode* root) {
545 size_t count;
546 if (n == root) {
547 count = n->mNumMeshes; // (not counting root node)
548 } else if (n->mNumMeshes > 1) {
549 count = n->mNumMeshes + 1;
550 } else {
551 count = 1;
552 }
553 for (size_t i = 0; i < n->mNumChildren; ++i) {
554 count += count_nodes(n->mChildren[i], root);
555 }
556 return count;
557 }
558
has_phong_mat(const aiScene * scene)559 bool has_phong_mat(const aiScene* scene)
560 {
561 // just search for any material with a shininess exponent
562 for (size_t i = 0; i < scene->mNumMaterials; ++i) {
563 aiMaterial* mat = scene->mMaterials[i];
564 float shininess = 0;
565 mat->Get(AI_MATKEY_SHININESS, shininess);
566 if (shininess > 0) {
567 return true;
568 }
569 }
570 return false;
571 }
572
count_images(const aiScene * scene)573 size_t count_images(const aiScene* scene) {
574 std::unordered_set<std::string> images;
575 aiString texpath;
576 for (size_t i = 0; i < scene->mNumMaterials; ++i) {
577 aiMaterial* mat = scene->mMaterials[i];
578 for (
579 size_t tt = aiTextureType_DIFFUSE;
580 tt < aiTextureType_UNKNOWN;
581 ++tt
582 ){
583 const aiTextureType textype = static_cast<aiTextureType>(tt);
584 const size_t texcount = mat->GetTextureCount(textype);
585 for (unsigned int j = 0; j < texcount; ++j) {
586 mat->GetTexture(textype, j, &texpath);
587 images.insert(std::string(texpath.C_Str()));
588 }
589 }
590 }
591 return images.size();
592 }
593
count_textures(const aiScene * scene)594 size_t count_textures(const aiScene* scene) {
595 size_t count = 0;
596 for (size_t i = 0; i < scene->mNumMaterials; ++i) {
597 aiMaterial* mat = scene->mMaterials[i];
598 for (
599 size_t tt = aiTextureType_DIFFUSE;
600 tt < aiTextureType_UNKNOWN;
601 ++tt
602 ){
603 // TODO: handle layered textures
604 if (mat->GetTextureCount(static_cast<aiTextureType>(tt)) > 0) {
605 count += 1;
606 }
607 }
608 }
609 return count;
610 }
611
count_deformers(const aiScene * scene)612 size_t count_deformers(const aiScene* scene) {
613 size_t count = 0;
614 for (size_t i = 0; i < scene->mNumMeshes; ++i) {
615 const size_t n = scene->mMeshes[i]->mNumBones;
616 if (n) {
617 // 1 main deformer, 1 subdeformer per bone
618 count += n + 1;
619 }
620 }
621 return count;
622 }
623
WriteDefinitions()624 void FBXExporter::WriteDefinitions ()
625 {
626 // basically this is just bookkeeping:
627 // determining how many of each type of object there are
628 // and specifying the base properties to use when otherwise unspecified.
629
630 // ascii section header
631 if (!binary) {
632 WriteAsciiSectionHeader("Object definitions");
633 }
634
635 // we need to count the objects
636 int32_t count;
637 int32_t total_count = 0;
638
639 // and store them
640 std::vector<FBX::Node> object_nodes;
641 FBX::Node n, pt, p;
642
643 // GlobalSettings
644 // this seems to always be here in Maya exports
645 n = FBX::Node("ObjectType", "GlobalSettings");
646 count = 1;
647 n.AddChild("Count", count);
648 object_nodes.push_back(n);
649 total_count += count;
650
651 // AnimationStack / FbxAnimStack
652 // this seems to always be here in Maya exports,
653 // but no harm seems to come of leaving it out.
654 count = mScene->mNumAnimations;
655 if (count) {
656 n = FBX::Node("ObjectType", "AnimationStack");
657 n.AddChild("Count", count);
658 pt = FBX::Node("PropertyTemplate", "FbxAnimStack");
659 p = FBX::Node("Properties70");
660 p.AddP70string("Description", "");
661 p.AddP70time("LocalStart", 0);
662 p.AddP70time("LocalStop", 0);
663 p.AddP70time("ReferenceStart", 0);
664 p.AddP70time("ReferenceStop", 0);
665 pt.AddChild(p);
666 n.AddChild(pt);
667 object_nodes.push_back(n);
668 total_count += count;
669 }
670
671 // AnimationLayer / FbxAnimLayer
672 // this seems to always be here in Maya exports,
673 // but no harm seems to come of leaving it out.
674 // Assimp doesn't support animation layers,
675 // so there will be one per aiAnimation
676 count = mScene->mNumAnimations;
677 if (count) {
678 n = FBX::Node("ObjectType", "AnimationLayer");
679 n.AddChild("Count", count);
680 pt = FBX::Node("PropertyTemplate", "FBXAnimLayer");
681 p = FBX::Node("Properties70");
682 p.AddP70("Weight", "Number", "", "A", double(100));
683 p.AddP70bool("Mute", 0);
684 p.AddP70bool("Solo", 0);
685 p.AddP70bool("Lock", 0);
686 p.AddP70color("Color", 0.8, 0.8, 0.8);
687 p.AddP70("BlendMode", "enum", "", "", int32_t(0));
688 p.AddP70("RotationAccumulationMode", "enum", "", "", int32_t(0));
689 p.AddP70("ScaleAccumulationMode", "enum", "", "", int32_t(0));
690 p.AddP70("BlendModeBypass", "ULongLong", "", "", int64_t(0));
691 pt.AddChild(p);
692 n.AddChild(pt);
693 object_nodes.push_back(n);
694 total_count += count;
695 }
696
697 // NodeAttribute
698 // this is completely absurd.
699 // there can only be one "NodeAttribute" template,
700 // but FbxSkeleton, FbxCamera, FbxLight all are "NodeAttributes".
701 // so if only one exists we should set the template for that,
702 // otherwise... we just pick one :/.
703 // the others have to set all their properties every instance,
704 // because there's no template.
705 count = 1; // TODO: select properly
706 if (count) {
707 // FbxSkeleton
708 n = FBX::Node("ObjectType", "NodeAttribute");
709 n.AddChild("Count", count);
710 pt = FBX::Node("PropertyTemplate", "FbxSkeleton");
711 p = FBX::Node("Properties70");
712 p.AddP70color("Color", 0.8, 0.8, 0.8);
713 p.AddP70double("Size", 33.333333333333);
714 p.AddP70("LimbLength", "double", "Number", "H", double(1));
715 // note: not sure what the "H" flag is for - hidden?
716 pt.AddChild(p);
717 n.AddChild(pt);
718 object_nodes.push_back(n);
719 total_count += count;
720 }
721
722 // Model / FbxNode
723 // <~~ node hierarchy
724 count = int32_t(count_nodes(mScene->mRootNode, mScene->mRootNode));
725 if (count) {
726 n = FBX::Node("ObjectType", "Model");
727 n.AddChild("Count", count);
728 pt = FBX::Node("PropertyTemplate", "FbxNode");
729 p = FBX::Node("Properties70");
730 p.AddP70enum("QuaternionInterpolate", 0);
731 p.AddP70vector("RotationOffset", 0.0, 0.0, 0.0);
732 p.AddP70vector("RotationPivot", 0.0, 0.0, 0.0);
733 p.AddP70vector("ScalingOffset", 0.0, 0.0, 0.0);
734 p.AddP70vector("ScalingPivot", 0.0, 0.0, 0.0);
735 p.AddP70bool("TranslationActive", 0);
736 p.AddP70vector("TranslationMin", 0.0, 0.0, 0.0);
737 p.AddP70vector("TranslationMax", 0.0, 0.0, 0.0);
738 p.AddP70bool("TranslationMinX", 0);
739 p.AddP70bool("TranslationMinY", 0);
740 p.AddP70bool("TranslationMinZ", 0);
741 p.AddP70bool("TranslationMaxX", 0);
742 p.AddP70bool("TranslationMaxY", 0);
743 p.AddP70bool("TranslationMaxZ", 0);
744 p.AddP70enum("RotationOrder", 0);
745 p.AddP70bool("RotationSpaceForLimitOnly", 0);
746 p.AddP70double("RotationStiffnessX", 0.0);
747 p.AddP70double("RotationStiffnessY", 0.0);
748 p.AddP70double("RotationStiffnessZ", 0.0);
749 p.AddP70double("AxisLen", 10.0);
750 p.AddP70vector("PreRotation", 0.0, 0.0, 0.0);
751 p.AddP70vector("PostRotation", 0.0, 0.0, 0.0);
752 p.AddP70bool("RotationActive", 0);
753 p.AddP70vector("RotationMin", 0.0, 0.0, 0.0);
754 p.AddP70vector("RotationMax", 0.0, 0.0, 0.0);
755 p.AddP70bool("RotationMinX", 0);
756 p.AddP70bool("RotationMinY", 0);
757 p.AddP70bool("RotationMinZ", 0);
758 p.AddP70bool("RotationMaxX", 0);
759 p.AddP70bool("RotationMaxY", 0);
760 p.AddP70bool("RotationMaxZ", 0);
761 p.AddP70enum("InheritType", 0);
762 p.AddP70bool("ScalingActive", 0);
763 p.AddP70vector("ScalingMin", 0.0, 0.0, 0.0);
764 p.AddP70vector("ScalingMax", 1.0, 1.0, 1.0);
765 p.AddP70bool("ScalingMinX", 0);
766 p.AddP70bool("ScalingMinY", 0);
767 p.AddP70bool("ScalingMinZ", 0);
768 p.AddP70bool("ScalingMaxX", 0);
769 p.AddP70bool("ScalingMaxY", 0);
770 p.AddP70bool("ScalingMaxZ", 0);
771 p.AddP70vector("GeometricTranslation", 0.0, 0.0, 0.0);
772 p.AddP70vector("GeometricRotation", 0.0, 0.0, 0.0);
773 p.AddP70vector("GeometricScaling", 1.0, 1.0, 1.0);
774 p.AddP70double("MinDampRangeX", 0.0);
775 p.AddP70double("MinDampRangeY", 0.0);
776 p.AddP70double("MinDampRangeZ", 0.0);
777 p.AddP70double("MaxDampRangeX", 0.0);
778 p.AddP70double("MaxDampRangeY", 0.0);
779 p.AddP70double("MaxDampRangeZ", 0.0);
780 p.AddP70double("MinDampStrengthX", 0.0);
781 p.AddP70double("MinDampStrengthY", 0.0);
782 p.AddP70double("MinDampStrengthZ", 0.0);
783 p.AddP70double("MaxDampStrengthX", 0.0);
784 p.AddP70double("MaxDampStrengthY", 0.0);
785 p.AddP70double("MaxDampStrengthZ", 0.0);
786 p.AddP70double("PreferedAngleX", 0.0);
787 p.AddP70double("PreferedAngleY", 0.0);
788 p.AddP70double("PreferedAngleZ", 0.0);
789 p.AddP70("LookAtProperty", "object", "", "");
790 p.AddP70("UpVectorProperty", "object", "", "");
791 p.AddP70bool("Show", 1);
792 p.AddP70bool("NegativePercentShapeSupport", 1);
793 p.AddP70int("DefaultAttributeIndex", -1);
794 p.AddP70bool("Freeze", 0);
795 p.AddP70bool("LODBox", 0);
796 p.AddP70(
797 "Lcl Translation", "Lcl Translation", "", "A",
798 double(0), double(0), double(0)
799 );
800 p.AddP70(
801 "Lcl Rotation", "Lcl Rotation", "", "A",
802 double(0), double(0), double(0)
803 );
804 p.AddP70(
805 "Lcl Scaling", "Lcl Scaling", "", "A",
806 double(1), double(1), double(1)
807 );
808 p.AddP70("Visibility", "Visibility", "", "A", double(1));
809 p.AddP70(
810 "Visibility Inheritance", "Visibility Inheritance", "", "",
811 int32_t(1)
812 );
813 pt.AddChild(p);
814 n.AddChild(pt);
815 object_nodes.push_back(n);
816 total_count += count;
817 }
818
819 // Geometry / FbxMesh
820 // <~~ aiMesh
821 count = mScene->mNumMeshes;
822
823 // Blendshapes are considered Geometry
824 int32_t bsDeformerCount=0;
825 for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
826 aiMesh* m = mScene->mMeshes[mi];
827 if (m->mNumAnimMeshes > 0) {
828 count+=m->mNumAnimMeshes;
829 bsDeformerCount+=m->mNumAnimMeshes; // One deformer per blendshape
830 bsDeformerCount++; // Plus one master blendshape deformer
831 }
832 }
833
834 if (count) {
835 n = FBX::Node("ObjectType", "Geometry");
836 n.AddChild("Count", count);
837 pt = FBX::Node("PropertyTemplate", "FbxMesh");
838 p = FBX::Node("Properties70");
839 p.AddP70color("Color", 0, 0, 0);
840 p.AddP70vector("BBoxMin", 0, 0, 0);
841 p.AddP70vector("BBoxMax", 0, 0, 0);
842 p.AddP70bool("Primary Visibility", 1);
843 p.AddP70bool("Casts Shadows", 1);
844 p.AddP70bool("Receive Shadows", 1);
845 pt.AddChild(p);
846 n.AddChild(pt);
847 object_nodes.push_back(n);
848 total_count += count;
849 }
850
851 // Material / FbxSurfacePhong, FbxSurfaceLambert, FbxSurfaceMaterial
852 // <~~ aiMaterial
853 // basically if there's any phong material this is defined as phong,
854 // and otherwise lambert.
855 // More complex materials cause a bare-bones FbxSurfaceMaterial definition
856 // and are treated specially, as they're not really supported by FBX.
857 // TODO: support Maya's Stingray PBS material
858 count = mScene->mNumMaterials;
859 if (count) {
860 bool has_phong = has_phong_mat(mScene);
861 n = FBX::Node("ObjectType", "Material");
862 n.AddChild("Count", count);
863 pt = FBX::Node("PropertyTemplate");
864 if (has_phong) {
865 pt.AddProperty("FbxSurfacePhong");
866 } else {
867 pt.AddProperty("FbxSurfaceLambert");
868 }
869 p = FBX::Node("Properties70");
870 if (has_phong) {
871 p.AddP70string("ShadingModel", "Phong");
872 } else {
873 p.AddP70string("ShadingModel", "Lambert");
874 }
875 p.AddP70bool("MultiLayer", 0);
876 p.AddP70colorA("EmissiveColor", 0.0, 0.0, 0.0);
877 p.AddP70numberA("EmissiveFactor", 1.0);
878 p.AddP70colorA("AmbientColor", 0.2, 0.2, 0.2);
879 p.AddP70numberA("AmbientFactor", 1.0);
880 p.AddP70colorA("DiffuseColor", 0.8, 0.8, 0.8);
881 p.AddP70numberA("DiffuseFactor", 1.0);
882 p.AddP70vector("Bump", 0.0, 0.0, 0.0);
883 p.AddP70vector("NormalMap", 0.0, 0.0, 0.0);
884 p.AddP70double("BumpFactor", 1.0);
885 p.AddP70colorA("TransparentColor", 0.0, 0.0, 0.0);
886 p.AddP70numberA("TransparencyFactor", 0.0);
887 p.AddP70color("DisplacementColor", 0.0, 0.0, 0.0);
888 p.AddP70double("DisplacementFactor", 1.0);
889 p.AddP70color("VectorDisplacementColor", 0.0, 0.0, 0.0);
890 p.AddP70double("VectorDisplacementFactor", 1.0);
891 if (has_phong) {
892 p.AddP70colorA("SpecularColor", 0.2, 0.2, 0.2);
893 p.AddP70numberA("SpecularFactor", 1.0);
894 p.AddP70numberA("ShininessExponent", 20.0);
895 p.AddP70colorA("ReflectionColor", 0.0, 0.0, 0.0);
896 p.AddP70numberA("ReflectionFactor", 1.0);
897 }
898 pt.AddChild(p);
899 n.AddChild(pt);
900 object_nodes.push_back(n);
901 total_count += count;
902 }
903
904 // Video / FbxVideo
905 // one for each image file.
906 count = int32_t(count_images(mScene));
907 if (count) {
908 n = FBX::Node("ObjectType", "Video");
909 n.AddChild("Count", count);
910 pt = FBX::Node("PropertyTemplate", "FbxVideo");
911 p = FBX::Node("Properties70");
912 p.AddP70bool("ImageSequence", 0);
913 p.AddP70int("ImageSequenceOffset", 0);
914 p.AddP70double("FrameRate", 0.0);
915 p.AddP70int("LastFrame", 0);
916 p.AddP70int("Width", 0);
917 p.AddP70int("Height", 0);
918 p.AddP70("Path", "KString", "XRefUrl", "", "");
919 p.AddP70int("StartFrame", 0);
920 p.AddP70int("StopFrame", 0);
921 p.AddP70double("PlaySpeed", 0.0);
922 p.AddP70time("Offset", 0);
923 p.AddP70enum("InterlaceMode", 0);
924 p.AddP70bool("FreeRunning", 0);
925 p.AddP70bool("Loop", 0);
926 p.AddP70enum("AccessMode", 0);
927 pt.AddChild(p);
928 n.AddChild(pt);
929 object_nodes.push_back(n);
930 total_count += count;
931 }
932
933 // Texture / FbxFileTexture
934 // <~~ aiTexture
935 count = int32_t(count_textures(mScene));
936 if (count) {
937 n = FBX::Node("ObjectType", "Texture");
938 n.AddChild("Count", count);
939 pt = FBX::Node("PropertyTemplate", "FbxFileTexture");
940 p = FBX::Node("Properties70");
941 p.AddP70enum("TextureTypeUse", 0);
942 p.AddP70numberA("Texture alpha", 1.0);
943 p.AddP70enum("CurrentMappingType", 0);
944 p.AddP70enum("WrapModeU", 0);
945 p.AddP70enum("WrapModeV", 0);
946 p.AddP70bool("UVSwap", 0);
947 p.AddP70bool("PremultiplyAlpha", 1);
948 p.AddP70vectorA("Translation", 0.0, 0.0, 0.0);
949 p.AddP70vectorA("Rotation", 0.0, 0.0, 0.0);
950 p.AddP70vectorA("Scaling", 1.0, 1.0, 1.0);
951 p.AddP70vector("TextureRotationPivot", 0.0, 0.0, 0.0);
952 p.AddP70vector("TextureScalingPivot", 0.0, 0.0, 0.0);
953 p.AddP70enum("CurrentTextureBlendMode", 1);
954 p.AddP70string("UVSet", "default");
955 p.AddP70bool("UseMaterial", 0);
956 p.AddP70bool("UseMipMap", 0);
957 pt.AddChild(p);
958 n.AddChild(pt);
959 object_nodes.push_back(n);
960 total_count += count;
961 }
962
963 // AnimationCurveNode / FbxAnimCurveNode
964 count = mScene->mNumAnimations * 3;
965 if (count) {
966 n = FBX::Node("ObjectType", "AnimationCurveNode");
967 n.AddChild("Count", count);
968 pt = FBX::Node("PropertyTemplate", "FbxAnimCurveNode");
969 p = FBX::Node("Properties70");
970 p.AddP70("d", "Compound", "", "");
971 pt.AddChild(p);
972 n.AddChild(pt);
973 object_nodes.push_back(n);
974 total_count += count;
975 }
976
977 // AnimationCurve / FbxAnimCurve
978 count = mScene->mNumAnimations * 9;
979 if (count) {
980 n = FBX::Node("ObjectType", "AnimationCurve");
981 n.AddChild("Count", count);
982 object_nodes.push_back(n);
983 total_count += count;
984 }
985
986 // Pose
987 count = 0;
988 for (size_t i = 0; i < mScene->mNumMeshes; ++i) {
989 aiMesh* mesh = mScene->mMeshes[i];
990 if (mesh->HasBones()) { ++count; }
991 }
992 if (count) {
993 n = FBX::Node("ObjectType", "Pose");
994 n.AddChild("Count", count);
995 object_nodes.push_back(n);
996 total_count += count;
997 }
998
999 // Deformer
1000 count = int32_t(count_deformers(mScene))+bsDeformerCount;
1001 if (count) {
1002 n = FBX::Node("ObjectType", "Deformer");
1003 n.AddChild("Count", count);
1004 object_nodes.push_back(n);
1005 total_count += count;
1006 }
1007
1008 // (template)
1009 count = 0;
1010 if (count) {
1011 n = FBX::Node("ObjectType", "");
1012 n.AddChild("Count", count);
1013 pt = FBX::Node("PropertyTemplate", "");
1014 p = FBX::Node("Properties70");
1015 pt.AddChild(p);
1016 n.AddChild(pt);
1017 object_nodes.push_back(n);
1018 total_count += count;
1019 }
1020
1021 // now write it all
1022 FBX::Node defs("Definitions");
1023 defs.AddChild("Version", int32_t(100));
1024 defs.AddChild("Count", int32_t(total_count));
1025 for (auto &on : object_nodes) {
1026 defs.AddChild(on);
1027 }
1028 defs.Dump(outfile, binary, 0);
1029 }
1030
1031
1032 // -------------------------------------------------------------------
1033 // some internal helper functions used for writing the objects section
1034 // (which holds the actual data)
1035 // -------------------------------------------------------------------
1036
get_node_for_mesh(unsigned int meshIndex,aiNode * node)1037 aiNode* get_node_for_mesh(unsigned int meshIndex, aiNode* node)
1038 {
1039 for (size_t i = 0; i < node->mNumMeshes; ++i) {
1040 if (node->mMeshes[i] == meshIndex) {
1041 return node;
1042 }
1043 }
1044 for (size_t i = 0; i < node->mNumChildren; ++i) {
1045 aiNode* ret = get_node_for_mesh(meshIndex, node->mChildren[i]);
1046 if (ret) { return ret; }
1047 }
1048 return nullptr;
1049 }
1050
get_world_transform(const aiNode * node,const aiScene * scene)1051 aiMatrix4x4 get_world_transform(const aiNode* node, const aiScene* scene)
1052 {
1053 std::vector<const aiNode*> node_chain;
1054 while (node != scene->mRootNode) {
1055 node_chain.push_back(node);
1056 node = node->mParent;
1057 }
1058 aiMatrix4x4 transform;
1059 for (auto n = node_chain.rbegin(); n != node_chain.rend(); ++n) {
1060 transform *= (*n)->mTransformation;
1061 }
1062 return transform;
1063 }
1064
to_ktime(double ticks,const aiAnimation * anim)1065 int64_t to_ktime(double ticks, const aiAnimation* anim) {
1066 if (anim->mTicksPerSecond <= 0) {
1067 return static_cast<int64_t>(ticks) * FBX::SECOND;
1068 }
1069 return (static_cast<int64_t>(ticks) / static_cast<int64_t>(anim->mTicksPerSecond)) * FBX::SECOND;
1070 }
1071
to_ktime(double time)1072 int64_t to_ktime(double time) {
1073 return (static_cast<int64_t>(time * FBX::SECOND));
1074 }
1075
WriteObjects()1076 void FBXExporter::WriteObjects ()
1077 {
1078 if (!binary) {
1079 WriteAsciiSectionHeader("Object properties");
1080 }
1081 // numbers should match those given in definitions! make sure to check
1082 StreamWriterLE outstream(outfile);
1083 FBX::Node object_node("Objects");
1084 int indent = 0;
1085 object_node.Begin(outstream, binary, indent);
1086 object_node.EndProperties(outstream, binary, indent);
1087 object_node.BeginChildren(outstream, binary, indent);
1088
1089 bool bJoinIdenticalVertices = mProperties->GetPropertyBool("bJoinIdenticalVertices", true);
1090 std::vector<std::vector<int32_t>> vVertexIndice;//save vertex_indices as it is needed later
1091
1092 // geometry (aiMesh)
1093 mesh_uids.clear();
1094 indent = 1;
1095 for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
1096 // it's all about this mesh
1097 aiMesh* m = mScene->mMeshes[mi];
1098
1099 // start the node record
1100 FBX::Node n("Geometry");
1101 int64_t uid = generate_uid();
1102 mesh_uids.push_back(uid);
1103 n.AddProperty(uid);
1104 n.AddProperty(FBX::SEPARATOR + "Geometry");
1105 n.AddProperty("Mesh");
1106 n.Begin(outstream, binary, indent);
1107 n.DumpProperties(outstream, binary, indent);
1108 n.EndProperties(outstream, binary, indent);
1109 n.BeginChildren(outstream, binary, indent);
1110 indent = 2;
1111
1112 // output vertex data - each vertex should be unique (probably)
1113 std::vector<double> flattened_vertices;
1114 // index of original vertex in vertex data vector
1115 std::vector<int32_t> vertex_indices;
1116 // map of vertex value to its index in the data vector
1117 std::map<aiVector3D,size_t> index_by_vertex_value;
1118 if(bJoinIdenticalVertices){
1119 int32_t index = 0;
1120 for (size_t vi = 0; vi < m->mNumVertices; ++vi) {
1121 aiVector3D vtx = m->mVertices[vi];
1122 auto elem = index_by_vertex_value.find(vtx);
1123 if (elem == index_by_vertex_value.end()) {
1124 vertex_indices.push_back(index);
1125 index_by_vertex_value[vtx] = index;
1126 flattened_vertices.push_back(vtx[0]);
1127 flattened_vertices.push_back(vtx[1]);
1128 flattened_vertices.push_back(vtx[2]);
1129 ++index;
1130 } else {
1131 vertex_indices.push_back(int32_t(elem->second));
1132 }
1133 }
1134 }
1135 else { // do not join vertex, respect the export flag
1136 vertex_indices.resize(m->mNumVertices);
1137 std::iota(vertex_indices.begin(), vertex_indices.end(), 0);
1138 for(unsigned int v = 0; v < m->mNumVertices; ++ v) {
1139 aiVector3D vtx = m->mVertices[v];
1140 flattened_vertices.push_back(vtx.x);
1141 flattened_vertices.push_back(vtx.y);
1142 flattened_vertices.push_back(vtx.z);
1143 }
1144 }
1145 vVertexIndice.push_back(vertex_indices);
1146
1147 FBX::Node::WritePropertyNode(
1148 "Vertices", flattened_vertices, outstream, binary, indent
1149 );
1150
1151 // output polygon data as a flattened array of vertex indices.
1152 // the last vertex index of each polygon is negated and - 1
1153 std::vector<int32_t> polygon_data;
1154 for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
1155 const aiFace &f = m->mFaces[fi];
1156 for (size_t pvi = 0; pvi < f.mNumIndices - 1; ++pvi) {
1157 polygon_data.push_back(vertex_indices[f.mIndices[pvi]]);
1158 }
1159 polygon_data.push_back(
1160 -1 - vertex_indices[f.mIndices[f.mNumIndices-1]]
1161 );
1162 }
1163 FBX::Node::WritePropertyNode(
1164 "PolygonVertexIndex", polygon_data, outstream, binary, indent
1165 );
1166
1167 // here could be edges but they're insane.
1168 // it's optional anyway, so let's ignore it.
1169
1170 FBX::Node::WritePropertyNode(
1171 "GeometryVersion", int32_t(124), outstream, binary, indent
1172 );
1173
1174 // normals, if any
1175 if (m->HasNormals()) {
1176 FBX::Node normals("LayerElementNormal", int32_t(0));
1177 normals.Begin(outstream, binary, indent);
1178 normals.DumpProperties(outstream, binary, indent);
1179 normals.EndProperties(outstream, binary, indent);
1180 normals.BeginChildren(outstream, binary, indent);
1181 indent = 3;
1182 FBX::Node::WritePropertyNode(
1183 "Version", int32_t(101), outstream, binary, indent
1184 );
1185 FBX::Node::WritePropertyNode(
1186 "Name", "", outstream, binary, indent
1187 );
1188 FBX::Node::WritePropertyNode(
1189 "MappingInformationType", "ByPolygonVertex",
1190 outstream, binary, indent
1191 );
1192 // TODO: vertex-normals or indexed normals when appropriate
1193 FBX::Node::WritePropertyNode(
1194 "ReferenceInformationType", "Direct",
1195 outstream, binary, indent
1196 );
1197 std::vector<double> normal_data;
1198 normal_data.reserve(3 * polygon_data.size());
1199 for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
1200 const aiFace &f = m->mFaces[fi];
1201 for (size_t pvi = 0; pvi < f.mNumIndices; ++pvi) {
1202 const aiVector3D &curN = m->mNormals[f.mIndices[pvi]];
1203 normal_data.push_back(curN.x);
1204 normal_data.push_back(curN.y);
1205 normal_data.push_back(curN.z);
1206 }
1207 }
1208 FBX::Node::WritePropertyNode(
1209 "Normals", normal_data, outstream, binary, indent
1210 );
1211 // note: version 102 has a NormalsW also... not sure what it is,
1212 // so we can stick with version 101 for now.
1213 indent = 2;
1214 normals.End(outstream, binary, indent, true);
1215 }
1216
1217 // colors, if any
1218 // TODO only one color channel currently
1219 const int32_t colorChannelIndex = 0;
1220 if (m->HasVertexColors(colorChannelIndex)) {
1221 FBX::Node vertexcolors("LayerElementColor", int32_t(colorChannelIndex));
1222 vertexcolors.Begin(outstream, binary, indent);
1223 vertexcolors.DumpProperties(outstream, binary, indent);
1224 vertexcolors.EndProperties(outstream, binary, indent);
1225 vertexcolors.BeginChildren(outstream, binary, indent);
1226 indent = 3;
1227 FBX::Node::WritePropertyNode(
1228 "Version", int32_t(101), outstream, binary, indent
1229 );
1230 char layerName[8];
1231 sprintf(layerName, "COLOR_%d", colorChannelIndex);
1232 FBX::Node::WritePropertyNode(
1233 "Name", (const char*)layerName, outstream, binary, indent
1234 );
1235 FBX::Node::WritePropertyNode(
1236 "MappingInformationType", "ByPolygonVertex",
1237 outstream, binary, indent
1238 );
1239 FBX::Node::WritePropertyNode(
1240 "ReferenceInformationType", "Direct",
1241 outstream, binary, indent
1242 );
1243 std::vector<double> color_data;
1244 color_data.reserve(4 * polygon_data.size());
1245 for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
1246 const aiFace &f = m->mFaces[fi];
1247 for (size_t pvi = 0; pvi < f.mNumIndices; ++pvi) {
1248 const aiColor4D &c = m->mColors[colorChannelIndex][f.mIndices[pvi]];
1249 color_data.push_back(c.r);
1250 color_data.push_back(c.g);
1251 color_data.push_back(c.b);
1252 color_data.push_back(c.a);
1253 }
1254 }
1255 FBX::Node::WritePropertyNode(
1256 "Colors", color_data, outstream, binary, indent
1257 );
1258 indent = 2;
1259 vertexcolors.End(outstream, binary, indent, true);
1260 }
1261
1262 // uvs, if any
1263 for (size_t uvi = 0; uvi < m->GetNumUVChannels(); ++uvi) {
1264 if (m->mNumUVComponents[uvi] > 2) {
1265 // FBX only supports 2-channel UV maps...
1266 // or at least i'm not sure how to indicate a different number
1267 std::stringstream err;
1268 err << "Only 2-channel UV maps supported by FBX,";
1269 err << " but mesh " << mi;
1270 if (m->mName.length) {
1271 err << " (" << m->mName.C_Str() << ")";
1272 }
1273 err << " UV map " << uvi;
1274 err << " has " << m->mNumUVComponents[uvi];
1275 err << " components! Data will be preserved,";
1276 err << " but may be incorrectly interpreted on load.";
1277 ASSIMP_LOG_WARN(err.str());
1278 }
1279 FBX::Node uv("LayerElementUV", int32_t(uvi));
1280 uv.Begin(outstream, binary, indent);
1281 uv.DumpProperties(outstream, binary, indent);
1282 uv.EndProperties(outstream, binary, indent);
1283 uv.BeginChildren(outstream, binary, indent);
1284 indent = 3;
1285 FBX::Node::WritePropertyNode(
1286 "Version", int32_t(101), outstream, binary, indent
1287 );
1288 // it doesn't seem like assimp keeps the uv map name,
1289 // so just leave it blank.
1290 FBX::Node::WritePropertyNode(
1291 "Name", "", outstream, binary, indent
1292 );
1293 FBX::Node::WritePropertyNode(
1294 "MappingInformationType", "ByPolygonVertex",
1295 outstream, binary, indent
1296 );
1297 FBX::Node::WritePropertyNode(
1298 "ReferenceInformationType", "IndexToDirect",
1299 outstream, binary, indent
1300 );
1301
1302 std::vector<double> uv_data;
1303 std::vector<int32_t> uv_indices;
1304 std::map<aiVector3D,int32_t> index_by_uv;
1305 int32_t index = 0;
1306 for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
1307 const aiFace &f = m->mFaces[fi];
1308 for (size_t pvi = 0; pvi < f.mNumIndices; ++pvi) {
1309 const aiVector3D &curUv =
1310 m->mTextureCoords[uvi][f.mIndices[pvi]];
1311 auto elem = index_by_uv.find(curUv);
1312 if (elem == index_by_uv.end()) {
1313 index_by_uv[curUv] = index;
1314 uv_indices.push_back(index);
1315 for (unsigned int x = 0; x < m->mNumUVComponents[uvi]; ++x) {
1316 uv_data.push_back(curUv[x]);
1317 }
1318 ++index;
1319 } else {
1320 uv_indices.push_back(elem->second);
1321 }
1322 }
1323 }
1324 FBX::Node::WritePropertyNode(
1325 "UV", uv_data, outstream, binary, indent
1326 );
1327 FBX::Node::WritePropertyNode(
1328 "UVIndex", uv_indices, outstream, binary, indent
1329 );
1330 indent = 2;
1331 uv.End(outstream, binary, indent, true);
1332 }
1333
1334 // i'm not really sure why this material section exists,
1335 // as the material is linked via "Connections".
1336 // it seems to always have the same "0" value.
1337 FBX::Node mat("LayerElementMaterial", int32_t(0));
1338 mat.AddChild("Version", int32_t(101));
1339 mat.AddChild("Name", "");
1340 mat.AddChild("MappingInformationType", "AllSame");
1341 mat.AddChild("ReferenceInformationType", "IndexToDirect");
1342 std::vector<int32_t> mat_indices = {0};
1343 mat.AddChild("Materials", mat_indices);
1344 mat.Dump(outstream, binary, indent);
1345
1346 // finally we have the layer specifications,
1347 // which select the normals / UV set / etc to use.
1348 // TODO: handle multiple uv sets correctly?
1349 FBX::Node layer("Layer", int32_t(0));
1350 layer.AddChild("Version", int32_t(100));
1351 FBX::Node le("LayerElement");
1352 le.AddChild("Type", "LayerElementNormal");
1353 le.AddChild("TypedIndex", int32_t(0));
1354 layer.AddChild(le);
1355 // TODO only 1 color channel currently
1356 le = FBX::Node("LayerElement");
1357 le.AddChild("Type", "LayerElementColor");
1358 le.AddChild("TypedIndex", int32_t(0));
1359 layer.AddChild(le);
1360 le = FBX::Node("LayerElement");
1361 le.AddChild("Type", "LayerElementMaterial");
1362 le.AddChild("TypedIndex", int32_t(0));
1363 layer.AddChild(le);
1364 le = FBX::Node("LayerElement");
1365 le.AddChild("Type", "LayerElementUV");
1366 le.AddChild("TypedIndex", int32_t(0));
1367 layer.AddChild(le);
1368 layer.Dump(outstream, binary, indent);
1369
1370 for(unsigned int lr = 1; lr < m->GetNumUVChannels(); ++ lr)
1371 {
1372 FBX::Node layerExtra("Layer", int32_t(lr));
1373 layerExtra.AddChild("Version", int32_t(100));
1374 FBX::Node leExtra("LayerElement");
1375 leExtra.AddChild("Type", "LayerElementUV");
1376 leExtra.AddChild("TypedIndex", int32_t(lr));
1377 layerExtra.AddChild(leExtra);
1378 layerExtra.Dump(outstream, binary, indent);
1379 }
1380 // finish the node record
1381 indent = 1;
1382 n.End(outstream, binary, indent, true);
1383 }
1384
1385
1386 // aiMaterial
1387 material_uids.clear();
1388 for (size_t i = 0; i < mScene->mNumMaterials; ++i) {
1389 // it's all about this material
1390 aiMaterial* m = mScene->mMaterials[i];
1391
1392 // these are used to receive material data
1393 float f; aiColor3D c;
1394
1395 // start the node record
1396 FBX::Node n("Material");
1397
1398 int64_t uid = generate_uid();
1399 material_uids.push_back(uid);
1400 n.AddProperty(uid);
1401
1402 aiString name;
1403 m->Get(AI_MATKEY_NAME, name);
1404 n.AddProperty(name.C_Str() + FBX::SEPARATOR + "Material");
1405
1406 n.AddProperty("");
1407
1408 n.AddChild("Version", int32_t(102));
1409 f = 0;
1410 m->Get(AI_MATKEY_SHININESS, f);
1411 bool phong = (f > 0);
1412 if (phong) {
1413 n.AddChild("ShadingModel", "phong");
1414 } else {
1415 n.AddChild("ShadingModel", "lambert");
1416 }
1417 n.AddChild("MultiLayer", int32_t(0));
1418
1419 FBX::Node p("Properties70");
1420
1421 // materials exported using the FBX SDK have two sets of fields.
1422 // there are the properties specified in the PropertyTemplate,
1423 // which are those supported by the modernFBX SDK,
1424 // and an extra set of properties with simpler names.
1425 // The extra properties are a legacy material system from pre-2009.
1426 //
1427 // In the modern system, each property has "color" and "factor".
1428 // Generally the interpretation of these seems to be
1429 // that the colour is multiplied by the factor before use,
1430 // but this is not always clear-cut.
1431 //
1432 // Usually assimp only stores the colour,
1433 // so we can just leave the factors at the default "1.0".
1434
1435 // first we can export the "standard" properties
1436 if (m->Get(AI_MATKEY_COLOR_AMBIENT, c) == aiReturn_SUCCESS) {
1437 p.AddP70colorA("AmbientColor", c.r, c.g, c.b);
1438 //p.AddP70numberA("AmbientFactor", 1.0);
1439 }
1440 if (m->Get(AI_MATKEY_COLOR_DIFFUSE, c) == aiReturn_SUCCESS) {
1441 p.AddP70colorA("DiffuseColor", c.r, c.g, c.b);
1442 //p.AddP70numberA("DiffuseFactor", 1.0);
1443 }
1444 if (m->Get(AI_MATKEY_COLOR_TRANSPARENT, c) == aiReturn_SUCCESS) {
1445 // "TransparentColor" / "TransparencyFactor"...
1446 // thanks FBX, for your insightful interpretation of consistency
1447 p.AddP70colorA("TransparentColor", c.r, c.g, c.b);
1448 // TransparencyFactor defaults to 0.0, so set it to 1.0.
1449 // note: Maya always sets this to 1.0,
1450 // so we can't use it sensibly as "Opacity".
1451 // In stead we rely on the legacy "Opacity" value, below.
1452 // Blender also relies on "Opacity" not "TransparencyFactor",
1453 // probably for a similar reason.
1454 p.AddP70numberA("TransparencyFactor", 1.0);
1455 }
1456 if (m->Get(AI_MATKEY_COLOR_REFLECTIVE, c) == aiReturn_SUCCESS) {
1457 p.AddP70colorA("ReflectionColor", c.r, c.g, c.b);
1458 }
1459 if (m->Get(AI_MATKEY_REFLECTIVITY, f) == aiReturn_SUCCESS) {
1460 p.AddP70numberA("ReflectionFactor", f);
1461 }
1462 if (phong) {
1463 if (m->Get(AI_MATKEY_COLOR_SPECULAR, c) == aiReturn_SUCCESS) {
1464 p.AddP70colorA("SpecularColor", c.r, c.g, c.b);
1465 }
1466 if (m->Get(AI_MATKEY_SHININESS_STRENGTH, f) == aiReturn_SUCCESS) {
1467 p.AddP70numberA("ShininessFactor", f);
1468 }
1469 if (m->Get(AI_MATKEY_SHININESS, f) == aiReturn_SUCCESS) {
1470 p.AddP70numberA("ShininessExponent", f);
1471 }
1472 if (m->Get(AI_MATKEY_REFLECTIVITY, f) == aiReturn_SUCCESS) {
1473 p.AddP70numberA("ReflectionFactor", f);
1474 }
1475 }
1476
1477 // Now the legacy system.
1478 // For safety let's include it.
1479 // thrse values don't exist in the property template,
1480 // and usually are completely ignored when loading.
1481 // One notable exception is the "Opacity" property,
1482 // which Blender uses as (1.0 - alpha).
1483 c.r = 0.0f; c.g = 0.0f; c.b = 0.0f;
1484 m->Get(AI_MATKEY_COLOR_EMISSIVE, c);
1485 p.AddP70vector("Emissive", c.r, c.g, c.b);
1486 c.r = 0.2f; c.g = 0.2f; c.b = 0.2f;
1487 m->Get(AI_MATKEY_COLOR_AMBIENT, c);
1488 p.AddP70vector("Ambient", c.r, c.g, c.b);
1489 c.r = 0.8f; c.g = 0.8f; c.b = 0.8f;
1490 m->Get(AI_MATKEY_COLOR_DIFFUSE, c);
1491 p.AddP70vector("Diffuse", c.r, c.g, c.b);
1492 // The FBX SDK determines "Opacity" from transparency colour (RGB)
1493 // and factor (F) as: O = (1.0 - F * ((R + G + B) / 3)).
1494 // However we actually have an opacity value,
1495 // so we should take it from AI_MATKEY_OPACITY if possible.
1496 // It might make more sense to use TransparencyFactor,
1497 // but Blender actually loads "Opacity" correctly, so let's use it.
1498 f = 1.0f;
1499 if (m->Get(AI_MATKEY_COLOR_TRANSPARENT, c) == aiReturn_SUCCESS) {
1500 f = 1.0f - ((c.r + c.g + c.b) / 3.0f);
1501 }
1502 m->Get(AI_MATKEY_OPACITY, f);
1503 p.AddP70double("Opacity", f);
1504 if (phong) {
1505 // specular color is multiplied by shininess_strength
1506 c.r = 0.2f; c.g = 0.2f; c.b = 0.2f;
1507 m->Get(AI_MATKEY_COLOR_SPECULAR, c);
1508 f = 1.0f;
1509 m->Get(AI_MATKEY_SHININESS_STRENGTH, f);
1510 p.AddP70vector("Specular", f*c.r, f*c.g, f*c.b);
1511 f = 20.0f;
1512 m->Get(AI_MATKEY_SHININESS, f);
1513 p.AddP70double("Shininess", f);
1514 // Legacy "Reflectivity" is F*F*((R+G+B)/3),
1515 // where F is the proportion of light reflected (AKA reflectivity),
1516 // and RGB is the reflective colour of the material.
1517 // No idea why, but we might as well set it the same way.
1518 f = 0.0f;
1519 m->Get(AI_MATKEY_REFLECTIVITY, f);
1520 c.r = 1.0f, c.g = 1.0f, c.b = 1.0f;
1521 m->Get(AI_MATKEY_COLOR_REFLECTIVE, c);
1522 p.AddP70double("Reflectivity", f*f*((c.r+c.g+c.b)/3.0));
1523 }
1524
1525 n.AddChild(p);
1526
1527 n.Dump(outstream, binary, indent);
1528 }
1529
1530 // we need to look up all the images we're using,
1531 // so we can generate uids, and eliminate duplicates.
1532 std::map<std::string, int64_t> uid_by_image;
1533 for (size_t i = 0; i < mScene->mNumMaterials; ++i) {
1534 aiString texpath;
1535 aiMaterial* mat = mScene->mMaterials[i];
1536 for (
1537 size_t tt = aiTextureType_DIFFUSE;
1538 tt < aiTextureType_UNKNOWN;
1539 ++tt
1540 ){
1541 const aiTextureType textype = static_cast<aiTextureType>(tt);
1542 const size_t texcount = mat->GetTextureCount(textype);
1543 for (size_t j = 0; j < texcount; ++j) {
1544 mat->GetTexture(textype, (unsigned int)j, &texpath);
1545 const std::string texstring = texpath.C_Str();
1546 auto elem = uid_by_image.find(texstring);
1547 if (elem == uid_by_image.end()) {
1548 uid_by_image[texstring] = generate_uid();
1549 }
1550 }
1551 }
1552 }
1553
1554 // FbxVideo - stores images used by textures.
1555 for (const auto &it : uid_by_image) {
1556 FBX::Node n("Video");
1557 const int64_t& uid = it.second;
1558 const std::string name = ""; // TODO: ... name???
1559 n.AddProperties(uid, name + FBX::SEPARATOR + "Video", "Clip");
1560 n.AddChild("Type", "Clip");
1561 FBX::Node p("Properties70");
1562 // TODO: get full path... relative path... etc... ugh...
1563 // for now just use the same path for everything,
1564 // and hopefully one of them will work out.
1565 std::string path = it.first;
1566 // try get embedded texture
1567 const aiTexture* embedded_texture = mScene->GetEmbeddedTexture(it.first.c_str());
1568 if (embedded_texture != nullptr) {
1569 // change the path (use original filename, if available. If name is empty, concatenate texture index with file extension)
1570 std::stringstream newPath;
1571 if (embedded_texture->mFilename.length > 0) {
1572 newPath << embedded_texture->mFilename.C_Str();
1573 } else if (embedded_texture->achFormatHint[0]) {
1574 int texture_index = std::stoi(path.substr(1, path.size() - 1));
1575 newPath << texture_index << "." << embedded_texture->achFormatHint;
1576 }
1577 path = newPath.str();
1578 // embed the texture
1579 size_t texture_size = static_cast<size_t>(embedded_texture->mWidth * std::max(embedded_texture->mHeight, 1u));
1580 if (binary) {
1581 // embed texture as binary data
1582 std::vector<uint8_t> tex_data;
1583 tex_data.resize(texture_size);
1584 memcpy(&tex_data[0], (char*)embedded_texture->pcData, texture_size);
1585 n.AddChild("Content", tex_data);
1586 } else {
1587 // embed texture in base64 encoding
1588 std::string encoded_texture = FBX::Util::EncodeBase64((char*)embedded_texture->pcData, texture_size);
1589 n.AddChild("Content", encoded_texture);
1590 }
1591 }
1592 p.AddP70("Path", "KString", "XRefUrl", "", path);
1593 n.AddChild(p);
1594 n.AddChild("UseMipMap", int32_t(0));
1595 n.AddChild("Filename", path);
1596 n.AddChild("RelativeFilename", path);
1597 n.Dump(outstream, binary, indent);
1598 }
1599
1600 // Textures
1601 // referenced by material_index/texture_type pairs.
1602 std::map<std::pair<size_t,size_t>,int64_t> texture_uids;
1603 const std::map<aiTextureType,std::string> prop_name_by_tt = {
1604 {aiTextureType_DIFFUSE, "DiffuseColor"},
1605 {aiTextureType_SPECULAR, "SpecularColor"},
1606 {aiTextureType_AMBIENT, "AmbientColor"},
1607 {aiTextureType_EMISSIVE, "EmissiveColor"},
1608 {aiTextureType_HEIGHT, "Bump"},
1609 {aiTextureType_NORMALS, "NormalMap"},
1610 {aiTextureType_SHININESS, "ShininessExponent"},
1611 {aiTextureType_OPACITY, "TransparentColor"},
1612 {aiTextureType_DISPLACEMENT, "DisplacementColor"},
1613 //{aiTextureType_LIGHTMAP, "???"},
1614 {aiTextureType_REFLECTION, "ReflectionColor"}
1615 //{aiTextureType_UNKNOWN, ""}
1616 };
1617 for (size_t i = 0; i < mScene->mNumMaterials; ++i) {
1618 // textures are attached to materials
1619 aiMaterial* mat = mScene->mMaterials[i];
1620 int64_t material_uid = material_uids[i];
1621
1622 for (
1623 size_t j = aiTextureType_DIFFUSE;
1624 j < aiTextureType_UNKNOWN;
1625 ++j
1626 ) {
1627 const aiTextureType tt = static_cast<aiTextureType>(j);
1628 size_t n = mat->GetTextureCount(tt);
1629
1630 if (n < 1) { // no texture of this type
1631 continue;
1632 }
1633
1634 if (n > 1) {
1635 // TODO: multilayer textures
1636 std::stringstream err;
1637 err << "Multilayer textures not supported (for now),";
1638 err << " skipping texture type " << j;
1639 err << " of material " << i;
1640 ASSIMP_LOG_WARN(err.str());
1641 }
1642
1643 // get image path for this (single-image) texture
1644 aiString tpath;
1645 if (mat->GetTexture(tt, 0, &tpath) != aiReturn_SUCCESS) {
1646 std::stringstream err;
1647 err << "Failed to get texture 0 for texture of type " << tt;
1648 err << " on material " << i;
1649 err << ", however GetTextureCount returned 1.";
1650 throw DeadlyExportError(err.str());
1651 }
1652 const std::string texture_path(tpath.C_Str());
1653
1654 // get connected image uid
1655 auto elem = uid_by_image.find(texture_path);
1656 if (elem == uid_by_image.end()) {
1657 // this should never happen
1658 std::stringstream err;
1659 err << "Failed to find video element for texture with path";
1660 err << " \"" << texture_path << "\"";
1661 err << ", type " << j << ", material " << i;
1662 throw DeadlyExportError(err.str());
1663 }
1664 const int64_t image_uid = elem->second;
1665
1666 // get the name of the material property to connect to
1667 auto elem2 = prop_name_by_tt.find(tt);
1668 if (elem2 == prop_name_by_tt.end()) {
1669 // don't know how to handle this type of texture,
1670 // so skip it.
1671 std::stringstream err;
1672 err << "Not sure how to handle texture of type " << j;
1673 err << " on material " << i;
1674 err << ", skipping...";
1675 ASSIMP_LOG_WARN(err.str());
1676 continue;
1677 }
1678 const std::string& prop_name = elem2->second;
1679
1680 // generate a uid for this texture
1681 const int64_t texture_uid = generate_uid();
1682
1683 // link the texture to the material
1684 connections.emplace_back(
1685 "C", "OP", texture_uid, material_uid, prop_name
1686 );
1687
1688 // link the image data to the texture
1689 connections.emplace_back("C", "OO", image_uid, texture_uid);
1690
1691 aiUVTransform trafo;
1692 unsigned int max = sizeof(aiUVTransform);
1693 aiGetMaterialFloatArray(mat, AI_MATKEY_UVTRANSFORM(aiTextureType_DIFFUSE, 0), (ai_real *)&trafo, &max);
1694
1695 // now write the actual texture node
1696 FBX::Node tnode("Texture");
1697 // TODO: some way to determine texture name?
1698 const std::string texture_name = "" + FBX::SEPARATOR + "Texture";
1699 tnode.AddProperties(texture_uid, texture_name, "");
1700 // there really doesn't seem to be a better type than this:
1701 tnode.AddChild("Type", "TextureVideoClip");
1702 tnode.AddChild("Version", int32_t(202));
1703 tnode.AddChild("TextureName", texture_name);
1704 FBX::Node p("Properties70");
1705 p.AddP70vectorA("Translation", trafo.mTranslation[0], trafo.mTranslation[1], 0.0);
1706 p.AddP70vectorA("Rotation", 0, 0, trafo.mRotation);
1707 p.AddP70vectorA("Scaling", trafo.mScaling[0], trafo.mScaling[1], 0.0);
1708 p.AddP70enum("CurrentTextureBlendMode", 0); // TODO: verify
1709 //p.AddP70string("UVSet", ""); // TODO: how should this work?
1710 p.AddP70bool("UseMaterial", 1);
1711 tnode.AddChild(p);
1712 // can't easily determine which texture path will be correct,
1713 // so just store what we have in every field.
1714 // these being incorrect is a common problem with FBX anyway.
1715 tnode.AddChild("FileName", texture_path);
1716 tnode.AddChild("RelativeFilename", texture_path);
1717 tnode.AddChild("ModelUVTranslation", double(0.0), double(0.0));
1718 tnode.AddChild("ModelUVScaling", double(1.0), double(1.0));
1719 tnode.AddChild("Texture_Alpha_Source", "None");
1720 tnode.AddChild(
1721 "Cropping", int32_t(0), int32_t(0), int32_t(0), int32_t(0)
1722 );
1723 tnode.Dump(outstream, binary, indent);
1724 }
1725 }
1726
1727 // Blendshapes, if any
1728 for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
1729 const aiMesh* m = mScene->mMeshes[mi];
1730 if (m->mNumAnimMeshes == 0) {
1731 continue;
1732 }
1733 // make a deformer for this mesh
1734 int64_t deformer_uid = generate_uid();
1735 FBX::Node dnode("Deformer");
1736 dnode.AddProperties(deformer_uid, m->mName.data + FBX::SEPARATOR + "Blendshapes", "BlendShape");
1737 dnode.AddChild("Version", int32_t(101));
1738 dnode.Dump(outstream, binary, indent);
1739 // connect it
1740 connections.emplace_back("C", "OO", deformer_uid, mesh_uids[mi]);
1741 std::vector<int32_t> vertex_indices = vVertexIndice[mi];
1742
1743 for (unsigned int am = 0; am < m->mNumAnimMeshes; ++am) {
1744 aiAnimMesh *pAnimMesh = m->mAnimMeshes[am];
1745 std::string blendshape_name = pAnimMesh->mName.data;
1746
1747 // start the node record
1748 FBX::Node bsnode("Geometry");
1749 int64_t blendshape_uid = generate_uid();
1750 mesh_uids.push_back(blendshape_uid);
1751 bsnode.AddProperty(blendshape_uid);
1752 bsnode.AddProperty(blendshape_name + FBX::SEPARATOR + "Blendshape");
1753 bsnode.AddProperty("Shape");
1754 bsnode.AddChild("Version", int32_t(100));
1755 bsnode.Begin(outstream, binary, indent);
1756 bsnode.DumpProperties(outstream, binary, indent);
1757 bsnode.EndProperties(outstream, binary, indent);
1758 bsnode.BeginChildren(outstream, binary, indent);
1759 indent++;
1760 if (pAnimMesh->HasPositions()) {
1761 std::vector<int32_t>shape_indices;
1762 std::vector<double>pPositionDiff;
1763 std::vector<double>pNormalDiff;
1764
1765 for (unsigned int vt = 0; vt < vertex_indices.size(); ++vt) {
1766 aiVector3D pDiff = (pAnimMesh->mVertices[vertex_indices[vt]] - m->mVertices[vertex_indices[vt]]);
1767 if(pDiff.Length()>1e-8){
1768 shape_indices.push_back(vertex_indices[vt]);
1769 pPositionDiff.push_back(pDiff[0]);
1770 pPositionDiff.push_back(pDiff[1]);
1771 pPositionDiff.push_back(pDiff[2]);
1772
1773 if (pAnimMesh->HasNormals()) {
1774 aiVector3D nDiff = (pAnimMesh->mNormals[vertex_indices[vt]] - m->mNormals[vertex_indices[vt]]);
1775 pNormalDiff.push_back(nDiff[0]);
1776 pNormalDiff.push_back(nDiff[1]);
1777 pNormalDiff.push_back(nDiff[2]);
1778 }
1779 }
1780 }
1781
1782 FBX::Node::WritePropertyNode(
1783 "Indexes", shape_indices, outstream, binary, indent
1784 );
1785
1786 FBX::Node::WritePropertyNode(
1787 "Vertices", pPositionDiff, outstream, binary, indent
1788 );
1789
1790 if (pNormalDiff.size()>0) {
1791 FBX::Node::WritePropertyNode(
1792 "Normals", pNormalDiff, outstream, binary, indent
1793 );
1794 }
1795 }
1796 indent--;
1797 bsnode.End(outstream, binary, indent, true);
1798
1799 // Add blendshape Channel Deformer
1800 FBX::Node sdnode("Deformer");
1801 const int64_t blendchannel_uid = generate_uid();
1802 sdnode.AddProperties(
1803 blendchannel_uid, blendshape_name + FBX::SEPARATOR + "SubDeformer", "BlendShapeChannel"
1804 );
1805 sdnode.AddChild("Version", int32_t(100));
1806 sdnode.AddChild("DeformPercent", float(0.0));
1807 FBX::Node p("Properties70");
1808 p.AddP70numberA("DeformPercent", 0.0);
1809 sdnode.AddChild(p);
1810 // TODO: Normally just one weight per channel, adding stub for later development
1811 std::vector<float>fFullWeights;
1812 fFullWeights.push_back(100.);
1813 sdnode.AddChild("FullWeights", fFullWeights);
1814 sdnode.Dump(outstream, binary, indent);
1815
1816 connections.emplace_back("C", "OO", blendchannel_uid, deformer_uid);
1817 connections.emplace_back("C", "OO", blendshape_uid, blendchannel_uid);
1818 }
1819 }
1820
1821 // bones.
1822 //
1823 // output structure:
1824 // subset of node hierarchy that are "skeleton",
1825 // i.e. do not have meshes but only bones.
1826 // but.. i'm not sure how anyone could guarantee that...
1827 //
1828 // input...
1829 // well, for each mesh it has "bones",
1830 // and the bone names correspond to nodes.
1831 // of course we also need the parent nodes,
1832 // as they give some of the transform........
1833 //
1834 // well. we can assume a sane input, i suppose.
1835 //
1836 // so input is the bone node hierarchy,
1837 // with an extra thing for the transformation of the MESH in BONE space.
1838 //
1839 // output is a set of bone nodes,
1840 // a "bindpose" which indicates the default local transform of all bones,
1841 // and a set of "deformers".
1842 // each deformer is parented to a mesh geometry,
1843 // and has one or more "subdeformer"s as children.
1844 // each subdeformer has one bone node as a child,
1845 // and represents the influence of that bone on the grandparent mesh.
1846 // the subdeformer has a list of indices, and weights,
1847 // with indices specifying vertex indices,
1848 // and weights specifying the corresponding influence of this bone.
1849 // it also has Transform and TransformLink elements,
1850 // specifying the transform of the MESH in BONE space,
1851 // and the transformation of the BONE in WORLD space,
1852 // likely in the bindpose.
1853 //
1854 // the input bone structure is different but similar,
1855 // storing the number of weights for this bone,
1856 // and an array of (vertex index, weight) pairs.
1857 //
1858 // one sticky point is that the number of vertices may not match,
1859 // because assimp splits vertices by normal, uv, etc.
1860
1861 // functor for aiNode sorting
1862 struct SortNodeByName
1863 {
1864 bool operator()(const aiNode *lhs, const aiNode *rhs) const
1865 {
1866 return strcmp(lhs->mName.C_Str(), rhs->mName.C_Str()) < 0;
1867 }
1868 };
1869
1870 // first we should mark the skeleton for each mesh.
1871 // the skeleton must include not only the aiBones,
1872 // but also all their parent nodes.
1873 // anything that affects the position of any bone node must be included.
1874 // Use SorNodeByName to make sure the exported result will be the same across all systems
1875 // Otherwise the aiNodes of the skeleton would be sorted based on the pointer address, which isn't consistent
1876 std::vector<std::set<const aiNode*, SortNodeByName>> skeleton_by_mesh(mScene->mNumMeshes);
1877 // at the same time we can build a list of all the skeleton nodes,
1878 // which will be used later to mark them as type "limbNode".
1879 std::unordered_set<const aiNode*> limbnodes;
1880
1881 //actual bone nodes in fbx, without parenting-up
1882 std::unordered_set<std::string> setAllBoneNamesInScene;
1883 for(unsigned int m = 0; m < mScene->mNumMeshes; ++ m)
1884 {
1885 aiMesh* pMesh = mScene->mMeshes[m];
1886 for(unsigned int b = 0; b < pMesh->mNumBones; ++ b)
1887 setAllBoneNamesInScene.insert(pMesh->mBones[b]->mName.data);
1888 }
1889 aiMatrix4x4 mxTransIdentity;
1890
1891 // and a map of nodes by bone name, as finding them is annoying.
1892 std::map<std::string,aiNode*> node_by_bone;
1893 for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
1894 const aiMesh* m = mScene->mMeshes[mi];
1895 std::set<const aiNode*, SortNodeByName> skeleton;
1896 for (size_t bi =0; bi < m->mNumBones; ++bi) {
1897 const aiBone* b = m->mBones[bi];
1898 const std::string name(b->mName.C_Str());
1899 auto elem = node_by_bone.find(name);
1900 aiNode* n;
1901 if (elem != node_by_bone.end()) {
1902 n = elem->second;
1903 } else {
1904 n = mScene->mRootNode->FindNode(b->mName);
1905 if (!n) {
1906 // this should never happen
1907 std::stringstream err;
1908 err << "Failed to find node for bone: \"" << name << "\"";
1909 throw DeadlyExportError(err.str());
1910 }
1911 node_by_bone[name] = n;
1912 limbnodes.insert(n);
1913 }
1914 skeleton.insert(n);
1915 // mark all parent nodes as skeleton as well,
1916 // up until we find the root node,
1917 // or else the node containing the mesh,
1918 // or else the parent of a node containing the mesh.
1919 for (
1920 const aiNode* parent = n->mParent;
1921 parent && parent != mScene->mRootNode;
1922 parent = parent->mParent
1923 ) {
1924 // if we've already done this node we can skip it all
1925 if (skeleton.count(parent)) {
1926 break;
1927 }
1928 // ignore fbx transform nodes as these will be collapsed later
1929 // TODO: cache this by aiNode*
1930 const std::string node_name(parent->mName.C_Str());
1931 if (node_name.find(MAGIC_NODE_TAG) != std::string::npos) {
1932 continue;
1933 }
1934 //not a bone in scene && no effect in transform
1935 if(setAllBoneNamesInScene.find(node_name)==setAllBoneNamesInScene.end()
1936 && parent->mTransformation == mxTransIdentity) {
1937 continue;
1938 }
1939 // otherwise check if this is the root of the skeleton
1940 bool end = false;
1941 // is the mesh part of this node?
1942 for (size_t i = 0; i < parent->mNumMeshes; ++i) {
1943 if (parent->mMeshes[i] == mi) {
1944 end = true;
1945 break;
1946 }
1947 }
1948 // is the mesh in one of the children of this node?
1949 for (size_t j = 0; j < parent->mNumChildren; ++j) {
1950 aiNode* child = parent->mChildren[j];
1951 for (size_t i = 0; i < child->mNumMeshes; ++i) {
1952 if (child->mMeshes[i] == mi) {
1953 end = true;
1954 break;
1955 }
1956 }
1957 if (end) { break; }
1958 }
1959
1960 // if it was the skeleton root we can finish here
1961 if (end) { break; }
1962 }
1963 }
1964 skeleton_by_mesh[mi] = skeleton;
1965 }
1966
1967 // we'll need the uids for the bone nodes, so generate them now
1968 for (size_t i = 0; i < mScene->mNumMeshes; ++i) {
1969 auto &s = skeleton_by_mesh[i];
1970 for (const aiNode* n : s) {
1971 auto elem = node_uids.find(n);
1972 if (elem == node_uids.end()) {
1973 node_uids[n] = generate_uid();
1974 }
1975 }
1976 }
1977
1978 // now, for each aiMesh, we need to export a deformer,
1979 // and for each aiBone a subdeformer,
1980 // which should have all the skinning info.
1981 // these will need to be connected properly to the mesh,
1982 // and we can do that all now.
1983 for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
1984 const aiMesh* m = mScene->mMeshes[mi];
1985 if (!m->HasBones()) {
1986 continue;
1987 }
1988 // make a deformer for this mesh
1989 int64_t deformer_uid = generate_uid();
1990 FBX::Node dnode("Deformer");
1991 dnode.AddProperties(deformer_uid, FBX::SEPARATOR + "Deformer", "Skin");
1992 dnode.AddChild("Version", int32_t(101));
1993 // "acuracy"... this is not a typo....
1994 dnode.AddChild("Link_DeformAcuracy", double(50));
1995 dnode.AddChild("SkinningType", "Linear"); // TODO: other modes?
1996 dnode.Dump(outstream, binary, indent);
1997
1998 // connect it
1999 connections.emplace_back("C", "OO", deformer_uid, mesh_uids[mi]);
2000
2001 //computed before
2002 std::vector<int32_t>& vertex_indices = vVertexIndice[mi];
2003
2004 // TODO, FIXME: this won't work if anything is not in the bind pose.
2005 // for now if such a situation is detected, we throw an exception.
2006 std::set<const aiBone*> not_in_bind_pose;
2007 std::set<const aiNode*> no_offset_matrix;
2008
2009 // first get this mesh's position in world space,
2010 // as we'll need it for each subdeformer.
2011 //
2012 // ...of course taking the position of the MESH doesn't make sense,
2013 // as it can be instanced to many nodes.
2014 // All we can do is assume no instancing,
2015 // and take the first node we find that contains the mesh.
2016 aiNode* mesh_node = get_node_for_mesh((unsigned int)mi, mScene->mRootNode);
2017 aiMatrix4x4 mesh_xform = get_world_transform(mesh_node, mScene);
2018
2019 // now make a subdeformer for each bone in the skeleton
2020 const std::set<const aiNode*, SortNodeByName> skeleton= skeleton_by_mesh[mi];
2021 for (const aiNode* bone_node : skeleton) {
2022 // if there's a bone for this node, find it
2023 const aiBone* b = nullptr;
2024 for (size_t bi = 0; bi < m->mNumBones; ++bi) {
2025 // TODO: this probably should index by something else
2026 const std::string name(m->mBones[bi]->mName.C_Str());
2027 if (node_by_bone[name] == bone_node) {
2028 b = m->mBones[bi];
2029 break;
2030 }
2031 }
2032 if (!b) {
2033 no_offset_matrix.insert(bone_node);
2034 }
2035
2036 // start the subdeformer node
2037 const int64_t subdeformer_uid = generate_uid();
2038 FBX::Node sdnode("Deformer");
2039 sdnode.AddProperties(
2040 subdeformer_uid, FBX::SEPARATOR + "SubDeformer", "Cluster"
2041 );
2042 sdnode.AddChild("Version", int32_t(100));
2043 sdnode.AddChild("UserData", "", "");
2044
2045 std::set<int32_t> setWeightedVertex;
2046 // add indices and weights, if any
2047 if (b) {
2048 std::vector<int32_t> subdef_indices;
2049 std::vector<double> subdef_weights;
2050 int32_t last_index = -1;
2051 for (size_t wi = 0; wi < b->mNumWeights; ++wi) {
2052 int32_t vi = vertex_indices[b->mWeights[wi].mVertexId];
2053 bool bIsWeightedAlready = (setWeightedVertex.find(vi) != setWeightedVertex.end());
2054 if (vi == last_index || bIsWeightedAlready) {
2055 // only for vertices we exported to fbx
2056 // TODO, FIXME: this assumes identically-located vertices
2057 // will always deform in the same way.
2058 // as assimp doesn't store a separate list of "positions",
2059 // there's not much that can be done about this
2060 // other than assuming that identical position means
2061 // identical vertex.
2062 continue;
2063 }
2064 setWeightedVertex.insert(vi);
2065 subdef_indices.push_back(vi);
2066 subdef_weights.push_back(b->mWeights[wi].mWeight);
2067 last_index = vi;
2068 }
2069 // yes, "indexes"
2070 sdnode.AddChild("Indexes", subdef_indices);
2071 sdnode.AddChild("Weights", subdef_weights);
2072 }
2073
2074 // transform is the transform of the mesh, but in bone space.
2075 // if the skeleton is in the bind pose,
2076 // we can take the inverse of the world-space bone transform
2077 // and multiply by the world-space transform of the mesh.
2078 aiMatrix4x4 bone_xform = get_world_transform(bone_node, mScene);
2079 aiMatrix4x4 inverse_bone_xform = bone_xform;
2080 inverse_bone_xform.Inverse();
2081 aiMatrix4x4 tr = inverse_bone_xform * mesh_xform;
2082
2083 sdnode.AddChild("Transform", tr);
2084
2085
2086 sdnode.AddChild("TransformLink", bone_xform);
2087 // note: this means we ALWAYS rely on the mesh node transform
2088 // being unchanged from the time the skeleton was bound.
2089 // there's not really any way around this at the moment.
2090
2091 // done
2092 sdnode.Dump(outstream, binary, indent);
2093
2094 // lastly, connect to the parent deformer
2095 connections.emplace_back(
2096 "C", "OO", subdeformer_uid, deformer_uid
2097 );
2098
2099 // we also need to connect the limb node to the subdeformer.
2100 connections.emplace_back(
2101 "C", "OO", node_uids[bone_node], subdeformer_uid
2102 );
2103 }
2104
2105 // if we cannot create a valid FBX file, simply die.
2106 // this will both prevent unnecessary bug reports,
2107 // and tell the user what they can do to fix the situation
2108 // (i.e. export their model in the bind pose).
2109 if (no_offset_matrix.size() && not_in_bind_pose.size()) {
2110 std::stringstream err;
2111 err << "Not enough information to construct bind pose";
2112 err << " for mesh " << mi << "!";
2113 err << " Transform matrix for bone \"";
2114 err << (*not_in_bind_pose.begin())->mName.C_Str() << "\"";
2115 if (not_in_bind_pose.size() > 1) {
2116 err << " (and " << not_in_bind_pose.size() - 1 << " more)";
2117 }
2118 err << " does not match mOffsetMatrix,";
2119 err << " and node \"";
2120 err << (*no_offset_matrix.begin())->mName.C_Str() << "\"";
2121 if (no_offset_matrix.size() > 1) {
2122 err << " (and " << no_offset_matrix.size() - 1 << " more)";
2123 }
2124 err << " has no offset matrix to rely on.";
2125 err << " Please ensure bones are in the bind pose to export.";
2126 throw DeadlyExportError(err.str());
2127 }
2128
2129 }
2130
2131 // BindPose
2132 //
2133 // This is a legacy system, which should be unnecessary.
2134 //
2135 // Somehow including it slows file loading by the official FBX SDK,
2136 // and as it can reconstruct it from the deformers anyway,
2137 // this is not currently included.
2138 //
2139 // The code is kept here in case it's useful in the future,
2140 // but it's pretty much a hack anyway,
2141 // as assimp doesn't store bindpose information for full skeletons.
2142 //
2143 /*for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
2144 aiMesh* mesh = mScene->mMeshes[mi];
2145 if (! mesh->HasBones()) { continue; }
2146 int64_t bindpose_uid = generate_uid();
2147 FBX::Node bpnode("Pose");
2148 bpnode.AddProperty(bindpose_uid);
2149 // note: this uid is never linked or connected to anything.
2150 bpnode.AddProperty(FBX::SEPARATOR + "Pose"); // blank name
2151 bpnode.AddProperty("BindPose");
2152
2153 bpnode.AddChild("Type", "BindPose");
2154 bpnode.AddChild("Version", int32_t(100));
2155
2156 aiNode* mesh_node = get_node_for_mesh(mi, mScene->mRootNode);
2157
2158 // next get the whole skeleton for this mesh.
2159 // we need it all to define the bindpose section.
2160 // the FBX SDK will complain if it's missing,
2161 // and also if parents of used bones don't have a subdeformer.
2162 // order shouldn't matter.
2163 std::set<aiNode*> skeleton;
2164 for (size_t bi = 0; bi < mesh->mNumBones; ++bi) {
2165 // bone node should have already been indexed
2166 const aiBone* b = mesh->mBones[bi];
2167 const std::string bone_name(b->mName.C_Str());
2168 aiNode* parent = node_by_bone[bone_name];
2169 // insert all nodes down to the root or mesh node
2170 while (
2171 parent
2172 && parent != mScene->mRootNode
2173 && parent != mesh_node
2174 ) {
2175 skeleton.insert(parent);
2176 parent = parent->mParent;
2177 }
2178 }
2179
2180 // number of pose nodes. includes one for the mesh itself.
2181 bpnode.AddChild("NbPoseNodes", int32_t(1 + skeleton.size()));
2182
2183 // the first pose node is always the mesh itself
2184 FBX::Node pose("PoseNode");
2185 pose.AddChild("Node", mesh_uids[mi]);
2186 aiMatrix4x4 mesh_node_xform = get_world_transform(mesh_node, mScene);
2187 pose.AddChild("Matrix", mesh_node_xform);
2188 bpnode.AddChild(pose);
2189
2190 for (aiNode* bonenode : skeleton) {
2191 // does this node have a uid yet?
2192 int64_t node_uid;
2193 auto node_uid_iter = node_uids.find(bonenode);
2194 if (node_uid_iter != node_uids.end()) {
2195 node_uid = node_uid_iter->second;
2196 } else {
2197 node_uid = generate_uid();
2198 node_uids[bonenode] = node_uid;
2199 }
2200
2201 // make a pose thingy
2202 pose = FBX::Node("PoseNode");
2203 pose.AddChild("Node", node_uid);
2204 aiMatrix4x4 node_xform = get_world_transform(bonenode, mScene);
2205 pose.AddChild("Matrix", node_xform);
2206 bpnode.AddChild(pose);
2207 }
2208
2209 // now write it
2210 bpnode.Dump(outstream, binary, indent);
2211 }*/
2212
2213 // lights
2214 indent = 1;
2215 lights_uids.clear();
2216 for (size_t li = 0; li < mScene->mNumLights; ++li) {
2217 aiLight* l = mScene->mLights[li];
2218
2219 int64_t uid = generate_uid();
2220 const std::string lightNodeAttributeName = l->mName.C_Str() + FBX::SEPARATOR + "NodeAttribute";
2221
2222 FBX::Node lna("NodeAttribute");
2223 lna.AddProperties(uid, lightNodeAttributeName, "Light");
2224 FBX::Node lnap("Properties70");
2225
2226 // Light color.
2227 lnap.AddP70colorA("Color", l->mColorDiffuse.r, l->mColorDiffuse.g, l->mColorDiffuse.b);
2228
2229 // TODO Assimp light description is quite concise and do not handle light intensity.
2230 // Default value to 1000W.
2231 lnap.AddP70numberA("Intensity", 1000);
2232
2233 // FBXLight::EType conversion
2234 switch (l->mType) {
2235 case aiLightSource_POINT:
2236 lnap.AddP70enum("LightType", 0);
2237 break;
2238 case aiLightSource_DIRECTIONAL:
2239 lnap.AddP70enum("LightType", 1);
2240 break;
2241 case aiLightSource_SPOT:
2242 lnap.AddP70enum("LightType", 2);
2243 lnap.AddP70numberA("InnerAngle", AI_RAD_TO_DEG(l->mAngleInnerCone));
2244 lnap.AddP70numberA("OuterAngle", AI_RAD_TO_DEG(l->mAngleOuterCone));
2245 break;
2246 // TODO Assimp do not handle 'area' nor 'volume' lights, but FBX does.
2247 /*case aiLightSource_AREA:
2248 lnap.AddP70enum("LightType", 3);
2249 lnap.AddP70enum("AreaLightShape", 0); // 0=Rectangle, 1=Sphere
2250 break;
2251 case aiLightSource_VOLUME:
2252 lnap.AddP70enum("LightType", 4);
2253 break;*/
2254 default:
2255 break;
2256 }
2257
2258 // Did not understood how to configure the decay so disabling attenuation.
2259 lnap.AddP70enum("DecayType", 0);
2260
2261 // Dump to FBX stream
2262 lna.AddChild(lnap);
2263 lna.AddChild("TypeFlags", FBX::FBXExportProperty("Light"));
2264 lna.AddChild("GeometryVersion", FBX::FBXExportProperty(int32_t(124)));
2265 lna.Dump(outstream, binary, indent);
2266
2267 // Store name and uid (will be used later when parsing scene nodes)
2268 lights_uids[l->mName.C_Str()] = uid;
2269 }
2270
2271 // TODO: cameras
2272
2273 // write nodes (i.e. model hierarchy)
2274 // start at root node
2275 WriteModelNodes(
2276 outstream, mScene->mRootNode, 0, limbnodes
2277 );
2278
2279 // animations
2280 //
2281 // in FBX there are:
2282 // * AnimationStack - corresponds to an aiAnimation
2283 // * AnimationLayer - a combinable animation component
2284 // * AnimationCurveNode - links the property to be animated
2285 // * AnimationCurve - defines animation data for a single property value
2286 //
2287 // the CurveNode also provides the default value for a property,
2288 // such as the X, Y, Z coordinates for animatable translation.
2289 //
2290 // the Curve only specifies values for one component of the property,
2291 // so there will be a separate AnimationCurve for X, Y, and Z.
2292 //
2293 // Assimp has:
2294 // * aiAnimation - basically corresponds to an AnimationStack
2295 // * aiNodeAnim - defines all animation for one aiNode
2296 // * aiVectorKey/aiQuatKey - define the keyframe data for T/R/S
2297 //
2298 // assimp has no equivalent for AnimationLayer,
2299 // and these are flattened on FBX import.
2300 // we can assume there will be one per AnimationStack.
2301 //
2302 // the aiNodeAnim contains all animation data for a single aiNode,
2303 // which will correspond to three AnimationCurveNode's:
2304 // one each for translation, rotation and scale.
2305 // The data for each of these will be put in 9 AnimationCurve's,
2306 // T.X, T.Y, T.Z, R.X, R.Y, R.Z, etc.
2307
2308 // AnimationStack / aiAnimation
2309 std::vector<int64_t> animation_stack_uids(mScene->mNumAnimations);
2310 for (size_t ai = 0; ai < mScene->mNumAnimations; ++ai) {
2311 int64_t animstack_uid = generate_uid();
2312 animation_stack_uids[ai] = animstack_uid;
2313 const aiAnimation* anim = mScene->mAnimations[ai];
2314
2315 FBX::Node asnode("AnimationStack");
2316 std::string name = anim->mName.C_Str() + FBX::SEPARATOR + "AnimStack";
2317 asnode.AddProperties(animstack_uid, name, "");
2318 FBX::Node p("Properties70");
2319 p.AddP70time("LocalStart", 0); // assimp doesn't store this
2320 p.AddP70time("LocalStop", to_ktime(anim->mDuration, anim));
2321 p.AddP70time("ReferenceStart", 0);
2322 p.AddP70time("ReferenceStop", to_ktime(anim->mDuration, anim));
2323 asnode.AddChild(p);
2324
2325 // this node absurdly always pretends it has children
2326 // (in this case it does, but just in case...)
2327 asnode.force_has_children = true;
2328 asnode.Dump(outstream, binary, indent);
2329
2330 // note: animation stacks are not connected to anything
2331 }
2332
2333 // AnimationLayer - one per aiAnimation
2334 std::vector<int64_t> animation_layer_uids(mScene->mNumAnimations);
2335 for (size_t ai = 0; ai < mScene->mNumAnimations; ++ai) {
2336 int64_t animlayer_uid = generate_uid();
2337 animation_layer_uids[ai] = animlayer_uid;
2338 FBX::Node alnode("AnimationLayer");
2339 alnode.AddProperties(animlayer_uid, FBX::SEPARATOR + "AnimLayer", "");
2340
2341 // this node absurdly always pretends it has children
2342 alnode.force_has_children = true;
2343 alnode.Dump(outstream, binary, indent);
2344
2345 // connect to the relevant animstack
2346 connections.emplace_back(
2347 "C", "OO", animlayer_uid, animation_stack_uids[ai]
2348 );
2349 }
2350
2351 // AnimCurveNode - three per aiNodeAnim
2352 std::vector<std::vector<std::array<int64_t,3>>> curve_node_uids;
2353 for (size_t ai = 0; ai < mScene->mNumAnimations; ++ai) {
2354 const aiAnimation* anim = mScene->mAnimations[ai];
2355 const int64_t layer_uid = animation_layer_uids[ai];
2356 std::vector<std::array<int64_t,3>> nodeanim_uids;
2357 for (size_t nai = 0; nai < anim->mNumChannels; ++nai) {
2358 const aiNodeAnim* na = anim->mChannels[nai];
2359 // get the corresponding aiNode
2360 const aiNode* node = mScene->mRootNode->FindNode(na->mNodeName);
2361 // and its transform
2362 const aiMatrix4x4 node_xfm = get_world_transform(node, mScene);
2363 aiVector3D T, R, S;
2364 node_xfm.Decompose(S, R, T);
2365
2366 // AnimationCurveNode uids
2367 std::array<int64_t,3> ids;
2368 ids[0] = generate_uid(); // T
2369 ids[1] = generate_uid(); // R
2370 ids[2] = generate_uid(); // S
2371
2372 // translation
2373 WriteAnimationCurveNode(outstream,
2374 ids[0], "T", T, "Lcl Translation",
2375 layer_uid, node_uids[node]
2376 );
2377
2378 // rotation
2379 WriteAnimationCurveNode(outstream,
2380 ids[1], "R", R, "Lcl Rotation",
2381 layer_uid, node_uids[node]
2382 );
2383
2384 // scale
2385 WriteAnimationCurveNode(outstream,
2386 ids[2], "S", S, "Lcl Scale",
2387 layer_uid, node_uids[node]
2388 );
2389
2390 // store the uids for later use
2391 nodeanim_uids.push_back(ids);
2392 }
2393 curve_node_uids.push_back(nodeanim_uids);
2394 }
2395
2396 // AnimCurve - defines actual keyframe data.
2397 // there's a separate curve for every component of every vector,
2398 // for example a transform curvenode will have separate X/Y/Z AnimCurve's
2399 for (size_t ai = 0; ai < mScene->mNumAnimations; ++ai) {
2400 const aiAnimation* anim = mScene->mAnimations[ai];
2401 for (size_t nai = 0; nai < anim->mNumChannels; ++nai) {
2402 const aiNodeAnim* na = anim->mChannels[nai];
2403 // get the corresponding aiNode
2404 const aiNode* node = mScene->mRootNode->FindNode(na->mNodeName);
2405 // and its transform
2406 const aiMatrix4x4 node_xfm = get_world_transform(node, mScene);
2407 aiVector3D T, R, S;
2408 node_xfm.Decompose(S, R, T);
2409 const std::array<int64_t,3>& ids = curve_node_uids[ai][nai];
2410
2411 std::vector<int64_t> times;
2412 std::vector<float> xval, yval, zval;
2413
2414 // position/translation
2415 for (size_t ki = 0; ki < na->mNumPositionKeys; ++ki) {
2416 const aiVectorKey& k = na->mPositionKeys[ki];
2417 times.push_back(to_ktime(k.mTime));
2418 xval.push_back(k.mValue.x);
2419 yval.push_back(k.mValue.y);
2420 zval.push_back(k.mValue.z);
2421 }
2422 // one curve each for X, Y, Z
2423 WriteAnimationCurve(outstream, T.x, times, xval, ids[0], "d|X");
2424 WriteAnimationCurve(outstream, T.y, times, yval, ids[0], "d|Y");
2425 WriteAnimationCurve(outstream, T.z, times, zval, ids[0], "d|Z");
2426
2427 // rotation
2428 times.clear(); xval.clear(); yval.clear(); zval.clear();
2429 for (size_t ki = 0; ki < na->mNumRotationKeys; ++ki) {
2430 const aiQuatKey& k = na->mRotationKeys[ki];
2431 times.push_back(to_ktime(k.mTime));
2432 // TODO: aiQuaternion method to convert to Euler...
2433 aiMatrix4x4 m(k.mValue.GetMatrix());
2434 aiVector3D qs, qr, qt;
2435 m.Decompose(qs, qr, qt);
2436 qr *= DEG;
2437 xval.push_back(qr.x);
2438 yval.push_back(qr.y);
2439 zval.push_back(qr.z);
2440 }
2441 WriteAnimationCurve(outstream, R.x, times, xval, ids[1], "d|X");
2442 WriteAnimationCurve(outstream, R.y, times, yval, ids[1], "d|Y");
2443 WriteAnimationCurve(outstream, R.z, times, zval, ids[1], "d|Z");
2444
2445 // scaling/scale
2446 times.clear(); xval.clear(); yval.clear(); zval.clear();
2447 for (size_t ki = 0; ki < na->mNumScalingKeys; ++ki) {
2448 const aiVectorKey& k = na->mScalingKeys[ki];
2449 times.push_back(to_ktime(k.mTime));
2450 xval.push_back(k.mValue.x);
2451 yval.push_back(k.mValue.y);
2452 zval.push_back(k.mValue.z);
2453 }
2454 WriteAnimationCurve(outstream, S.x, times, xval, ids[2], "d|X");
2455 WriteAnimationCurve(outstream, S.y, times, yval, ids[2], "d|Y");
2456 WriteAnimationCurve(outstream, S.z, times, zval, ids[2], "d|Z");
2457 }
2458 }
2459
2460 indent = 0;
2461 object_node.End(outstream, binary, indent, true);
2462 }
2463
2464 // convenience map of magic node name strings to FBX properties,
2465 // including the expected type of transform.
2466 const std::map<std::string,std::pair<std::string,char>> transform_types = {
2467 {"Translation", {"Lcl Translation", 't'}},
2468 {"RotationOffset", {"RotationOffset", 't'}},
2469 {"RotationPivot", {"RotationPivot", 't'}},
2470 {"PreRotation", {"PreRotation", 'r'}},
2471 {"Rotation", {"Lcl Rotation", 'r'}},
2472 {"PostRotation", {"PostRotation", 'r'}},
2473 {"RotationPivotInverse", {"RotationPivotInverse", 'i'}},
2474 {"ScalingOffset", {"ScalingOffset", 't'}},
2475 {"ScalingPivot", {"ScalingPivot", 't'}},
2476 {"Scaling", {"Lcl Scaling", 's'}},
2477 {"ScalingPivotInverse", {"ScalingPivotInverse", 'i'}},
2478 {"GeometricScaling", {"GeometricScaling", 's'}},
2479 {"GeometricRotation", {"GeometricRotation", 'r'}},
2480 {"GeometricTranslation", {"GeometricTranslation", 't'}},
2481 {"GeometricTranslationInverse", {"GeometricTranslationInverse", 'i'}},
2482 {"GeometricRotationInverse", {"GeometricRotationInverse", 'i'}},
2483 {"GeometricScalingInverse", {"GeometricScalingInverse", 'i'}}
2484 };
2485
2486 // write a single model node to the stream
WriteModelNode(StreamWriterLE & outstream,bool,const aiNode * node,int64_t node_uid,const std::string & type,const std::vector<std::pair<std::string,aiVector3D>> & transform_chain,TransformInheritance inherit_type)2487 void FBXExporter::WriteModelNode(
2488 StreamWriterLE& outstream,
2489 bool,
2490 const aiNode* node,
2491 int64_t node_uid,
2492 const std::string& type,
2493 const std::vector<std::pair<std::string,aiVector3D>>& transform_chain,
2494 TransformInheritance inherit_type
2495 ){
2496 const aiVector3D zero = {0, 0, 0};
2497 const aiVector3D one = {1, 1, 1};
2498 FBX::Node m("Model");
2499 std::string name = node->mName.C_Str() + FBX::SEPARATOR + "Model";
2500 m.AddProperties(node_uid, name, type);
2501 m.AddChild("Version", int32_t(232));
2502 FBX::Node p("Properties70");
2503 p.AddP70bool("RotationActive", 1);
2504 p.AddP70int("DefaultAttributeIndex", 0);
2505 p.AddP70enum("InheritType", inherit_type);
2506 if (transform_chain.empty()) {
2507 // decompose 4x4 transform matrix into TRS
2508 aiVector3D t, r, s;
2509 node->mTransformation.Decompose(s, r, t);
2510 if (t != zero) {
2511 p.AddP70(
2512 "Lcl Translation", "Lcl Translation", "", "A",
2513 double(t.x), double(t.y), double(t.z)
2514 );
2515 }
2516 if (r != zero) {
2517 p.AddP70(
2518 "Lcl Rotation", "Lcl Rotation", "", "A",
2519 double(DEG*r.x), double(DEG*r.y), double(DEG*r.z)
2520 );
2521 }
2522 if (s != one) {
2523 p.AddP70(
2524 "Lcl Scaling", "Lcl Scaling", "", "A",
2525 double(s.x), double(s.y), double(s.z)
2526 );
2527 }
2528 } else {
2529 // apply the transformation chain.
2530 // these transformation elements are created when importing FBX,
2531 // which has a complex transformation hierarchy for each node.
2532 // as such we can bake the hierarchy back into the node on export.
2533 for (auto &item : transform_chain) {
2534 auto elem = transform_types.find(item.first);
2535 if (elem == transform_types.end()) {
2536 // then this is a bug
2537 std::stringstream err;
2538 err << "unrecognized FBX transformation type: ";
2539 err << item.first;
2540 throw DeadlyExportError(err.str());
2541 }
2542 const std::string &cur_name = elem->second.first;
2543 const aiVector3D &v = item.second;
2544 if (cur_name.compare(0, 4, "Lcl ") == 0) {
2545 // special handling for animatable properties
2546 p.AddP70( cur_name, cur_name, "", "A", double(v.x), double(v.y), double(v.z) );
2547 } else {
2548 p.AddP70vector(cur_name, v.x, v.y, v.z);
2549 }
2550 }
2551 }
2552 m.AddChild(p);
2553
2554 // not sure what these are for,
2555 // but they seem to be omnipresent
2556 m.AddChild("Shading", FBXExportProperty(true));
2557 m.AddChild("Culling", FBXExportProperty("CullingOff"));
2558
2559 m.Dump(outstream, binary, 1);
2560 }
2561
2562 // wrapper for WriteModelNodes to create and pass a blank transform chain
WriteModelNodes(StreamWriterLE & s,const aiNode * node,int64_t parent_uid,const std::unordered_set<const aiNode * > & limbnodes)2563 void FBXExporter::WriteModelNodes(
2564 StreamWriterLE& s,
2565 const aiNode* node,
2566 int64_t parent_uid,
2567 const std::unordered_set<const aiNode*>& limbnodes
2568 ) {
2569 std::vector<std::pair<std::string,aiVector3D>> chain;
2570 WriteModelNodes(s, node, parent_uid, limbnodes, chain);
2571 }
2572
WriteModelNodes(StreamWriterLE & outstream,const aiNode * node,int64_t parent_uid,const std::unordered_set<const aiNode * > & limbnodes,std::vector<std::pair<std::string,aiVector3D>> & transform_chain)2573 void FBXExporter::WriteModelNodes(
2574 StreamWriterLE& outstream,
2575 const aiNode* node,
2576 int64_t parent_uid,
2577 const std::unordered_set<const aiNode*>& limbnodes,
2578 std::vector<std::pair<std::string,aiVector3D>>& transform_chain
2579 ) {
2580 // first collapse any expanded transformation chains created by FBX import.
2581 std::string node_name(node->mName.C_Str());
2582 if (node_name.find(MAGIC_NODE_TAG) != std::string::npos) {
2583 auto pos = node_name.find(MAGIC_NODE_TAG) + MAGIC_NODE_TAG.size() + 1;
2584 std::string type_name = node_name.substr(pos);
2585 auto elem = transform_types.find(type_name);
2586 if (elem == transform_types.end()) {
2587 // then this is a bug and should be fixed
2588 std::stringstream err;
2589 err << "unrecognized FBX transformation node";
2590 err << " of type " << type_name << " in node " << node_name;
2591 throw DeadlyExportError(err.str());
2592 }
2593 aiVector3D t, r, s;
2594 node->mTransformation.Decompose(s, r, t);
2595 switch (elem->second.second) {
2596 case 'i': // inverse
2597 // we don't need to worry about the inverse matrices
2598 break;
2599 case 't': // translation
2600 transform_chain.emplace_back(elem->first, t);
2601 break;
2602 case 'r': // rotation
2603 r *= float(DEG);
2604 transform_chain.emplace_back(elem->first, r);
2605 break;
2606 case 's': // scale
2607 transform_chain.emplace_back(elem->first, s);
2608 break;
2609 default:
2610 // this should never happen
2611 std::stringstream err;
2612 err << "unrecognized FBX transformation type code: ";
2613 err << elem->second.second;
2614 throw DeadlyExportError(err.str());
2615 }
2616 // now continue on to any child nodes
2617 for (unsigned i = 0; i < node->mNumChildren; ++i) {
2618 WriteModelNodes(
2619 outstream,
2620 node->mChildren[i],
2621 parent_uid,
2622 limbnodes,
2623 transform_chain
2624 );
2625 }
2626 return;
2627 }
2628
2629 int64_t node_uid = 0;
2630 // generate uid and connect to parent, if not the root node,
2631 if (node != mScene->mRootNode) {
2632 auto elem = node_uids.find(node);
2633 if (elem != node_uids.end()) {
2634 node_uid = elem->second;
2635 } else {
2636 node_uid = generate_uid();
2637 node_uids[node] = node_uid;
2638 }
2639 connections.emplace_back("C", "OO", node_uid, parent_uid);
2640 }
2641
2642 // what type of node is this?
2643 if (node == mScene->mRootNode) {
2644 // handled later
2645 } else if (node->mNumMeshes == 1) {
2646 // connect to child mesh, which should have been written previously
2647 connections.emplace_back(
2648 "C", "OO", mesh_uids[node->mMeshes[0]], node_uid
2649 );
2650 // also connect to the material for the child mesh
2651 connections.emplace_back(
2652 "C", "OO",
2653 material_uids[mScene->mMeshes[node->mMeshes[0]]->mMaterialIndex],
2654 node_uid
2655 );
2656 // write model node
2657 WriteModelNode(
2658 outstream, binary, node, node_uid, "Mesh", transform_chain
2659 );
2660 } else if (limbnodes.count(node)) {
2661 WriteModelNode(
2662 outstream, binary, node, node_uid, "LimbNode", transform_chain
2663 );
2664 // we also need to write a nodeattribute to mark it as a skeleton
2665 int64_t node_attribute_uid = generate_uid();
2666 FBX::Node na("NodeAttribute");
2667 na.AddProperties(
2668 node_attribute_uid, FBX::SEPARATOR + "NodeAttribute", "LimbNode"
2669 );
2670 na.AddChild("TypeFlags", FBXExportProperty("Skeleton"));
2671 na.Dump(outstream, binary, 1);
2672 // and connect them
2673 connections.emplace_back("C", "OO", node_attribute_uid, node_uid);
2674 } else {
2675 const auto& lightIt = lights_uids.find(node->mName.C_Str());
2676 if(lightIt != lights_uids.end()) {
2677 // Node has a light connected to it.
2678 WriteModelNode(
2679 outstream, binary, node, node_uid, "Light", transform_chain
2680 );
2681 connections.emplace_back("C", "OO", lightIt->second, node_uid);
2682 } else {
2683 // generate a null node so we can add children to it
2684 WriteModelNode(
2685 outstream, binary, node, node_uid, "Null", transform_chain
2686 );
2687 }
2688 }
2689
2690 // if more than one child mesh, make nodes for each mesh
2691 if (node->mNumMeshes > 1 || node == mScene->mRootNode) {
2692 for (size_t i = 0; i < node->mNumMeshes; ++i) {
2693 // make a new model node
2694 int64_t new_node_uid = generate_uid();
2695 // connect to parent node
2696 connections.emplace_back("C", "OO", new_node_uid, node_uid);
2697 // connect to child mesh, which should have been written previously
2698 connections.emplace_back(
2699 "C", "OO", mesh_uids[node->mMeshes[i]], new_node_uid
2700 );
2701 // also connect to the material for the child mesh
2702 connections.emplace_back(
2703 "C", "OO",
2704 material_uids[
2705 mScene->mMeshes[node->mMeshes[i]]->mMaterialIndex
2706 ],
2707 new_node_uid
2708 );
2709
2710 aiNode new_node;
2711 // take name from mesh name, if it exists
2712 new_node.mName = mScene->mMeshes[node->mMeshes[i]]->mName;
2713 // write model node
2714 WriteModelNode(
2715 outstream, binary, &new_node, new_node_uid, "Mesh", std::vector<std::pair<std::string,aiVector3D>>()
2716 );
2717 }
2718 }
2719
2720 // now recurse into children
2721 for (size_t i = 0; i < node->mNumChildren; ++i) {
2722 WriteModelNodes(
2723 outstream, node->mChildren[i], node_uid, limbnodes
2724 );
2725 }
2726 }
2727
WriteAnimationCurveNode(StreamWriterLE & outstream,int64_t uid,const std::string & name,aiVector3D default_value,const std::string & property_name,int64_t layer_uid,int64_t node_uid)2728 void FBXExporter::WriteAnimationCurveNode(
2729 StreamWriterLE &outstream,
2730 int64_t uid,
2731 const std::string &name, // "T", "R", or "S"
2732 aiVector3D default_value,
2733 const std::string &property_name, // "Lcl Translation" etc
2734 int64_t layer_uid,
2735 int64_t node_uid) {
2736 FBX::Node n("AnimationCurveNode");
2737 n.AddProperties(uid, name + FBX::SEPARATOR + "AnimCurveNode", "");
2738 FBX::Node p("Properties70");
2739 p.AddP70numberA("d|X", default_value.x);
2740 p.AddP70numberA("d|Y", default_value.y);
2741 p.AddP70numberA("d|Z", default_value.z);
2742 n.AddChild(p);
2743 n.Dump(outstream, binary, 1);
2744 // connect to layer
2745 this->connections.emplace_back("C", "OO", uid, layer_uid);
2746 // connect to bone
2747 this->connections.emplace_back("C", "OP", uid, node_uid, property_name);
2748 }
2749
WriteAnimationCurve(StreamWriterLE & outstream,double default_value,const std::vector<int64_t> & times,const std::vector<float> & values,int64_t curvenode_uid,const std::string & property_link)2750 void FBXExporter::WriteAnimationCurve(
2751 StreamWriterLE& outstream,
2752 double default_value,
2753 const std::vector<int64_t>& times,
2754 const std::vector<float>& values,
2755 int64_t curvenode_uid,
2756 const std::string& property_link // "d|X", "d|Y", etc
2757 ) {
2758 FBX::Node n("AnimationCurve");
2759 int64_t curve_uid = generate_uid();
2760 n.AddProperties(curve_uid, FBX::SEPARATOR + "AnimCurve", "");
2761 n.AddChild("Default", default_value);
2762 n.AddChild("KeyVer", int32_t(4009));
2763 n.AddChild("KeyTime", times);
2764 n.AddChild("KeyValueFloat", values);
2765 // TODO: keyattr flags and data (STUB for now)
2766 n.AddChild("KeyAttrFlags", std::vector<int32_t>{0});
2767 n.AddChild("KeyAttrDataFloat", std::vector<float>{0,0,0,0});
2768 n.AddChild(
2769 "KeyAttrRefCount",
2770 std::vector<int32_t>{static_cast<int32_t>(times.size())}
2771 );
2772 n.Dump(outstream, binary, 1);
2773 this->connections.emplace_back(
2774 "C", "OP", curve_uid, curvenode_uid, property_link
2775 );
2776 }
2777
2778
WriteConnections()2779 void FBXExporter::WriteConnections ()
2780 {
2781 // we should have completed the connection graph already,
2782 // so basically just dump it here
2783 if (!binary) {
2784 WriteAsciiSectionHeader("Object connections");
2785 }
2786 // TODO: comments with names in the ascii version
2787 FBX::Node conn("Connections");
2788 StreamWriterLE outstream(outfile);
2789 conn.Begin(outstream, binary, 0);
2790 conn.BeginChildren(outstream, binary, 0);
2791 for (auto &n : connections) {
2792 n.Dump(outstream, binary, 1);
2793 }
2794 conn.End(outstream, binary, 0, !connections.empty());
2795 connections.clear();
2796 }
2797
2798 #endif // ASSIMP_BUILD_NO_FBX_EXPORTER
2799 #endif // ASSIMP_BUILD_NO_EXPORT
2800