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

Implement ascii FBX export.

It's available under the 'fbxa' format id.
Tommy 7 жил өмнө
parent
commit
826243f289

+ 2 - 2
code/Exporter.cpp

@@ -98,7 +98,7 @@ void ExportSceneAssbin(const char*, IOSystem*, const aiScene*, const ExportPrope
 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 ExportSceneFBXA(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportScene3MF( const char*, IOSystem*, const aiScene*, const ExportProperties* );
 
 // ------------------------------------------------------------------------------------------------
@@ -173,7 +173,7 @@ Exporter::ExportFormatEntry gExporters[] =
 
 #ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
     Exporter::ExportFormatEntry( "fbx", "Autodesk FBX (binary)", "fbx", &ExportSceneFBX, 0 ),
-    //Exporter::ExportFormatEntry( "fbxa", "Autodesk FBX (ascii)", "fbx", &ExportSceneFBXA, 0 ),
+    Exporter::ExportFormatEntry( "fbxa", "Autodesk FBX (ascii)", "fbx", &ExportSceneFBXA, 0 ),
 #endif
 
 #ifndef ASSIMP_BUILD_NO_3MF_EXPORTER

+ 329 - 45
code/FBXExportNode.cpp

@@ -45,10 +45,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include "FBXCommon.h"
 
 #include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/Exceptional.h> // DeadlyExportError
 #include <assimp/ai_assert.h>
 
 #include <string>
+#include <ostream>
+#include <sstream> // ostringstream
 #include <memory> // shared_ptr
+#include <cstdio> // snprintf
 
 // AddP70<type> helpers... there's no usable pattern here,
 // so all are defined as separate functions.
@@ -145,33 +149,174 @@ void FBX::Node::AddP70time(
 }
 
 
-// public member functions for writing to binary fbx
+// public member functions for writing nodes to stream
 
-void FBX::Node::Dump(std::shared_ptr<Assimp::IOStream> outfile)
-{
-    Assimp::StreamWriterLE outstream(outfile);
-    Dump(outstream);
+void FBX::Node::Dump(
+    std::shared_ptr<Assimp::IOStream> outfile,
+    bool binary, int indent
+) {
+    if (binary) {
+        Assimp::StreamWriterLE outstream(outfile);
+        DumpBinary(outstream);
+    } else {
+        std::ostringstream ss;
+        DumpAscii(ss, indent);
+        std::string s = ss.str();
+        outfile->Write(s.c_str(), s.size(), 1);
+    }
+}
+
+void FBX::Node::Dump(
+    Assimp::StreamWriterLE &outstream,
+    bool binary, int indent
+) {
+    if (binary) {
+        DumpBinary(outstream);
+    } else {
+        std::ostringstream ss;
+        DumpAscii(ss, indent);
+        outstream.PutString(ss.str());
+    }
+}
+
+
+// public member functions for low-level writing
+
+void FBX::Node::Begin(
+    Assimp::StreamWriterLE &s,
+    bool binary, int indent
+) {
+    if (binary) {
+        BeginBinary(s);
+    } else {
+        // assume we're at the correct place to start already
+        (void)indent;
+        std::ostringstream ss;
+        BeginAscii(ss, indent);
+        s.PutString(ss.str());
+    }
+}
+
+void FBX::Node::DumpProperties(
+    Assimp::StreamWriterLE& s,
+    bool binary, int indent
+) {
+    if (binary) {
+        DumpPropertiesBinary(s);
+    } else {
+        std::ostringstream ss;
+        DumpPropertiesAscii(ss, indent);
+        s.PutString(ss.str());
+    }
 }
 
-void FBX::Node::Dump(Assimp::StreamWriterLE &s)
+void FBX::Node::EndProperties(
+    Assimp::StreamWriterLE &s,
+    bool binary, int indent
+) {
+    EndProperties(s, binary, indent, properties.size());
+}
+
+void FBX::Node::EndProperties(
+    Assimp::StreamWriterLE &s,
+    bool binary, int indent,
+    size_t num_properties
+) {
+    if (binary) {
+        EndPropertiesBinary(s, num_properties);
+    } else {
+        // nothing to do
+        (void)indent;
+    }
+}
+
+void FBX::Node::BeginChildren(
+    Assimp::StreamWriterLE &s,
+    bool binary, int indent
+) {
+    if (binary) {
+        // nothing to do
+    } else {
+        std::ostringstream ss;
+        BeginChildrenAscii(ss, indent);
+        s.PutString(ss.str());
+    }
+}
+
+void FBX::Node::DumpChildren(
+    Assimp::StreamWriterLE& s,
+    bool binary, int indent
+) {
+    if (binary) {
+        DumpChildrenBinary(s);
+    } else {
+        std::ostringstream ss;
+        DumpChildrenAscii(ss, indent);
+        s.PutString(ss.str());
+    }
+}
+
+void FBX::Node::End(
+    Assimp::StreamWriterLE &s,
+    bool binary, int indent,
+    bool has_children
+) {
+    if (binary) {
+        EndBinary(s, has_children);
+    } else {
+        std::ostringstream ss;
+        EndAscii(ss, indent, has_children);
+        s.PutString(ss.str());
+    }
+}
+
+
+// public member functions for writing to binary fbx
+
+void FBX::Node::DumpBinary(Assimp::StreamWriterLE &s)
 {
     // write header section (with placeholders for some things)
-    Begin(s);
+    BeginBinary(s);
 
     // write properties
-    DumpProperties(s);
+    DumpPropertiesBinary(s);
 
     // go back and fill in property related placeholders
-    EndProperties(s, properties.size());
+    EndPropertiesBinary(s, properties.size());
 
     // write children
-    DumpChildren(s);
+    DumpChildrenBinary(s);
 
     // finish, filling in end offset placeholder
-    End(s, force_has_children || !children.empty());
+    EndBinary(s, force_has_children || !children.empty());
 }
 
-void FBX::Node::Begin(Assimp::StreamWriterLE &s)
+
+// public member functions for writing to ascii fbx
+
+void FBX::Node::DumpAscii(std::ostream &s, int indent)
+{
+    // write name
+    BeginAscii(s, indent);
+
+    // write properties
+    DumpPropertiesAscii(s, indent);
+
+    if (force_has_children || !children.empty()) {
+        // begin children (with a '{')
+        BeginChildrenAscii(s, indent + 1);
+        // write children
+        DumpChildrenAscii(s, indent + 1);
+    }
+
+    // finish (also closing the children bracket '}')
+    EndAscii(s, indent, force_has_children || !children.empty());
+}
+
+
+// private member functions for low-level writing to fbx
+
+void FBX::Node::BeginBinary(Assimp::StreamWriterLE &s)
 {
     // remember start pos so we can come back and write the end pos
     this->start_pos = s.Tell();
@@ -189,26 +334,14 @@ void FBX::Node::Begin(Assimp::StreamWriterLE &s)
     this->property_start = s.Tell();
 }
 
-void FBX::Node::DumpProperties(Assimp::StreamWriterLE& s)
+void FBX::Node::DumpPropertiesBinary(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);
+        p.DumpBinary(s);
     }
 }
 
-void FBX::Node::EndProperties(Assimp::StreamWriterLE &s)
-{
-    EndProperties(s, properties.size());
-}
-
-void FBX::Node::EndProperties(
+void FBX::Node::EndPropertiesBinary(
     Assimp::StreamWriterLE &s,
     size_t num_properties
 ) {
@@ -222,7 +355,14 @@ void FBX::Node::EndProperties(
     s.Seek(pos);
 }
 
-void FBX::Node::End(
+void FBX::Node::DumpChildrenBinary(Assimp::StreamWriterLE& s)
+{
+    for (FBX::Node& child : children) {
+        child.DumpBinary(s);
+    }
+}
+
+void FBX::Node::EndBinary(
     Assimp::StreamWriterLE &s,
     bool has_children
 ) {
@@ -237,48 +377,192 @@ void FBX::Node::End(
 }
 
 
-// static member functions
+void FBX::Node::BeginAscii(std::ostream& s, int indent)
+{
+    s << '\n';
+    for (int i = 0; i < indent; ++i) { s << '\t'; }
+    s << name << ": ";
+}
 
-// 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.
+void FBX::Node::DumpPropertiesAscii(std::ostream &s, int indent)
+{
+    for (size_t i = 0; i < properties.size(); ++i) {
+        if (i > 0) { s << ", "; }
+        properties[i].DumpAscii(s, indent);
+    }
+}
+
+void FBX::Node::BeginChildrenAscii(std::ostream& s, int indent)
+{
+    // only call this if there are actually children
+    s << " {";
+    (void)indent;
+}
+
+void FBX::Node::DumpChildrenAscii(std::ostream& s, int indent)
+{
+    // children will need a lot of padding and corralling
+    if (children.size() || force_has_children) {
+        for (size_t i = 0; i < children.size(); ++i) {
+            // no compression in ascii files, so skip this node if it exists
+            if (children[i].name == "EncryptionType") { continue; }
+            // the child can dump itself
+            children[i].DumpAscii(s, indent);
+        }
+    }
+}
+
+void FBX::Node::EndAscii(std::ostream& s, int indent, bool has_children)
+{
+    if (!has_children) { return; } // nothing to do
+    s << '\n';
+    for (int i = 0; i < indent; ++i) { s << '\t'; }
+    s << "}";
+}
+
+// private helpers for static member functions
+
+// ascii property node from vector of doubles
+void FBX::Node::WritePropertyNodeAscii(
+    const std::string& name,
+    const std::vector<double>& v,
+    Assimp::StreamWriterLE& s,
+    int indent
+){
+    char buffer[32];
+    FBX::Node node(name);
+    node.Begin(s, false, indent);
+    std::string vsize = std::to_string(v.size());
+    // *<size> {
+    s.PutChar('*'); s.PutString(vsize); s.PutString(" {\n");
+    // indent + 1
+    for (int i = 0; i < indent + 1; ++i) { s.PutChar('\t'); }
+    // a: value,value,value,...
+    s.PutString("a: ");
+    int count = 0;
+    for (size_t i = 0; i < v.size(); ++i) {
+        if (i > 0) { s.PutChar(','); }
+        int len = snprintf(buffer, sizeof(buffer), "%f", v[i]);
+        count += len;
+        if (count > 2048) { s.PutChar('\n'); count = 0; }
+        if (len < 0 || len > 31) {
+            // this should never happen
+            throw DeadlyExportError("failed to convert double to string");
+        }
+        for (int j = 0; j < len; ++j) { s.PutChar(buffer[j]); }
+    }
+    // }
+    s.PutChar('\n');
+    for (int i = 0; i < indent; ++i) { s.PutChar('\t'); }
+    s.PutChar('}'); s.PutChar(' ');
+    node.End(s, false, indent, false);
+}
+
+// ascii property node from vector of int32_t
+void FBX::Node::WritePropertyNodeAscii(
+    const std::string& name,
+    const std::vector<int32_t>& v,
+    Assimp::StreamWriterLE& s,
+    int indent
+){
+    char buffer[32];
+    FBX::Node node(name);
+    node.Begin(s, false, indent);
+    std::string vsize = std::to_string(v.size());
+    // *<size> {
+    s.PutChar('*'); s.PutString(vsize); s.PutString(" {\n");
+    // indent + 1
+    for (int i = 0; i < indent + 1; ++i) { s.PutChar('\t'); }
+    // a: value,value,value,...
+    s.PutString("a: ");
+    int count = 0;
+    for (size_t i = 0; i < v.size(); ++i) {
+        if (i > 0) { s.PutChar(','); }
+        int len = snprintf(buffer, sizeof(buffer), "%d", v[i]);
+        count += len;
+        if (count > 2048) { s.PutChar('\n'); count = 0; }
+        if (len < 0 || len > 31) {
+            // this should never happen
+            throw DeadlyExportError("failed to convert double to string");
+        }
+        for (int j = 0; j < len; ++j) { s.PutChar(buffer[j]); }
+    }
+    // }
+    s.PutChar('\n');
+    for (int i = 0; i < indent; ++i) { s.PutChar('\t'); }
+    s.PutChar('}'); s.PutChar(' ');
+    node.End(s, false, indent, false);
+}
+
+// binary property node from vector of doubles
 // TODO: optional zip compression!
-void FBX::Node::WritePropertyNode(
+void FBX::Node::WritePropertyNodeBinary(
     const std::string& name,
     const std::vector<double>& v,
     Assimp::StreamWriterLE& s
 ){
-    Node node(name);
-    node.Begin(s);
+    FBX::Node node(name);
+    node.BeginBinary(s);
     s.PutU1('d');
     s.PutU4(uint32_t(v.size())); // number of elements
     s.PutU4(0); // no encoding (1 would be zip-compressed)
     s.PutU4(uint32_t(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);
+    node.EndPropertiesBinary(s, 1);
+    node.EndBinary(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.
+// binary property node from vector of int32_t
 // TODO: optional zip compression!
-void FBX::Node::WritePropertyNode(
+void FBX::Node::WritePropertyNodeBinary(
     const std::string& name,
     const std::vector<int32_t>& v,
     Assimp::StreamWriterLE& s
 ){
-    Node node(name);
-    node.Begin(s);
+    FBX::Node node(name);
+    node.BeginBinary(s);
     s.PutU1('i');
     s.PutU4(uint32_t(v.size())); // number of elements
     s.PutU4(0); // no encoding (1 would be zip-compressed)
     s.PutU4(uint32_t(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);
+    node.EndPropertiesBinary(s, 1);
+    node.EndBinary(s, false);
 }
 
+// public 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.
+void FBX::Node::WritePropertyNode(
+    const std::string& name,
+    const std::vector<double>& v,
+    Assimp::StreamWriterLE& s,
+    bool binary, int indent
+){
+    if (binary) {
+        FBX::Node::WritePropertyNodeBinary(name, v, s);
+    } else {
+        FBX::Node::WritePropertyNodeAscii(name, v, s, indent);
+    }
+}
+
+// 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.
+void FBX::Node::WritePropertyNode(
+    const std::string& name,
+    const std::vector<int32_t>& v,
+    Assimp::StreamWriterLE& s,
+    bool binary, int indent
+){
+    if (binary) {
+        FBX::Node::WritePropertyNodeBinary(name, v, s);
+    } else {
+        FBX::Node::WritePropertyNodeAscii(name, v, s, indent);
+    }
+}
 
 #endif // ASSIMP_BUILD_NO_FBX_EXPORTER
 #endif // ASSIMP_BUILD_NO_EXPORT

+ 69 - 12
code/FBXExportNode.h

@@ -142,19 +142,48 @@ public: // support specifically for dealing with Properties70 nodes
 
 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);
+    // write the full node to the given file or stream
+    void Dump(
+        std::shared_ptr<Assimp::IOStream> outfile,
+        bool binary, int indent
+    );
+    void Dump(Assimp::StreamWriterLE &s, bool binary, int indent);
 
     // 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);
+    void Begin(Assimp::StreamWriterLE &s, bool binary, int indent);
+    void DumpProperties(Assimp::StreamWriterLE& s, bool binary, int indent);
+    void EndProperties(Assimp::StreamWriterLE &s, bool binary, int indent);
+    void EndProperties(
+        Assimp::StreamWriterLE &s, bool binary, int indent,
+        size_t num_properties
+    );
+    void BeginChildren(Assimp::StreamWriterLE &s, bool binary, int indent);
+    void DumpChildren(Assimp::StreamWriterLE& s, bool binary, int indent);
+    void End(
+        Assimp::StreamWriterLE &s, bool binary, int indent,
+        bool has_children
+    );
+
+private: // internal functions used for writing
+
+    void DumpBinary(Assimp::StreamWriterLE &s);
+    void DumpAscii(Assimp::StreamWriterLE &s, int indent);
+    void DumpAscii(std::ostream &s, int indent);
+
+    void BeginBinary(Assimp::StreamWriterLE &s);
+    void DumpPropertiesBinary(Assimp::StreamWriterLE& s);
+    void EndPropertiesBinary(Assimp::StreamWriterLE &s);
+    void EndPropertiesBinary(Assimp::StreamWriterLE &s, size_t num_properties);
+    void DumpChildrenBinary(Assimp::StreamWriterLE& s);
+    void EndBinary(Assimp::StreamWriterLE &s, bool has_children);
+
+    void BeginAscii(std::ostream &s, int indent);
+    void DumpPropertiesAscii(std::ostream &s, int indent);
+    void BeginChildrenAscii(std::ostream &s, int indent);
+    void DumpChildrenAscii(std::ostream &s, int indent);
+    void EndAscii(std::ostream &s, int indent, bool has_children);
 
 private: // data used for binary dumps
     size_t start_pos; // starting position in stream
@@ -169,11 +198,12 @@ public: // static member functions
     static void WritePropertyNode(
         const std::string& name,
         const T value,
-        Assimp::StreamWriterLE& s
+        Assimp::StreamWriterLE& s,
+        bool binary, int indent
     ) {
         FBX::Property p(value);
         FBX::Node node(name, p);
-        node.Dump(s);
+        node.Dump(s, binary, indent);
     }
 
     // convenience function to create and write a property node,
@@ -182,17 +212,44 @@ public: // static member functions
     static void WritePropertyNode(
         const std::string& name,
         const std::vector<double>& v,
-        Assimp::StreamWriterLE& s
+        Assimp::StreamWriterLE& s,
+        bool binary, int indent
     );
 
     // 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,
+        bool binary, int indent
+    );
+
+private: // static helper functions
+    static void WritePropertyNodeAscii(
+        const std::string& name,
+        const std::vector<double>& v,
+        Assimp::StreamWriterLE& s,
+        int indent
+    );
+    static void WritePropertyNodeAscii(
+        const std::string& name,
+        const std::vector<int32_t>& v,
+        Assimp::StreamWriterLE& s,
+        int indent
+    );
+    static void WritePropertyNodeBinary(
+        const std::string& name,
+        const std::vector<double>& v,
+        Assimp::StreamWriterLE& s
+    );
+    static void WritePropertyNodeBinary(
         const std::string& name,
         const std::vector<int32_t>& v,
         Assimp::StreamWriterLE& s
     );
+
 };
 
 

+ 141 - 14
code/FBXExportProperty.cpp

@@ -48,7 +48,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include <string>
 #include <vector>
-#include <sstream> // stringstream
+#include <ostream>
+#include <locale>
+#include <sstream> // ostringstream
 
 
 // constructors for single element properties
@@ -164,18 +166,18 @@ size_t FBX::Property::size()
     }
 }
 
-void FBX::Property::Dump(Assimp::StreamWriterLE &s)
+void FBX::Property::DumpBinary(Assimp::StreamWriterLE &s)
 {
     s.PutU1(type);
-    uint8_t* d;
+    uint8_t* d = data.data();
     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 'C': s.PutU1(*(reinterpret_cast<uint8_t*>(d))); return;
+    case 'Y': s.PutI2(*(reinterpret_cast<int16_t*>(d))); return;
+    case 'I': s.PutI4(*(reinterpret_cast<int32_t*>(d))); return;
+    case 'F': s.PutF4(*(reinterpret_cast<float*>(d))); return;
+    case 'D': s.PutF8(*(reinterpret_cast<double*>(d))); return;
+    case 'L': s.PutI8(*(reinterpret_cast<int64_t*>(d))); return;
     case 'S':
     case 'R':
         s.PutU4(uint32_t(data.size()));
@@ -187,7 +189,6 @@ void FBX::Property::Dump(Assimp::StreamWriterLE &s)
         s.PutU4(0); // no encoding (1 would be zip-compressed)
         // TODO: compress if large?
         s.PutU4(uint32_t(data.size())); // data size
-        d = data.data();
         for (size_t i = 0; i < N; ++i) {
             s.PutI4((reinterpret_cast<int32_t*>(d))[i]);
         }
@@ -198,7 +199,6 @@ void FBX::Property::Dump(Assimp::StreamWriterLE &s)
         s.PutU4(0); // no encoding (1 would be zip-compressed)
         // TODO: compress if large?
         s.PutU4(uint32_t(data.size())); // data size
-        d = data.data();
         for (size_t i = 0; i < N; ++i) {
             s.PutI8((reinterpret_cast<int64_t*>(d))[i]);
         }
@@ -209,7 +209,6 @@ void FBX::Property::Dump(Assimp::StreamWriterLE &s)
         s.PutU4(0); // no encoding (1 would be zip-compressed)
         // TODO: compress if large?
         s.PutU4(uint32_t(data.size())); // data size
-        d = data.data();
         for (size_t i = 0; i < N; ++i) {
             s.PutF4((reinterpret_cast<float*>(d))[i]);
         }
@@ -220,18 +219,146 @@ void FBX::Property::Dump(Assimp::StreamWriterLE &s)
         s.PutU4(0); // no encoding (1 would be zip-compressed)
         // TODO: compress if large?
         s.PutU4(uint32_t(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;
+        std::ostringstream err;
         err << "Tried to dump property with invalid type '";
         err << type << "'!";
         throw DeadlyExportError(err.str());
     }
 }
 
+void FBX::Property::DumpAscii(Assimp::StreamWriterLE &outstream, int indent)
+{
+    std::ostringstream ss;
+    ss.imbue(std::locale::classic());
+    ss.precision(15); // this seems to match official FBX SDK exports
+    DumpAscii(ss, indent);
+    outstream.PutString(ss.str());
+}
+
+void FBX::Property::DumpAscii(std::ostream& s, int indent)
+{
+    // no writing type... or anything. just shove it into the stream.
+    uint8_t* d = data.data();
+    size_t N;
+    size_t swap = data.size();
+    size_t count = 0;
+    switch (type) {
+    case 'C':
+        if (*(reinterpret_cast<uint8_t*>(d))) { s << 'T'; }
+        else { s << 'F'; }
+        return;
+    case 'Y': s << *(reinterpret_cast<int16_t*>(d)); return;
+    case 'I': s << *(reinterpret_cast<int32_t*>(d)); return;
+    case 'F': s << *(reinterpret_cast<float*>(d)); return;
+    case 'D': s << *(reinterpret_cast<double*>(d)); return;
+    case 'L': s << *(reinterpret_cast<int64_t*>(d)); return;
+    case 'S':
+        // first search to see if it has "\x00\x01" in it -
+        // which separates fields which are reversed in the ascii version.
+        // yeah.
+        // FBX, yeah.
+        for (size_t i = 0; i < data.size(); ++i) {
+            if (data[i] == '\0') {
+                swap = i;
+                break;
+            }
+        }
+    case 'R':
+        s << '"';
+        // we might as well check this now,
+        // probably it will never happen
+        for (size_t i = 0; i < data.size(); ++i) {
+            char c = data[i];
+            if (c == '"') {
+                throw runtime_error("can't handle quotes in property string");
+            }
+        }
+        // first write the SWAPPED member (if any)
+        for (size_t i = swap + 2; i < data.size(); ++i) {
+            char c = data[i];
+            s << c;
+        }
+        // then a separator
+        if (swap != data.size()) {
+            s << "::";
+        }
+        // then the initial member
+        for (size_t i = 0; i < swap; ++i) {
+            char c = data[i];
+            s << c;
+        }
+        s << '"';
+        return;
+    case 'i':
+        N = data.size() / 4; // number of elements
+        s << '*' << N << " {\n";
+        for (int i = 0; i < indent + 1; ++i) { s << '\t'; }
+        s << "a: ";
+        for (size_t i = 0; i < N; ++i) {
+            if (i > 0) { s << ','; }
+            if (count++ > 120) { s << '\n'; count = 0; }
+            s << (reinterpret_cast<int32_t*>(d))[i];
+        }
+        s << '\n';
+        for (int i = 0; i < indent; ++i) { s << '\t'; }
+        s << "} ";
+        return;
+    case 'l':
+        N = data.size() / 8;
+        s << '*' << N << " {\n";
+        for (int i = 0; i < indent + 1; ++i) { s << '\t'; }
+        s << "a: ";
+        for (size_t i = 0; i < N; ++i) {
+            if (i > 0) { s << ','; }
+            if (count++ > 120) { s << '\n'; count = 0; }
+            s << (reinterpret_cast<int64_t*>(d))[i];
+        }
+        s << '\n';
+        for (int i = 0; i < indent; ++i) { s << '\t'; }
+        s << "} ";
+        return;
+    case 'f':
+        N = data.size() / 4;
+        s << '*' << N << " {\n";
+        for (int i = 0; i < indent + 1; ++i) { s << '\t'; }
+        s << "a: ";
+        for (size_t i = 0; i < N; ++i) {
+            if (i > 0) { s << ','; }
+            if (count++ > 120) { s << '\n'; count = 0; }
+            s << (reinterpret_cast<float*>(d))[i];
+        }
+        s << '\n';
+        for (int i = 0; i < indent; ++i) { s << '\t'; }
+        s << "} ";
+        return;
+    case 'd':
+        N = data.size() / 8;
+        s << '*' << N << " {\n";
+        for (int i = 0; i < indent + 1; ++i) { s << '\t'; }
+        s << "a: ";
+        // set precision to something that can handle doubles
+        s.precision(15);
+        for (size_t i = 0; i < N; ++i) {
+            if (i > 0) { s << ','; }
+            if (count++ > 120) { s << '\n'; count = 0; }
+            s << (reinterpret_cast<double*>(d))[i];
+        }
+        s << '\n';
+        for (int i = 0; i < indent; ++i) { s << '\t'; }
+        s << "} ";
+        return;
+    default:
+        std::ostringstream err;
+        err << "Tried to dump property with invalid type '";
+        err << type << "'!";
+        throw runtime_error(err.str());
+    }
+}
+
 #endif // ASSIMP_BUILD_NO_FBX_EXPORTER
 #endif // ASSIMP_BUILD_NO_EXPORT

+ 5 - 1
code/FBXExportProperty.h

@@ -53,6 +53,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include <string>
 #include <vector>
+#include <ostream>
 #include <type_traits> // is_void
 
 namespace FBX {
@@ -113,7 +114,10 @@ public:
     size_t size();
 
     // write this property node as binary data to the given stream
-    void Dump(Assimp::StreamWriterLE &s);
+    void DumpBinary(Assimp::StreamWriterLE &s);
+    void DumpAscii(Assimp::StreamWriterLE &s, int indent=0);
+    void DumpAscii(std::ostream &s, int indent=0);
+    // note: make sure the ostream is in classic "C" locale
 
 private:
     char type;

+ 186 - 89
code/FBXExporter.cpp

@@ -66,7 +66,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <vector>
 #include <array>
 #include <unordered_set>
-#include <iostream> // endl
 
 // RESOURCES:
 // https://code.blender.org/2013/08/fbx-binary-file-format-specification/
@@ -89,6 +88,8 @@ namespace FBX {
         "\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";
+    const std::string COMMENT_UNDERLINE =
+        ";------------------------------------------------------------------";
 }
 
 using namespace Assimp;
@@ -115,7 +116,7 @@ namespace Assimp {
     // ---------------------------------------------------------------------
     // Worker function for exporting a scene to ASCII FBX.
     // Prototyped and registered in Exporter.cpp
-    /*void ExportSceneFBXA (
+    void ExportSceneFBXA (
         const char* pFile,
         IOSystem* pIOSystem,
         const aiScene* pScene,
@@ -126,7 +127,7 @@ namespace Assimp {
 
         // perform ascii export
         exporter.ExportAscii(pFile, pIOSystem);
-    }*/ // TODO
+    }
 
 } // end of namespace Assimp
 
@@ -194,27 +195,43 @@ void FBXExporter::ExportAscii (
         );
     }
 
-    // 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 the ascii header
+    WriteAsciiHeader();
 
     // write all the sections
     WriteAllNodes();
 
+    // make sure the file ends with a newline.
+    // note: if the file is opened in text mode,
+    // this should do the right cross-platform thing.
+    outfile->Write("\n", 1, 1);
+
     // explicitly release file pointer,
     // so we don't have to rely on class destruction.
     outfile.reset();
 }
 
+void FBXExporter::WriteAsciiHeader()
+{
+    // basically just a comment at the top of the file
+    std::stringstream head;
+    head << "; FBX " << EXPORT_VERSION_STR << " project file\n";
+    head << "; Created by the Open Asset Import Library (Assimp)\n";
+    head << "; http://assimp.org\n";
+    head << "; -------------------------------------------------\n";
+    const std::string ascii_header = head.str();
+    outfile->Write(ascii_header.c_str(), ascii_header.size(), 1);
+}
+
+void FBXExporter::WriteAsciiSectionHeader(const std::string& title)
+{
+    StreamWriterLE outstream(outfile);
+    std::stringstream s;
+    s << "\n\n; " << title << '\n';
+    s << FBX::COMMENT_UNDERLINE << "\n";
+    outstream.PutString(s.str());
+}
+
 void FBXExporter::WriteBinaryHeader()
 {
     // first a specific sequence of 23 bytes, always the same
@@ -294,28 +311,39 @@ void FBXExporter::WriteAllNodes ()
 //FBXHeaderExtension top-level node
 void FBXExporter::WriteHeaderExtension ()
 {
+    if (!binary) {
+        // no title, follows directly from the top comment
+    }
     FBX::Node n("FBXHeaderExtension");
     StreamWriterLE outstream(outfile);
+    int indent = 0;
 
     // begin node
-    n.Begin(outstream);
+    n.Begin(outstream, binary, indent);
 
     // write properties
     // (none)
 
     // finish properties
-    n.EndProperties(outstream, 0);
+    n.EndProperties(outstream, binary, indent, 0);
+
+    // begin children
+    n.BeginChildren(outstream, binary, indent);
+
+    indent = 1;
 
     // write child nodes
     FBX::Node::WritePropertyNode(
-        "FBXHeaderVersion", int32_t(1003), outstream
+        "FBXHeaderVersion", int32_t(1003), outstream, binary, indent
     );
     FBX::Node::WritePropertyNode(
-        "FBXVersion", int32_t(EXPORT_VERSION_INT), outstream
-    );
-    FBX::Node::WritePropertyNode(
-        "EncryptionType", int32_t(0), outstream
+        "FBXVersion", int32_t(EXPORT_VERSION_INT), outstream, binary, indent
     );
+    if (binary) {
+        FBX::Node::WritePropertyNode(
+            "EncryptionType", int32_t(0), outstream, binary, indent
+        );
+    }
 
     FBX::Node CreationTimeStamp("CreationTimeStamp");
     time_t rawtime;
@@ -329,36 +357,50 @@ void FBXExporter::WriteHeaderExtension ()
     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);
+    CreationTimeStamp.Dump(outstream, binary, indent);
 
     std::stringstream creator;
     creator << "Open Asset Import Library (Assimp) " << aiGetVersionMajor()
             << "." << aiGetVersionMinor() << "." << aiGetVersionRevision();
-    FBX::Node::WritePropertyNode("Creator", creator.str(), outstream);
+    FBX::Node::WritePropertyNode(
+        "Creator", creator.str(), outstream, binary, indent
+    );
 
-    FBX::Node sceneinfo("SceneInfo");
+    //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);
+    //sceneinfo.Dump(outstream, binary, indent);
+
+    indent = 0;
 
     // finish node
-    n.End(outstream, true);
+    n.End(outstream, binary, indent, true);
 
     // that's it for FBXHeaderExtension...
+    if (!binary) { return; }
 
     // 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);
+    FBX::Node::WritePropertyNode(
+        "FileId", raw, outstream, binary, indent
+    );
+    FBX::Node::WritePropertyNode(
+        "CreationTime", GENERIC_CTIME, outstream, binary, indent
+    );
+    FBX::Node::WritePropertyNode(
+        "Creator", creator.str(), outstream, binary, indent
+    );
 }
 
 void FBXExporter::WriteGlobalSettings ()
 {
+    if (!binary) {
+        // no title, follows directly from the header extension
+    }
     FBX::Node gs("GlobalSettings");
     gs.AddChild("Version", int32_t(1000));
 
@@ -385,11 +427,15 @@ void FBXExporter::WriteGlobalSettings ()
     p.AddP70int("CurrentTimeMarker", -1);
     gs.AddChild(p);
 
-    gs.Dump(outfile);
+    gs.Dump(outfile, binary, 0);
 }
 
 void FBXExporter::WriteDocuments ()
 {
+    if (!binary) {
+        WriteAsciiSectionHeader("Documents Description");
+    }
+    
     // not sure what the use of multiple documents would be,
     // or whether any end-application supports it
     FBX::Node docs("Documents");
@@ -411,16 +457,19 @@ void FBXExporter::WriteDocuments ()
     doc.AddChild("RootNode", int64_t(0));
 
     docs.AddChild(doc);
-    docs.Dump(outfile);
+    docs.Dump(outfile, binary, 0);
 }
 
 void FBXExporter::WriteReferences ()
 {
+    if (!binary) {
+        WriteAsciiSectionHeader("Document References");
+    }
     // always empty for now.
     // not really sure what this is for.
     FBX::Node n("References");
     n.force_has_children = true;
-    n.Dump(outfile);
+    n.Dump(outfile, binary, 0);
 }
 
 
@@ -469,9 +518,6 @@ size_t count_images(const aiScene* scene) {
             }
         }
     }
-    //for (auto &s : images) {
-    //    std::cout << "found image: " << s << std::endl;
-    //}
     return images.size();
 }
 
@@ -511,6 +557,11 @@ void FBXExporter::WriteDefinitions ()
     // determining how many of each type of object there are
     // and specifying the base properties to use when otherwise unspecified.
 
+    // ascii section header
+    if (!binary) {
+        WriteAsciiSectionHeader("Object definitions");
+    }
+
     // we need to count the objects
     int32_t count;
     int32_t total_count = 0;
@@ -890,7 +941,7 @@ void FBXExporter::WriteDefinitions ()
     defs.AddChild("Version", int32_t(100));
     defs.AddChild("Count", int32_t(total_count));
     for (auto &n : object_nodes) { defs.AddChild(n); }
-    defs.Dump(outfile);
+    defs.Dump(outfile, binary, 0);
 }
 
 
@@ -936,14 +987,20 @@ int64_t to_ktime(double ticks, const aiAnimation* anim) {
 
 void FBXExporter::WriteObjects ()
 {
+    if (!binary) {
+        WriteAsciiSectionHeader("Object properties");
+    }
     // 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);
+    int indent = 0;
+    object_node.Begin(outstream, binary, indent);
+    object_node.EndProperties(outstream, binary, indent);
+    object_node.BeginChildren(outstream, binary, indent);
 
     // geometry (aiMesh)
     mesh_uids.clear();
+    indent = 1;
     for (size_t mi = 0; mi < mScene->mNumMeshes; ++mi) {
         // it's all about this mesh
         aiMesh* m = mScene->mMeshes[mi];
@@ -955,9 +1012,11 @@ void FBXExporter::WriteObjects ()
         n.AddProperty(uid);
         n.AddProperty(FBX::SEPARATOR + "Geometry");
         n.AddProperty("Mesh");
-        n.Begin(outstream);
-        n.DumpProperties(outstream);
-        n.EndProperties(outstream);
+        n.Begin(outstream, binary, indent);
+        n.DumpProperties(outstream, binary, indent);
+        n.EndProperties(outstream, binary, indent);
+        n.BeginChildren(outstream, binary, indent);
+        indent = 2;
 
         // output vertex data - each vertex should be unique (probably)
         std::vector<double> flattened_vertices;
@@ -981,7 +1040,7 @@ void FBXExporter::WriteObjects ()
             }
         }
         FBX::Node::WritePropertyNode(
-            "Vertices", flattened_vertices, outstream
+            "Vertices", flattened_vertices, outstream, binary, indent
         );
 
         // output polygon data as a flattened array of vertex indices.
@@ -997,30 +1056,38 @@ void FBXExporter::WriteObjects ()
             );
         }
         FBX::Node::WritePropertyNode(
-            "PolygonVertexIndex", polygon_data, outstream
+            "PolygonVertexIndex", polygon_data, outstream, binary, indent
         );
 
         // 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
