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