Coverage Report

Created: 2025-11-11 06:59

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