+            "GeometryVersion", int32_t(124), outstream, binary, indent
         );
 
         // normals, if any
         if (m->HasNormals()) {
             FBX::Node normals("LayerElementNormal", 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);
+            normals.Begin(outstream, binary, indent);
+            normals.DumpProperties(outstream, binary, indent);
+            normals.EndProperties(outstream, binary, indent);
+            normals.BeginChildren(outstream, binary, indent);
+            indent = 3;
+            FBX::Node::WritePropertyNode(
+                "Version", int32_t(101), outstream, binary, indent
+            );
+            FBX::Node::WritePropertyNode(
+                "Name", "", outstream, binary, indent
+            );
             FBX::Node::WritePropertyNode(
-                "MappingInformationType", "ByPolygonVertex", outstream
+                "MappingInformationType", "ByPolygonVertex",
+                outstream, binary, indent
             );
             // TODO: vertex-normals or indexed normals when appropriate
             FBX::Node::WritePropertyNode(
-                "ReferenceInformationType", "Direct", outstream
+                "ReferenceInformationType", "Direct",
+                outstream, binary, indent
             );
             std::vector<double> normal_data;
             normal_data.reserve(3 * polygon_data.size());
@@ -1033,10 +1100,13 @@ void FBXExporter::WriteObjects ()
                     normal_data.push_back(n.z);
                 }
             }
