From 826243f289cb4cdbc1c5e79dc8593ce6805a5f49 Mon Sep 17 00:00:00 2001 From: Tommy Date: Wed, 28 Mar 2018 21:40:26 +0200 Subject: [PATCH] Implement ascii FBX export. It's available under the 'fbxa' format id. --- code/Exporter.cpp | 4 +- code/FBXExportNode.cpp | 376 ++++++++++++++++++++++++++++++++----- code/FBXExportNode.h | 81 ++++++-- code/FBXExportProperty.cpp | 155 +++++++++++++-- code/FBXExportProperty.h | 6 +- code/FBXExporter.cpp | 275 ++++++++++++++++++--------- code/FBXExporter.h | 14 ++ 7 files changed, 747 insertions(+), 164 deletions(-) diff --git a/code/Exporter.cpp b/code/Exporter.cpp index 49523e658..b7478c0be 100644 --- a/code/Exporter.cpp +++ b/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 diff --git a/code/FBXExportNode.cpp b/code/FBXExportNode.cpp index 621e72f09..514d5f360 100644 --- a/code/FBXExportNode.cpp +++ b/code/FBXExportNode.cpp @@ -45,10 +45,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "FBXCommon.h" #include // StreamWriterLE +#include // DeadlyExportError #include #include +#include +#include // ostringstream #include // shared_ptr +#include // snprintf // AddP70 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 nodes to stream + +void FBX::Node::Dump( + std::shared_ptr 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::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::Dump(std::shared_ptr outfile) -{ - Assimp::StreamWriterLE outstream(outfile); - Dump(outstream); -} - -void FBX::Node::Dump(Assimp::StreamWriterLE &s) +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); + p.DumpBinary(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( +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& 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()); + // * { + 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& 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()); + // * { + 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& 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& 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& 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& 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 diff --git a/code/FBXExportNode.h b/code/FBXExportNode.h index 58d3d14ee..5ddd8c77b 100644 --- a/code/FBXExportNode.h +++ b/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 outfile); - void Dump(Assimp::StreamWriterLE &s); + // write the full node to the given file or stream + void Dump( + std::shared_ptr 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,7 +212,8 @@ public: // static member functions static void WritePropertyNode( const std::string& name, const std::vector& v, - Assimp::StreamWriterLE& s + Assimp::StreamWriterLE& s, + bool binary, int indent ); // convenience function to create and write a property node, @@ -191,8 +222,34 @@ public: // static member functions static void WritePropertyNode( const std::string& name, const std::vector& v, + Assimp::StreamWriterLE& s, + bool binary, int indent + ); + +private: // static helper functions + static void WritePropertyNodeAscii( + const std::string& name, + const std::vector& v, + Assimp::StreamWriterLE& s, + int indent + ); + static void WritePropertyNodeAscii( + const std::string& name, + const std::vector& v, + Assimp::StreamWriterLE& s, + int indent + ); + static void WritePropertyNodeBinary( + const std::string& name, + const std::vector& v, Assimp::StreamWriterLE& s ); + static void WritePropertyNodeBinary( + const std::string& name, + const std::vector& v, + Assimp::StreamWriterLE& s + ); + }; diff --git a/code/FBXExportProperty.cpp b/code/FBXExportProperty.cpp index e139bb95a..431750274 100644 --- a/code/FBXExportProperty.cpp +++ b/code/FBXExportProperty.cpp @@ -48,7 +48,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include -#include // stringstream +#include +#include +#include // 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(data.data()))); return; - case 'Y': s.PutI2(*(reinterpret_cast(data.data()))); return; - case 'I': s.PutI4(*(reinterpret_cast(data.data()))); return; - case 'F': s.PutF4(*(reinterpret_cast(data.data()))); return; - case 'D': s.PutF8(*(reinterpret_cast(data.data()))); return; - case 'L': s.PutI8(*(reinterpret_cast(data.data()))); return; + case 'C': s.PutU1(*(reinterpret_cast(d))); return; + case 'Y': s.PutI2(*(reinterpret_cast(d))); return; + case 'I': s.PutI4(*(reinterpret_cast(d))); return; + case 'F': s.PutF4(*(reinterpret_cast(d))); return; + case 'D': s.PutF8(*(reinterpret_cast(d))); return; + case 'L': s.PutI8(*(reinterpret_cast(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(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(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(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(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(d))) { s << 'T'; } + else { s << 'F'; } + return; + case 'Y': s << *(reinterpret_cast(d)); return; + case 'I': s << *(reinterpret_cast(d)); return; + case 'F': s << *(reinterpret_cast(d)); return; + case 'D': s << *(reinterpret_cast(d)); return; + case 'L': s << *(reinterpret_cast(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(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(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(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(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 diff --git a/code/FBXExportProperty.h b/code/FBXExportProperty.h index 40a020688..cb3b0113f 100644 --- a/code/FBXExportProperty.h +++ b/code/FBXExportProperty.h @@ -53,6 +53,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include #include // 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; diff --git a/code/FBXExporter.cpp b/code/FBXExporter.cpp index 46782cc2e..e8846be08 100644 --- a/code/FBXExporter.cpp +++ b/code/FBXExporter.cpp @@ -66,7 +66,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include // 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 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 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( - "MappingInformationType", "ByPolygonVertex", outstream + "Version", int32_t(101), outstream, binary, indent + ); + FBX::Node::WritePropertyNode( + "Name", "", outstream, binary, indent + ); + FBX::Node::WritePropertyNode( + "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 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 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 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> 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>& 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{static_cast(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(); } diff --git a/code/FBXExporter.h b/code/FBXExporter.h index 553cf60fe..3b9de8acb 100644 --- a/code/FBXExporter.h +++ b/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 //#include @@ -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
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& limbnodes, std::vector>& transform_chain ); + void WriteModelNode( // nor this + StreamWriterLE& s, + bool binary, + const aiNode* node, + int64_t node_uid, + const std::string& type, + const std::vector>& xfm_chain, + FBX::TransformInheritance ti_type=FBX::TransformInheritance_RSrs + ); void WriteAnimationCurveNode( StreamWriterLE& outstream, int64_t uid,