2
0
Эх сурвалжийг харах

Merge https://github.com/assimp/assimp

Marco Di Benedetto 7 жил өмнө
parent
commit
b0ac2d9daf

+ 16 - 1
CREDITS

@@ -157,12 +157,27 @@ Contributed ExportProperties interface
 Contributed X File exporter
 Contributed Step (stp) exporter
 
+- Thomas Iorns (mesilliac)
+Initial FBX Export support
+
 For a more detailed list just check: https://github.com/assimp/assimp/network/members
 
-Patreons:
+
+========
+Patreons
+========
+
+Huge thanks to our Patreons!
+
 - migenius
 - Marcus
 - Cort
 - elect
 - Steffen
 
+
+===================
+Commercial Sponsors
+===================
+
+- MyDidimo (mydidimo.com): Sponsored development of FBX Export support

+ 10 - 1
code/3MFXmlTags.h

@@ -45,6 +45,7 @@ namespace Assimp {
 namespace D3MF {
 
 namespace XmlTag {
+    // Model-data specific tags
     static const std::string model = "model";
     static const std::string model_unit = "unit";
     static const std::string metadata = "metadata";
@@ -62,6 +63,8 @@ namespace XmlTag {
     static const std::string v2 = "v2";
     static const std::string v3 = "v3";
     static const std::string id = "id";
+    static const std::string pid = "pid";
+    static const std::string p1 = "p1";
     static const std::string name = "name";
     static const std::string type = "type";
     static const std::string build = "build";
@@ -69,6 +72,13 @@ namespace XmlTag {
     static const std::string objectid = "objectid";
     static const std::string transform = "transform";
 
+    // Material definitions
+    static const std::string basematerials = "basematerials";
+    static const std::string basematerials_base = "base";
+    static const std::string basematerials_name = "name";
+    static const std::string basematerials_displaycolor = "displaycolor";
+
+    // Meta info tags
     static const std::string CONTENT_TYPES_ARCHIVE = "[Content_Types].xml";
     static const std::string ROOT_RELATIONSHIPS_ARCHIVE = "_rels/.rels";
     static const std::string SCHEMA_CONTENTTYPES = "http://schemas.openxmlformats.org/package/2006/content-types";
@@ -83,7 +93,6 @@ namespace XmlTag {
     static const std::string PACKAGE_TEXTURE_RELATIONSHIP_TYPE = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture";
     static const std::string PACKAGE_CORE_PROPERTIES_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
     static const std::string PACKAGE_THUMBNAIL_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail";
-
 }
 
 } // Namespace D3MF

+ 1 - 1
code/BlenderDNA.h

@@ -205,7 +205,7 @@ enum ErrorPolicy {
 
 // -------------------------------------------------------------------------------
 /** Represents a data structure in a BLEND file. A Structure defines n fields
- *  and their locatios and encodings the input stream. Usually, every
+ *  and their locations and encodings the input stream. Usually, every
  *  Structure instance pertains to one equally-named data structure in the
  *  BlenderScene.h header. This class defines various utilities to map a
  *  binary `blob` read from the file to such a structure instance with

+ 1 - 1
code/BlenderDNA.inl

@@ -502,7 +502,7 @@ const FileBlockHead* Structure :: LocateFileBlockForAddress(const Pointer & ptrv
 {
     // the file blocks appear in list sorted by
     // with ascending base addresses so we can run a
-    // binary search to locate the pointee quickly.
+    // binary search to locate the pointer quickly.
 
     // NOTE: Blender seems to distinguish between side-by-side
     // data (stored in the same data block) and far pointers,

+ 1 - 1
code/BlenderScene.cpp

@@ -116,7 +116,7 @@ template <> void Structure :: Convert<MTex> (
     ReadField<ErrorPolicy_Igno>(temp,"projy",db);
     dest.projy = static_cast<Assimp::Blender::MTex::Projection>(temp);
     ReadField<ErrorPolicy_Igno>(temp,"projz",db);
-    dest.projx = static_cast<Assimp::Blender::MTex::Projection>(temp);
+    dest.projz = static_cast<Assimp::Blender::MTex::Projection>(temp);
     ReadField<ErrorPolicy_Igno>(dest.mapping,"mapping",db);
     ReadFieldArray<ErrorPolicy_Igno>(dest.ofs,"ofs",db);
     ReadFieldArray<ErrorPolicy_Igno>(dest.size,"size",db);

+ 8 - 1
code/CMakeLists.txt

@@ -522,6 +522,13 @@ ADD_ASSIMP_IMPORTER( FBX
   FBXDeformer.cpp
   FBXBinaryTokenizer.cpp
   FBXDocumentUtil.cpp
+  FBXExporter.h
+  FBXExporter.cpp
+  FBXExportNode.h
+  FBXExportNode.cpp
+  FBXExportProperty.h
+  FBXExportProperty.cpp
+  FBXCommon.h
 )
 
 SET( PostProcessing_SRCS
@@ -641,7 +648,7 @@ ADD_ASSIMP_IMPORTER( X
   XFileExporter.cpp
 )
 
-ADD_ASSIMP_IMPORTER(X3D
+ADD_ASSIMP_IMPORTER( X3D
   X3DExporter.cpp
   X3DExporter.hpp
   X3DImporter.cpp

+ 134 - 62
code/D3MFImporter.cpp

@@ -61,6 +61,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <unzip.h>
 #include <assimp/irrXMLWrapper.h>
 #include "3MFXmlTags.h"
+#include <assimp/fast_atof.h>
+
+#include <iomanip>
 
 namespace Assimp {
 namespace D3MF {
@@ -68,7 +71,9 @@ namespace D3MF {
 class XmlSerializer {
 public:
     XmlSerializer(XmlReader* xmlReader)
-    : xmlReader(xmlReader) {
+    : mMeshes()
+    , mMaterials() 
+    , xmlReader(xmlReader){
 		// empty
     }
 
@@ -77,14 +82,21 @@ public:
     }
 
     void ImportXml(aiScene* scene) {
+        if ( nullptr == scene ) {
+            return;
+        }
+
         scene->mRootNode = new aiNode();
         std::vector<aiNode*> children;
 
         while(ReadToEndElement(D3MF::XmlTag::model)) {
-            if(xmlReader->getNodeName() == D3MF::XmlTag::object) {
+            const std::string nodeName( xmlReader->getNodeName() );
+            if( nodeName == D3MF::XmlTag::object) {
                 children.push_back(ReadObject(scene));
-            } else if(xmlReader->getNodeName() == D3MF::XmlTag::build) {
-
+            } else if( nodeName == D3MF::XmlTag::build) {
+                // 
+            } else if ( nodeName == D3MF::XmlTag::basematerials ) {
+                ReadBaseMaterials();
             }
         }
 
@@ -92,11 +104,16 @@ public:
             scene->mRootNode->mName.Set( "3MF" );
         }
 
-        scene->mNumMeshes = static_cast<unsigned int>(meshes.size());
+        scene->mNumMeshes = static_cast<unsigned int>( mMeshes.size());
         scene->mMeshes = new aiMesh*[scene->mNumMeshes]();
 
-        std::copy(meshes.begin(), meshes.end(), scene->mMeshes);
+        std::copy( mMeshes.begin(), mMeshes.end(), scene->mMeshes);
 
+        scene->mNumMaterials = mMaterials.size();
+        if ( 0 != scene->mNumMaterials ) {
+            scene->mMaterials = new aiMaterial*[ scene->mNumMaterials ];
+            std::copy( mMaterials.begin(), mMaterials.end(), scene->mMaterials );
+        }
         scene->mRootNode->mNumChildren = static_cast<unsigned int>(children.size());
         scene->mRootNode->mChildren = new aiNode*[scene->mRootNode->mNumChildren]();
 
@@ -104,8 +121,7 @@ public:
     }
 
 private:
-    aiNode* ReadObject(aiScene* scene)
-    {
+    aiNode* ReadObject(aiScene* scene) {
         std::unique_ptr<aiNode> node(new aiNode());
 
         std::vector<unsigned long> meshIds;
@@ -124,19 +140,16 @@ private:
         node->mParent = scene->mRootNode;
         node->mName.Set(name);
 
-        size_t meshIdx = meshes.size();
+        size_t meshIdx = mMeshes.size();
 
-        while(ReadToEndElement(D3MF::XmlTag::object))
-        {
-            if(xmlReader->getNodeName() == D3MF::XmlTag::mesh)
-            {
+        while(ReadToEndElement(D3MF::XmlTag::object)) {
+            if(xmlReader->getNodeName() == D3MF::XmlTag::mesh) {
                 auto mesh = ReadMesh();
 
                 mesh->mName.Set(name);
-                meshes.push_back(mesh);
+                mMeshes.push_back(mesh);
                 meshIds.push_back(static_cast<unsigned long>(meshIdx));
-                meshIdx++;
-
+                ++meshIdx;
             }
         }
 
@@ -147,19 +160,14 @@ private:
         std::copy(meshIds.begin(), meshIds.end(), node->mMeshes);
 
         return node.release();
-
     }
 
     aiMesh* ReadMesh() {
         aiMesh* mesh = new aiMesh();
-        while(ReadToEndElement(D3MF::XmlTag::mesh))
-        {
-            if(xmlReader->getNodeName() == D3MF::XmlTag::vertices)
-            {
+        while(ReadToEndElement(D3MF::XmlTag::mesh)) {
+            if(xmlReader->getNodeName() == D3MF::XmlTag::vertices) {
                 ImportVertices(mesh);
-            }
-            else if(xmlReader->getNodeName() == D3MF::XmlTag::triangles)
-            {
+            } else if(xmlReader->getNodeName() == D3MF::XmlTag::triangles) {
                 ImportTriangles(mesh);
             }
         }
@@ -167,14 +175,11 @@ private:
         return mesh;
     }
 
-    void ImportVertices(aiMesh* mesh)
-    {
+    void ImportVertices(aiMesh* mesh) {
         std::vector<aiVector3D> vertices;
 
-        while(ReadToEndElement(D3MF::XmlTag::vertices))
-        {
-            if(xmlReader->getNodeName() == D3MF::XmlTag::vertex)
-            {
+        while(ReadToEndElement(D3MF::XmlTag::vertices)) {
+            if(xmlReader->getNodeName() == D3MF::XmlTag::vertex) {
                 vertices.push_back(ReadVertex());
             }
         }
@@ -182,11 +187,9 @@ private:
         mesh->mVertices = new aiVector3D[mesh->mNumVertices];
 
         std::copy(vertices.begin(), vertices.end(), mesh->mVertices);
-
     }
 
-    aiVector3D ReadVertex()
-    {
+    aiVector3D ReadVertex() {
         aiVector3D vertex;
 
         vertex.x = ai_strtof(xmlReader->getAttributeValue(D3MF::XmlTag::x.c_str()), nullptr);
@@ -196,16 +199,18 @@ private:
         return vertex;
     }
 
-    void ImportTriangles(aiMesh* mesh)
-    {
+    void ImportTriangles(aiMesh* mesh) {
          std::vector<aiFace> faces;
 
-
-         while(ReadToEndElement(D3MF::XmlTag::triangles))
-         {
-             if(xmlReader->getNodeName() == D3MF::XmlTag::triangle)
-             {
+         while(ReadToEndElement(D3MF::XmlTag::triangles)) {
+             const std::string nodeName( xmlReader->getNodeName() );
+             if(xmlReader->getNodeName() == D3MF::XmlTag::triangle) {
                  faces.push_back(ReadTriangle());
+                 const char *pidToken( xmlReader->getAttributeValue( D3MF::XmlTag::p1.c_str() ) );
+                 if ( nullptr != pidToken ) {
+                     int matIdx( std::atoi( pidToken ) );
+                     mesh->mMaterialIndex = matIdx;
+                 }
              }
          }
 
@@ -216,8 +221,7 @@ private:
         std::copy(faces.begin(), faces.end(), mesh->mFaces);
     }
 
-    aiFace ReadTriangle()
-    {
+    aiFace ReadTriangle() {
         aiFace face;
 
         face.mNumIndices = 3;
@@ -229,45 +233,113 @@ private:
         return face;
     }
 
+    void ReadBaseMaterials() {
+        while ( ReadToEndElement( D3MF::XmlTag::basematerials ) ) {
+            mMaterials.push_back( readMaterialDef() );
+            xmlReader->read();
+        }
+    }
+
+    bool parseColor( const char *color, aiColor4D &diffuse ) {
+        if ( nullptr == color ) {
+            return false;
+        }
+
+        const size_t len( strlen( color ) );
+        if ( 9 != len ) {
+            return false;
+        }
+
+        const char *buf( color );
+        if ( '#' != *buf ) {
+            return false;
+        }
+        ++buf;
+        char comp[ 3 ] = { 0,0,'\0' };
+
+        comp[ 0 ] = *buf;
+        ++buf;
+        comp[ 1 ] = *buf;
+        ++buf;
+        diffuse.r = static_cast<ai_real>( strtol( comp, NULL, 16 ) );
+
+
+        comp[ 0 ] = *buf;
+        ++buf;
+        comp[ 1 ] = *buf;
+        ++buf;
+        diffuse.g = static_cast< ai_real >( strtol( comp, NULL, 16 ) );
+
+        comp[ 0 ] = *buf;
+        ++buf;
+        comp[ 1 ] = *buf;
+        ++buf;
+        diffuse.b = static_cast< ai_real >( strtol( comp, NULL, 16 ) );
+
+        comp[ 0 ] = *buf;
+        ++buf;
+        comp[ 1 ] = *buf;
+        ++buf;
+        diffuse.a = static_cast< ai_real >( strtol( comp, NULL, 16 ) );
+
+        return true;
+    }
+
+    aiMaterial *readMaterialDef() {
+        aiMaterial *mat( nullptr );
+        const char *name( nullptr );
+        const char *color( nullptr );
+        const std::string nodeName( xmlReader->getNodeName() );
+        if ( nodeName == D3MF::XmlTag::basematerials_base ) {
+            name = xmlReader->getAttributeValue( D3MF::XmlTag::basematerials_name.c_str() );
+
+            aiString matName;
+            matName.Set( name );
+            mat = new aiMaterial;
+            mat->AddProperty( &matName, AI_MATKEY_NAME );
+
+            color = xmlReader->getAttributeValue( D3MF::XmlTag::basematerials_displaycolor.c_str() );
+            aiColor4D diffuse;
+            if ( parseColor( color, diffuse ) ) {
+                mat->AddProperty<aiColor4D>( &diffuse, 1, AI_MATKEY_COLOR_DIFFUSE );
+            }
+        }
+
+        return mat;
+    }
+
 private:
-    bool ReadToStartElement(const std::string& startTag)
-    {
-        while(xmlReader->read())
-        {
-            if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT && xmlReader->getNodeName() == startTag)
-            {
+    bool ReadToStartElement(const std::string& startTag) {
+        while(xmlReader->read()) {
+            const std::string &nodeName( xmlReader->getNodeName() );
+            if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT && nodeName == startTag) {
                 return true;
-            }
-            else if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT_END &&
-                     xmlReader->getNodeName() == startTag)
-            {
+            } else if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT_END && nodeName == startTag) {
                 return false;
             }
         }
-        //DefaultLogger::get()->error("unexpected EOF, expected closing <" + closeTag + "> tag");
+
         return false;
     }
 
-    bool ReadToEndElement(const std::string& closeTag)
-    {
-        while(xmlReader->read())
-        {
+    bool ReadToEndElement(const std::string& closeTag) {
+        while(xmlReader->read()) {
+            const std::string &nodeName( xmlReader->getNodeName() );
             if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT) {
                 return true;
-            }
-            else if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT_END
-                     && xmlReader->getNodeName() == closeTag)
-            {
+            } else if (xmlReader->getNodeType() == irr::io::EXN_ELEMENT_END && nodeName == closeTag) {
                 return false;
             }
         }
         DefaultLogger::get()->error("unexpected EOF, expected closing <" + closeTag + "> tag");
+
         return false;
     }
 
 
 private:
-    std::vector<aiMesh*> meshes;
+    std::vector<aiMesh*> mMeshes;
+    std::vector<aiMaterial*> mMaterials;
     XmlReader* xmlReader;
 };
 

+ 7 - 0
code/Exporter.cpp

@@ -97,6 +97,8 @@ void ExportSceneGLB2(const char*, IOSystem*, const aiScene*, const ExportPropert
 void ExportSceneAssbin(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportSceneAssxml(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportSceneX3D(const char*, IOSystem*, const aiScene*, const ExportProperties*);
+void ExportSceneFBX(const char*, IOSystem*, const aiScene*, const ExportProperties*);
+//void ExportSceneFBXA(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportScene3MF( const char*, IOSystem*, const aiScene*, const ExportProperties* );
 
 // ------------------------------------------------------------------------------------------------
@@ -169,6 +171,11 @@ Exporter::ExportFormatEntry gExporters[] =
     Exporter::ExportFormatEntry( "x3d", "Extensible 3D", "x3d" , &ExportSceneX3D, 0 ),
 #endif
 
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+    Exporter::ExportFormatEntry( "fbx", "Autodesk FBX (binary)", "fbx", &ExportSceneFBX, 0 ),
+    //Exporter::ExportFormatEntry( "fbxa", "Autodesk FBX (ascii)", "fbx", &ExportSceneFBXA, 0 ),
+#endif
+
 #ifndef ASSIMP_BUILD_NO_3MF_EXPORTER
     Exporter::ExportFormatEntry( "3mf", "The 3MF-File-Format", "3mf", &ExportScene3MF, 0 )
 #endif

+ 86 - 0
code/FBXCommon.h

@@ -0,0 +1,86 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file FBXCommon.h
+* Some useful constants and enums for dealing with FBX files.
+*/
+#ifndef AI_FBXCOMMON_H_INC
+#define AI_FBXCOMMON_H_INC
+
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+
+namespace FBX
+{
+    const std::string NULL_RECORD = { // 13 null bytes
+        '\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0'
+    }; // who knows why
+    const std::string SEPARATOR = {'\x00', '\x01'}; // for use inside strings
+    const std::string MAGIC_NODE_TAG = "_$AssimpFbx$"; // from import
+    const int64_t SECOND = 46186158000; // FBX's kTime unit
+
+    // rotation order. We'll probably use EulerXYZ for everything
+    enum RotOrder {
+        RotOrder_EulerXYZ = 0,
+        RotOrder_EulerXZY,
+        RotOrder_EulerYZX,
+        RotOrder_EulerYXZ,
+        RotOrder_EulerZXY,
+        RotOrder_EulerZYX,
+
+        RotOrder_SphericXYZ,
+
+        RotOrder_MAX // end-of-enum sentinel
+    };
+
+    // transformation inheritance method. Most of the time RSrs
+    enum TransformInheritance {
+        TransformInheritance_RrSs = 0,
+        TransformInheritance_RSrs,
+        TransformInheritance_Rrs,
+
+        TransformInheritance_MAX // end-of-enum sentinel
+    };
+}
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#endif // AI_FBXCOMMON_H_INC

+ 96 - 14
code/FBXConverter.cpp

@@ -142,6 +142,7 @@ void Converter::ConvertNodes( uint64_t id, aiNode& parent, const aiMatrix4x4& pa
     nodes.reserve( conns.size() );
 
     std::vector<aiNode*> nodes_chain;
+    std::vector<aiNode*> post_nodes_chain;
 
     try {
         for( const Connection* con : conns ) {
@@ -161,6 +162,7 @@ void Converter::ConvertNodes( uint64_t id, aiNode& parent, const aiMatrix4x4& pa
 
             if ( model ) {
                 nodes_chain.clear();
+                post_nodes_chain.clear();
 
                 aiMatrix4x4 new_abs_transform = parent_transform;
 
@@ -168,7 +170,7 @@ void Converter::ConvertNodes( uint64_t id, aiNode& parent, const aiMatrix4x4& pa
                 // assimp (or rather: the complicated transformation chain that
                 // is employed by fbx) means that we may need multiple aiNode's
                 // to represent a fbx node's transformation.
-                GenerateTransformationNodeChain( *model, nodes_chain );
+                GenerateTransformationNodeChain( *model, nodes_chain, post_nodes_chain );
 
                 ai_assert( nodes_chain.size() );
 
@@ -213,8 +215,39 @@ void Converter::ConvertNodes( uint64_t id, aiNode& parent, const aiMatrix4x4& pa
                 // attach geometry
                 ConvertModel( *model, *nodes_chain.back(), new_abs_transform );
 
-                // attach sub-nodes
-                ConvertNodes( model->ID(), *nodes_chain.back(), new_abs_transform );
+                // check if there will be any child nodes
+                const std::vector<const Connection*>& child_conns
+                    = doc.GetConnectionsByDestinationSequenced( model->ID(), "Model" );
+
+                // if so, link the geometric transform inverse nodes
+                // before we attach any child nodes
+                if (child_conns.size()) {
+                    for( aiNode* postnode : post_nodes_chain ) {
+                        ai_assert( postnode );
+
+                        if ( last_parent != &parent ) {
+                            last_parent->mNumChildren = 1;
+                            last_parent->mChildren = new aiNode*[ 1 ];
+                            last_parent->mChildren[ 0 ] = postnode;
+                        }
+
+                        postnode->mParent = last_parent;
+                        last_parent = postnode;
+
+                        new_abs_transform *= postnode->mTransformation;
+                    }
+                } else {
+                    // free the nodes we allocated as we don't need them
+                    Util::delete_fun<aiNode> deleter;
+                    std::for_each(
+                        post_nodes_chain.begin(),
+                        post_nodes_chain.end(),
+                        deleter
+                    );
+                }
+
+                // attach sub-nodes (if any)
+                ConvertNodes( model->ID(), *last_parent, new_abs_transform );
 
                 if ( doc.Settings().readLights ) {
                     ConvertLights( *model );
@@ -240,6 +273,7 @@ void Converter::ConvertNodes( uint64_t id, aiNode& parent, const aiMatrix4x4& pa
         Util::delete_fun<aiNode> deleter;
         std::for_each( nodes.begin(), nodes.end(), deleter );
         std::for_each( nodes_chain.begin(), nodes_chain.end(), deleter );
+        std::for_each( post_nodes_chain.begin(), post_nodes_chain.end(), deleter );
     }
 }
 
@@ -396,6 +430,12 @@ const char* Converter::NameTransformationComp( TransformationComp comp )
         return "GeometricRotation";
     case TransformationComp_GeometricTranslation:
         return "GeometricTranslation";
+    case TransformationComp_GeometricScalingInverse:
+        return "GeometricScalingInverse";
+    case TransformationComp_GeometricRotationInverse:
+        return "GeometricRotationInverse";
+    case TransformationComp_GeometricTranslationInverse:
+        return "GeometricTranslationInverse";
     case TransformationComp_MAXIMUM: // this is to silence compiler warnings
     default:
         break;
@@ -437,6 +477,12 @@ const char* Converter::NameTransformationCompProperty( TransformationComp comp )
         return "GeometricRotation";
     case TransformationComp_GeometricTranslation:
         return "GeometricTranslation";
+    case TransformationComp_GeometricScalingInverse:
+        return "GeometricScalingInverse";
+    case TransformationComp_GeometricRotationInverse:
+        return "GeometricRotationInverse";
+    case TransformationComp_GeometricTranslationInverse:
+        return "GeometricTranslationInverse";
     case TransformationComp_MAXIMUM: // this is to silence compiler warnings
         break;
     }
@@ -548,17 +594,25 @@ bool Converter::NeedsComplexTransformationChain( const Model& model )
     bool ok;
 
     const float zero_epsilon = 1e-6f;
+    const aiVector3D all_ones(1.0f, 1.0f, 1.0f);
     for ( size_t i = 0; i < TransformationComp_MAXIMUM; ++i ) {
         const TransformationComp comp = static_cast< TransformationComp >( i );
 
-        if ( comp == TransformationComp_Rotation || comp == TransformationComp_Scaling || comp == TransformationComp_Translation ||
-                comp == TransformationComp_GeometricScaling || comp == TransformationComp_GeometricRotation || comp == TransformationComp_GeometricTranslation ) {
+        if ( comp == TransformationComp_Rotation || comp == TransformationComp_Scaling || comp == TransformationComp_Translation ) {
             continue;
         }
 
+        bool scale_compare = ( comp == TransformationComp_GeometricScaling || comp == TransformationComp_Scaling );
+
         const aiVector3D& v = PropertyGet<aiVector3D>( props, NameTransformationCompProperty( comp ), ok );
-        if ( ok && v.SquareLength() > zero_epsilon ) {
-            return true;
+        if ( ok && scale_compare ) {
+            if ( (v - all_ones).SquareLength() > zero_epsilon ) {
+                return true;
+            }
+        } else if ( ok ) {
+            if ( v.SquareLength() > zero_epsilon ) {
+                return true;
+            }
         }
     }
 
@@ -570,7 +624,7 @@ std::string Converter::NameTransformationChainNode( const std::string& name, Tra
     return name + std::string( MAGIC_NODE_TAG ) + "_" + NameTransformationComp( comp );
 }
 
-void Converter::GenerateTransformationNodeChain( const Model& model, std::vector<aiNode*>& output_nodes )
+void Converter::GenerateTransformationNodeChain( const Model& model, std::vector<aiNode*>& output_nodes, std::vector<aiNode*>& post_output_nodes )
 {
     const PropertyTable& props = model.Props();
     const Model::RotOrder rot = model.RotationOrder();
@@ -582,6 +636,7 @@ void Converter::GenerateTransformationNodeChain( const Model& model, std::vector
 
     // generate transformation matrices for all the different transformation components
     const float zero_epsilon = 1e-6f;
+    const aiVector3D all_ones(1.0f, 1.0f, 1.0f);
     bool is_complex = false;
 
     const aiVector3D& PreRotation = PropertyGet<aiVector3D>( props, "PreRotation", ok );
@@ -634,7 +689,7 @@ void Converter::GenerateTransformationNodeChain( const Model& model, std::vector
     }
 
     const aiVector3D& Scaling = PropertyGet<aiVector3D>( props, "Lcl Scaling", ok );
-    if ( ok && std::fabs( Scaling.SquareLength() - 1.0f ) > zero_epsilon ) {
+    if ( ok && (Scaling - all_ones).SquareLength() > zero_epsilon ) {
         aiMatrix4x4::Scaling( Scaling, chain[ TransformationComp_Scaling ] );
     }
 
@@ -644,18 +699,38 @@ void Converter::GenerateTransformationNodeChain( const Model& model, std::vector
     }
 
     const aiVector3D& GeometricScaling = PropertyGet<aiVector3D>( props, "GeometricScaling", ok );
-    if ( ok && std::fabs( GeometricScaling.SquareLength() - 1.0f ) > zero_epsilon ) {
+    if ( ok && (GeometricScaling - all_ones).SquareLength() > zero_epsilon ) {
+        is_complex = true;
         aiMatrix4x4::Scaling( GeometricScaling, chain[ TransformationComp_GeometricScaling ] );
+        aiVector3D GeometricScalingInverse = GeometricScaling;
+        bool canscale = true;
+        for (size_t i = 0; i < 3; ++i) {
+            if ( std::fabs( GeometricScalingInverse[i] ) > zero_epsilon ) {
+                GeometricScalingInverse[i] = 1.0f / GeometricScaling[i];
+            } else {
+                FBXImporter::LogError( "cannot invert geometric scaling matrix with a 0.0 scale component" );
+                canscale = false;
+                break;
+            }
+        }
+        if (canscale) {
+            aiMatrix4x4::Scaling( GeometricScalingInverse, chain[ TransformationComp_GeometricScalingInverse ] );
+        }
     }
 
     const aiVector3D& GeometricRotation = PropertyGet<aiVector3D>( props, "GeometricRotation", ok );
     if ( ok && GeometricRotation.SquareLength() > zero_epsilon ) {
+        is_complex = true;
         GetRotationMatrix( rot, GeometricRotation, chain[ TransformationComp_GeometricRotation ] );
+        GetRotationMatrix( rot, GeometricRotation, chain[ TransformationComp_GeometricRotationInverse ] );
+        chain[ TransformationComp_GeometricRotationInverse ].Inverse();
     }
 
     const aiVector3D& GeometricTranslation = PropertyGet<aiVector3D>( props, "GeometricTranslation", ok );
     if ( ok && GeometricTranslation.SquareLength() > zero_epsilon ) {
+        is_complex = true;
         aiMatrix4x4::Translation( GeometricTranslation, chain[ TransformationComp_GeometricTranslation ] );
+        aiMatrix4x4::Translation( -GeometricTranslation, chain[ TransformationComp_GeometricTranslationInverse ] );
     }
 
     // is_complex needs to be consistent with NeedsComplexTransformationChain()
@@ -690,10 +765,18 @@ void Converter::GenerateTransformationNodeChain( const Model& model, std::vector
             }
 
             aiNode* nd = new aiNode();
-            output_nodes.push_back( nd );
-
             nd->mName.Set( NameTransformationChainNode( name, comp ) );
             nd->mTransformation = chain[ i ];
+
+            // geometric inverses go in a post-node chain
+            if ( comp == TransformationComp_GeometricScalingInverse ||
+                 comp == TransformationComp_GeometricRotationInverse ||
+                 comp == TransformationComp_GeometricTranslationInverse
+            ) {
+                post_output_nodes.push_back( nd );
+            } else {
+                output_nodes.push_back( nd );
+            }
         }
 
         ai_assert( output_nodes.size() );
@@ -2209,8 +2292,7 @@ void Converter::GenerateNodeAnimations( std::vector<aiNodeAnim*>& node_anims,
 
             has_any = true;
 
-            if ( comp != TransformationComp_Rotation && comp != TransformationComp_Scaling && comp != TransformationComp_Translation &&
-                comp != TransformationComp_GeometricScaling && comp != TransformationComp_GeometricRotation && comp != TransformationComp_GeometricTranslation )
+            if ( comp != TransformationComp_Rotation && comp != TransformationComp_Scaling && comp != TransformationComp_Translation )
             {
                 has_complex = true;
             }

+ 5 - 2
code/FBXConverter.h

@@ -82,7 +82,10 @@ public:
     *  The different parts that make up the final local transformation of a fbx-node
     */
     enum TransformationComp {
-        TransformationComp_Translation = 0,
+        TransformationComp_GeometricScalingInverse = 0,
+        TransformationComp_GeometricRotationInverse,
+        TransformationComp_GeometricTranslationInverse,
+        TransformationComp_Translation,
         TransformationComp_RotationOffset,
         TransformationComp_RotationPivot,
         TransformationComp_PreRotation,
@@ -153,7 +156,7 @@ private:
     /**
     *  note: memory for output_nodes will be managed by the caller
     */
-    void GenerateTransformationNodeChain(const Model& model, std::vector<aiNode*>& output_nodes);
+    void GenerateTransformationNodeChain(const Model& model, std::vector<aiNode*>& output_nodes, std::vector<aiNode*>& post_output_nodes);
 
     // ------------------------------------------------------------------------------------------------
     void SetupNodeMetadata(const Model& model, aiNode& nd);

+ 284 - 0
code/FBXExportNode.cpp

@@ -0,0 +1,284 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+#ifndef ASSIMP_BUILD_NO_EXPORT
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#include "FBXExportNode.h"
+#include "FBXCommon.h"
+
+#include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/ai_assert.h>
+
+#include <string>
+#include <memory> // shared_ptr
+
+// AddP70<type> helpers... there's no usable pattern here,
+// so all are defined as separate functions.
+// Even "animatable" properties are often completely different
+// from the standard (nonanimated) property definition,
+// so they are specified with an 'A' suffix.
+
+void FBX::Node::AddP70int(
+    const std::string& name, int32_t value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "int", "Integer", "", value);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70bool(
+    const std::string& name, bool value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "bool", "", "", int32_t(value));
+    AddChild(n);
+}
+
+void FBX::Node::AddP70double(
+    const std::string& name, double value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "double", "Number", "", value);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70numberA(
+    const std::string& name, double value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "Number", "", "A", value);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70color(
+    const std::string& name, double r, double g, double b
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "ColorRGB", "Color", "", r, g, b);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70colorA(
+    const std::string& name, double r, double g, double b
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "Color", "", "A", r, g, b);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70vector(
+    const std::string& name, double x, double y, double z
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "Vector3D", "Vector", "", x, y, z);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70vectorA(
+    const std::string& name, double x, double y, double z
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "Vector", "", "A", x, y, z);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70string(
+    const std::string& name, const std::string& value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "KString", "", "", value);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70enum(
+    const std::string& name, int32_t value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "enum", "", "", value);
+    AddChild(n);
+}
+
+void FBX::Node::AddP70time(
+    const std::string& name, int64_t value
+) {
+    FBX::Node n("P");
+    n.AddProperties(name, "KTime", "Time", "", value);
+    AddChild(n);
+}
+
+
+// public member functions for writing to binary fbx
+
+void FBX::Node::Dump(std::shared_ptr<Assimp::IOStream> outfile)
+{
+    Assimp::StreamWriterLE outstream(outfile);
+    Dump(outstream);
+}
+
+void FBX::Node::Dump(Assimp::StreamWriterLE &s)
+{
+    // write header section (with placeholders for some things)
+    Begin(s);
+
+    // write properties
+    DumpProperties(s);
+
+    // go back and fill in property related placeholders
+    EndProperties(s, properties.size());
+
+    // write children
+    DumpChildren(s);
+
+    // finish, filling in end offset placeholder
+    End(s, !children.empty());
+}
+
+void FBX::Node::Begin(Assimp::StreamWriterLE &s)
+{
+    // remember start pos so we can come back and write the end pos
+    this->start_pos = s.Tell();
+
+    // placeholders for end pos and property section info
+    s.PutU4(0); // end pos
+    s.PutU4(0); // number of properties
+    s.PutU4(0); // total property section length
+
+    // node name
+    s.PutU1(name.size()); // length of node name
+    s.PutString(name); // node name as raw bytes
+
+    // property data comes after here
+    this->property_start = s.Tell();
+}
+
+void FBX::Node::DumpProperties(Assimp::StreamWriterLE& s)
+{
+    for (auto &p : properties) {
+        p.Dump(s);
+    }
+}
+
+void FBX::Node::DumpChildren(Assimp::StreamWriterLE& s)
+{
+    for (FBX::Node& child : children) {
+        child.Dump(s);
+    }
+}
+
+void FBX::Node::EndProperties(Assimp::StreamWriterLE &s)
+{
+    EndProperties(s, properties.size());
+}
+
+void FBX::Node::EndProperties(
+    Assimp::StreamWriterLE &s,
+    size_t num_properties
+) {
+    if (num_properties == 0) { return; }
+    size_t pos = s.Tell();
+    ai_assert(pos > property_start);
+    size_t property_section_size = pos - property_start;
+    s.Seek(start_pos + 4);
+    s.PutU4(num_properties);
+    s.PutU4(property_section_size);
+    s.Seek(pos);
+}
+
+void FBX::Node::End(
+    Assimp::StreamWriterLE &s,
+    bool has_children
+) {
+    // if there were children, add a null record
+    if (has_children) { s.PutString(FBX::NULL_RECORD); }
+
+    // now go back and write initial pos
+    this->end_pos = s.Tell();
+    s.Seek(start_pos);
+    s.PutU4(end_pos);
+    s.Seek(end_pos);
+}
+
+
+// static member functions
+
+// convenience function to create and write a property node,
+// holding a single property which is an array of values.
+// does not copy the data, so is efficient for large arrays.
+// TODO: optional zip compression!
+void FBX::Node::WritePropertyNode(
+    const std::string& name,
+    const std::vector<double>& v,
+    Assimp::StreamWriterLE& s
+){
+    Node node(name);
+    node.Begin(s);
+    s.PutU1('d');
+    s.PutU4(v.size()); // number of elements
+    s.PutU4(0); // no encoding (1 would be zip-compressed)
+    s.PutU4(v.size() * 8); // data size
+    for (auto it = v.begin(); it != v.end(); ++it) { s.PutF8(*it); }
+    node.EndProperties(s, 1);
+    node.End(s, false);
+}
+
+// convenience function to create and write a property node,
+// holding a single property which is an array of values.
+// does not copy the data, so is efficient for large arrays.
+// TODO: optional zip compression!
+void FBX::Node::WritePropertyNode(
+    const std::string& name,
+    const std::vector<int32_t>& v,
+    Assimp::StreamWriterLE& s
+){
+    Node node(name);
+    node.Begin(s);
+    s.PutU1('i');
+    s.PutU4(v.size()); // number of elements
+    s.PutU4(0); // no encoding (1 would be zip-compressed)
+    s.PutU4(v.size() * 4); // data size
+    for (auto it = v.begin(); it != v.end(); ++it) { s.PutI4(*it); }
+    node.EndProperties(s, 1);
+    node.End(s, false);
+}
+
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+#endif // ASSIMP_BUILD_NO_EXPORT

+ 197 - 0
code/FBXExportNode.h

@@ -0,0 +1,197 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file FBXExportNode.h
+* Declares the FBX::Node helper class for fbx export.
+*/
+#ifndef AI_FBXEXPORTNODE_H_INC
+#define AI_FBXEXPORTNODE_H_INC
+
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#include "FBXExportProperty.h"
+
+#include <assimp/StreamWriter.h> // StreamWriterLE
+
+#include <string>
+#include <vector>
+
+namespace FBX {
+    class Node;
+}
+
+class FBX::Node
+{
+public: // public data members
+    // TODO: accessors
+    std::string name; // node name
+    std::vector<FBX::Property> properties; // node properties
+    std::vector<FBX::Node> children; // child nodes
+
+public: // constructors
+    Node() = default;
+    Node(const std::string& n) : name(n) {}
+    Node(const std::string& n, const FBX::Property &p)
+        : name(n)
+        { properties.push_back(p); }
+    Node(const std::string& n, const std::vector<FBX::Property> &pv)
+        : name(n), properties(pv) {}
+
+public: // functions to add properties or children
+    // add a single property to the node
+    template <typename T>
+    void AddProperty(T value) {
+        properties.emplace_back(value);
+    }
+
+    // convenience function to add multiple properties at once
+    template <typename T, typename... More>
+    void AddProperties(T value, More... more) {
+        properties.emplace_back(value);
+        AddProperties(more...);
+    }
+    void AddProperties() {}
+
+    // add a child node directly
+    void AddChild(const Node& node) { children.push_back(node); }
+
+    // convenience function to add a child node with a single property
+    template <typename... More>
+    void AddChild(
+        const std::string& name,
+        More... more
+    ) {
+        FBX::Node c(name);
+        c.AddProperties(more...);
+        children.push_back(c);
+    }
+
+public: // support specifically for dealing with Properties70 nodes
+
+    // it really is simpler to make these all separate functions.
+    // the versions with 'A' suffixes are for animatable properties.
+    // those often follow a completely different format internally in FBX.
+    void AddP70int(const std::string& name, int32_t value);
+    void AddP70bool(const std::string& name, bool value);
+    void AddP70double(const std::string& name, double value);
+    void AddP70numberA(const std::string& name, double value);
+    void AddP70color(const std::string& name, double r, double g, double b);
+    void AddP70colorA(const std::string& name, double r, double g, double b);
+    void AddP70vector(const std::string& name, double x, double y, double z);
+    void AddP70vectorA(const std::string& name, double x, double y, double z);
+    void AddP70string(const std::string& name, const std::string& value);
+    void AddP70enum(const std::string& name, int32_t value);
+    void AddP70time(const std::string& name, int64_t value);
+
+    // template for custom P70 nodes.
+    // anything that doesn't fit in the above can be created manually.
+    template <typename... More>
+    void AddP70(
+        const std::string& name,
+        const std::string& type,
+        const std::string& type2,
+        const std::string& flags,
+        More... more
+    ) {
+        Node n("P");
+        n.AddProperties(name, type, type2, flags, more...);
+        AddChild(n);
+    }
+
+public: // member functions for writing data to a file or stream
+
+    // write the full node as binary data to the given file or stream
+    void Dump(std::shared_ptr<Assimp::IOStream> outfile);
+    void Dump(Assimp::StreamWriterLE &s);
+
+    // these other functions are for writing data piece by piece.
+    // they must be used carefully.
+    // for usage examples see FBXExporter.cpp.
+    void Begin(Assimp::StreamWriterLE &s);
+    void DumpProperties(Assimp::StreamWriterLE& s);
+    void EndProperties(Assimp::StreamWriterLE &s);
+    void EndProperties(Assimp::StreamWriterLE &s, size_t num_properties);
+    void DumpChildren(Assimp::StreamWriterLE& s);
+    void End(Assimp::StreamWriterLE &s, bool has_children);
+
+private: // data used for binary dumps
+    size_t start_pos; // starting position in stream
+    size_t end_pos; // ending position in stream
+    size_t property_start; // starting position of property section
+
+public: // static member functions
+
+    // convenience function to create a node with a single property,
+    // and write it to the stream.
+    template <typename T>
+    static void WritePropertyNode(
+        const std::string& name,
+        const T value,
+        Assimp::StreamWriterLE& s
+    ) {
+        FBX::Property p(value);
+        FBX::Node node(name, p);
+        node.Dump(s);
+    }
+
+    // convenience function to create and write a property node,
+    // holding a single property which is an array of values.
+    // does not copy the data, so is efficient for large arrays.
+    static void WritePropertyNode(
+        const std::string& name,
+        const std::vector<double>& v,
+        Assimp::StreamWriterLE& s
+    );
+
+    // convenience function to create and write a property node,
+    // holding a single property which is an array of values.
+    // does not copy the data, so is efficient for large arrays.
+    static void WritePropertyNode(
+        const std::string& name,
+        const std::vector<int32_t>& v,
+        Assimp::StreamWriterLE& s
+    );
+};
+
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#endif // AI_FBXEXPORTNODE_H_INC

+ 201 - 0
code/FBXExportProperty.cpp

@@ -0,0 +1,201 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+#ifndef ASSIMP_BUILD_NO_EXPORT
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#include "FBXExportProperty.h"
+
+#include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/Exceptional.h> // DeadlyExportError
+
+#include <string>
+#include <vector>
+#include <sstream> // stringstream
+
+
+// constructors for single element properties
+
+FBX::Property::Property(bool v)
+    : type('C'), data(1)
+{
+    data = {uint8_t(v)};
+}
+
+FBX::Property::Property(int16_t v) : type('Y'), data(2)
+{
+    uint8_t* d = data.data();
+    (reinterpret_cast<int16_t*>(d))[0] = v;
+}
+
+FBX::Property::Property(int32_t v) : type('I'), data(4)
+{
+    uint8_t* d = data.data();
+    (reinterpret_cast<int32_t*>(d))[0] = v;
+}
+
+FBX::Property::Property(float v) : type('F'), data(4)
+{
+    uint8_t* d = data.data();
+    (reinterpret_cast<float*>(d))[0] = v;
+}
+
+FBX::Property::Property(double v) : type('D'), data(8)
+{
+    uint8_t* d = data.data();
+    (reinterpret_cast<double*>(d))[0] = v;
+}
+
+FBX::Property::Property(int64_t v) : type('L'), data(8)
+{
+    uint8_t* d = data.data();
+    (reinterpret_cast<int64_t*>(d))[0] = v;
+}
+
+
+// constructors for array-type properties
+
+FBX::Property::Property(const char* c, bool raw)
+    : Property(std::string(c), raw)
+{}
+
+// strings can either be saved as "raw" (R) data, or "string" (S) data
+FBX::Property::Property(const std::string& s, bool raw)
+    : type(raw ? 'R' : 'S'), data(s.size())
+{
+    for (size_t i = 0; i < s.size(); ++i) {
+        data[i] = uint8_t(s[i]);
+    }
+}
+
+FBX::Property::Property(const std::vector<uint8_t>& r)
+    : type('R'), data(r)
+{}
+
+FBX::Property::Property(const std::vector<int32_t>& va)
+    : type('i'), data(4*va.size())
+{
+    int32_t* d = reinterpret_cast<int32_t*>(data.data());
+    for (size_t i = 0; i < va.size(); ++i) { d[i] = va[i]; }
+}
+
+FBX::Property::Property(const std::vector<double>& va)
+    : type('d'), data(8*va.size())
+{
+    double* d = reinterpret_cast<double*>(data.data());
+    for (size_t i = 0; i < va.size(); ++i) { d[i] = va[i]; }
+}
+
+FBX::Property::Property(const aiMatrix4x4& vm)
+    : type('d'), data(8*16)
+{
+    double* d = reinterpret_cast<double*>(data.data());
+    for (size_t c = 0; c < 4; ++c) {
+        for (size_t r = 0; r < 4; ++r) {
+            d[4*c+r] = vm[r][c];
+        }
+    }
+}
+
+// public member functions
+
+size_t FBX::Property::size()
+{
+    switch (type) {
+    case 'C': case 'Y': case 'I': case 'F': case 'D': case 'L':
+        return data.size() + 1;
+    case 'S': case 'R':
+        return data.size() + 5;
+    case 'i': case 'd':
+        return data.size() + 13;
+    default:
+        throw DeadlyExportError("Requested size on property of unknown type");
+    }
+}
+
+void FBX::Property::Dump(Assimp::StreamWriterLE &s)
+{
+    s.PutU1(type);
+    uint8_t* d;
+    size_t N;
+    switch (type) {
+    case 'C': s.PutU1(*(reinterpret_cast<uint8_t*>(data.data()))); return;
+    case 'Y': s.PutI2(*(reinterpret_cast<int16_t*>(data.data()))); return;
+    case 'I': s.PutI4(*(reinterpret_cast<int32_t*>(data.data()))); return;
+    case 'F': s.PutF4(*(reinterpret_cast<float*>(data.data()))); return;
+    case 'D': s.PutF8(*(reinterpret_cast<double*>(data.data()))); return;
+    case 'L': s.PutI8(*(reinterpret_cast<int64_t*>(data.data()))); return;
+    case 'S':
+    case 'R':
+        s.PutU4(data.size());
+        for (size_t i = 0; i < data.size(); ++i) { s.PutU1(data[i]); }
+        return;
+    case 'i':
+        N = data.size() / 4;
+        s.PutU4(N); // number of elements
+        s.PutU4(0); // no encoding (1 would be zip-compressed)
+        // TODO: compress if large?
+        s.PutU4(data.size()); // data size
+        d = data.data();
+        for (size_t i = 0; i < N; ++i) {
+            s.PutI4((reinterpret_cast<int32_t*>(d))[i]);
+        }
+        return;
+    case 'd':
+        N = data.size() / 8;
+        s.PutU4(N); // number of elements
+        s.PutU4(0); // no encoding (1 would be zip-compressed)
+        // TODO: compress if large?
+        s.PutU4(data.size()); // data size
+        d = data.data();
+        for (size_t i = 0; i < N; ++i) {
+            s.PutF8((reinterpret_cast<double*>(d))[i]);
+        }
+        return;
+    default:
+        std::stringstream err;
+        err << "Tried to dump property with invalid type '";
+        err << type << "'!";
+        throw DeadlyExportError(err.str());
+    }
+}
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+#endif // ASSIMP_BUILD_NO_EXPORT

+ 123 - 0
code/FBXExportProperty.h

@@ -0,0 +1,123 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file FBXExportProperty.h
+* Declares the FBX::Property helper class for fbx export.
+*/
+#ifndef AI_FBXEXPORTPROPERTY_H_INC
+#define AI_FBXEXPORTPROPERTY_H_INC
+
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+
+#include <assimp/types.h> // aiMatrix4x4
+#include <assimp/StreamWriter.h> // StreamWriterLE
+
+#include <string>
+#include <vector>
+#include <type_traits> // is_void
+
+namespace FBX {
+    class Property;
+}
+
+/** FBX::Property
+ *
+ *  Holds a value of any of FBX's recognized types,
+ *  each represented by a particular one-character code.
+ *  C : 1-byte uint8, usually 0x00 or 0x01 to represent boolean false and true
+ *  Y : 2-byte int16
+ *  I : 4-byte int32
+ *  F : 4-byte float
+ *  D : 8-byte double
+ *  L : 8-byte int64
+ *  i : array of int32
+ *  f : array of float
+ *  d : array of double
+ *  l : array of int64
+ *  b : array of 1-byte booleans (0x00 or 0x01)
+ *  S : string (array of 1-byte char)
+ *  R : raw data (array of bytes)
+ */
+class FBX::Property
+{
+public:
+    // constructors for basic types.
+    // all explicit to avoid accidental typecasting
+    explicit Property(bool v);
+    // TODO: determine if there is actually a byte type,
+    // or if this always means <bool>. 'C' seems to imply <char>,
+    // so possibly the above was intended to represent both.
+    explicit Property(int16_t v);
+    explicit Property(int32_t v);
+    explicit Property(float v);
+    explicit Property(double v);
+    explicit Property(int64_t v);
+    // strings can either be stored as 'R' (raw) or 'S' (string) type
+    explicit Property(const char* c, bool raw=false);
+    explicit Property(const std::string& s, bool raw=false);
+    explicit Property(const std::vector<uint8_t>& r);
+    explicit Property(const std::vector<int32_t>& va);
+    explicit Property(const std::vector<double>& va);
+    explicit Property(const aiMatrix4x4& vm);
+
+    // this will catch any type not defined above,
+    // so that we don't accidentally convert something we don't want.
+    // for example (const char*) --> (bool)... seriously wtf C++
+    template <class T>
+    explicit Property(T v) : type('X') {
+        static_assert(std::is_void<T>::value, "TRIED TO CREATE FBX PROPERTY WITH UNSUPPORTED TYPE, CHECK YOUR PROPERTY INSTANTIATION");
+    } // note: no line wrap so it appears verbatim on the compiler error
+
+    // the size of this property node in a binary file, in bytes
+    size_t size();
+
+    // write this property node as binary data to the given stream
+    void Dump(Assimp::StreamWriterLE &s);
+
+private:
+    char type;
+    std::vector<uint8_t> data;
+};
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#endif // AI_FBXEXPORTPROPERTY_H_INC

+ 2044 - 0
code/FBXExporter.cpp

@@ -0,0 +1,2044 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+#ifndef ASSIMP_BUILD_NO_EXPORT
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#include "FBXExporter.h"
+#include "FBXExportNode.h"
+#include "FBXExportProperty.h"
+#include "FBXCommon.h"
+
+#include <assimp/version.h> // aiGetVersion
+#include <assimp/IOSystem.hpp>
+#include <assimp/Exporter.hpp>
+#include <assimp/DefaultLogger.hpp>
+#include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/Exceptional.h> // DeadlyExportError
+#include <assimp/material.h> // aiTextureType
+#include <assimp/scene.h>
+#include <assimp/mesh.h>
+
+// Header files, standard library.
+#include <memory> // shared_ptr
+#include <string>
+#include <sstream> // stringstream
+#include <ctime> // localtime, tm_*
+#include <map>
+#include <set>
+#include <unordered_set>
+#include <iostream> // endl
+
+// RESOURCES:
+// https://code.blender.org/2013/08/fbx-binary-file-format-specification/
+// https://wiki.blender.org/index.php/User:Mont29/Foundation/FBX_File_Structure
+
+const double DEG = 57.29577951308232087679815481; // degrees per radian
+
+// some constants that we'll use for writing metadata
+namespace FBX {
+    const std::string EXPORT_VERSION_STR = "7.4.0";
+    const uint32_t EXPORT_VERSION_INT = 7400; // 7.4 == 2014/2015
+    // FBX files have some hashed values that depend on the creation time field,
+    // but for now we don't actually know how to generate these.
+    // what we can do is set them to a known-working version.
+    // this is the data that Blender uses in their FBX export process.
+    const std::string GENERIC_CTIME = "1970-01-01 10:00:00:000";
+    const std::string GENERIC_FILEID =
+        "\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1";
+    const std::string GENERIC_FOOTID =
+        "\xfa\xbc\xab\x09\xd0\xc8\xd4\x66\xb1\x76\xfb\x83\x1c\xf7\x26\x7e";
+    const std::string FOOT_MAGIC =
+        "\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b";
+}
+
+using namespace Assimp;
+using namespace FBX;
+
+namespace Assimp {
+
+    // ---------------------------------------------------------------------
+    // Worker function for exporting a scene to binary FBX.
+    // Prototyped and registered in Exporter.cpp
+    void ExportSceneFBX (
+        const char* pFile,
+        IOSystem* pIOSystem,
+        const aiScene* pScene,
+        const ExportProperties* pProperties
+    ){
+        // initialize the exporter
+        FBXExporter exporter(pScene, pProperties);
+
+        // perform binary export
+        exporter.ExportBinary(pFile, pIOSystem);
+    }
+
+    // ---------------------------------------------------------------------
+    // Worker function for exporting a scene to ASCII FBX.
+    // Prototyped and registered in Exporter.cpp
+    /*void ExportSceneFBXA (
+        const char* pFile,
+        IOSystem* pIOSystem,
+        const aiScene* pScene,
+        const ExportProperties* pProperties
+    ){
+        // initialize the exporter
+        FBXExporter exporter(pScene, pProperties);
+
+        // perform ascii export
+        exporter.ExportAscii(pFile, pIOSystem);
+    }*/ // TODO
+
+} // end of namespace Assimp
+
+FBXExporter::FBXExporter (
+    const aiScene* pScene,
+    const ExportProperties* pProperties
+)
+    : mScene(pScene)
+    , mProperties(pProperties)
+{
+    // will probably need to determine UIDs, connections, etc here.
+    // basically anything that needs to be known
+    // before we start writing sections to the stream.
+}
+
+void FBXExporter::ExportBinary (
+    const char* pFile,
+    IOSystem* pIOSystem
+){
+    // remember that we're exporting in binary mode
+    binary = true;
+
+    // we're not currently using these preferences,
+    // but clang will cry about it if we never touch it.
+    // TODO: some of these might be relevant to export
+    (void)mProperties;
+
+    // open the indicated file for writing (in binary mode)
+    outfile.reset(pIOSystem->Open(pFile,"wb"));
+    if (!outfile) {
+        throw DeadlyExportError(
+            "could not open output .fbx file: " + std::string(pFile)
+        );
+    }
+
+    // first a binary-specific file header
+    WriteBinaryHeader();
+
+    // the rest of the file is in node entries.
+    // we have to serialize each entry before we write to the output,
+    // as the first thing we write is the byte offset of the _next_ entry.
+    // Either that or we can skip back to write the offset when we finish.
+    WriteAllNodes();
+
+    // finally we have a binary footer to the file
+    WriteBinaryFooter();
+
+    // explicitly release file pointer,
+    // so we don't have to rely on class destruction.
+    outfile.reset();
+}
+
+void FBXExporter::ExportAscii (
+    const char* pFile,
+    IOSystem* pIOSystem
+){
+    // remember that we're exporting in ascii mode
+    binary = false;
+
+    // open the indicated file for writing in text mode
+    outfile.reset(pIOSystem->Open(pFile,"wt"));
+    if (!outfile) {
+        throw DeadlyExportError(
+            "could not open output .fbx file: " + std::string(pFile)
+        );
+    }
+
+    // this isn't really necessary,
+    // but the Autodesk FBX SDK puts a similar comment at the top of the file.
+    // Theirs declares that the file copyright is owned by Autodesk...
+    std::stringstream head;
+    using std::endl;
+    head << "; FBX " << EXPORT_VERSION_STR << " project file" << endl;
+    head << "; Created by the Open Asset Import Library (Assimp)" << endl;
+    head << "; http://assimp.org" << endl;
+    head << "; -------------------------------------------------" << endl;
+    head << endl;
+    const std::string ascii_header = head.str();
+    outfile->Write(ascii_header.c_str(), ascii_header.size(), 1);
+
+    // write all the sections
+    WriteAllNodes();
+
+    // explicitly release file pointer,
+    // so we don't have to rely on class destruction.
+    outfile.reset();
+}
+
+void FBXExporter::WriteBinaryHeader()
+{
+    // first a specific sequence of 23 bytes, always the same
+    const char binary_header[24] = "Kaydara FBX Binary\x20\x20\x00\x1a\x00";
+    outfile->Write(binary_header, 1, 23);
+
+    // then FBX version number, "multiplied" by 1000, as little-endian uint32.
+    // so 7.3 becomes 7300 == 0x841C0000, 7.4 becomes 7400 == 0xE81C0000, etc
+    {
+        StreamWriterLE outstream(outfile);
+        outstream.PutU4(EXPORT_VERSION_INT);
+    } // StreamWriter destructor writes the data to the file
+
+    // after this the node data starts immediately
+    // (probably with the FBXHEaderExtension node)
+}
+
+void FBXExporter::WriteBinaryFooter()
+{
+    outfile->Write(NULL_RECORD.c_str(), NULL_RECORD.size(), 1);
+
+    outfile->Write(GENERIC_FOOTID.c_str(), GENERIC_FOOTID.size(), 1);
+    for (size_t i = 0; i < 4; ++i) {
+        outfile->Write("\x00", 1, 1);
+    }
+
+    // here some padding is added for alignment to 16 bytes.
+    // if already aligned, the full 16 bytes is added.
+    size_t pos = outfile->Tell();
+    size_t pad = 16 - (pos % 16);
+    for (size_t i = 0; i < pad; ++i) {
+        outfile->Write("\x00", 1, 1);
+    }
+
+    // now the file version again
+    {
+        StreamWriterLE outstream(outfile);
+        outstream.PutU4(EXPORT_VERSION_INT);
+    } // StreamWriter destructor writes the data to the file
+
+    // and finally some binary footer added to all files
+    for (size_t i = 0; i < 120; ++i) {
+        outfile->Write("\x00", 1, 1);
+    }
+    outfile->Write(FOOT_MAGIC.c_str(), FOOT_MAGIC.size(), 1);
+}
+
+void FBXExporter::WriteAllNodes ()
+{
+    // header
+    // (and fileid, creation time, creator, if binary)
+    WriteHeaderExtension();
+
+    // global settings
+    WriteGlobalSettings();
+
+    // documents
+    WriteDocuments();
+
+    // references
+    WriteReferences();
+
+    // definitions
+    WriteDefinitions();
+
+    // objects
+    WriteObjects();
+
+    // connections
+    WriteConnections();
+
+    // WriteTakes? (deprecated since at least 2015 (fbx 7.4))
+}
+
+//FBXHeaderExtension top-level node
+void FBXExporter::WriteHeaderExtension ()
+{
+    FBX::Node n("FBXHeaderExtension");
+    StreamWriterLE outstream(outfile);
+
+    // begin node
+    n.Begin(outstream);
+
+    // write properties
+    // (none)
+
+    // finish properties
+    n.EndProperties(outstream, 0);
+
+    // write child nodes
+    FBX::Node::WritePropertyNode(
+        "FBXHeaderVersion", int32_t(1003), outstream
+    );
+    FBX::Node::WritePropertyNode(
+        "FBXVersion", int32_t(EXPORT_VERSION_INT), outstream
+    );
+    FBX::Node::WritePropertyNode(
+        "EncryptionType", int32_t(0), outstream
+    );
+
+    FBX::Node CreationTimeStamp("CreationTimeStamp");
+    time_t rawtime;
+    time(&rawtime);
+    struct tm * now = localtime(&rawtime);
+    CreationTimeStamp.AddChild("Version", int32_t(1000));
+    CreationTimeStamp.AddChild("Year", int32_t(now->tm_year + 1900));
+    CreationTimeStamp.AddChild("Month", int32_t(now->tm_mon + 1));
+    CreationTimeStamp.AddChild("Day", int32_t(now->tm_mday));
+    CreationTimeStamp.AddChild("Hour", int32_t(now->tm_hour));
+    CreationTimeStamp.AddChild("Minute", int32_t(now->tm_min));
+    CreationTimeStamp.AddChild("Second", int32_t(now->tm_sec));
+    CreationTimeStamp.AddChild("Millisecond", int32_t(0));
+    CreationTimeStamp.Dump(outstream);
+
+    std::stringstream creator;
+    creator << "Open Asset Import Library (Assimp) " << aiGetVersionMajor()
+            << "." << aiGetVersionMinor() << "." << aiGetVersionRevision();
+    FBX::Node::WritePropertyNode("Creator", creator.str(), outstream);
+
+    FBX::Node sceneinfo("SceneInfo");
+    //sceneinfo.AddProperty("GlobalInfo" + FBX::SEPARATOR + "SceneInfo");
+    // not sure if any of this is actually needed,
+    // so just write an empty node for now.
+    sceneinfo.Dump(outstream);
+
+    // finish node
+    n.End(outstream, true);
+
+    // that's it for FBXHeaderExtension...
+
+    // but binary files also need top-level FileID, CreationTime, Creator:
+    std::vector<uint8_t> raw(GENERIC_FILEID.size());
+    for (size_t i = 0; i < GENERIC_FILEID.size(); ++i) {
+        raw[i] = uint8_t(GENERIC_FILEID[i]);
+    }
+    FBX::Node::WritePropertyNode("FileId", raw, outstream);
+    FBX::Node::WritePropertyNode("CreationTime", GENERIC_CTIME, outstream);
+    FBX::Node::WritePropertyNode("Creator", creator.str(), outstream);
+}
+
+void FBXExporter::WriteGlobalSettings ()
+{
+    FBX::Node gs("GlobalSettings");
+    gs.AddChild("Version", int32_t(1000));
+
+    FBX::Node p("Properties70");
+    p.AddP70int("UpAxis", 1);
+    p.AddP70int("UpAxisSign", 1);
+    p.AddP70int("FrontAxis", 2);
+    p.AddP70int("FrontAxisSign", 1);
+    p.AddP70int("CoordAxis", 0);
+    p.AddP70int("CoordAxisSign", 1);
+    p.AddP70int("OriginalUpAxis", 1);
+    p.AddP70int("OriginalUpAxisSign", 1);
+    p.AddP70double("UnitScaleFactor", 1.0);
+    p.AddP70double("OriginalUnitScaleFactor", 1.0);
+    p.AddP70color("AmbientColor", 0.0, 0.0, 0.0);
+    p.AddP70string("DefaultCamera", "Producer Perspective");
+    p.AddP70enum("TimeMode", 11);
+    p.AddP70enum("TimeProtocol", 2);
+    p.AddP70enum("SnapOnFrameMode", 0);
+    p.AddP70time("TimeSpanStart", 0); // TODO: animation support
+    p.AddP70time("TimeSpanStop", FBX::SECOND); // TODO: animation support
+    p.AddP70double("CustomFrameRate", -1.0);
+    p.AddP70("TimeMarker", "Compound", "", ""); // not sure what this is
+    p.AddP70int("CurrentTimeMarker", -1);
+    gs.AddChild(p);
+
+    gs.Dump(outfile);
+}
+
+void FBXExporter::WriteDocuments ()
+{
+    // not sure what the use of multiple documents would be,
+    // or whether any end-application supports it
+    FBX::Node docs("Documents");
+    docs.AddChild("Count", int32_t(1));
+    FBX::Node doc("Document");
+
+    // generate uid
+    int64_t uid = generate_uid();
+    doc.AddProperties(uid, "", "Scene");
+    FBX::Node p("Properties70");
+    p.AddP70("SourceObject", "object", "", ""); // what is this even for?
+    p.AddP70string("ActiveAnimStackName", ""); // should do this properly?
+    doc.AddChild(p);
+
+    // UID for root node in scene heirarchy.
+    // always set to 0 in the case of a single document.
+    // not sure what happens if more than one document exists,
+    // but that won't matter to us as we're exporting a single scene.
+    doc.AddChild("RootNode", int64_t(0));
+
+    docs.AddChild(doc);
+    docs.Dump(outfile);
+}
+
+void FBXExporter::WriteReferences ()
+{
+    // always empty for now.
+    // not really sure what this is for.
+    FBX::Node n("References");
+    n.Dump(outfile);
+}
+
+
+// ---------------------------------------------------------------
+// some internal helper functions used for writing the definitions
+// (before any actual data is written)
+// ---------------------------------------------------------------
+
+size_t count_nodes(const aiNode* n) {
+    size_t count = 1;
+    for (size_t i = 0; i < n->mNumChildren; ++i) {
+        count += count_nodes(n->mChildren[i]);
+    }
+    return count;
+}
+
+bool has_phong_mat(const aiScene* scene)
+{
+    // just search for any material with a shininess exponent
+    for (size_t i = 0; i < scene->mNumMaterials; ++i) {
+        aiMaterial* mat = scene->mMaterials[i];
+        float shininess = 0;
+        mat->Get(AI_MATKEY_SHININESS, shininess);
+        if (shininess > 0) {
+            return true;
+        }
+    }
+    return false;
+}
+
+size_t count_images(const aiScene* scene) {
+    std::unordered_set<std::string> images;
+    aiString texpath;
+    for (size_t i = 0; i < scene->mNumMaterials; ++i) {
+        aiMaterial* mat = scene->mMaterials[i];
+        for (
+            size_t tt = aiTextureType_DIFFUSE;
+            tt < aiTextureType_UNKNOWN;
+            ++tt
+        ){
+            const aiTextureType textype = static_cast<aiTextureType>(tt);
+            const size_t texcount = mat->GetTextureCount(textype);
+            for (size_t j = 0; j < texcount; ++j) {
+                mat->GetTexture(textype, j, &texpath);
+                images.insert(std::string(texpath.C_Str()));
+            }
+        }
+    }
+    //for (auto &s : images) {
+    //    std::cout << "found image: " << s << std::endl;
+    //}
+    return images.size();
+}
+
+size_t count_textures(const aiScene* scene) {
+    size_t count = 0;
+    for (size_t i = 0; i < scene->mNumMaterials; ++i) {
+        aiMaterial* mat = scene->mMaterials[i];
+        for (
+            size_t tt = aiTextureType_DIFFUSE;
+            tt < aiTextureType_UNKNOWN;
+            ++tt
+        ){
+            // TODO: handle layered textures
+            if (mat->GetTextureCount(static_cast<aiTextureType>(tt)) > 0) {
+                count += 1;
+            }
+        }
+    }
+    return count;
+}
+
+size_t count_deformers(const aiScene* scene) {
+    size_t count = 0;
+    for (size_t i = 0; i < scene->mNumMeshes; ++i) {
+        const size_t n = scene->mMeshes[i]->mNumBones;
+        if (n) {
+            // 1 main deformer, 1 subdeformer per bone
+            count += n + 1;
+        }
+    }
+    return count;
+}
+
+void FBXExporter::WriteDefinitions ()
+{
+    // basically this is just bookkeeping:
+    // determining how many of each type of object there are
+    // and specifying the base properties to use when otherwise unspecified.
+
+    // we need to count the objects
+    int32_t count;
+    int32_t total_count = 0;
+
+    // and store them
+    std::vector<FBX::Node> object_nodes;
+    FBX::Node n, pt, p;
+
+    // GlobalSettings
+    // this seems to always be here in Maya exports
+    n = FBX::Node("ObjectType", Property("GlobalSettings"));
+    count = 1;
+    n.AddChild("Count", count);
+    object_nodes.push_back(n);
+    total_count += count;
+
+    // AnimationStack / FbxAnimStack
+    // this seems to always be here in Maya exports
+    count = 0;
+    if (count) {
+        n = FBX::Node("ObjectType", Property("AnimationStack"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxAnimStack"));
+        p = FBX::Node("Properties70");
+        p.AddP70string("Description", "");
+        p.AddP70time("LocalStart", 0);
+        p.AddP70time("LocalStop", 0);
+        p.AddP70time("ReferenceStart", 0);
+        p.AddP70time("ReferenceStop", 0);
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // AnimationLayer / FbxAnimLayer
+    // this seems to always be here in Maya exports
+    count = 0;
+    if (count) {
+        n = FBX::Node("ObjectType", Property("AnimationLayer"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FBXAnimLayer"));
+        p = FBX::Node("Properties70");
+        p.AddP70("Weight", "Number", "", "A", double(100));
+        p.AddP70bool("Mute", 0);
+        p.AddP70bool("Solo", 0);
+        p.AddP70bool("Lock", 0);
+        p.AddP70color("Color", 0.8, 0.8, 0.8);
+        p.AddP70("BlendMode", "enum", "", "", int32_t(0));
+        p.AddP70("RotationAccumulationMode", "enum", "", "", int32_t(0));
+        p.AddP70("ScaleAccumulationMode", "enum", "", "", int32_t(0));
+        p.AddP70("BlendModeBypass", "ULongLong", "", "", int64_t(0));
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // NodeAttribute
+    // this is completely absurd.
+    // there can only be one "NodeAttribute" template,
+    // but FbxSkeleton, FbxCamera, FbxLight all are "NodeAttributes".
+    // so if only one exists we should set the template for that,
+    // otherwise... we just pick one :/.
+    // the others have to set all their properties every instance,
+    // because there's no template.
+    count = 1; // TODO: select properly
+    if (count) {
+        // FbxSkeleton
+        n = FBX::Node("ObjectType", Property("NodeAttribute"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxSkeleton"));
+        p = FBX::Node("Properties70");
+        p.AddP70color("Color", 0.8, 0.8, 0.8);
+        p.AddP70double("Size", 33.333333333333);
+        p.AddP70("LimbLength", "double", "Number", "H", double(1));
+        // note: not sure what the "H" flag is for - hidden?
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Model / FbxNode
+    // <~~ node heirarchy
+    count = count_nodes(mScene->mRootNode) - 1; // (not counting root node)
+    if (count) {
+        n = FBX::Node("ObjectType", Property("Model"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxNode"));
+        p = FBX::Node("Properties70");
+        p.AddP70enum("QuaternionInterpolate", 0);
+        p.AddP70vector("RotationOffset", 0.0, 0.0, 0.0);
+        p.AddP70vector("RotationPivot", 0.0, 0.0, 0.0);
+        p.AddP70vector("ScalingOffset", 0.0, 0.0, 0.0);
+        p.AddP70vector("ScalingPivot", 0.0, 0.0, 0.0);
+        p.AddP70bool("TranslationActive", 0);
+        p.AddP70vector("TranslationMin", 0.0, 0.0, 0.0);
+        p.AddP70vector("TranslationMax", 0.0, 0.0, 0.0);
+        p.AddP70bool("TranslationMinX", 0);
+        p.AddP70bool("TranslationMinY", 0);
+        p.AddP70bool("TranslationMinZ", 0);
+        p.AddP70bool("TranslationMaxX", 0);
+        p.AddP70bool("TranslationMaxY", 0);
+        p.AddP70bool("TranslationMaxZ", 0);
+        p.AddP70enum("RotationOrder", 0);
+        p.AddP70bool("RotationSpaceForLimitOnly", 0);
+        p.AddP70double("RotationStiffnessX", 0.0);
+        p.AddP70double("RotationStiffnessY", 0.0);
+        p.AddP70double("RotationStiffnessZ", 0.0);
+        p.AddP70double("AxisLen", 10.0);
+        p.AddP70vector("PreRotation", 0.0, 0.0, 0.0);
+        p.AddP70vector("PostRotation", 0.0, 0.0, 0.0);
+        p.AddP70bool("RotationActive", 0);
+        p.AddP70vector("RotationMin", 0.0, 0.0, 0.0);
+        p.AddP70vector("RotationMax", 0.0, 0.0, 0.0);
+        p.AddP70bool("RotationMinX", 0);
+        p.AddP70bool("RotationMinY", 0);
+        p.AddP70bool("RotationMinZ", 0);
+        p.AddP70bool("RotationMaxX", 0);
+        p.AddP70bool("RotationMaxY", 0);
+        p.AddP70bool("RotationMaxZ", 0);
+        p.AddP70enum("InheritType", 0);
+        p.AddP70bool("ScalingActive", 0);
+        p.AddP70vector("ScalingMin", 0.0, 0.0, 0.0);
+        p.AddP70vector("ScalingMax", 1.0, 1.0, 1.0);
+        p.AddP70bool("ScalingMinX", 0);
+        p.AddP70bool("ScalingMinY", 0);
+        p.AddP70bool("ScalingMinZ", 0);
+        p.AddP70bool("ScalingMaxX", 0);
+        p.AddP70bool("ScalingMaxY", 0);
+        p.AddP70bool("ScalingMaxZ", 0);
+        p.AddP70vector("GeometricTranslation", 0.0, 0.0, 0.0);
+        p.AddP70vector("GeometricRotation", 0.0, 0.0, 0.0);
+        p.AddP70vector("GeometricScaling", 1.0, 1.0, 1.0);
+        p.AddP70double("MinDampRangeX", 0.0);
+        p.AddP70double("MinDampRangeY", 0.0);
+        p.AddP70double("MinDampRangeZ", 0.0);
+        p.AddP70double("MaxDampRangeX", 0.0);
+        p.AddP70double("MaxDampRangeY", 0.0);
+        p.AddP70double("MaxDampRangeZ", 0.0);
+        p.AddP70double("MinDampStrengthX", 0.0);
+        p.AddP70double("MinDampStrengthY", 0.0);
+        p.AddP70double("MinDampStrengthZ", 0.0);
+        p.AddP70double("MaxDampStrengthX", 0.0);
+        p.AddP70double("MaxDampStrengthY", 0.0);
+        p.AddP70double("MaxDampStrengthZ", 0.0);
+        p.AddP70double("PreferedAngleX", 0.0);
+        p.AddP70double("PreferedAngleY", 0.0);
+        p.AddP70double("PreferedAngleZ", 0.0);
+        p.AddP70("LookAtProperty", "object", "", "");
+        p.AddP70("UpVectorProperty", "object", "", "");
+        p.AddP70bool("Show", 1);
+        p.AddP70bool("NegativePercentShapeSupport", 1);
+        p.AddP70int("DefaultAttributeIndex", -1);
+        p.AddP70bool("Freeze", 0);
+        p.AddP70bool("LODBox", 0);
+        p.AddP70(
+            "Lcl Translation", "Lcl Translation", "", "A",
+            double(0), double(0), double(0)
+        );
+        p.AddP70(
+            "Lcl Rotation", "Lcl Rotation", "", "A",
+            double(0), double(0), double(0)
+        );
+        p.AddP70(
+            "Lcl Scaling", "Lcl Scaling", "", "A",
+            double(1), double(1), double(1)
+        );
+        p.AddP70("Visibility", "Visibility", "", "A", double(1));
+        p.AddP70(
+            "Visibility Inheritance", "Visibility Inheritance", "", "",
+            int32_t(1)
+        );
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Geometry / FbxMesh
+    // <~~ aiMesh
+    count = mScene->mNumMeshes;
+    if (count) {
+        n = FBX::Node("ObjectType", Property("Geometry"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxMesh"));
+        p = FBX::Node("Properties70");
+        p.AddP70color("Color", 0, 0, 0);
+        p.AddP70vector("BBoxMin", 0, 0, 0);
+        p.AddP70vector("BBoxMax", 0, 0, 0);
+        p.AddP70bool("Primary Visibility", 1);
+        p.AddP70bool("Casts Shadows", 1);
+        p.AddP70bool("Receive Shadows", 1);
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Material / FbxSurfacePhong, FbxSurfaceLambert, FbxSurfaceMaterial
+    // <~~ aiMaterial
+    // basically if there's any phong material this is defined as phong,
+    // and otherwise lambert.
+    // More complex materials cause a bare-bones FbxSurfaceMaterial definition
+    // and are treated specially, as they're not really supported by FBX.
+    // TODO: support Maya's Stingray PBS material
+    count = mScene->mNumMaterials;
+    if (count) {
+        bool has_phong = has_phong_mat(mScene);
+        n = FBX::Node("ObjectType", Property("Material"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate");
+        if (has_phong) {
+            pt.AddProperty("FbxSurfacePhong");
+        } else {
+            pt.AddProperty("FbxSurfaceLambert");
+        }
+        p = FBX::Node("Properties70");
+        if (has_phong) {
+            p.AddP70string("ShadingModel", "Phong");
+        } else {
+            p.AddP70string("ShadingModel", "Lambert");
+        }
+        p.AddP70bool("MultiLayer", 0);
+        p.AddP70colorA("EmissiveColor", 0.0, 0.0, 0.0);
+        p.AddP70numberA("EmissiveFactor", 1.0);
+        p.AddP70colorA("AmbientColor", 0.2, 0.2, 0.2);
+        p.AddP70numberA("AmbientFactor", 1.0);
+        p.AddP70colorA("DiffuseColor", 0.8, 0.8, 0.8);
+        p.AddP70numberA("DiffuseFactor", 1.0);
+        p.AddP70vector("Bump", 0.0, 0.0, 0.0);
+        p.AddP70vector("NormalMap", 0.0, 0.0, 0.0);
+        p.AddP70double("BumpFactor", 1.0);
+        p.AddP70colorA("TransparentColor", 0.0, 0.0, 0.0);
+        p.AddP70numberA("TransparencyFactor", 0.0);
+        p.AddP70color("DisplacementColor", 0.0, 0.0, 0.0);
+        p.AddP70double("DisplacementFactor", 1.0);
+        p.AddP70color("VectorDisplacementColor", 0.0, 0.0, 0.0);
+        p.AddP70double("VectorDisplacementFactor", 1.0);
+        if (has_phong) {
+            p.AddP70colorA("SpecularColor", 0.2, 0.2, 0.2);
+            p.AddP70numberA("SpecularFactor", 1.0);
+            p.AddP70numberA("ShininessExponent", 20.0);
+            p.AddP70colorA("ReflectionColor", 0.0, 0.0, 0.0);
+            p.AddP70numberA("ReflectionFactor", 1.0);
+        }
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Video / FbxVideo
+    // one for each image file.
+    count = count_images(mScene);
+    if (count) {
+        n = FBX::Node("ObjectType", Property("Video"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxVideo"));
+        p = FBX::Node("Properties70");
+        p.AddP70bool("ImageSequence", 0);
+        p.AddP70int("ImageSequenceOffset", 0);
+        p.AddP70double("FrameRate", 0.0);
+        p.AddP70int("LastFrame", 0);
+        p.AddP70int("Width", 0);
+        p.AddP70int("Height", 0);
+        p.AddP70("Path", "KString", "XRefUrl", "", "");
+        p.AddP70int("StartFrame", 0);
+        p.AddP70int("StopFrame", 0);
+        p.AddP70double("PlaySpeed", 0.0);
+        p.AddP70time("Offset", 0);
+        p.AddP70enum("InterlaceMode", 0);
+        p.AddP70bool("FreeRunning", 0);
+        p.AddP70bool("Loop", 0);
+        p.AddP70enum("AccessMode", 0);
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Texture / FbxFileTexture
+    // <~~ aiTexture
+    count = count_textures(mScene);
+    if (count) {
+        n = FBX::Node("ObjectType", Property("Texture"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxFileTexture"));
+        p = FBX::Node("Properties70");
+        p.AddP70enum("TextureTypeUse", 0);
+        p.AddP70numberA("Texture alpha", 1.0);
+        p.AddP70enum("CurrentMappingType", 0);
+        p.AddP70enum("WrapModeU", 0);
+        p.AddP70enum("WrapModeV", 0);
+        p.AddP70bool("UVSwap", 0);
+        p.AddP70bool("PremultiplyAlpha", 1);
+        p.AddP70vectorA("Translation", 0.0, 0.0, 0.0);
+        p.AddP70vectorA("Rotation", 0.0, 0.0, 0.0);
+        p.AddP70vectorA("Scaling", 1.0, 1.0, 1.0);
+        p.AddP70vector("TextureRotationPivot", 0.0, 0.0, 0.0);
+        p.AddP70vector("TextureScalingPivot", 0.0, 0.0, 0.0);
+        p.AddP70enum("CurrentTextureBlendMode", 1);
+        p.AddP70string("UVSet", "default");
+        p.AddP70bool("UseMaterial", 0);
+        p.AddP70bool("UseMipMap", 0);
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // AnimationCurveNode / FbxAnimCurveNode
+    count = 0;
+    if (count) {
+        n = FBX::Node("ObjectType", Property("AnimationCurveNode"));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property("FbxAnimCurveNode"));
+        p = FBX::Node("Properties70");
+        p.AddP70("d", "Compound", "", "");
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Pose
+    count = 0;
+    for (size_t i = 0; i < mScene->mNumMeshes; ++i) {
+        aiMesh* mesh = mScene->mMeshes[i];
+        if (mesh->HasBones()) { ++count; }
+    }
+    if (count) {
+        n = FBX::Node("ObjectType", Property("Pose"));
+        n.AddChild("Count", count);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // Deformer
+    count = count_deformers(mScene);
+    if (count) {
+        n = FBX::Node("ObjectType", Property("Deformer"));
+        n.AddChild("Count", count);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // (template)
+    count = 0;
+    if (count) {
+        n = FBX::Node("ObjectType", Property(""));
+        n.AddChild("Count", count);
+        pt = FBX::Node("PropertyTemplate", Property(""));
+        p = FBX::Node("Properties70");
+        pt.AddChild(p);
+        n.AddChild(pt);
+        object_nodes.push_back(n);
+        total_count += count;
+    }
+
+    // now write it all
+    FBX::Node defs("Definitions");
+    defs.AddChild("Version", int32_t(100));
+    defs.AddChild("Count", int32_t(total_count));
+    for (auto &n : object_nodes) { defs.AddChild(n); }
+    defs.Dump(outfile);
+}
+
+
+// -------------------------------------------------------------------
+// some internal helper functions used for writing the objects section
+// (which holds the actual data)
+// -------------------------------------------------------------------
+
+aiNode* get_node_for_mesh(unsigned int meshIndex, aiNode* node)
+{
+    for (size_t i = 0; i < node->mNumMeshes; ++i) {
+        if (node->mMeshes[i] == meshIndex) {
+            return node;
+        }
+    }
+    for (size_t i = 0; i < node->mNumChildren; ++i) {
+        aiNode* ret = get_node_for_mesh(meshIndex, node->mChildren[i]);
+        if (ret) { return ret; }
+    }
+    return nullptr;
+}
+
+aiMatrix4x4 get_world_transform(const aiNode* node, const aiScene* scene)
+{
+    std::vector<const aiNode*> node_chain;
+    while (node != scene->mRootNode) {
+        node_chain.push_back(node);
+        node = node->mParent;
+    }
+    aiMatrix4x4 transform;
+    for (auto n = node_chain.rbegin(); n != node_chain.rend(); ++n) {
+        transform *= (*n)->mTransformation;
+    }
+    return transform;
+}
+
+
+void FBXExporter::WriteObjects ()
+{
+    // numbers should match those given in definitions! make sure to check
+    StreamWriterLE outstream(outfile);
+    FBX::Node object_node("Objects");
+    object_node.Begin(outstream);
+    object_node.EndProperties(outstream);
+
+    // geometry (aiMesh)
+    mesh_uids.clear();
+    for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
+        // it's all about this mesh
+        aiMesh* m = mScene->mMeshes[mi];
+
+        // start the node record
+        FBX::Node n("Geometry");
+        int64_t uid = generate_uid();
+        mesh_uids.push_back(uid);
+        n.AddProperty(uid);
+        n.AddProperty(FBX::SEPARATOR + "Geometry");
+        n.AddProperty("Mesh");
+        n.Begin(outstream);
+        n.DumpProperties(outstream);
+        n.EndProperties(outstream);
+
+        // output vertex data - each vertex should be unique (probably)
+        std::vector<double> flattened_vertices;
+        // index of original vertex in vertex data vector
+        std::vector<int32_t> vertex_indices;
+        // map of vertex value to its index in the data vector
+        std::map<aiVector3D,size_t> index_by_vertex_value;
+        size_t index = 0;
+        for (size_t vi = 0; vi < m->mNumVertices; ++vi) {
+            aiVector3D vtx = m->mVertices[vi];
+            auto elem = index_by_vertex_value.find(vtx);
+            if (elem == index_by_vertex_value.end()) {
+                vertex_indices.push_back(index);
+                index_by_vertex_value[vtx] = index;
+                flattened_vertices.push_back(vtx[0]);
+                flattened_vertices.push_back(vtx[1]);
+                flattened_vertices.push_back(vtx[2]);
+                ++index;
+            } else {
+                vertex_indices.push_back(elem->second);
+            }
+        }
+        FBX::Node::WritePropertyNode(
+            "Vertices", flattened_vertices, outstream
+        );
+
+        // output polygon data as a flattened array of vertex indices.
+        // the last vertex index of each polygon is negated and - 1
+        std::vector<int32_t> polygon_data;
+        for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
+            const aiFace &f = m->mFaces[fi];
+            for (size_t pvi = 0; pvi < f.mNumIndices - 1; ++pvi) {
+                polygon_data.push_back(vertex_indices[f.mIndices[pvi]]);
+            }
+            polygon_data.push_back(
+                -1 - vertex_indices[f.mIndices[f.mNumIndices-1]]
+            );
+        }
+        FBX::Node::WritePropertyNode(
+            "PolygonVertexIndex", polygon_data, outstream
+        );
+
+        // here could be edges but they're insane.
+        // it's optional anyway, so let's ignore it.
+
+        FBX::Node::WritePropertyNode(
+            "GeometryVersion", int32_t(124), outstream
+        );
+
+        // normals, if any
+        if (m->HasNormals()) {
+            FBX::Node normals("LayerElementNormal", Property(int32_t(0)));
+            normals.Begin(outstream);
+            normals.DumpProperties(outstream);
+            normals.EndProperties(outstream);
+            FBX::Node::WritePropertyNode("Version", int32_t(101), outstream);
+            FBX::Node::WritePropertyNode("Name", "", outstream);
+            FBX::Node::WritePropertyNode(
+                "MappingInformationType", "ByPolygonVertex", outstream
+            );
+            // TODO: vertex-normals or indexed normals when appropriate
+            FBX::Node::WritePropertyNode(
+                "ReferenceInformationType", "Direct", outstream
+            );
+            std::vector<double> normal_data;
+            normal_data.reserve(3 * polygon_data.size());
+            for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
+                const aiFace &f = m->mFaces[fi];
+                for (size_t pvi = 0; pvi < f.mNumIndices; ++pvi) {
+                    const aiVector3D &n = m->mNormals[f.mIndices[pvi]];
+                    normal_data.push_back(n.x);
+                    normal_data.push_back(n.y);
+                    normal_data.push_back(n.z);
+                }
+            }
+            FBX::Node::WritePropertyNode("Normals", normal_data, outstream);
+            // note: version 102 has a NormalsW also... not sure what it is,
+            // so we can stick with version 101 for now.
+            normals.End(outstream, true);
+        }
+
+        // uvs, if any
+        for (size_t uvi = 0; uvi < m->GetNumUVChannels(); ++uvi) {
+            if (m->mNumUVComponents[uvi] > 2) {
+                // FBX only supports 2-channel UV maps...
+                // or at least i'm not sure how to indicate a different number
+                std::stringstream err;
+                err << "Only 2-channel UV maps supported by FBX,";
+                err << " but mesh " << mi;
+                if (m->mName.length) {
+                    err << " (" << m->mName.C_Str() << ")";
+                }
+                err << " UV map " << uvi;
+                err << " has " << m->mNumUVComponents[uvi];
+                err << " components! Data will be preserved,";
+                err << " but may be incorrectly interpreted on load.";
+                DefaultLogger::get()->warn(err.str());
+            }
+            FBX::Node uv("LayerElementUV", Property(int32_t(uvi)));
+            uv.Begin(outstream);
+            uv.DumpProperties(outstream);
+            uv.EndProperties(outstream);
+            FBX::Node::WritePropertyNode("Version", int32_t(101), outstream);
+            // it doesn't seem like assimp keeps the uv map name,
+            // so just leave it blank.
+            FBX::Node::WritePropertyNode("Name", "", outstream);
+            FBX::Node::WritePropertyNode(
+                "MappingInformationType", "ByPolygonVertex", outstream
+            );
+            FBX::Node::WritePropertyNode(
+                "ReferenceInformationType", "IndexToDirect", outstream
+            );
+
+            std::vector<double> uv_data;
+            std::vector<int32_t> uv_indices;
+            std::map<aiVector3D,int32_t> index_by_uv;
+            size_t index = 0;
+            for (size_t fi = 0; fi < m->mNumFaces; ++fi) {
+                const aiFace &f = m->mFaces[fi];
+                for (size_t pvi = 0; pvi < f.mNumIndices; ++pvi) {
+                    const aiVector3D &uv =
+                        m->mTextureCoords[uvi][f.mIndices[pvi]];
+                    auto elem = index_by_uv.find(uv);
+                    if (elem == index_by_uv.end()) {
+                        index_by_uv[uv] = index;
+                        uv_indices.push_back(index);
+                        for (size_t x = 0; x < m->mNumUVComponents[uvi]; ++x) {
+                            uv_data.push_back(uv[x]);
+                        }
+                        ++index;
+                    } else {
+                        uv_indices.push_back(elem->second);
+                    }
+                }
+            }
+            FBX::Node::WritePropertyNode("UV", uv_data, outstream);
+            FBX::Node::WritePropertyNode("UVIndex", uv_indices, outstream);
+            uv.End(outstream, true);
+        }
+
+        // i'm not really sure why this material section exists,
+        // as the material is linked via "Connections".
+        // it seems to always have the same "0" value.
+        FBX::Node mat("LayerElementMaterial", Property(int32_t(0)));
+        mat.AddChild("Version", int32_t(101));
+        mat.AddChild("Name", "");
+        mat.AddChild("MappingInformationType", "AllSame");
+        mat.AddChild("ReferenceInformationType", "IndexToDirect");
+        std::vector<int32_t> mat_indices = {0};
+        mat.AddChild("Materials", mat_indices);
+        mat.Dump(outstream);
+
+        // finally we have the layer specifications,
+        // which select the normals / UV set / etc to use.
+        // TODO: handle multiple uv sets correctly?
+        FBX::Node layer("Layer", Property(int32_t(0)));
+        layer.AddChild("Version", int32_t(100));
+        FBX::Node le("LayerElement");
+        le.AddChild("Type", "LayerElementNormal");
+        le.AddChild("TypedIndex", int32_t(0));
+        layer.AddChild(le);
+        le = FBX::Node("LayerElement");
+        le.AddChild("Type", "LayerElementMaterial");
+        le.AddChild("TypedIndex", int32_t(0));
+        layer.AddChild(le);
+        le = FBX::Node("LayerElement");
+        le.AddChild("Type", "LayerElementUV");
+        le.AddChild("TypedIndex", int32_t(0));
+        layer.AddChild(le);
+        layer.Dump(outstream);
+
+        // finish the node record
+        n.End(outstream, true);
+    }
+
+    // aiMaterial
+    material_uids.clear();
+    for (size_t i = 0; i < mScene->mNumMaterials; ++i) {
+        // it's all about this material
+        aiMaterial* m = mScene->mMaterials[i];
+
+        // these are used to recieve material data
+        float f; aiColor3D c;
+
+        // start the node record
+        FBX::Node n("Material");
+
+        int64_t uid = generate_uid();
+        material_uids.push_back(uid);
+        n.AddProperty(uid);
+
+        aiString name;
+        m->Get(AI_MATKEY_NAME, name);
+        n.AddProperty(name.C_Str() + FBX::SEPARATOR + "Material");
+
+        n.AddProperty("");
+
+        n.AddChild("Version", int32_t(102));
+        f = 0;
+        m->Get(AI_MATKEY_SHININESS, f);
+        bool phong = (f > 0);
+        if (phong) {
+            n.AddChild("ShadingModel", "phong");
+        } else {
+            n.AddChild("ShadingModel", "lambert");
+        }
+        n.AddChild("MultiLayer", int32_t(0));
+
+        FBX::Node p("Properties70");
+
+        // materials exported using the FBX SDK have two sets of fields.
+        // there are the properties specified in the PropertyTemplate,
+        // which are those supported by the modernFBX SDK,
+        // and an extra set of properties with simpler names.
+        // The extra properties are a legacy material system from pre-2009.
+        //
+        // In the modern system, each property has "color" and "factor".
+        // Generally the interpretation of these seems to be
+        // that the colour is multiplied by the factor before use,
+        // but this is not always clear-cut.
+        //
+        // Usually assimp only stores the colour,
+        // so we can just leave the factors at the default "1.0".
+
+        // first we can export the "standard" properties
+        if (m->Get(AI_MATKEY_COLOR_AMBIENT, c) == aiReturn_SUCCESS) {
+            p.AddP70colorA("AmbientColor", c.r, c.g, c.b);
+            //p.AddP70numberA("AmbientFactor", 1.0);
+        }
+        if (m->Get(AI_MATKEY_COLOR_DIFFUSE, c) == aiReturn_SUCCESS) {
+            p.AddP70colorA("DiffuseColor", c.r, c.g, c.b);
+            //p.AddP70numberA("DiffuseFactor", 1.0);
+        }
+        if (m->Get(AI_MATKEY_COLOR_TRANSPARENT, c) == aiReturn_SUCCESS) {
+            // "TransparentColor" / "TransparencyFactor"...
+            // thanks FBX, for your insightful interpretation of consistency
+            p.AddP70colorA("TransparentColor", c.r, c.g, c.b);
+            // TransparencyFactor defaults to 0.0, so set it to 1.0.
+            // note: Maya always sets this to 1.0,
+            // so we can't use it sensibly as "Opacity".
+            // In stead we rely on the legacy "Opacity" value, below.
+            // Blender also relies on "Opacity" not "TransparencyFactor",
+            // probably for a similar reason.
+            p.AddP70numberA("TransparencyFactor", 1.0);
+        }
+        if (m->Get(AI_MATKEY_COLOR_REFLECTIVE, c) == aiReturn_SUCCESS) {
+            p.AddP70colorA("ReflectionColor", c.r, c.g, c.b);
+        }
+        if (m->Get(AI_MATKEY_REFLECTIVITY, f) == aiReturn_SUCCESS) {
+            p.AddP70numberA("ReflectionFactor", f);
+        }
+        if (phong) {
+            if (m->Get(AI_MATKEY_COLOR_SPECULAR, c) == aiReturn_SUCCESS) {
+                p.AddP70colorA("SpecularColor", c.r, c.g, c.b);
+            }
+            if (m->Get(AI_MATKEY_SHININESS_STRENGTH, f) == aiReturn_SUCCESS) {
+                p.AddP70numberA("ShininessFactor", f);
+            }
+            if (m->Get(AI_MATKEY_SHININESS, f) == aiReturn_SUCCESS) {
+                p.AddP70numberA("ShininessExponent", f);
+            }
+            if (m->Get(AI_MATKEY_REFLECTIVITY, f) == aiReturn_SUCCESS) {
+                p.AddP70numberA("ReflectionFactor", f);
+            }
+        }
+
+        // Now the legacy system.
+        // For safety let's include it.
+        // thrse values don't exist in the property template,
+        // and usualy are completely ignored when loading.
+        // One notable exception is the "Opacity" property,
+        // which Blender uses as (1.0 - alpha).
+        c.r = 0; c.g = 0; c.b = 0;
+        m->Get(AI_MATKEY_COLOR_EMISSIVE, c);
+        p.AddP70vector("Emissive", c.r, c.g, c.b);
+        c.r = 0.2; c.g = 0.2; c.b = 0.2;
+        m->Get(AI_MATKEY_COLOR_AMBIENT, c);
+        p.AddP70vector("Ambient", c.r, c.g, c.b);
+        c.r = 0.8; c.g = 0.8; c.b = 0.8;
+        m->Get(AI_MATKEY_COLOR_DIFFUSE, c);
+        p.AddP70vector("Diffuse", c.r, c.g, c.b);
+        // The FBX SDK determines "Opacity" from transparency colour (RGB)
+        // and factor (F) as: O = (1.0 - F * ((R + G + B) / 3)).
+        // However we actually have an opacity value,
+        // so we should take it from AI_MATKEY_OPACITY if possible.
+        // It might make more sense to use TransparencyFactor,
+        // but Blender actually loads "Opacity" correctly, so let's use it.
+        f = 1.0;
+        if (m->Get(AI_MATKEY_COLOR_TRANSPARENT, c) == aiReturn_SUCCESS) {
+            f = 1.0 - ((c.r + c.g + c.b) / 3);
+        }
+        m->Get(AI_MATKEY_OPACITY, f);
+        p.AddP70double("Opacity", f);
+        if (phong) {
+            // specular color is multiplied by shininess_strength
+            c.r = 0.2; c.g = 0.2; c.b = 0.2;
+            m->Get(AI_MATKEY_COLOR_SPECULAR, c);
+            f = 1.0;
+            m->Get(AI_MATKEY_SHININESS_STRENGTH, f);
+            p.AddP70vector("Specular", f*c.r, f*c.g, f*c.b);
+            f = 20.0;
+            m->Get(AI_MATKEY_SHININESS, f);
+            p.AddP70double("Shininess", f);
+            // Legacy "Reflectivity" is F*F*((R+G+B)/3),
+            // where F is the proportion of light reflected (AKA reflectivity),
+            // and RGB is the reflective colour of the material.
+            // No idea why, but we might as well set it the same way.
+            f = 0.0;
+            m->Get(AI_MATKEY_REFLECTIVITY, f);
+            c.r = 1.0, c.g = 1.0, c.b = 1.0;
+            m->Get(AI_MATKEY_COLOR_REFLECTIVE, c);
+            p.AddP70double("Reflectivity", f*f*((c.r+c.g+c.b)/3.0));
+        }
+
+        n.AddChild(p);
+
+        n.Dump(outstream);
+    }
+
+    // we need to look up all the images we're using,
+    // so we can generate uids, and eliminate duplicates.
+    std::map<std::string, int64_t> uid_by_image;
+    for (size_t i = 0; i < mScene->mNumMaterials; ++i) {
+        aiString texpath;
+        aiMaterial* mat = mScene->mMaterials[i];
+        for (
+            size_t tt = aiTextureType_DIFFUSE;
+            tt < aiTextureType_UNKNOWN;
+            ++tt
+        ){
+            const aiTextureType textype = static_cast<aiTextureType>(tt);
+            const size_t texcount = mat->GetTextureCount(textype);
+            for (size_t j = 0; j < texcount; ++j) {
+                mat->GetTexture(textype, j, &texpath);
+                const std::string texstring = texpath.C_Str();
+                auto elem = uid_by_image.find(texstring);
+                if (elem == uid_by_image.end()) {
+                    uid_by_image[texstring] = generate_uid();
+                }
+            }
+        }
+    }
+
+    // FbxVideo - stores images used by textures.
+    for (const auto &it : uid_by_image) {
+        if (it.first.compare(0, 1, "*") == 0) {
+            // TODO: embedded textures
+            continue;
+        }
+        FBX::Node n("Video");
+        const int64_t& uid = it.second;
+        const std::string name = ""; // TODO: ... name???
+        n.AddProperties(uid, name + FBX::SEPARATOR + "Video", "Clip");
+        n.AddChild("Type", "Clip");
+        FBX::Node p("Properties70");
+        // TODO: get full path... relative path... etc... ugh...
+        // for now just use the same path for everything,
+        // and hopefully one of them will work out.
+        const std::string& path = it.first;
+        p.AddP70("Path", "KString", "XRefUrl", "", path);
+        n.AddChild(p);
+        n.AddChild("UseMipMap", int32_t(0));
+        n.AddChild("Filename", path);
+        n.AddChild("RelativeFilename", path);
+        n.Dump(outstream);
+    }
+
+    // Textures
+    // referenced by material_index/texture_type pairs.
+    std::map<std::pair<size_t,size_t>,int64_t> texture_uids;
+    const std::map<aiTextureType,std::string> prop_name_by_tt = {
+        {aiTextureType_DIFFUSE, "DiffuseColor"},
+        {aiTextureType_SPECULAR, "SpecularColor"},
+        {aiTextureType_AMBIENT, "AmbientColor"},
+        {aiTextureType_EMISSIVE, "EmissiveColor"},
+        {aiTextureType_HEIGHT, "Bump"},
+        {aiTextureType_NORMALS, "NormalMap"},
+        {aiTextureType_SHININESS, "ShininessExponent"},
+        {aiTextureType_OPACITY, "TransparentColor"},
+        {aiTextureType_DISPLACEMENT, "DisplacementColor"},
+        //{aiTextureType_LIGHTMAP, "???"},
+        {aiTextureType_REFLECTION, "ReflectionColor"}
+        //{aiTextureType_UNKNOWN, ""}
+    };
+    for (size_t i = 0; i < mScene->mNumMaterials; ++i) {
+        // textures are attached to materials
+        aiMaterial* mat = mScene->mMaterials[i];
+        int64_t material_uid = material_uids[i];
+
+        for (
+            size_t j = aiTextureType_DIFFUSE;
+            j < aiTextureType_UNKNOWN;
+            ++j
+        ) {
+            const aiTextureType tt = static_cast<aiTextureType>(j);
+            size_t n = mat->GetTextureCount(tt);
+
+            if (n < 1) { // no texture of this type
+                continue;
+            }
+
+            if (n > 1) {
+                // TODO: multilayer textures
+                std::stringstream err;
+                err << "Multilayer textures not supported (for now),";
+                err << " skipping texture type " << j;
+                err << " of material " << i;
+                DefaultLogger::get()->warn(err.str());
+            }
+
+            // get image path for this (single-image) texture
+            aiString tpath;
+            if (mat->GetTexture(tt, 0, &tpath) != aiReturn_SUCCESS) {
+                std::stringstream err;
+                err << "Failed to get texture 0 for texture of type " << tt;
+                err << " on material " << i;
+                err << ", however GetTextureCount returned 1.";
+                throw DeadlyExportError(err.str());
+            }
+            const std::string texture_path(tpath.C_Str());
+
+            // get connected image uid
+            auto elem = uid_by_image.find(texture_path);
+            if (elem == uid_by_image.end()) {
+                // this should never happen
+                std::stringstream err;
+                err << "Failed to find video element for texture with path";
+                err << " \"" << texture_path << "\"";
+                err << ", type " << j << ", material " << i;
+                throw DeadlyExportError(err.str());
+            }
+            const int64_t image_uid = elem->second;
+
+            // get the name of the material property to connect to
+            auto elem2 = prop_name_by_tt.find(tt);
+            if (elem2 == prop_name_by_tt.end()) {
+                // don't know how to handle this type of texture,
+                // so skip it.
+                std::stringstream err;
+                err << "Not sure how to handle texture of type " << j;
+                err << " on material " << i;
+                err << ", skipping...";
+                DefaultLogger::get()->warn(err.str());
+                continue;
+            }
+            const std::string& prop_name = elem2->second;
+
+            // generate a uid for this texture
+            const int64_t texture_uid = generate_uid();
+
+            // link the texture to the material
+            FBX::Node c("C");
+            c.AddProperties("OP", texture_uid, material_uid, prop_name);
+            connections.push_back(c);
+
+            // link the image data to the texture
+            c = FBX::Node("C");
+            c.AddProperties("OO", image_uid, texture_uid);
+            connections.push_back(c);
+
+            // now write the actual texture node
+            FBX::Node tnode("Texture");
+            // TODO: some way to determine texture name?
+            const std::string texture_name = "" + FBX::SEPARATOR + "Texture";
+            tnode.AddProperties(texture_uid, texture_name, "");
+            // there really doesn't seem to be a better type than this:
+            tnode.AddChild("Type", "TextureVideoClip");
+            tnode.AddChild("Version", int32_t(202));
+            tnode.AddChild("TextureName", texture_name);
+            FBX::Node p("Properties70");
+            p.AddP70enum("CurrentTextureBlendMode", 0); // TODO: verify
+            //p.AddP70string("UVSet", ""); // TODO: how should this work?
+            p.AddP70bool("UseMaterial", 1);
+            tnode.AddChild(p);
+            // can't easily detrmine which texture path will be correct,
+            // so just store what we have in every field.
+            // these being incorrect is a common problem with FBX anyway.
+            tnode.AddChild("FileName", texture_path);
+            tnode.AddChild("RelativeFilename", texture_path);
+            tnode.AddChild("ModelUVTranslation", double(0.0), double(0.0));
+            tnode.AddChild("ModelUVScaling", double(1.0), double(1.0));
+            tnode.AddChild("Texture_Alpha_Soutce", "None");
+            tnode.AddChild(
+                "Cropping", int32_t(0), int32_t(0), int32_t(0), int32_t(0)
+            );
+            tnode.Dump(outstream);
+        }
+    }
+
+    // bones.
+    //
+    // output structure:
+    // subset of node heirarchy that are "skeleton",
+    // i.e. do not have meshes but only bones.
+    // but.. i'm not sure how anyone could guarantee that...
+    //
+    // input...
+    // well, for each mesh it has "bones",
+    // and the bone names correspond to nodes.
+    // of course we also need the parent nodes,
+    // as they give some of the transform........
+    //
+    // well. we can assume a sane input, i suppose.
+    //
+    // so input is the bone node heirarchy,
+    // with an extra thing for the transformation of the MESH in BONE space.
+    //
+    // output is a set of bone nodes,
+    // a "bindpose" which indicates the default local transform of all bones,
+    // and a set of "deformers".
+    // each deformer is parented to a mesh geometry,
+    // and has one or more "subdeformer"s as children.
+    // each subdeformer has one bone node as a child,
+    // and represents the influence of that bone on the grandparent mesh.
+    // the subdeformer has a list of indices, and weights,
+    // with indices specifying vertex indices,
+    // and weights specifying the correspoding influence of this bone.
+    // it also has Transform and TransformLink elements,
+    // specifying the transform of the MESH in BONE space,
+    // and the transformation of the BONE in WORLD space,
+    // likely in the bindpose.
+    //
+    // the input bone structure is different but similar,
+    // storing the number of weights for this bone,
+    // and an array of (vertex index, weight) pairs.
+    //
+    // one sticky point is that the number of vertices may not match,
+    // because assimp splits vertices by normal, uv, etc.
+
+    // first we should mark all the skeleton nodes,
+    // so that they can be treated as LimbNode in stead of Mesh or Null.
+    // at the same time we can build up a map of bone nodes.
+    std::unordered_set<const aiNode*> limbnodes;
+    std::map<std::string,aiNode*> node_by_bone;
+    for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
+        const aiMesh* m = mScene->mMeshes[mi];
+        for (size_t bi =0; bi < m->mNumBones; ++bi) {
+            const aiBone* b = m->mBones[bi];
+            const std::string name(b->mName.C_Str());
+            if (node_by_bone.count(name) > 0) {
+                // already processed, skip
+                continue;
+            }
+            aiNode* n = mScene->mRootNode->FindNode(b->mName);
+            if (!n) {
+                // this should never happen
+                std::stringstream err;
+                err << "Failed to find node for bone: \"" << name << "\"";
+                throw DeadlyExportError(err.str());
+            }
+            node_by_bone[name] = n;
+            limbnodes.insert(n);
+            if (n == mScene->mRootNode) { continue; }
+            // mark all parent nodes as skeleton as well,
+            // up until we find the root node,
+            // or else the node containing the mesh,
+            // or else the parent of a node containig the mesh.
+            for (
+                const aiNode* parent = n->mParent;
+                parent != mScene->mRootNode;
+                parent = parent->mParent
+            ) {
+                bool end = false;
+                for (size_t i = 0; i < parent->mNumMeshes; ++i) {
+                    if (parent->mMeshes[i] == mi) {
+                        end = true;
+                        break;
+                    }
+                }
+                for (size_t j = 0; j < parent->mNumChildren; ++j) {
+                    aiNode* child = parent->mChildren[j];
+                    for (size_t i = 0; i < child->mNumMeshes; ++i) {
+                        if (child->mMeshes[i] == mi) {
+                            end = true;
+                            break;
+                        }
+                    }
+                    if (end) { break; }
+                }
+                if (end) { break; }
+                limbnodes.insert(parent);
+            }
+        }
+    }
+
+    // we'll need the uids for the bone nodes, so generate them now
+    std::map<std::string,int64_t> bone_uids;
+    for (auto &bone : limbnodes) {
+        std::string bone_name(bone->mName.C_Str());
+        aiNode* bone_node = mScene->mRootNode->FindNode(bone->mName);
+        if (!bone_node) {
+            throw DeadlyExportError("Couldn't find node for bone" + bone_name);
+        }
+        auto elem = node_uids.find(bone_node);
+        if (elem == node_uids.end()) {
+            int64_t uid = generate_uid();
+            node_uids[bone_node] = uid;
+            bone_uids[bone_name] = uid;
+        } else {
+            bone_uids[bone_name] = elem->second;
+        }
+    }
+
+    // now, for each aiMesh, we need to export a deformer,
+    // and for each aiBone a subdeformer,
+    // which should have all the skinning info.
+    // these will need to be connected properly to the mesh,
+    // and we can do that all now.
+    for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
+        const aiMesh* m = mScene->mMeshes[mi];
+        if (!m->HasBones()) {
+            continue;
+        }
+        // make a deformer for this mesh
+        int64_t deformer_uid = generate_uid();
+        FBX::Node dnode("Deformer");
+        dnode.AddProperties(deformer_uid, FBX::SEPARATOR + "Deformer", "Skin");
+        dnode.AddChild("Version", int32_t(101));
+        // "acuracy"... this is not a typo....
+        dnode.AddChild("Link_DeformAcuracy", double(50));
+        dnode.AddChild("SkinningType", "Linear"); // TODO: other modes?
+        dnode.Dump(outstream);
+
+        // connect it
+        FBX::Node c("C");
+        c.AddProperties("OO", deformer_uid, mesh_uids[mi]);
+        connections.push_back(c); // TODO: emplace_back
+
+        // we will be indexing by vertex...
+        // but there might be a different number of "vertices"
+        // between assimp and our output FBX.
+        // this code is cut-and-pasted from the geometry section above...
+        // ideally this should not be so.
+        // ---
+        // index of original vertex in vertex data vector
+        std::vector<int32_t> vertex_indices;
+        // map of vertex value to its index in the data vector
+        std::map<aiVector3D,size_t> index_by_vertex_value;
+        size_t index = 0;
+        for (size_t vi = 0; vi < m->mNumVertices; ++vi) {
+            aiVector3D vtx = m->mVertices[vi];
+            auto elem = index_by_vertex_value.find(vtx);
+            if (elem == index_by_vertex_value.end()) {
+                vertex_indices.push_back(index);
+                index_by_vertex_value[vtx] = index;
+                ++index;
+            } else {
+                vertex_indices.push_back(elem->second);
+            }
+        }
+
+        // first get this mesh's position in world space,
+        // as we'll need it for each subdeformer.
+        //
+        // ...of course taking the position of the MESH doesn't make sense,
+        // as it can be instanced to many nodes.
+        // All we can do is assume no instancing,
+        // and take the first node we find that contains the mesh.
+        //
+        // We could in stead take the transform from the bone's node,
+        // but there's no guarantee that the bone is in the bindpose,
+        // so this would be even less reliable.
+        aiNode* mesh_node = get_node_for_mesh(mi, mScene->mRootNode);
+        aiMatrix4x4 mesh_node_xform = get_world_transform(mesh_node, mScene);
+
+        // now make a subdeformer for each bone
+        for (size_t bi =0; bi < m->mNumBones; ++bi) {
+            const aiBone* b = m->mBones[bi];
+            const std::string name(b->mName.C_Str());
+            const int64_t subdeformer_uid = generate_uid();
+            FBX::Node sdnode("Deformer");
+            sdnode.AddProperties(
+                subdeformer_uid, FBX::SEPARATOR + "SubDeformer", "Cluster"
+            );
+            sdnode.AddChild("Version", int32_t(100));
+            sdnode.AddChild("UserData", "", "");
+
+            // get indices and weights
+            std::vector<int32_t> subdef_indices;
+            std::vector<double> subdef_weights;
+            int32_t last_index = -1;
+            for (size_t wi = 0; wi < b->mNumWeights; ++wi) {
+                int32_t vi = vertex_indices[b->mWeights[wi].mVertexId];
+                if (vi == last_index) {
+                    // only for vertices we exported to fbx
+                    // TODO, FIXME: this assumes identically-located vertices
+                    // will always deform in the same way.
+                    // as assimp doesn't store a separate list of "positions",
+                    // there's not much that can be done about this
+                    // other than assuming that identical position means
+                    // identical vertex.
+                    continue;
+                }
+                subdef_indices.push_back(vi);
+                subdef_weights.push_back(b->mWeights[wi].mWeight);
+                last_index = vi;
+            }
+            // yes, "indexes"
+            sdnode.AddChild("Indexes", subdef_indices);
+            sdnode.AddChild("Weights", subdef_weights);
+            // transform is the transform of the mesh, but in bone space...
+            // which is exactly what assimp's mOffsetMatrix is,
+            // no matter what the assimp docs may say.
+            aiMatrix4x4 tr = b->mOffsetMatrix;
+            sdnode.AddChild("Transform", tr);
+            // transformlink should be the position of the bone in world space,
+            // in the bind pose.
+            // For now let's use the inverse of mOffsetMatrix,
+            // and the (assumedly static) mesh position in world space.
+            // TODO: find a better way of doing this? there aren't many options
+            tr = b->mOffsetMatrix;
+            tr.Inverse();
+            tr *= mesh_node_xform;
+            sdnode.AddChild("TransformLink", tr);
+
+            // done
+            sdnode.Dump(outstream);
+
+            // lastly, connect to the parent deformer
+            c = FBX::Node("C");
+            c.AddProperties("OO", subdeformer_uid, deformer_uid);
+            connections.push_back(c); // TODO: emplace_back
+
+            // we also need to connect the limb node to the subdeformer.
+            c = FBX::Node("C");
+            c.AddProperties("OO", bone_uids[name], subdeformer_uid);
+            connections.push_back(c); // TODO: emplace_back
+        }
+
+
+    }
+
+    // BindPose
+    //
+    // This is a legacy system, which should be unnecessary.
+    //
+    // Somehow including it slows file loading by the official FBX SDK,
+    // and as it can reconstruct it from the deformers anyway,
+    // this is not currently included.
+    //
+    // The code is kept here in case it's useful in the future,
+    // but it's pretty much a hack anyway,
+    // as assimp doesn't store bindpose information for full skeletons.
+    //
+    /*for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
+        aiMesh* mesh = mScene->mMeshes[mi];
+        if (! mesh->HasBones()) { continue; }
+        int64_t bindpose_uid = generate_uid();
+        FBX::Node bpnode("Pose");
+        bpnode.AddProperty(bindpose_uid);
+        // note: this uid is never linked or connected to anything.
+        bpnode.AddProperty(FBX::SEPARATOR + "Pose"); // blank name
+        bpnode.AddProperty("BindPose");
+
+        bpnode.AddChild("Type", "BindPose");
+        bpnode.AddChild("Version", int32_t(100));
+
+        aiNode* mesh_node = get_node_for_mesh(mi, mScene->mRootNode);
+
+        // next get the whole skeleton for this mesh.
+        // we need it all to define the bindpose section.
+        // the FBX SDK will complain if it's missing,
+        // and also if parents of used bones don't have a subdeformer.
+        // order shouldn't matter.
+        std::set<aiNode*> skeleton;
+        for (size_t bi = 0; bi < mesh->mNumBones; ++bi) {
+            // bone node should have already been indexed
+            const aiBone* b = mesh->mBones[bi];
+            const std::string bone_name(b->mName.C_Str());
+            aiNode* parent = node_by_bone[bone_name];
+            // insert all nodes down to the root or mesh node
+            while (
+                parent
+                && parent != mScene->mRootNode
+                && parent != mesh_node
+            ) {
+                skeleton.insert(parent);
+                parent = parent->mParent;
+            }
+        }
+
+        // number of pose nodes. includes one for the mesh itself.
+        bpnode.AddChild("NbPoseNodes", int32_t(1 + skeleton.size()));
+
+        // the first pose node is always the mesh itself
+        FBX::Node pose("PoseNode");
+        pose.AddChild("Node", mesh_uids[mi]);
+        aiMatrix4x4 mesh_node_xform = get_world_transform(mesh_node, mScene);
+        pose.AddChild("Matrix", mesh_node_xform);
+        bpnode.AddChild(pose);
+
+        for (aiNode* bonenode : skeleton) {
+            // does this node have a uid yet?
+            int64_t node_uid;
+            auto node_uid_iter = node_uids.find(bonenode);
+            if (node_uid_iter != node_uids.end()) {
+                node_uid = node_uid_iter->second;
+            } else {
+                node_uid = generate_uid();
+                node_uids[bonenode] = node_uid;
+            }
+
+            // make a pose thingy
+            pose = FBX::Node("PoseNode");
+            pose.AddChild("Node", node_uid);
+            aiMatrix4x4 node_xform = get_world_transform(bonenode, mScene);
+            pose.AddChild("Matrix", node_xform);
+            bpnode.AddChild(pose);
+        }
+
+        // now write it
+        bpnode.Dump(outstream);
+    }*/
+
+    // TODO: cameras, lights
+
+    // write nodes (i.e. model heirarchy)
+    // start at root node
+    WriteModelNodes(
+        outstream, mScene->mRootNode, 0, bone_uids
+    );
+
+    object_node.End(outstream, true);
+}
+
+// convenience map of magic node name strings to FBX properties,
+// including the expected type of transform.
+const std::map<std::string,std::pair<std::string,char>> transform_types = {
+    {"Translation", {"Lcl Translation", 't'}},
+    {"RotationOffset", {"RotationOffset", 't'}},
+    {"RotationPivot", {"RotationPivot", 't'}},
+    {"PreRotation", {"PreRotation", 'r'}},
+    {"Rotation", {"Lcl Rotation", 'r'}},
+    {"PostRotation", {"PostRotation", 'r'}},
+    {"RotationPivotInverse", {"RotationPivotInverse", 'i'}},
+    {"ScalingOffset", {"ScalingOffset", 't'}},
+    {"ScalingPivot", {"ScalingPivot", 't'}},
+    {"Scaling", {"Lcl Scaling", 's'}},
+    {"ScalingPivotInverse", {"ScalingPivotInverse", 'i'}},
+    {"GeometricScaling", {"GeometricScaling", 's'}},
+    {"GeometricRotation", {"GeometricRotation", 'r'}},
+    {"GeometricTranslation", {"GeometricTranslation", 't'}},
+    {"GeometricTranslationInverse", {"GeometricTranslationInverse", 'i'}},
+    {"GeometricRotationInverse", {"GeometricRotationInverse", 'i'}},
+    {"GeometricScalingInverse", {"GeometricScalingInverse", 'i'}}
+};
+
+// write a single model node to the stream
+void WriteModelNode(
+    StreamWriterLE& outstream,
+    const aiNode* node,
+    int64_t node_uid,
+    const std::string& type,
+    const std::vector<std::pair<std::string,aiVector3D>>& transform_chain,
+    TransformInheritance inherit_type=TransformInheritance_RSrs
+){
+    const aiVector3D zero = {0, 0, 0};
+    const aiVector3D one = {1, 1, 1};
+    FBX::Node m("Model");
+    std::string name = node->mName.C_Str() + FBX::SEPARATOR + "Model";
+    m.AddProperties(node_uid, name, type);
+    m.AddChild("Version", int32_t(232));
+    FBX::Node p("Properties70");
+    p.AddP70bool("RotationActive", 1);
+    p.AddP70int("DefaultAttributeIndex", 0);
+    p.AddP70enum("InheritType", inherit_type);
+    if (transform_chain.empty()) {
+        // decompose 4x4 transform matrix into TRS
+        aiVector3D t, r, s;
+        node->mTransformation.Decompose(s, r, t);
+        if (t != zero) {
+            p.AddP70(
+                "Lcl Translation", "Lcl Translation", "", "A",
+                double(t.x), double(t.y), double(t.z)
+            );
+        }
+        if (r != zero) {
+            p.AddP70(
+                "Lcl Rotation", "Lcl Rotation", "", "A",
+                double(DEG*r.x), double(DEG*r.y), double(DEG*r.z)
+            );
+        }
+        if (s != one) {
+            p.AddP70(
+                "Lcl Scaling", "Lcl Scaling", "", "A",
+                double(s.x), double(s.y), double(s.z)
+            );
+        }
+    } else {
+        // apply the transformation chain.
+        // these transformation elements are created when importing FBX,
+        // which has a complex transformation heirarchy for each node.
+        // as such we can bake the heirarchy back into the node on export.
+        for (auto &item : transform_chain) {
+            auto elem = transform_types.find(item.first);
+            if (elem == transform_types.end()) {
+                // then this is a bug
+                std::stringstream err;
+                err << "unrecognized FBX transformation type: ";
+                err << item.first;
+                throw DeadlyExportError(err.str());
+            }
+            const std::string &name = elem->second.first;
+            const aiVector3D &v = item.second;
+            if (name.compare(0, 4, "Lcl ") == 0) {
+                // special handling for animatable properties
+                p.AddP70(
+                    name, name, "", "A",
+                    double(v.x), double(v.y), double(v.z)
+                );
+            } else {
+                p.AddP70vector(name, v.x, v.y, v.z);
+            }
+        }
+    }
+    m.AddChild(p);
+
+    // not sure what these are for,
+    // but they seem to be omnipresent
+    m.AddChild("Shading", Property(true));
+    m.AddChild("Culling", Property("CullingOff"));
+
+    m.Dump(outstream);
+}
+
+// wrapper for WriteModelNodes to create and pass a blank transform chain
+void FBXExporter::WriteModelNodes(
+    StreamWriterLE& s,
+    const aiNode* node,
+    int64_t parent_uid,
+    const std::map<std::string,int64_t>& bone_uids
+) {
+    std::vector<std::pair<std::string,aiVector3D>> chain;
+    WriteModelNodes(s, node, parent_uid, bone_uids, chain);
+}
+
+void FBXExporter::WriteModelNodes(
+    StreamWriterLE& outstream,
+    const aiNode* node,
+    int64_t parent_uid,
+    const std::map<std::string,int64_t>& bone_uids,
+    std::vector<std::pair<std::string,aiVector3D>>& transform_chain
+) {
+    // first collapse any expanded transformation chains created by FBX import.
+    std::string node_name(node->mName.C_Str());
+    if (node_name.find(MAGIC_NODE_TAG) != std::string::npos) {
+        if (node->mNumChildren != 1) {
+            // this should never happen
+            std::stringstream err;
+            err << "FBX transformation node should have exactly 1 child,";
+            err << " but " << node->mNumChildren << " found";
+            err << " on node \"" << node_name << "\"!";
+            throw DeadlyExportError(err.str());
+        }
+        aiNode* next_node = node->mChildren[0];
+        auto pos = node_name.find(MAGIC_NODE_TAG) + MAGIC_NODE_TAG.size() + 1;
+        std::string type_name = node_name.substr(pos);
+        auto elem = transform_types.find(type_name);
+        if (elem == transform_types.end()) {
+            // then this is a bug and should be fixed
+            std::stringstream err;
+            err << "unrecognized FBX transformation node";
+            err << " of type " << type_name << " in node " << node_name;
+            throw DeadlyExportError(err.str());
+        }
+        aiVector3D t, r, s;
+        node->mTransformation.Decompose(s, r, t);
+        switch (elem->second.second) {
+        case 'i': // inverse
+            // we don't need to worry about the inverse matrices
+            break;
+        case 't': // translation
+            transform_chain.emplace_back(elem->first, t);
+            break;
+        case 'r': // rotation
+            r *= DEG;
+            transform_chain.emplace_back(elem->first, r);
+            break;
+        case 's': // scale
+            transform_chain.emplace_back(elem->first, s);
+            break;
+        default:
+            // this should never happen
+            std::stringstream err;
+            err << "unrecognized FBX transformation type code: ";
+            err << elem->second.second;
+            throw DeadlyExportError(err.str());
+        }
+        // now just continue to the next node
+        WriteModelNodes(
+            outstream, next_node, parent_uid, bone_uids, transform_chain
+        );
+        return;
+    }
+
+    int64_t node_uid = 0;
+    // generate uid and connect to parent, if not the root node,
+    if (node != mScene->mRootNode) {
+        auto elem = node_uids.find(node);
+        if (elem != node_uids.end()) {
+            node_uid = elem->second;
+        } else {
+            node_uid = generate_uid();
+            node_uids[node] = node_uid;
+        }
+        FBX::Node c("C");
+        c.AddProperties("OO", node_uid, parent_uid);
+        connections.push_back(c);
+    }
+
+    // what type of node is this?
+    if (node == mScene->mRootNode) {
+        // handled later
+    } else if (node->mNumMeshes == 1) {
+        // connect to child mesh, which should have been written previously
+        FBX::Node c("C");
+        c.AddProperties("OO", mesh_uids[node->mMeshes[0]], node_uid);
+        connections.push_back(c);
+        // also connect to the material for the child mesh
+        c = FBX::Node("C");
+        c.AddProperties(
+            "OO",
+            material_uids[mScene->mMeshes[node->mMeshes[0]]->mMaterialIndex],
+            node_uid
+        );
+        connections.push_back(c);
+        // write model node
+        WriteModelNode(outstream, node, node_uid, "Mesh", transform_chain);
+    } else if (bone_uids.count(node_name)) {
+        WriteModelNode(outstream, node, node_uid, "LimbNode", transform_chain);
+        // we also need to write a nodeattribute to mark it as a skeleton
+        int64_t node_attribute_uid = generate_uid();
+        FBX::Node na("NodeAttribute");
+        na.AddProperties(
+            node_attribute_uid, FBX::SEPARATOR + "NodeAttribute", "LimbNode"
+        );
+        na.AddChild("TypeFlags", Property("Skeleton"));
+        na.Dump(outstream);
+        // and connect them
+        FBX::Node c("C");
+        c.AddProperties("OO", node_attribute_uid, node_uid);
+        connections.push_back(c);
+    } else {
+        // generate a null node so we can add children to it
+        WriteModelNode(outstream, node, node_uid, "Null", transform_chain);
+    }
+
+    // if more than one child mesh, make nodes for each mesh
+    if (node->mNumMeshes > 1 || node == mScene->mRootNode) {
+        for (size_t i = 0; i < node->mNumMeshes; ++i) {
+            // make a new model node
+            int64_t new_node_uid = generate_uid();
+            // connect to parent node
+            FBX::Node c("C");
+            c.AddProperties("OO", new_node_uid, node_uid);
+            connections.push_back(c);
+            // connect to child mesh, which should have been written previously
+            c = FBX::Node("C");
+            c.AddProperties("OO", mesh_uids[node->mMeshes[i]], new_node_uid);
+            connections.push_back(c);
+            // also connect to the material for the child mesh
+            c = FBX::Node("C");
+            c.AddProperties(
+                "OO",
+                material_uids[
+                    mScene->mMeshes[node->mMeshes[i]]->mMaterialIndex
+                ],
+                new_node_uid
+            );
+            connections.push_back(c);
+            // write model node
+            FBX::Node m("Model");
+            // take name from mesh name, if it exists
+            std::string name = mScene->mMeshes[node->mMeshes[i]]->mName.C_Str();
+            name += FBX::SEPARATOR + "Model";
+            m.AddProperties(new_node_uid, name, "Mesh");
+            m.AddChild("Version", int32_t(232));
+            FBX::Node p("Properties70");
+            p.AddP70enum("InheritType", 1);
+            m.AddChild(p);
+            m.Dump(outstream);
+        }
+    }
+
+    // now recurse into children
+    for (size_t i = 0; i < node->mNumChildren; ++i) {
+        WriteModelNodes(
+            outstream, node->mChildren[i], node_uid, bone_uids
+        );
+    }
+}
+
+void FBXExporter::WriteConnections ()
+{
+    // we should have completed the connection graph already,
+    // so basically just dump it here
+    FBX::Node conn("Connections");
+    StreamWriterLE outstream(outfile);
+    conn.Begin(outstream);
+    for (auto &n : connections) {
+        n.Dump(outstream);
+    }
+    conn.End(outstream, !connections.empty());
+    connections.clear();
+}
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+#endif // ASSIMP_BUILD_NO_EXPORT

+ 146 - 0
code/FBXExporter.h

@@ -0,0 +1,146 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file FBXExporter.h
+* Declares the exporter class to write a scene to an fbx file
+*/
+#ifndef AI_FBXEXPORTER_H_INC
+#define AI_FBXEXPORTER_H_INC
+
+#ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#include "FBXExportNode.h" // FBX::Node
+
+#include <assimp/types.h>
+//#include <assimp/material.h>
+#include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/Exceptional.h> // DeadlyExportError
+
+#include <vector>
+#include <map>
+#include <memory> // shared_ptr
+#include <sstream> // stringstream
+
+struct aiScene;
+struct aiNode;
+//struct aiMaterial;
+
+namespace Assimp
+{
+    class IOSystem;
+    class IOStream;
+    class ExportProperties;
+
+    // ---------------------------------------------------------------------
+    /** Helper class to export a given scene to an FBX file. */
+    // ---------------------------------------------------------------------
+    class FBXExporter
+    {
+    public:
+        /// Constructor for a specific scene to export
+        FBXExporter(const aiScene* pScene, const ExportProperties* pProperties);
+
+        // call one of these methods to export
+        void ExportBinary(const char* pFile, IOSystem* pIOSystem);
+        void ExportAscii(const char* pFile, IOSystem* pIOSystem);
+
+    private:
+        bool binary; // whether current export is in binary or ascii format
+        const aiScene* mScene; // the scene to export
+        const ExportProperties* mProperties; // currently unused
+        std::shared_ptr<IOStream> outfile; // file to write to
+
+        std::vector<FBX::Node> connections; // conection storage
+
+        std::vector<int64_t> mesh_uids;
+        std::vector<int64_t> material_uids;
+        std::map<const aiNode*,int64_t> node_uids;
+
+        // this crude unique-ID system is actually fine
+        int64_t last_uid = 999999;
+        int64_t generate_uid() { return ++last_uid; }
+
+        // binary files have a specific header and footer,
+        // in addition to the actual data
+        void WriteBinaryHeader();
+        void WriteBinaryFooter();
+
+        // WriteAllNodes does the actual export.
+        // It just calls all the Write<Section> methods below in order.
+        void WriteAllNodes();
+
+        // Methods to write individual sections.
+        // The order here matches the order inside an FBX file.
+        // Each method corresponds to a top-level FBX section,
+        // except WriteHeader which also includes some binary-only sections
+        // and WriteFooter which is binary data only.
+        void WriteHeaderExtension();
+        // WriteFileId(); // binary-only, included in WriteHeader
+        // WriteCreationTime(); // binary-only, included in WriteHeader
+        // WriteCreator(); // binary-only, included in WriteHeader
+        void WriteGlobalSettings();
+        void WriteDocuments();
+        void WriteReferences();
+        void WriteDefinitions();
+        void WriteObjects();
+        void WriteConnections();
+        // WriteTakes(); // deprecated since at least 2015 (fbx 7.4)
+
+        // helpers
+        void WriteModelNodes(
+            Assimp::StreamWriterLE& s,
+            const aiNode* node,
+            int64_t parent_uid,
+            const std::map<std::string,int64_t>& bone_uids
+        );
+        void WriteModelNodes( // usually don't call this directly
+            StreamWriterLE& s,
+            const aiNode* node,
+            int64_t parent_uid,
+            const std::map<std::string,int64_t>& bone_uids,
+            std::vector<std::pair<std::string,aiVector3D>>& transform_chain
+        );
+    };
+}
+
+#endif // ASSIMP_BUILD_NO_FBX_EXPORTER
+
+#endif // AI_FBXEXPORTER_H_INC

+ 11 - 11
code/FBXMeshGeometry.cpp

@@ -428,16 +428,19 @@ void ResolveVertexDataArray(std::vector<T>& data_out, const Scope& source,
     const std::vector<unsigned int>& mapping_offsets,
     const std::vector<unsigned int>& mappings)
 {
+    bool isDirect = ReferenceInformationType == "Direct";
+    bool isIndexToDirect = ReferenceInformationType == "IndexToDirect";
 
+    // fallback to direct data if there is no index data element
+    if ( isIndexToDirect && !HasElement( source, indexDataElementName ) ) {
+        isDirect = true;
+        isIndexToDirect = false;
+    }
 
     // handle permutations of Mapping and Reference type - it would be nice to
     // deal with this more elegantly and with less redundancy, but right
     // now it seems unavoidable.
-    if (MappingInformationType == "ByVertice" && ReferenceInformationType == "Direct") {
-        if ( !HasElement( source, indexDataElementName ) ) {
-            return;
-        }
-
+    if (MappingInformationType == "ByVertice" && isDirect) {
         std::vector<T> tempData;
 		ParseVectorDataArray(tempData, GetRequiredElement(source, dataElementName));
 
@@ -450,14 +453,11 @@ void ResolveVertexDataArray(std::vector<T>& data_out, const Scope& source,
             }
         }
     }
-    else if (MappingInformationType == "ByVertice" && ReferenceInformationType == "IndexToDirect") {
+    else if (MappingInformationType == "ByVertice" && isIndexToDirect) {
 		std::vector<T> tempData;
 		ParseVectorDataArray(tempData, GetRequiredElement(source, dataElementName));
 
         data_out.resize(vertex_count);
-        if ( !HasElement( source, indexDataElementName ) ) {
-            return;
-        }
 
         std::vector<int> uvIndices;
         ParseVectorDataArray(uvIndices,GetRequiredElement(source,indexDataElementName));
@@ -472,7 +472,7 @@ void ResolveVertexDataArray(std::vector<T>& data_out, const Scope& source,
             }
         }
     }
-    else if (MappingInformationType == "ByPolygonVertex" && ReferenceInformationType == "Direct") {
+    else if (MappingInformationType == "ByPolygonVertex" && isDirect) {
 		std::vector<T> tempData;
 		ParseVectorDataArray(tempData, GetRequiredElement(source, dataElementName));
 
@@ -485,7 +485,7 @@ void ResolveVertexDataArray(std::vector<T>& data_out, const Scope& source,
 
 		data_out.swap(tempData);
     }
-    else if (MappingInformationType == "ByPolygonVertex" && ReferenceInformationType == "IndexToDirect") {
+    else if (MappingInformationType == "ByPolygonVertex" && isIndexToDirect) {
 		std::vector<T> tempData;
 		ParseVectorDataArray(tempData, GetRequiredElement(source, dataElementName));
 

+ 108 - 63
code/FileSystemFilter.h

@@ -42,13 +42,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *  Implements a filter system to filter calls to Exists() and Open()
  *  in order to improve the success rate of file opening ...
  */
+#pragma once
 #ifndef AI_FILESYSTEMFILTER_H_INC
 #define AI_FILESYSTEMFILTER_H_INC
 
-#include "../include/assimp/IOSystem.hpp"
-#include "../include/assimp/DefaultLogger.hpp"
-#include "../include/assimp/fast_atof.h"
-#include "../include/assimp/ParsingUtils.h"
+#include <assimp/IOSystem.hpp>
+#include <assimp/DefaultLogger.hpp>
+#include <assimp/fast_atof.h>
+#include <assimp/ParsingUtils.h>
 
 namespace Assimp    {
 
@@ -64,90 +65,89 @@ class FileSystemFilter : public IOSystem
 public:
     /** Constructor. */
     FileSystemFilter(const std::string& file, IOSystem* old)
-        : wrapped  (old)
-        , src_file (file)
-        , sep(wrapped->getOsSeparator())
-    {
-        ai_assert(NULL != wrapped);
+    : mWrapped  (old)
+    , mSrc_file(file)
+    , sep(mWrapped->getOsSeparator()) {
+        ai_assert(nullptr != mWrapped);
 
         // Determine base directory
-        base = src_file;
+        mBase = mSrc_file;
         std::string::size_type ss2;
-        if (std::string::npos != (ss2 = base.find_last_of("\\/")))  {
-            base.erase(ss2,base.length()-ss2);
-        }
-        else {
-            base = "";
-        //  return;
+        if (std::string::npos != (ss2 = mBase.find_last_of("\\/")))  {
+            mBase.erase(ss2,mBase.length()-ss2);
+        } else {
+            mBase = "";
         }
 
         // make sure the directory is terminated properly
         char s;
 
-        if (base.length() == 0) {
-            base = ".";
-            base += getOsSeparator();
-        }
-        else if ((s = *(base.end()-1)) != '\\' && s != '/') {
-            base += getOsSeparator();
+        if ( mBase.empty() ) {
+            mBase = ".";
+            mBase += getOsSeparator();
+        } else if ((s = *(mBase.end()-1)) != '\\' && s != '/') {
+            mBase += getOsSeparator();
         }
 
-        DefaultLogger::get()->info("Import root directory is \'" + base + "\'");
+        DefaultLogger::get()->info("Import root directory is \'" + mBase + "\'");
     }
 
     /** Destructor. */
-    ~FileSystemFilter()
-    {
-        // haha
+    ~FileSystemFilter() {
+        // empty
     }
 
     // -------------------------------------------------------------------
     /** Tests for the existence of a file at the given path. */
-    bool Exists( const char* pFile) const
-    {
+    bool Exists( const char* pFile) const {
+        ai_assert( nullptr != mWrapped );
+        
         std::string tmp = pFile;
 
         // Currently this IOSystem is also used to open THE ONE FILE.
-        if (tmp != src_file)    {
+        if (tmp != mSrc_file)    {
             BuildPath(tmp);
             Cleanup(tmp);
         }
 
-        return wrapped->Exists(tmp);
+        return mWrapped->Exists(tmp);
     }
 
     // -------------------------------------------------------------------
     /** Returns the directory separator. */
-    char getOsSeparator() const
-    {
+    char getOsSeparator() const {
         return sep;
     }
 
     // -------------------------------------------------------------------
     /** Open a new file with a given path. */
-    IOStream* Open( const char* pFile, const char* pMode = "rb")
-    {
-        ai_assert(pFile);
-        ai_assert(pMode);
+    IOStream* Open( const char* pFile, const char* pMode = "rb") {
+        ai_assert( nullptr != mWrapped );
+        if ( nullptr == pFile || nullptr == pMode ) {
+            return nullptr;
+        }
+        
+        ai_assert( nullptr != pFile );
+        ai_assert( nullptr != pMode );
 
         // First try the unchanged path
-        IOStream* s = wrapped->Open(pFile,pMode);
+        IOStream* s = mWrapped->Open(pFile,pMode);
 
-        if (!s) {
+        if (nullptr == s) {
             std::string tmp = pFile;
 
             // Try to convert between absolute and relative paths
             BuildPath(tmp);
-            s = wrapped->Open(tmp,pMode);
+            s = mWrapped->Open(tmp,pMode);
 
-            if (!s) {
+            if (nullptr == s) {
                 // Finally, look for typical issues with paths
                 // and try to correct them. This is our last
                 // resort.
                 tmp = pFile;
                 Cleanup(tmp);
                 BuildPath(tmp);
-                s = wrapped->Open(tmp,pMode);
+                s = mWrapped->Open(tmp,pMode);
             }
         }
 
@@ -156,27 +156,75 @@ public:
 
     // -------------------------------------------------------------------
     /** Closes the given file and releases all resources associated with it. */
-    void Close( IOStream* pFile)
-    {
-        return wrapped->Close(pFile);
+    void Close( IOStream* pFile) {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->Close(pFile);
     }
 
     // -------------------------------------------------------------------
     /** Compare two paths */
-    bool ComparePaths (const char* one, const char* second) const
-    {
-        return wrapped->ComparePaths (one,second);
+    bool ComparePaths (const char* one, const char* second) const {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->ComparePaths (one,second);
     }
 
-private:
+    // -------------------------------------------------------------------
+    /** Pushes a new directory onto the directory stack. */
+    bool PushDirectory(const std::string &path ) {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->PushDirectory(path);
+    }
+
+    // -------------------------------------------------------------------
+    /** Returns the top directory from the stack. */
+    const std::string &CurrentDirectory() const {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->CurrentDirectory();
+    }
+
+    // -------------------------------------------------------------------
+    /** Returns the number of directories stored on the stack. */
+    size_t StackSize() const {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->StackSize();
+    }
+
+    // -------------------------------------------------------------------
+    /** Pops the top directory from the stack. */
+    bool PopDirectory() {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->PopDirectory();
+    }
 
+    // -------------------------------------------------------------------
+    /** Creates an new directory at the given path. */
+    bool CreateDirectory(const std::string &path) {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->CreateDirectory(path);
+    }
+
+    // -------------------------------------------------------------------
+    /** Will change the current directory to the given path. */
+    bool ChangeDirectory(const std::string &path) {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->ChangeDirectory(path);
+    }
+
+    // -------------------------------------------------------------------
+    /** Delete file. */
+    bool DeleteFile(const std::string &file) {
+        ai_assert( nullptr != mWrapped );
+        return mWrapped->DeleteFile(file);
+    }
+
+private:
     // -------------------------------------------------------------------
     /** Build a valid path from a given relative or absolute path.
      */
-    void BuildPath (std::string& in) const
-    {
+    void BuildPath (std::string& in) const {
+        ai_assert( nullptr != mWrapped );
         // if we can already access the file, great.
-        if (in.length() < 3 || wrapped->Exists(in)) {
+        if (in.length() < 3 || mWrapped->Exists(in)) {
             return;
         }
 
@@ -184,8 +232,8 @@ private:
         if (in[1] != ':') {
 
             // append base path and try
-            const std::string tmp = base + in;
-            if (wrapped->Exists(tmp)) {
+            const std::string tmp = mBase + in;
+            if (mWrapped->Exists(tmp)) {
                 in = tmp;
                 return;
             }
@@ -207,7 +255,7 @@ private:
             std::string::size_type last_dirsep = std::string::npos;
 
             while(true) {
-                tmp = base;
+                tmp = mBase;
                 tmp += sep;
 
                 std::string::size_type dirsep = in.rfind('/', last_dirsep);
@@ -223,7 +271,7 @@ private:
                 last_dirsep = dirsep-1;
 
                 tmp += in.substr(dirsep+1, in.length()-pos);
-                if (wrapped->Exists(tmp)) {
+                if (mWrapped->Exists(tmp)) {
                     in = tmp;
                     return;
                 }
@@ -236,15 +284,14 @@ private:
     // -------------------------------------------------------------------
     /** Cleanup the given path
      */
-    void Cleanup (std::string& in) const
-    {
-        char last = 0;
+    void Cleanup (std::string& in) const {
         if(in.empty()) {
             return;
         }
 
         // Remove a very common issue when we're parsing file names: spaces at the
         // beginning of the path.
+        char last = 0;
         std::string::iterator it = in.begin();
         while (IsSpaceOrNewLine( *it ))++it;
         if (it != in.begin()) {
@@ -274,9 +321,7 @@ private:
                     it = in.erase(it);
                     --it;
                 }
-            }
-            else if (*it == '%' && in.end() - it > 2) {
-
+            } else if (*it == '%' && in.end() - it > 2) {
                 // Hex sequence in URIs
                 if( IsHex((&*it)[0]) && IsHex((&*it)[1]) ) {
                     *it = HexOctetToDecimal(&*it);
@@ -290,8 +335,8 @@ private:
     }
 
 private:
-    IOSystem* wrapped;
-    std::string src_file, base;
+    IOSystem *mWrapped;
+    std::string mSrc_file, mBase;
     char sep;
 };
 

+ 7 - 7
code/FindInstancesProcess.h

@@ -60,9 +60,9 @@ namespace Assimp    {
  *  @param in Input mesh
  *  @return Hash.
  */
-inline uint64_t GetMeshHash(aiMesh* in)
-{
-    ai_assert(NULL != in);
+inline
+uint64_t GetMeshHash(aiMesh* in) {
+    ai_assert(nullptr != in);
 
     // ... get an unique value representing the vertex format of the mesh
     const unsigned int fhash = GetMeshVFormatUnique(in);
@@ -78,14 +78,14 @@ inline uint64_t GetMeshHash(aiMesh* in)
 /** @brief Perform a component-wise comparison of two arrays
  *
  *  @param first First array
- *  @param second Second aray
+ *  @param second Second array
  *  @param size Size of both arrays
  *  @param e Epsilon
  *  @return true if the arrays are identical
  */
-inline bool CompareArrays(const aiVector3D* first, const aiVector3D* second,
-    unsigned int size, float e)
-{
+inline
+bool CompareArrays(const aiVector3D* first, const aiVector3D* second,
+        unsigned int size, float e) {
     for (const aiVector3D* end = first+size; first != end; ++first,++second) {
         if ( (*first - *second).SquareLength() >= e)
             return false;

+ 1 - 13
code/Importer.cpp

@@ -190,7 +190,7 @@ Importer::~Importer()
     delete pimpl->mIOHandler;
     delete pimpl->mProgressHandler;
 
-    // Kill imported scene. Destructors should do that recursivly
+    // Kill imported scene. Destructor's should do that recursively
     delete pimpl->mScene;
 
     // Delete shared post-processing data
@@ -200,18 +200,6 @@ Importer::~Importer()
     delete pimpl;
 }
 
-// ------------------------------------------------------------------------------------------------
-// Copy constructor - copies the config of another Importer, not the scene
-Importer::Importer(const Importer &other)
-	: pimpl(NULL) {
-    new(this) Importer();
-
-    pimpl->mIntProperties    = other.pimpl->mIntProperties;
-    pimpl->mFloatProperties  = other.pimpl->mFloatProperties;
-    pimpl->mStringProperties = other.pimpl->mStringProperties;
-    pimpl->mMatrixProperties = other.pimpl->mMatrixProperties;
-}
-
 // ------------------------------------------------------------------------------------------------
 // Register a custom post-processing step
 aiReturn Importer::RegisterPPStep(BaseProcess* pImp)

+ 1 - 1
code/OgreXmlSerializer.cpp

@@ -835,7 +835,7 @@ void OgreXmlSerializer::ReadAnimationTracks(Animation *dest)
 
 void OgreXmlSerializer::ReadAnimationKeyFrames(Animation *anim, VertexAnimationTrack *dest)
 {
-    static const aiVector3D zeroVec(0.f, 0.f, 0.f);
+    const aiVector3D zeroVec(0.f, 0.f, 0.f);
 
     NextNode();
     while(m_currentNodeName == nnKeyFrame)

+ 15 - 12
code/OpenGEXImporter.cpp

@@ -731,17 +731,22 @@ enum MeshAttribute {
     TexCoord
 };
 
+static const std::string PosToken = "position";
+static const std::string ColToken = "color";
+static const std::string NormalToken = "normal";
+static const std::string TexCoordToken = "texcoord";
+
 //------------------------------------------------------------------------------------------------
 static MeshAttribute getAttributeByName( const char *attribName ) {
     ai_assert( nullptr != attribName  );
 
-    if ( 0 == strncmp( "position", attribName, strlen( "position" ) ) ) {
+    if ( 0 == strncmp( PosToken.c_str(), attribName, PosToken.size() ) ) {
         return Position;
-    } else if ( 0 == strncmp( "color", attribName, strlen( "color" ) ) ) {
+    } else if ( 0 == strncmp( ColToken.c_str(), attribName, ColToken.size() ) ) {
         return Color;
-    } else if( 0 == strncmp( "normal", attribName, strlen( "normal" ) ) ) {
+    } else if( 0 == strncmp( NormalToken.c_str(), attribName, NormalToken.size() ) ) {
         return Normal;
-    } else if( 0 == strncmp( "texcoord", attribName, strlen( "texcoord" ) ) ) {
+    } else if( 0 == strncmp( TexCoordToken.c_str(), attribName, TexCoordToken.size() ) ) {
         return TexCoord;
     }
 
@@ -1098,14 +1103,12 @@ void OpenGEXImporter::handleParamNode( ODDLParser::DDLNode *node, aiScene * /*pS
             return;
         }
         const float floatVal( val->getFloat() );
-        if ( prop->m_value  != nullptr ) {
-            if ( 0 == ASSIMP_strincmp( "fov", prop->m_value->getString(), 3 ) ) {
-                m_currentCamera->mHorizontalFOV = floatVal;
-            } else if ( 0 == ASSIMP_strincmp( "near", prop->m_value->getString(), 3 ) ) {
-                m_currentCamera->mClipPlaneNear = floatVal;
-            } else if ( 0 == ASSIMP_strincmp( "far", prop->m_value->getString(), 3 ) ) {
-                m_currentCamera->mClipPlaneFar = floatVal;
-            }
+        if ( 0 == ASSIMP_strincmp( "fov", prop->m_value->getString(), 3 ) ) {
+            m_currentCamera->mHorizontalFOV = floatVal;
+        } else if ( 0 == ASSIMP_strincmp( "near", prop->m_value->getString(), 4 ) ) {
+            m_currentCamera->mClipPlaneNear = floatVal;
+        } else if ( 0 == ASSIMP_strincmp( "far", prop->m_value->getString(), 3 ) ) {
+            m_currentCamera->mClipPlaneFar = floatVal;
         }
     }
 }

+ 7 - 2
code/PostStepRegistry.cpp

@@ -125,6 +125,9 @@ corresponding preprocessor flag to selectively disable steps.
 #ifndef ASSIMP_BUILD_NO_DEBONE_PROCESS
 #   include "DeboneProcess.h"
 #endif
+#if (!defined ASSIMP_BUILD_NO_GLOBALSCALE_PROCESS)
+#   include "ScaleProcess.h"
+#endif
 
 namespace Assimp {
 
@@ -136,7 +139,7 @@ void GetPostProcessingStepInstanceList(std::vector< BaseProcess* >& out)
     // of sequence it is executed. Steps that are added here are not
     // validated - as RegisterPPStep() does - all dependencies must be given.
     // ----------------------------------------------------------------------------
-    out.reserve(30);
+    out.reserve(31);
 #if (!defined ASSIMP_BUILD_NO_MAKELEFTHANDED_PROCESS)
     out.push_back( new MakeLeftHandedProcess());
 #endif
@@ -197,7 +200,9 @@ void GetPostProcessingStepInstanceList(std::vector< BaseProcess* >& out)
 #if (!defined ASSIMP_BUILD_NO_GENFACENORMALS_PROCESS)
     out.push_back( new GenFaceNormalsProcess());
 #endif
-
+#if (!defined ASSIMP_BUILD_NO_GLOBALSCALE_PROCESS)
+    out.push_back( new ScaleProcess());
+#endif
     // .........................................................................
     // DON'T change the order of these five ..
     // XXX this is actually a design weakness that dates back to the time

+ 4 - 0
code/ScaleProcess.cpp

@@ -39,6 +39,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 ----------------------------------------------------------------------
 */
+#ifndef ASSIMP_BUILD_NO_GLOBALSCALE_PROCESS
+
 #include "ScaleProcess.h"
 
 #include <assimp/scene.h>
@@ -104,3 +106,5 @@ void ScaleProcess::applyScaling( aiNode *currentNode ) {
 }
 
 } // Namespace Assimp
+
+#endif // !! ASSIMP_BUILD_NO_GLOBALSCALE_PROCESS

+ 24 - 23
code/SceneCombiner.cpp

@@ -1256,29 +1256,30 @@ void SceneCombiner::Copy(aiMetadata** _dest, const aiMetadata* src) {
         aiMetadataEntry& out = dest->mValues[i];
         out.mType = in.mType;
         switch (dest->mValues[i].mType) {
-        case AI_BOOL:
-            out.mData = new bool(*static_cast<bool*>(in.mData));
-            break;
-        case AI_INT32:
-            out.mData = new int32_t(*static_cast<int32_t*>(in.mData));
-            break;
-        case AI_UINT64:
-            out.mData = new uint64_t(*static_cast<uint64_t*>(in.mData));
-            break;
-        case AI_FLOAT:
-            out.mData = new float(*static_cast<float*>(in.mData));
-            break;
-        case AI_DOUBLE:
-            out.mData = new double(*static_cast<double*>(in.mData));
-            break;
-        case AI_AISTRING:
-            out.mData = new aiString(*static_cast<aiString*>(in.mData));
-            break;
-        case AI_AIVECTOR3D:
-            out.mData = new aiVector3D(*static_cast<aiVector3D*>(in.mData));
-            break;
-        default:
-            ai_assert(false);
+            case AI_BOOL:
+                out.mData = new bool(*static_cast<bool*>(in.mData));
+                break;
+            case AI_INT32:
+                out.mData = new int32_t(*static_cast<int32_t*>(in.mData));
+                break;
+            case AI_UINT64:
+                out.mData = new uint64_t(*static_cast<uint64_t*>(in.mData));
+                break;
+            case AI_FLOAT:
+                out.mData = new float(*static_cast<float*>(in.mData));
+                break;
+            case AI_DOUBLE:
+                out.mData = new double(*static_cast<double*>(in.mData));
+                break;
+            case AI_AISTRING:
+                out.mData = new aiString(*static_cast<aiString*>(in.mData));
+                break;
+            case AI_AIVECTOR3D:
+                out.mData = new aiVector3D(*static_cast<aiVector3D*>(in.mData));
+                break;
+            default:
+                ai_assert(false);
+                break;
         }
     }
 }

+ 1 - 1
code/SpatialSort.cpp

@@ -294,7 +294,7 @@ void SpatialSort::FindIdenticalPositions( const aiVector3D& pPosition,
         index++;
 
     // Now start iterating from there until the first position lays outside of the distance range.
-    // Add all positions inside the distance range within the tolerance to the result aray
+    // Add all positions inside the distance range within the tolerance to the result array
     std::vector<Entry>::const_iterator it = mPositions.begin() + index;
     while( ToBinary(it->mDistance) < maxDistBinary)
     {

+ 6 - 9
code/VertexTriangleAdjacency.cpp

@@ -48,7 +48,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include "VertexTriangleAdjacency.h"
 #include <assimp/mesh.h>
 
-
 using namespace Assimp;
 
 // ------------------------------------------------------------------------------------------------
@@ -60,8 +59,8 @@ VertexTriangleAdjacency::VertexTriangleAdjacency(aiFace *pcFaces,
     // compute the number of referenced vertices if it wasn't specified by the caller
     const aiFace* const pcFaceEnd = pcFaces + iNumFaces;
     if (!iNumVertices)  {
-
         for (aiFace* pcFace = pcFaces; pcFace != pcFaceEnd; ++pcFace)   {
+            ai_assert( nullptr != pcFace );
             ai_assert(3 == pcFace->mNumIndices);
             iNumVertices = std::max(iNumVertices,pcFace->mIndices[0]);
             iNumVertices = std::max(iNumVertices,pcFace->mIndices[1]);
@@ -69,19 +68,18 @@ VertexTriangleAdjacency::VertexTriangleAdjacency(aiFace *pcFaces,
         }
     }
 
-    this->iNumVertices = iNumVertices;
+    mNumVertices = iNumVertices;
 
     unsigned int* pi;
 
     // allocate storage
     if (bComputeNumTriangles)   {
         pi = mLiveTriangles = new unsigned int[iNumVertices+1];
-        memset(mLiveTriangles,0,sizeof(unsigned int)*(iNumVertices+1));
+        ::memset(mLiveTriangles,0,sizeof(unsigned int)*(iNumVertices+1));
         mOffsetTable = new unsigned int[iNumVertices+2]+1;
-    }
-    else {
+    } else {
         pi = mOffsetTable = new unsigned int[iNumVertices+2]+1;
-        memset(mOffsetTable,0,sizeof(unsigned int)*(iNumVertices+1));
+        ::memset(mOffsetTable,0,sizeof(unsigned int)*(iNumVertices+1));
         mLiveTriangles = NULL; // important, otherwise the d'tor would crash
     }
 
@@ -90,8 +88,7 @@ VertexTriangleAdjacency::VertexTriangleAdjacency(aiFace *pcFaces,
     *piEnd++ = 0u;
 
     // first pass: compute the number of faces referencing each vertex
-    for (aiFace* pcFace = pcFaces; pcFace != pcFaceEnd; ++pcFace)
-    {
+    for (aiFace* pcFace = pcFaces; pcFace != pcFaceEnd; ++pcFace) {
         pi[pcFace->mIndices[0]]++;
         pi[pcFace->mIndices[1]]++;
         pi[pcFace->mIndices[2]]++;

+ 9 - 20
code/VertexTriangleAdjacency.h

@@ -60,10 +60,8 @@ namespace Assimp    {
  *  @note Although it is called #VertexTriangleAdjacency, the current version does also
  *    support arbitrary polygons. */
 // --------------------------------------------------------------------------------------------
-class ASSIMP_API VertexTriangleAdjacency
-{
+class ASSIMP_API VertexTriangleAdjacency {
 public:
-
     // ----------------------------------------------------------------------------
     /** @brief Construction from an existing index buffer
      *  @param pcFaces Index buffer
@@ -77,39 +75,30 @@ public:
         unsigned int iNumVertices = 0,
         bool bComputeNumTriangles = true);
 
-
     // ----------------------------------------------------------------------------
     /** @brief Destructor */
     ~VertexTriangleAdjacency();
 
-
-public:
-
     // ----------------------------------------------------------------------------
     /** @brief Get all triangles adjacent to a vertex
      *  @param iVertIndex Index of the vertex
      *  @return A pointer to the adjacency list. */
-    unsigned int* GetAdjacentTriangles(unsigned int iVertIndex) const
-    {
-        ai_assert(iVertIndex < iNumVertices);
+    unsigned int* GetAdjacentTriangles(unsigned int iVertIndex) const {
+        ai_assert(iVertIndex < mNumVertices);
         return &mAdjacencyTable[ mOffsetTable[iVertIndex]];
     }
 
-
     // ----------------------------------------------------------------------------
     /** @brief Get the number of triangles that are referenced by
      *    a vertex. This function returns a reference that can be modified
      *  @param iVertIndex Index of the vertex
      *  @return Number of referenced triangles */
-    unsigned int& GetNumTrianglesPtr(unsigned int iVertIndex)
-    {
-        ai_assert(iVertIndex < iNumVertices && NULL != mLiveTriangles);
+    unsigned int& GetNumTrianglesPtr(unsigned int iVertIndex) {
+        ai_assert( iVertIndex < mNumVertices );
+        ai_assert( nullptr != mLiveTriangles );
         return mLiveTriangles[iVertIndex];
     }
 
-
-public:
-
     //! Offset table
     unsigned int* mOffsetTable;
 
@@ -120,9 +109,9 @@ public:
     unsigned int* mLiveTriangles;
 
     //! Debug: Number of referenced vertices
-    unsigned int iNumVertices;
-
+    unsigned int mNumVertices;
 };
-}
+
+} //! ns Assimp
 
 #endif // !! AI_VTADJACENCY_H_INC

+ 2 - 27
code/glTF2Asset.h

@@ -137,7 +137,7 @@ namespace glTF2
     // Vec/matrix types, as raw float arrays
     typedef float (vec3)[3];
     typedef float (vec4)[4];
-	typedef float (mat4)[16];
+    typedef float (mat4)[16];
 
     namespace Util
     {
@@ -166,33 +166,8 @@ namespace glTF2
 
     //! Magic number for GLB files
 	#define AI_GLB_MAGIC_NUMBER "glTF"
+	#include <assimp/pbrmaterial.h>
 
-    #define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_FACTOR "$mat.gltf.pbrMetallicRoughness.baseColorFactor", 0, 0
-	#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLIC_FACTOR "$mat.gltf.pbrMetallicRoughness.metallicFactor", 0, 0
-	#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR "$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0
-    #define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_TEXTURE aiTextureType_DIFFUSE, 1
-	#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE aiTextureType_UNKNOWN, 0
-	#define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0
-	#define AI_MATKEY_GLTF_ALPHACUTOFF "$mat.gltf.alphaCutoff", 0, 0
-	#define AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS "$mat.gltf.pbrSpecularGlossiness", 0, 0
-	#define AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS_GLOSSINESS_FACTOR "$mat.gltf.pbrMetallicRoughness.glossinessFactor", 0, 0
-
-	#define _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE "$tex.file.texCoord"
-	#define _AI_MATKEY_GLTF_MAPPINGNAME_BASE "$tex.mappingname"
-	#define _AI_MATKEY_GLTF_MAPPINGID_BASE "$tex.mappingid"
-	#define _AI_MATKEY_GLTF_MAPPINGFILTER_MAG_BASE "$tex.mappingfiltermag"
-	#define _AI_MATKEY_GLTF_MAPPINGFILTER_MIN_BASE "$tex.mappingfiltermin"
-    #define _AI_MATKEY_GLTF_SCALE_BASE "$tex.scale"
-    #define _AI_MATKEY_GLTF_STRENGTH_BASE "$tex.strength"
-
-	#define AI_MATKEY_GLTF_TEXTURE_TEXCOORD _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, type, N
-	#define AI_MATKEY_GLTF_MAPPINGNAME(type, N) _AI_MATKEY_GLTF_MAPPINGNAME_BASE, type, N
-	#define AI_MATKEY_GLTF_MAPPINGID(type, N) _AI_MATKEY_GLTF_MAPPINGID_BASE, type, N
-	#define AI_MATKEY_GLTF_MAPPINGFILTER_MAG(type, N) _AI_MATKEY_GLTF_MAPPINGFILTER_MAG_BASE, type, N
-	#define AI_MATKEY_GLTF_MAPPINGFILTER_MIN(type, N) _AI_MATKEY_GLTF_MAPPINGFILTER_MIN_BASE, type, N
-    #define AI_MATKEY_GLTF_TEXTURE_SCALE(type, N) _AI_MATKEY_GLTF_SCALE_BASE, type, N
-    #define AI_MATKEY_GLTF_TEXTURE_STRENGTH(type, N) _AI_MATKEY_GLTF_STRENGTH_BASE, type, N
-    
     #ifdef ASSIMP_API
         #include "./../include/assimp/Compiler/pushpack1.h"
     #endif

+ 2 - 2
include/assimp/IOStreamBuffer.h

@@ -248,9 +248,9 @@ bool IOStreamBuffer<T>::getNextDataLine( std::vector<T> &buffer, T continuationT
         }
     }
 
-    bool continuationFound( false ), endOfDataLine( false );
+    bool continuationFound( false );
     size_t i = 0;
-    while ( !endOfDataLine ) {
+    for( ;; ) {
         if ( continuationToken == m_cache[ m_cachePos ] ) {
             continuationFound = true;
             ++m_cachePos;

+ 1 - 1
include/assimp/Importer.hpp

@@ -137,7 +137,7 @@ public:
      * If this Importer owns a scene it won't be copied.
      * Call ReadFile() to start the import process.
      */
-    Importer(const Importer& other);
+    Importer(const Importer& other)=delete;
 
     // -------------------------------------------------------------------
     /** Assignment operator has been deleted

+ 3 - 3
include/assimp/MemoryIOWrapper.h

@@ -99,19 +99,19 @@ public:
     // Seek specific position
     aiReturn Seek(size_t pOffset, aiOrigin pOrigin) {
         if (aiOrigin_SET == pOrigin) {
-            if (pOffset >= length) {
+            if (pOffset > length) {
                 return AI_FAILURE;
             }
             pos = pOffset;
         }
         else if (aiOrigin_END == pOrigin) {
-            if (pOffset >= length) {
+            if (pOffset > length) {
                 return AI_FAILURE;
             }
             pos = length-pOffset;
         }
         else {
-            if (pOffset+pos >= length) {
+            if (pOffset+pos > length) {
                 return AI_FAILURE;
             }
             pos += pOffset;

+ 14 - 13
include/assimp/ParsingUtils.h

@@ -66,49 +66,50 @@ static const unsigned int BufferSize = 4096;
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE char_t ToLower( char_t in)
-{
+AI_FORCE_INLINE
+char_t ToLower( char_t in ) {
     return (in >= (char_t)'A' && in <= (char_t)'Z') ? (char_t)(in+0x20) : in;
 }
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE char_t ToUpper( char_t in) {
+AI_FORCE_INLINE
+char_t ToUpper( char_t in) {
     return (in >= (char_t)'a' && in <= (char_t)'z') ? (char_t)(in-0x20) : in;
 }
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE bool IsUpper( char_t in)
-{
+AI_FORCE_INLINE
+bool IsUpper( char_t in) {
     return (in >= (char_t)'A' && in <= (char_t)'Z');
 }
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE bool IsLower( char_t in)
-{
+AI_FORCE_INLINE
+bool IsLower( char_t in) {
     return (in >= (char_t)'a' && in <= (char_t)'z');
 }
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE bool IsSpace( char_t in)
-{
+AI_FORCE_INLINE
+bool IsSpace( char_t in) {
     return (in == (char_t)' ' || in == (char_t)'\t');
 }
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE bool IsLineEnd( char_t in)
-{
+AI_FORCE_INLINE
+bool IsLineEnd( char_t in) {
     return (in==(char_t)'\r'||in==(char_t)'\n'||in==(char_t)'\0'||in==(char_t)'\f');
 }
 
 // ---------------------------------------------------------------------------------
 template <class char_t>
-AI_FORCE_INLINE bool IsSpaceOrNewLine( char_t in)
-{
+AI_FORCE_INLINE
+bool IsSpaceOrNewLine( char_t in) {
     return IsSpace<char_t>(in) || IsLineEnd<char_t>(in);
 }
 

+ 1 - 1
include/assimp/SGSpatialSort.h

@@ -123,7 +123,7 @@ protected:
 
         Entry() { /** intentionally not initialized.*/ }
         Entry( unsigned int pIndex, const aiVector3D& pPosition, float pDistance,uint32_t pSG)
-            :
+        :
             mIndex( pIndex),
             mPosition( pPosition),
             mSmoothGroups (pSG),

+ 10 - 8
include/assimp/SpatialSort.h

@@ -47,8 +47,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <vector>
 #include <assimp/types.h>
 
-namespace Assimp
-{
+namespace Assimp {
 
 // ------------------------------------------------------------------------------------------------
 /** A little helper class to quickly find all vertices in the epsilon environment of a given
@@ -148,17 +147,20 @@ protected:
     aiVector3D mPlaneNormal;
 
     /** An entry in a spatially sorted position array. Consists of a vertex index,
-     * its position and its precalculated distance from the reference plane */
-    struct Entry
-    {
+     * its position and its pre-calculated distance from the reference plane */
+    struct Entry {
         unsigned int mIndex; ///< The vertex referred by this entry
         aiVector3D mPosition; ///< Position
         ai_real mDistance; ///< Distance of this vertex to the sorting plane
 
-        Entry() { /** intentionally not initialized.*/ }
+        Entry()
+        : mIndex( 999999999 ), mPosition(), mDistance( 99999. ) {
+            // empty        
+        }
         Entry( unsigned int pIndex, const aiVector3D& pPosition, ai_real pDistance)
-            : mIndex( pIndex), mPosition( pPosition), mDistance( pDistance)
-        {   }
+        : mIndex( pIndex), mPosition( pPosition), mDistance( pDistance) {
+            // empty
+        }
 
         bool operator < (const Entry& e) const { return mDistance < e.mDistance; }
     };

+ 59 - 1
include/assimp/StreamWriter.h

@@ -58,7 +58,7 @@ namespace Assimp {
 // --------------------------------------------------------------------------------------------
 /** Wrapper class around IOStream to allow for consistent writing of binary data in both
  *  little and big endian format. Don't attempt to instance the template directly. Use
- *  StreamWriterLE to read from a little-endian stream and StreamWriterBE to read from a
+ *  StreamWriterLE to write to a little-endian stream and StreamWriterBE to write to a
  *  BE stream. Alternatively, there is StreamWriterAny if the endianness of the output
  *  stream is to be determined at runtime.
  */
@@ -108,6 +108,38 @@ public:
         stream->Flush();
     }
 
+public:
+
+    // ---------------------------------------------------------------------
+    /** Flush the contents of the internal buffer, and the output IOStream */
+    void Flush()
+    {
+        stream->Write(&buffer[0], 1, buffer.size());
+        stream->Flush();
+        buffer.clear();
+        cursor = 0;
+    }
+
+    // ---------------------------------------------------------------------
+    /** Seek to the given offset / origin in the output IOStream.
+     *
+     *  Flushes the internal buffer and the output IOStream prior to seeking. */
+    aiReturn Seek(size_t pOffset, aiOrigin pOrigin=aiOrigin_SET)
+    {
+        Flush();
+        return stream->Seek(pOffset, pOrigin);
+    }
+
+    // ---------------------------------------------------------------------
+    /** Tell the current position in the output IOStream.
+     *
+     *  First flushes the internal buffer and the output IOStream. */
+    size_t Tell()
+    {
+        Flush();
+        return stream->Tell();
+    }
+
 public:
 
     // ---------------------------------------------------------------------
@@ -171,6 +203,32 @@ public:
         Put(n);
     }
 
+    // ---------------------------------------------------------------------
+    /** Write an aiString to the stream */
+    void PutString(const aiString& s)
+    {
+        // as Put(T f) below
+        if (cursor + s.length >= buffer.size()) {
+            buffer.resize(cursor + s.length);
+        }
+        void* dest = &buffer[cursor];
+        ::memcpy(dest, s.C_Str(), s.length);
+        cursor += s.length;
+    }
+
+    // ---------------------------------------------------------------------
+    /** Write a std::string to the stream */
+    void PutString(const std::string& s)
+    {
+        // as Put(T f) below
+        if (cursor + s.size() >= buffer.size()) {
+            buffer.resize(cursor + s.size());
+        }
+        void* dest = &buffer[cursor];
+        ::memcpy(dest, s.c_str(), s.size());
+        cursor += s.size();
+    }
+
 public:
 
     // ---------------------------------------------------------------------

+ 85 - 97
include/assimp/fast_atof.h

@@ -14,8 +14,8 @@
 // ------------------------------------------------------------------------------------
 
 
-#ifndef __FAST_A_TO_F_H_INCLUDED__
-#define __FAST_A_TO_F_H_INCLUDED__
+#ifndef FAST_A_TO_F_H_INCLUDED
+#define FAST_A_TO_F_H_INCLUDED
 
 #include <cmath>
 #include <limits>
@@ -26,15 +26,13 @@
 #include "StringComparison.h"
 #include <assimp/DefaultLogger.hpp>
 
-
 #ifdef _MSC_VER
 #  include <stdint.h>
 #else
 #  include <assimp/Compiler/pstdint.h>
 #endif
 
-namespace Assimp
-{
+namespace Assimp {
 
 const double fast_atof_table[16] =  {  // we write [16] here instead of [] to work around a swig bug
     0.0,
@@ -59,69 +57,65 @@ const double fast_atof_table[16] =  {  // we write [16] here instead of [] to wo
 // ------------------------------------------------------------------------------------
 // Convert a string in decimal format to a number
 // ------------------------------------------------------------------------------------
-inline unsigned int strtoul10( const char* in, const char** out=0)
-{
+inline
+unsigned int strtoul10( const char* in, const char** out=0) {
     unsigned int value = 0;
 
-    bool running = true;
-    while ( running )
-    {
-        if ( *in < '0' || *in > '9' )
+    for ( ;; ) {
+        if ( *in < '0' || *in > '9' ) {
             break;
+        }
 
         value = ( value * 10 ) + ( *in - '0' );
         ++in;
     }
-    if (out)*out = in;
+    if ( out ) {
+        *out = in;
+    }
     return value;
 }
 
 // ------------------------------------------------------------------------------------
 // Convert a string in octal format to a number
 // ------------------------------------------------------------------------------------
-inline unsigned int strtoul8( const char* in, const char** out=0)
-{
-    unsigned int value = 0;
-
-    bool running = true;
-    while ( running )
-    {
-        if ( *in < '0' || *in > '7' )
+inline
+unsigned int strtoul8( const char* in, const char** out=0) {
+    unsigned int value( 0 );
+    for ( ;; ) {
+        if ( *in < '0' || *in > '7' ) {
             break;
+        }
 
         value = ( value << 3 ) + ( *in - '0' );
         ++in;
     }
-    if (out)*out = in;
+    if ( out ) {
+        *out = in;
+    }
     return value;
 }
 
 // ------------------------------------------------------------------------------------
 // Convert a string in hex format to a number
 // ------------------------------------------------------------------------------------
-inline unsigned int strtoul16( const char* in, const char** out=0)
-{
-    unsigned int value = 0;
-
-    bool running = true;
-    while ( running )
-    {
-        if ( *in >= '0' && *in <= '9' )
-        {
+inline
+unsigned int strtoul16( const char* in, const char** out=0) {
+    unsigned int value( 0 );
+    for ( ;; ) {
+        if ( *in >= '0' && *in <= '9' ) {
             value = ( value << 4u ) + ( *in - '0' );
-        }
-        else if (*in >= 'A' && *in <= 'F')
-        {
+        } else if (*in >= 'A' && *in <= 'F') {
             value = ( value << 4u ) + ( *in - 'A' ) + 10;
-        }
-        else if (*in >= 'a' && *in <= 'f')
-        {
+        } else if (*in >= 'a' && *in <= 'f') {
             value = ( value << 4u ) + ( *in - 'a' ) + 10;
+        } else {
+            break;
         }
-        else break;
         ++in;
     }
-    if (out)*out = in;
+    if ( out ) {
+        *out = in;
+    }
     return value;
 }
 
@@ -129,17 +123,16 @@ inline unsigned int strtoul16( const char* in, const char** out=0)
 // Convert just one hex digit
 // Return value is UINT_MAX if the input character is not a hex digit.
 // ------------------------------------------------------------------------------------
-inline unsigned int HexDigitToDecimal(char in)
-{
-    unsigned int out = UINT_MAX;
-    if (in >= '0' && in <= '9')
+inline
+unsigned int HexDigitToDecimal(char in) {
+    unsigned int out( UINT_MAX );
+    if ( in >= '0' && in <= '9' ) {
         out = in - '0';
-
-    else if (in >= 'a' && in <= 'f')
+    } else if ( in >= 'a' && in <= 'f' ) {
         out = 10u + in - 'a';
-
-    else if (in >= 'A' && in <= 'F')
+    } else if ( in >= 'A' && in <= 'F' ) {
         out = 10u + in - 'A';
+    }
 
     // return value is UINT_MAX if the input is not a hex digit
     return out;
@@ -148,8 +141,8 @@ inline unsigned int HexDigitToDecimal(char in)
 // ------------------------------------------------------------------------------------
 // Convert a hex-encoded octet (2 characters, i.e. df or 1a).
 // ------------------------------------------------------------------------------------
-inline uint8_t HexOctetToDecimal(const char* in)
-{
+inline
+uint8_t HexOctetToDecimal(const char* in) {
     return ((uint8_t)HexDigitToDecimal(in[0])<<4)+(uint8_t)HexDigitToDecimal(in[1]);
 }
 
@@ -157,11 +150,12 @@ inline uint8_t HexOctetToDecimal(const char* in)
 // ------------------------------------------------------------------------------------
 // signed variant of strtoul10
 // ------------------------------------------------------------------------------------
-inline int strtol10( const char* in, const char** out=0)
-{
+inline
+int strtol10( const char* in, const char** out=0) {
     bool inv = (*in=='-');
-    if (inv || *in=='+')
+    if ( inv || *in == '+' ) {
         ++in;
+    }
 
     int value = strtoul10(in,out);
     if (inv) {
@@ -176,10 +170,9 @@ inline int strtol10( const char* in, const char** out=0)
 // 0NNN   - oct
 // NNN    - dec
 // ------------------------------------------------------------------------------------
-inline unsigned int strtoul_cppstyle( const char* in, const char** out=0)
-{
-    if ('0' == in[0])
-    {
+inline
+unsigned int strtoul_cppstyle( const char* in, const char** out=0) {
+    if ('0' == in[0]) {
         return 'x' == in[1] ? strtoul16(in+2,out) : strtoul8(in+1,out);
     }
     return strtoul10(in, out);
@@ -189,19 +182,19 @@ inline unsigned int strtoul_cppstyle( const char* in, const char** out=0)
 // Special version of the function, providing higher accuracy and safety
 // It is mainly used by fast_atof to prevent ugly and unwanted integer overflows.
 // ------------------------------------------------------------------------------------
-inline uint64_t strtoul10_64( const char* in, const char** out=0, unsigned int* max_inout=0)
-{
+inline
+uint64_t strtoul10_64( const char* in, const char** out=0, unsigned int* max_inout=0) {
     unsigned int cur = 0;
     uint64_t value = 0;
 
-    if ( *in < '0' || *in > '9' )
-        throw std::invalid_argument(std::string("The string \"") + in + "\" cannot be converted into a value.");
+    if ( *in < '0' || *in > '9' ) {
+        throw std::invalid_argument( std::string( "The string \"" ) + in + "\" cannot be converted into a value." );
+    }
 
-    bool running = true;
-    while ( running )
-    {
-        if ( *in < '0' || *in > '9' )
+    for ( ;; ) {
+        if ( *in < '0' || *in > '9' ) {
             break;
+        }
 
         const uint64_t new_value = ( value * 10 ) + ( *in - '0' );
 
@@ -210,7 +203,6 @@ inline uint64_t strtoul10_64( const char* in, const char** out=0, unsigned int*
             DefaultLogger::get()->warn( std::string( "Converting the string \"" ) + in + "\" into a value resulted in overflow." );
             return 0;
         }
-            //throw std::overflow_error();
 
         value = new_value;
 
@@ -218,21 +210,23 @@ inline uint64_t strtoul10_64( const char* in, const char** out=0, unsigned int*
         ++cur;
 
         if (max_inout && *max_inout == cur) {
-
             if (out) { /* skip to end */
-                while (*in >= '0' && *in <= '9')
+                while ( *in >= '0' && *in <= '9' ) {
                     ++in;
+                }
                 *out = in;
             }
 
             return value;
         }
     }
-    if (out)
+    if ( out ) {
         *out = in;
+    }
 
-    if (max_inout)
+    if ( max_inout ) {
         *max_inout = cur;
+    }
 
     return value;
 }
@@ -240,11 +234,12 @@ inline uint64_t strtoul10_64( const char* in, const char** out=0, unsigned int*
 // ------------------------------------------------------------------------------------
 // signed variant of strtoul10_64
 // ------------------------------------------------------------------------------------
-inline int64_t strtol10_64(const char* in, const char** out = 0, unsigned int* max_inout = 0)
-{
+inline
+int64_t strtol10_64(const char* in, const char** out = 0, unsigned int* max_inout = 0) {
     bool inv = (*in == '-');
-    if (inv || *in == '+')
+    if ( inv || *in == '+' ) {
         ++in;
+    }
 
     int64_t value = strtoul10_64(in, out, max_inout);
     if (inv) {
@@ -253,7 +248,6 @@ inline int64_t strtol10_64(const char* in, const char** out = 0, unsigned int* m
     return value;
 }
 
-
 // Number of relevant decimals for floating-point parsing.
 #define AI_FAST_ATOF_RELAVANT_DECIMALS 15
 
@@ -262,9 +256,9 @@ inline int64_t strtol10_64(const char* in, const char** out = 0, unsigned int* m
 //! about 6 times faster than atof in win32.
 // If you find any bugs, please send them to me, niko (at) irrlicht3d.org.
 // ------------------------------------------------------------------------------------
-template <typename Real>
-inline const char* fast_atoreal_move(const char* c, Real& out, bool check_comma = true)
-{
+template<typename Real>
+inline
+const char* fast_atoreal_move(const char* c, Real& out, bool check_comma = true) {
     Real f = 0;
 
     bool inv = (*c == '-');
@@ -272,42 +266,36 @@ inline const char* fast_atoreal_move(const char* c, Real& out, bool check_comma
         ++c;
     }
 
-    if ((c[0] == 'N' || c[0] == 'n') && ASSIMP_strincmp(c, "nan", 3) == 0)
-    {
+    if ((c[0] == 'N' || c[0] == 'n') && ASSIMP_strincmp(c, "nan", 3) == 0) {
         out = std::numeric_limits<Real>::quiet_NaN();
         c += 3;
         return c;
     }
 
-    if ((c[0] == 'I' || c[0] == 'i') && ASSIMP_strincmp(c, "inf", 3) == 0)
-    {
+    if ((c[0] == 'I' || c[0] == 'i') && ASSIMP_strincmp(c, "inf", 3) == 0) {
         out = std::numeric_limits<Real>::infinity();
         if (inv) {
             out = -out;
         }
         c += 3;
-        if ((c[0] == 'I' || c[0] == 'i') && ASSIMP_strincmp(c, "inity", 5) == 0)
-        {
+        if ((c[0] == 'I' || c[0] == 'i') && ASSIMP_strincmp(c, "inity", 5) == 0) {
             c += 5;
         }
         return c;
-    }
+     }
 
     if (!(c[0] >= '0' && c[0] <= '9') &&
-        !((c[0] == '.' || (check_comma && c[0] == ',')) && c[1] >= '0' && c[1] <= '9'))
-    {
+            !((c[0] == '.' || (check_comma && c[0] == ',')) && c[1] >= '0' && c[1] <= '9')) {
         throw std::invalid_argument("Cannot parse string "
                                     "as real number: does not start with digit "
                                     "or decimal point followed by digit.");
     }
 
-    if (*c != '.' && (! check_comma || c[0] != ','))
-    {
+    if (*c != '.' && (! check_comma || c[0] != ',')) {
         f = static_cast<Real>( strtoul10_64 ( c, &c) );
     }
 
-    if ((*c == '.' || (check_comma && c[0] == ',')) && c[1] >= '0' && c[1] <= '9')
-    {
+    if ((*c == '.' || (check_comma && c[0] == ',')) && c[1] >= '0' && c[1] <= '9') {
         ++c;
 
         // NOTE: The original implementation is highly inaccurate here. The precision of a single
@@ -332,7 +320,6 @@ inline const char* fast_atoreal_move(const char* c, Real& out, bool check_comma
     // A major 'E' must be allowed. Necessary for proper reading of some DXF files.
     // Thanks to Zhao Lei to point out that this if() must be outside the if (*c == '.' ..)
     if (*c == 'e' || *c == 'E') {
-
         ++c;
         const bool einv = (*c=='-');
         if (einv || *c=='+') {
@@ -358,30 +345,31 @@ inline const char* fast_atoreal_move(const char* c, Real& out, bool check_comma
 
 // ------------------------------------------------------------------------------------
 // The same but more human.
-inline ai_real fast_atof(const char* c)
-{
+inline
+ai_real fast_atof(const char* c) {
     ai_real ret(0.0);
     fast_atoreal_move<ai_real>(c, ret);
+
     return ret;
 }
 
 
-inline ai_real fast_atof( const char* c, const char** cout)
-{
+inline
+ai_real fast_atof( const char* c, const char** cout) {
     ai_real ret(0.0);
     *cout = fast_atoreal_move<ai_real>(c, ret);
 
     return ret;
 }
 
-inline ai_real fast_atof( const char** inout)
-{
+inline
+ai_real fast_atof( const char** inout) {
     ai_real ret(0.0);
     *inout = fast_atoreal_move<ai_real>(*inout, ret);
 
     return ret;
 }
 
-} // end of namespace Assimp
+} //! namespace Assimp
 
-#endif
+#endif // FAST_A_TO_F_H_INCLUDED

+ 49 - 17
include/assimp/mesh.h

@@ -200,8 +200,7 @@ struct aiFace
 // ---------------------------------------------------------------------------
 /** @brief A single influence of a bone on a vertex.
  */
-struct aiVertexWeight
-{
+struct aiVertexWeight {
     //! Index of the vertex which is influenced by the bone.
     unsigned int mVertexId;
 
@@ -214,15 +213,26 @@ struct aiVertexWeight
     //! Default constructor
     aiVertexWeight()
     : mVertexId(0)
-    , mWeight(0.0f)
-    { }
+    , mWeight(0.0f) {
+        // empty
+    }
 
     //! Initialisation from a given index and vertex weight factor
     //! \param pID ID
     //! \param pWeight Vertex weight factor
-    aiVertexWeight( unsigned int pID, float pWeight)
-        : mVertexId( pID), mWeight( pWeight)
-    { /* nothing to do here */ }
+    aiVertexWeight( unsigned int pID, float pWeight )
+    : mVertexId( pID )
+    , mWeight( pWeight ) {
+        // empty
+    }
+
+    bool operator == ( const aiVertexWeight &rhs ) const {
+        return ( mVertexId == rhs.mVertexId && mWeight == rhs.mWeight );
+    }
+
+    bool operator != ( const aiVertexWeight &rhs ) const {
+        return ( *this == rhs );
+    }
 
 #endif // __cplusplus
 };
@@ -233,31 +243,41 @@ struct aiVertexWeight
  *
  *  A bone has a name by which it can be found in the frame hierarchy and by
  *  which it can be addressed by animations. In addition it has a number of
- *  influences on vertices.
+ *  influences on vertices, and a matrix relating the mesh position to the
+ *  position of the bone at the time of binding.
  */
-struct aiBone
-{
+struct aiBone {
     //! The name of the bone.
     C_STRUCT aiString mName;
 
-    //! The number of vertices affected by this bone
+    //! The number of vertices affected by this bone.
     //! The maximum value for this member is #AI_MAX_BONE_WEIGHTS.
     unsigned int mNumWeights;
 
-    //! The vertices affected by this bone
+    //! The influence weights of this bone, by vertex index.
     C_STRUCT aiVertexWeight* mWeights;
 
-    //! Matrix that transforms from mesh space to bone space in bind pose
+    /** Matrix that transforms from bone space to mesh space in bind pose.
+     *
+     * This matrix describes the position of the mesh
+     * in the local space of this bone when the skeleton was bound.
+     * Thus it can be used directly to determine a desired vertex position,
+     * given the world-space transform of the bone when animated,
+     * and the position of the vertex in mesh space.
+     *
+     * It is sometimes called an inverse-bind matrix,
+     * or inverse bind pose matrix.
+     */
     C_STRUCT aiMatrix4x4 mOffsetMatrix;
 
 #ifdef __cplusplus
 
     //! Default constructor
     aiBone()
-        : mName()
-        , mNumWeights( 0 )
-      , mWeights( NULL )
-    {
+    : mName()
+    , mNumWeights( 0 )
+    , mWeights( nullptr ) {
+        // empty
     }
 
     //! Copy constructor
@@ -298,7 +318,19 @@ struct aiBone
         return *this;
     }
 
+    bool operator == ( const aiBone &rhs ) const {
+        if ( mName != rhs.mName || mNumWeights != rhs.mNumWeights ) {
+            return false;
+        }
 
+        for ( size_t i = 0; i < mNumWeights; ++i ) {
+            if ( mWeights[ i ] != rhs.mWeights[ i ] ) {
+                return false;
+            }
+        }
+
+        return true;
+    }
     //! Destructor - deletes the array of vertex weights
     ~aiBone()
     {

+ 76 - 0
include/assimp/pbrmaterial.h

@@ -0,0 +1,76 @@
+/*
+---------------------------------------------------------------------------
+Open Asset Import Library (assimp)
+---------------------------------------------------------------------------
+
+Copyright (c) 2006-2018, assimp team
+
+
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the following
+conditions are met:
+
+* Redistributions of source code must retain the above
+  copyright notice, this list of conditions and the
+  following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the
+  following disclaimer in the documentation and/or other
+  materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+  contributors may be used to endorse or promote products
+  derived from this software without specific prior
+  written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+---------------------------------------------------------------------------
+*/
+
+/** @file pbrmaterial.h
+ *  @brief Defines the material system of the library
+ */
+#ifndef AI_PBRMATERIAL_H_INC
+#define AI_PBRMATERIAL_H_INC
+
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_FACTOR "$mat.gltf.pbrMetallicRoughness.baseColorFactor", 0, 0
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLIC_FACTOR "$mat.gltf.pbrMetallicRoughness.metallicFactor", 0, 0
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR "$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_TEXTURE aiTextureType_DIFFUSE, 1
+#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE aiTextureType_UNKNOWN, 0
+#define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0
+#define AI_MATKEY_GLTF_ALPHACUTOFF "$mat.gltf.alphaCutoff", 0, 0
+#define AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS "$mat.gltf.pbrSpecularGlossiness", 0, 0
+#define AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS_GLOSSINESS_FACTOR "$mat.gltf.pbrMetallicRoughness.glossinessFactor", 0, 0
+
+#define _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE "$tex.file.texCoord"
+#define _AI_MATKEY_GLTF_MAPPINGNAME_BASE "$tex.mappingname"
+#define _AI_MATKEY_GLTF_MAPPINGID_BASE "$tex.mappingid"
+#define _AI_MATKEY_GLTF_MAPPINGFILTER_MAG_BASE "$tex.mappingfiltermag"
+#define _AI_MATKEY_GLTF_MAPPINGFILTER_MIN_BASE "$tex.mappingfiltermin"
+#define _AI_MATKEY_GLTF_SCALE_BASE "$tex.scale"
+#define _AI_MATKEY_GLTF_STRENGTH_BASE "$tex.strength"
+
+#define AI_MATKEY_GLTF_TEXTURE_TEXCOORD(type, N) _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, type, N
+#define AI_MATKEY_GLTF_MAPPINGNAME(type, N) _AI_MATKEY_GLTF_MAPPINGNAME_BASE, type, N
+#define AI_MATKEY_GLTF_MAPPINGID(type, N) _AI_MATKEY_GLTF_MAPPINGID_BASE, type, N
+#define AI_MATKEY_GLTF_MAPPINGFILTER_MAG(type, N) _AI_MATKEY_GLTF_MAPPINGFILTER_MAG_BASE, type, N
+#define AI_MATKEY_GLTF_MAPPINGFILTER_MIN(type, N) _AI_MATKEY_GLTF_MAPPINGFILTER_MIN_BASE, type, N
+#define AI_MATKEY_GLTF_TEXTURE_SCALE(type, N) _AI_MATKEY_GLTF_SCALE_BASE, type, N
+#define AI_MATKEY_GLTF_TEXTURE_STRENGTH(type, N) _AI_MATKEY_GLTF_STRENGTH_BASE, type, N
+
+#endif //!!AI_PBRMATERIAL_H_INC

+ 42 - 7
tools/assimp_cmd/Info.cpp

@@ -47,9 +47,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include "Main.h"
 
 const char* AICMD_MSG_INFO_HELP_E =
-"assimp info <file> [-r]\n"
+"assimp info <file> [-r] [-v]\n"
 "\tPrint basic structure of a 3D model\n"
-"\t-r,--raw: No postprocessing, do a raw import\n";
+"\t-r,--raw: No postprocessing, do a raw import\n"
+"\t-v,--verbose: Print verbose info such as node transform data\n";
 
 
 // -----------------------------------------------------------------------------------
@@ -184,7 +185,7 @@ std::string FindPTypes(const aiScene* scene)
 
 // -----------------------------------------------------------------------------------
 void PrintHierarchy(const aiNode* root, unsigned int maxnest, unsigned int maxline,
-					unsigned int cline, unsigned int cnest=0)
+					unsigned int cline, bool verbose, unsigned int cnest=0)
 {
 	if (cline++ >= maxline || cnest >= maxnest) {
 		return;
@@ -194,8 +195,29 @@ void PrintHierarchy(const aiNode* root, unsigned int maxnest, unsigned int maxli
 		printf("-- ");
 	}
 	printf("\'%s\', meshes: %u\n",root->mName.data,root->mNumMeshes);
+
+	if (verbose) {
+		// print the actual transform
+		//printf(",");
+		aiVector3D s, r, t;
+		root->mTransformation.Decompose(s, r, t);
+		if (s.x != 1.0 || s.y != 1.0 || s.z != 1.0) {
+			for(unsigned int i = 0; i < cnest; ++i) { printf("   "); }
+			printf("      S:[%f %f %f]\n", s.x, s.y, s.z);
+		}
+		if (r.x || r.y || r.z) {
+			for(unsigned int i = 0; i < cnest; ++i) { printf("   "); }
+			printf("      R:[%f %f %f]\n", r.x, r.y, r.z);
+		}
+		if (t.x || t.y || t.z) {
+			for(unsigned int i = 0; i < cnest; ++i) { printf("   "); }
+			printf("      T:[%f %f %f]\n", t.x, t.y, t.z);
+		}
+	}
+	//printf("\n");
+
 	for (unsigned int i = 0; i < root->mNumChildren; ++i ) {
-		PrintHierarchy(root->mChildren[i],maxnest,maxline,cline,cnest+1);
+		PrintHierarchy(root->mChildren[i],maxnest,maxline,cline,verbose,cnest+1);
 		if(i == root->mNumChildren-1) {
 			for(unsigned int i = 0; i < cnest; ++i) {
 				printf("   ");
@@ -230,10 +252,23 @@ int Assimp_Info (const char* const* params, unsigned int num)
 
 	const std::string in  = std::string(params[0]);
 
+	// get -r and -v arguments
+	bool raw = false;
+	bool verbose = false;
+	for(unsigned int i = 1; i < num; ++i) {
+		if (!strcmp(params[i],"--raw")||!strcmp(params[i],"-r")) {
+			raw = true;
+		}
+		if (!strcmp(params[i],"--verbose")||!strcmp(params[i],"-v")) {
+			verbose = true;
+		}
+	}
+
 	// do maximum post-processing unless -r was specified
 	ImportData import;
-	import.ppFlags = num>1&&(!strcmp(params[1],"--raw")||!strcmp(params[1],"-r")) ? 0
-		: aiProcessPreset_TargetRealtime_MaxQuality;
+	if (!raw) {
+		import.ppFlags = aiProcessPreset_TargetRealtime_MaxQuality;
+	}
 
 	// import the main model
 	const aiScene* scene = ImportModel(import,in);
@@ -346,7 +381,7 @@ int Assimp_Info (const char* const* params, unsigned int num)
 
 	printf("\nNode hierarchy:\n");
 	unsigned int cline=0;
-	PrintHierarchy(scene->mRootNode,20,1000,cline);
+	PrintHierarchy(scene->mRootNode,20,1000,cline,verbose);
 
 	printf("\n");
 	return 0;