-            FBX::Node::WritePropertyNode("Normals", normal_data, outstream);
+            FBX::Node::WritePropertyNode(
+                "Normals", normal_data, outstream, binary, indent
+            );
             // 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);
+            indent = 2;
+            normals.End(outstream, binary, indent, true);
         }
 
         // uvs, if any
@@ -1057,18 +1127,26 @@ void FBXExporter::WriteObjects ()
                 DefaultLogger::get()->warn(err.str());
             }
             FBX::Node uv("LayerElementUV", int32_t(uvi));
-            uv.Begin(outstream);
-            uv.DumpProperties(outstream);
-            uv.EndProperties(outstream);
-            FBX::Node::WritePropertyNode("Version", int32_t(101), outstream);
+            uv.Begin(outstream, binary, indent);
+            uv.DumpProperties(outstream, binary, indent);
+            uv.EndProperties(outstream, binary, indent);
+            uv.BeginChildren(outstream, binary, indent);
+            indent = 3;
+            FBX::Node::WritePropertyNode(
+                "Version", int32_t(101), outstream, binary, indent
+            );
             // 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
+                "Name", "", outstream, binary, indent
             );
             FBX::Node::WritePropertyNode(
-                "ReferenceInformationType", "IndexToDirect", outstream
+                "MappingInformationType", "ByPolygonVertex",
+                outstream, binary, indent
+            );
+            FBX::Node::WritePropertyNode(
+                "ReferenceInformationType", "IndexToDirect",
+                outstream, binary, indent
             );
 
             std::vector<double> uv_data;
@@ -1093,9 +1171,14 @@ void FBXExporter::WriteObjects ()
                     }
                 }
             }
-            FBX::Node::WritePropertyNode("UV", uv_data, outstream);
-            FBX::Node::WritePropertyNode("UVIndex", uv_indices, outstream);
-            uv.End(outstream, true);
+            FBX::Node::WritePropertyNode(
+                "UV", uv_data, outstream, binary, indent
+            );
+            FBX::Node::WritePropertyNode(
+                "UVIndex", uv_indices, outstream, binary, indent
+            );
+            indent = 2;
+            uv.End(outstream, binary, indent, true);
         }
 
         // i'm not really sure why this material section exists,
@@ -1108,7 +1191,7 @@ void FBXExporter::WriteObjects ()
         mat.AddChild("ReferenceInformationType", "IndexToDirect");
         std::vector<int32_t> mat_indices = {0};
         mat.AddChild("Materials", mat_indices);
-        mat.Dump(outstream);
+        mat.Dump(outstream, binary, indent);
 
         // finally we have the layer specifications,
         // which select the normals / UV set / etc to use.
@@ -1127,10 +1210,11 @@ void FBXExporter::WriteObjects ()
         le.AddChild("Type", "LayerElementUV");
         le.AddChild("TypedIndex", int32_t(0));
         layer.AddChild(le);
-        layer.Dump(outstream);
+        layer.Dump(outstream, binary, indent);
 
         // finish the node record
-        n.End(outstream, true);
+        indent = 1;
+        n.End(outstream, binary, indent, true);
     }
 
     // aiMaterial
@@ -1274,7 +1358,7 @@ void FBXExporter::WriteObjects ()
 
         n.AddChild(p);
 
-        n.Dump(outstream);
+        n.Dump(outstream, binary, indent);
     }
 
     // we need to look up all the images we're using,
@@ -1322,7 +1406,7 @@ void FBXExporter::WriteObjects ()
         n.AddChild("UseMipMap", int32_t(0));
         n.AddChild("Filename", path);
         n.AddChild("RelativeFilename", path);
-        n.Dump(outstream);
+        n.Dump(outstream, binary, indent);
     }
 
     // Textures
@@ -1441,7 +1525,7 @@ void FBXExporter::WriteObjects ()
             tnode.AddChild(
                 "Cropping", int32_t(0), int32_t(0), int32_t(0), int32_t(0)
             );
-            tnode.Dump(outstream);
+            tnode.Dump(outstream, binary, indent);
         }
     }
 
@@ -1594,7 +1678,7 @@ void FBXExporter::WriteObjects ()
         // "acuracy"... this is not a typo....
         dnode.AddChild("Link_DeformAcuracy", double(50));
         dnode.AddChild("SkinningType", "Linear"); // TODO: other modes?
-        dnode.Dump(outstream);
+        dnode.Dump(outstream, binary, indent);
 
         // connect it
         connections.emplace_back("C", "OO", deformer_uid, mesh_uids[mi]);
@@ -1738,7 +1822,7 @@ void FBXExporter::WriteObjects ()
             // there's not really any way around this at the moment.
 
             // done
-            sdnode.Dump(outstream);
+            sdnode.Dump(outstream, binary, indent);
 
             // lastly, connect to the parent deformer
             connections.emplace_back(
@@ -1856,7 +1940,7 @@ void FBXExporter::WriteObjects ()
         }
 
         // now write it
-        bpnode.Dump(outstream);
+        bpnode.Dump(outstream, binary, indent);
     }*/
 
     // TODO: cameras, lights
@@ -1916,7 +2000,7 @@ void FBXExporter::WriteObjects ()
         // this node absurdly always pretends it has children
         // (in this case it does, but just in case...)
         asnode.force_has_children = true;
-        asnode.Dump(outstream);
+        asnode.Dump(outstream, binary, indent);
 
         // note: animation stacks are not connected to anything
     }
@@ -1931,7 +2015,7 @@ void FBXExporter::WriteObjects ()
 
         // this node absurdly always pretends it has children
         alnode.force_has_children = true;
-        alnode.Dump(outstream);
+        alnode.Dump(outstream, binary, indent);
 
         // connect to the relevant animstack
         connections.emplace_back(
@@ -2048,7 +2132,8 @@ void FBXExporter::WriteObjects ()
         }
     }
 
-    object_node.End(outstream, true);
+    indent = 0;
+    object_node.End(outstream, binary, indent, true);
 }
 
 // convenience map of magic node name strings to FBX properties,
@@ -2074,13 +2159,14 @@ const std::map<std::string,std::pair<std::string,char>> transform_types = {
 };
 
 // write a single model node to the stream
-void WriteModelNode(
+void FBXExporter::WriteModelNode(
     StreamWriterLE& outstream,
+    bool binary,
     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
+    TransformInheritance inherit_type
 ){
     const aiVector3D zero = {0, 0, 0};
     const aiVector3D one = {1, 1, 1};
@@ -2148,7 +2234,7 @@ void WriteModelNode(
     m.AddChild("Shading", Property(true));
     m.AddChild("Culling", Property("CullingOff"));
 
-    m.Dump(outstream);
+    m.Dump(outstream, binary, 1);
 }
 
 // wrapper for WriteModelNodes to create and pass a blank transform chain
@@ -2249,9 +2335,13 @@ void FBXExporter::WriteModelNodes(
             node_uid
         );
         // write model node
-        WriteModelNode(outstream, node, node_uid, "Mesh", transform_chain);
+        WriteModelNode(
+            outstream, binary, node, node_uid, "Mesh", transform_chain
+        );
     } else if (limbnodes.count(node)) {
-        WriteModelNode(outstream, node, node_uid, "LimbNode", transform_chain);
+        WriteModelNode(
+            outstream, binary, 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");
@@ -2259,12 +2349,14 @@ void FBXExporter::WriteModelNodes(
             node_attribute_uid, FBX::SEPARATOR + "NodeAttribute", "LimbNode"
         );
         na.AddChild("TypeFlags", Property("Skeleton"));
-        na.Dump(outstream);
+        na.Dump(outstream, binary, 1);
         // and connect them
         connections.emplace_back("C", "OO", node_attribute_uid, node_uid);
     } else {
         // generate a null node so we can add children to it
-        WriteModelNode(outstream, node, node_uid, "Null", transform_chain);
+        WriteModelNode(
+            outstream, binary, node, node_uid, "Null", transform_chain
+        );
     }
 
     // if more than one child mesh, make nodes for each mesh
@@ -2296,7 +2388,7 @@ void FBXExporter::WriteModelNodes(
             FBX::Node p("Properties70");
             p.AddP70enum("InheritType", 1);
             m.AddChild(p);
-            m.Dump(outstream);
+            m.Dump(outstream, binary, 1);
         }
     }
 
@@ -2325,7 +2417,7 @@ void FBXExporter::WriteAnimationCurveNode(
     p.AddP70numberA("d|Y", default_value.y);
     p.AddP70numberA("d|Z", default_value.z);
     n.AddChild(p);
-    n.Dump(outstream);
+    n.Dump(outstream, binary, 1);
     // connect to layer
     this->connections.emplace_back("C", "OO", uid, layer_uid);
     // connect to bone
@@ -2356,7 +2448,7 @@ void FBXExporter::WriteAnimationCurve(
         "KeyAttrRefCount",
         std::vector<int32_t>{static_cast<int32_t>(times.size())}
     );
-    n.Dump(outstream);
+    n.Dump(outstream, binary, 1);
     this->connections.emplace_back(
         "C", "OP", curve_uid, curvenode_uid, property_link
     );
@@ -2367,13 +2459,18 @@ void FBXExporter::WriteConnections ()
 {
     // we should have completed the connection graph already,
     // so basically just dump it here
+    if (!binary) {
+        WriteAsciiSectionHeader("Object connections");
+    }
+    // TODO: comments with names in the ascii version
     FBX::Node conn("Connections");
     StreamWriterLE outstream(outfile);
-    conn.Begin(outstream);
+    conn.Begin(outstream, binary, 0);
+    conn.BeginChildren(outstream, binary, 0);
     for (auto &n : connections) {
-        n.Dump(outstream);
+        n.Dump(outstream, binary, 1);
     }
-    conn.End(outstream, !connections.empty());
+    conn.End(outstream, binary, 0, !connections.empty());
     connections.clear();
 }
 

+ 14 - 0
code/FBXExporter.h

@@ -48,6 +48,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
 
 #include "FBXExportNode.h" // FBX::Node
+#include "FBXCommon.h" // FBX::TransformInheritance
 
 #include <assimp/types.h>
 //#include <assimp/material.h>
@@ -104,6 +105,9 @@ namespace Assimp
         void WriteBinaryHeader();
         void WriteBinaryFooter();
 
+        // ascii files have a comment at the top
+        void WriteAsciiHeader();
+
         // WriteAllNodes does the actual export.
         // It just calls all the Write<Section> methods below in order.
         void WriteAllNodes();
@@ -126,6 +130,7 @@ namespace Assimp
         // WriteTakes(); // deprecated since at least 2015 (fbx 7.4)
 
         // helpers
+        void WriteAsciiSectionHeader(const std::string& title);
         void WriteModelNodes(
             Assimp::StreamWriterLE& s,
             const aiNode* node,
@@ -139,6 +144,15 @@ namespace Assimp
             const std::unordered_set<const aiNode*>& limbnodes,
             std::vector<std::pair<std::string,aiVector3D>>& transform_chain
         );
+        void WriteModelNode( // nor this
+            StreamWriterLE& s,
+            bool binary,
+            const aiNode* node,
+            int64_t node_uid,
+            const std::string& type,
+            const std::vector<std::pair<std::string,aiVector3D>>& xfm_chain,
+            FBX::TransformInheritance ti_type=FBX::TransformInheritance_RSrs
+        );
         void WriteAnimationCurveNode(
             StreamWriterLE& outstream,
             int64_t uid,