Browse Source

Merge branch 'master' into master

Kim Kulling 5 years ago
parent
commit
0c3c88270a
63 changed files with 8092 additions and 496 deletions
  1. 127 0
      .clang-format
  2. 4 0
      .gitignore
  3. 6 1
      CMakeLists.txt
  4. 1 0
      Readme.md
  5. 4 1
      appveyor.yml
  6. 9 4
      assimpTargets-debug.cmake.in
  7. 10 4
      assimpTargets-release.cmake.in
  8. 1 5
      assimpTargets.cmake.in
  9. 1 1
      code/3DS/3DSConverter.cpp
  10. 14 0
      code/CMakeLists.txt
  11. 1 1
      code/Collada/ColladaHelper.h
  12. 3 53
      code/Collada/ColladaLoader.cpp
  13. 1 4
      code/Collada/ColladaLoader.h
  14. 68 4
      code/Collada/ColladaParser.cpp
  15. 4 1
      code/Collada/ColladaParser.h
  16. 31 2
      code/Common/DefaultIOStream.cpp
  17. 69 61
      code/Common/Exporter.cpp
  18. 6 0
      code/Common/ImporterRegistry.cpp
  19. 7 0
      code/Common/PostStepRegistry.cpp
  20. 5 9
      code/Common/Version.cpp
  21. 9 9
      code/Common/scene.cpp
  22. 4 3
      code/FBX/FBXCommon.h
  23. 8 0
      code/FBX/FBXCompileConfig.h
  24. 225 113
      code/FBX/FBXConverter.cpp
  25. 55 28
      code/FBX/FBXConverter.h
  26. 37 2
      code/FBX/FBXDocument.h
  27. 7 7
      code/FBX/FBXExportNode.cpp
  28. 2 2
      code/FBX/FBXExporter.cpp
  29. 102 110
      code/FBX/FBXImporter.cpp
  30. 395 0
      code/M3D/M3DExporter.cpp
  31. 98 0
      code/M3D/M3DExporter.h
  32. 737 0
      code/M3D/M3DImporter.cpp
  33. 106 0
      code/M3D/M3DImporter.h
  34. 106 0
      code/M3D/M3DMaterials.h
  35. 4565 0
      code/M3D/m3d.h
  36. 268 0
      code/PostProcessing/ArmaturePopulate.cpp
  37. 112 0
      code/PostProcessing/ArmaturePopulate.h
  38. 1 1
      code/glTF2/glTF2AssetWriter.inl
  39. 3 1
      code/glTF2/glTF2Exporter.cpp
  40. 8 2
      contrib/irrXML/CXMLReaderImpl.h
  41. 0 2
      include/assimp/Exporter.hpp
  42. 13 8
      include/assimp/defs.h
  43. 26 10
      include/assimp/mesh.h
  44. 15 0
      include/assimp/postprocess.h
  45. 11 11
      include/assimp/scene.h
  46. 2 0
      test/CMakeLists.txt
  47. BIN
      test/models/Collada/duck.zae
  48. 544 0
      test/models/FBX/box_orphant_embedded_texture.fbx
  49. BIN
      test/models/FBX/huesitos.fbx
  50. BIN
      test/models/M3D/WusonBlitz0.m3d
  51. BIN
      test/models/M3D/WusonBlitz1.m3d
  52. BIN
      test/models/M3D/WusonBlitz2.m3d
  53. BIN
      test/models/M3D/cube_normals.m3d
  54. BIN
      test/models/M3D/cube_usemtl.m3d
  55. 33 0
      test/models/M3D/cube_with_vertexcolors.a3d
  56. BIN
      test/models/M3D/cube_with_vertexcolors.m3d
  57. BIN
      test/models/M3D/suzanne.m3d
  58. 1 1
      test/unit/Main.cpp
  59. 83 0
      test/unit/utArmaturePopulate.cpp
  60. 45 5
      test/unit/utColladaImportExport.cpp
  61. 30 29
      test/unit/utFBXImporterExporter.cpp
  62. 68 0
      test/unit/utM3DImportExport.cpp
  63. 1 1
      test/unit/utVersion.cpp

+ 127 - 0
.clang-format

@@ -0,0 +1,127 @@
+# Commented out parameters are those with the same value as base LLVM style
+# We can uncomment them if we want to change their value, or enforce the
+# chosen value in case the base style changes (last sync: Clang 6.0.1).
+---
+### General config, applies to all languages ###
+BasedOnStyle:  LLVM
+AccessModifierOffset: -4
+AlignAfterOpenBracket: DontAlign
+# AlignConsecutiveAssignments: false
+# AlignConsecutiveDeclarations: false
+# AlignEscapedNewlines: Right
+# AlignOperands:   true
+AlignTrailingComments: false
+AllowAllParametersOfDeclarationOnNextLine: false
+# AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: true
+# AllowShortLoopsOnASingleLine: false
+# AlwaysBreakAfterDefinitionReturnType: None
+# AlwaysBreakAfterReturnType: None
+# AlwaysBreakBeforeMultilineStrings: false
+# AlwaysBreakTemplateDeclarations: false
+# BinPackArguments: true
+# BinPackParameters: true
+# BraceWrapping:
+#   AfterClass:      false
+#   AfterControlStatement: false
+#   AfterEnum:       false
+#   AfterFunction:   false
+#   AfterNamespace:  false
+#   AfterObjCDeclaration: false
+#   AfterStruct:     false
+#   AfterUnion:      false
+#   AfterExternBlock: false
+#   BeforeCatch:     false
+#   BeforeElse:      false
+#   IndentBraces:    false
+#   SplitEmptyFunction: true
+#   SplitEmptyRecord: true
+#   SplitEmptyNamespace: true
+# BreakBeforeBinaryOperators: None
+# BreakBeforeBraces: Attach
+# BreakBeforeInheritanceComma: false
+BreakBeforeTernaryOperators: false
+# BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: AfterColon
+# BreakStringLiterals: true
+ColumnLimit:     0
+# CommentPragmas:  '^ IWYU pragma:'
+# CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: true
+ConstructorInitializerIndentWidth: 8
+ContinuationIndentWidth: 8
+Cpp11BracedListStyle: false
+# DerivePointerAlignment: false
+# DisableFormat:   false
+# ExperimentalAutoDetectBinPacking: false
+# FixNamespaceComments: true
+# ForEachMacros:
+#   - foreach
+#   - Q_FOREACH
+#   - BOOST_FOREACH
+# IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '".*"'
+    Priority:        1
+  - Regex:           '^<.*\.h>'
+    Priority:        2
+  - Regex:           '^<.*'
+    Priority:        3
+# IncludeIsMainRegex: '(Test)?$'
+IndentCaseLabels: true
+# IndentPPDirectives: None
+IndentWidth:     4
+# IndentWrappedFunctionNames: false
+# JavaScriptQuotes: Leave
+# JavaScriptWrapImports: true
+# KeepEmptyLinesAtTheStartOfBlocks: true
+# MacroBlockBegin: ''
+# MacroBlockEnd:   ''
+# MaxEmptyLinesToKeep: 1
+# NamespaceIndentation: None
+# PenaltyBreakAssignment: 2
+# PenaltyBreakBeforeFirstCallParameter: 19
+# PenaltyBreakComment: 300
+# PenaltyBreakFirstLessLess: 120
+# PenaltyBreakString: 1000
+# PenaltyExcessCharacter: 1000000
+# PenaltyReturnTypeOnItsOwnLine: 60
+# PointerAlignment: Right
+# RawStringFormats:
+#   - Delimiter:       pb
+#     Language:        TextProto
+#     BasedOnStyle:    google
+# ReflowComments:  true
+# SortIncludes:    true
+# SortUsingDeclarations: true
+# SpaceAfterCStyleCast: false
+# SpaceAfterTemplateKeyword: true
+# SpaceBeforeAssignmentOperators: true
+# SpaceBeforeParens: ControlStatements
+# SpaceInEmptyParentheses: false
+# SpacesBeforeTrailingComments: 1
+# SpacesInAngles:  false
+# SpacesInContainerLiterals: true
+# SpacesInCStyleCastParentheses: false
+# SpacesInParentheses: false
+# SpacesInSquareBrackets: false
+TabWidth:        4
+UseTab:          Always
+---
+### C++ specific config ###
+Language:        Cpp
+Standard:        Cpp11
+---
+### ObjC specific config ###
+Language:        ObjC
+Standard:        Cpp11
+ObjCBlockIndentWidth: 4
+# ObjCSpaceAfterProperty: false
+# ObjCSpaceBeforeProtocolList: true
+---
+### Java specific config ###
+Language:        Java
+# BreakAfterJavaFieldAnnotations: false
+...

+ 4 - 0
.gitignore

@@ -3,6 +3,10 @@ build
 .project
 *.kdev4*
 
+# build artefacts
+*.o
+*.a
+
 # Visual Studio
 *.sln
 *.ncb

+ 6 - 1
CMakeLists.txt

@@ -253,7 +253,7 @@ ELSEIF(MSVC)
   IF(MSVC12)
     ADD_COMPILE_OPTIONS(/wd4351)
   ENDIF()
-  SET(CMAKE_CXX_FLAGS_DEBUG "/D_DEBUG /MDd /Ob2 /DEBUG:FULL /Zi")
+  SET(CMAKE_CXX_FLAGS_DEBUG "/D_DEBUG /MDd /Ob2 /Zi")
 ELSEIF ( "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" )
   IF(NOT HUNTER_ENABLED)
     SET(CMAKE_CXX_FLAGS "-fPIC -std=c++11 ${CMAKE_CXX_FLAGS}")
@@ -391,6 +391,11 @@ IF(HUNTER_ENABLED)
   )
 ELSE(HUNTER_ENABLED)
   # cmake configuration files
+  if(${BUILD_SHARED_LIBS})
+    set(BUILD_LIB_TYPE SHARED)
+  else()
+    set(BUILD_LIB_TYPE STATIC)
+  endif()
   CONFIGURE_FILE("${CMAKE_CURRENT_SOURCE_DIR}/assimp-config.cmake.in"         "${CMAKE_CURRENT_BINARY_DIR}/assimp-config.cmake" @ONLY IMMEDIATE)
   CONFIGURE_FILE("${CMAKE_CURRENT_SOURCE_DIR}/assimpTargets.cmake.in"         "${CMAKE_CURRENT_BINARY_DIR}/assimpTargets.cmake" @ONLY IMMEDIATE)
   IF (is_multi_config)

+ 1 - 0
Readme.md

@@ -67,6 +67,7 @@ __Importers__:
 - [LWO](https://en.wikipedia.org/wiki/LightWave_3D)
 - LWS
 - LXO
+- [M3D](https://gitlab.com/bztsrc/model3d)
 - MD2
 - MD3
 - MD5

+ 4 - 1
appveyor.yml

@@ -14,6 +14,7 @@ matrix:
   fast_finish: true
     
 image:
+  - Visual Studio 2013
   - Visual Studio 2015
   - Visual Studio 2017
   - Visual Studio 2019
@@ -29,11 +30,13 @@ install:
   - set PATH=C:\Ruby24-x64\bin;%PATH%
   - set CMAKE_DEFINES -DASSIMP_WERROR=ON
   - if [%COMPILER%]==[MinGW] set PATH=C:\MinGW\bin;%PATH%
+  - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2013" set CMAKE_GENERATOR_NAME=Visual Studio 12 2013
   - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" set CMAKE_GENERATOR_NAME=Visual Studio 14 2015
   - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" set CMAKE_GENERATOR_NAME=Visual Studio 15 2017
   - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2019" set CMAKE_GENERATOR_NAME=Visual Studio 16 2019
   - cmake %CMAKE_DEFINES% -G "%CMAKE_GENERATOR_NAME%" -A %platform% .
-  # Rename sh.exe as sh.exe in PATH interferes with MinGW
+  # Rename sh.exe as sh.exe in PATH interferes with MinGW  - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" set CMAKE_GENERATOR_NAME=Visual Studio 14 2015
+
   - rename "C:\Program Files\Git\usr\bin\sh.exe" "sh2.exe"
   - set PATH=%PATH%;"C:\\Program Files (x86)\\Inno Setup 5"
   - ps: Invoke-WebRequest -Uri https://download.microsoft.com/download/5/7/b/57b2947c-7221-4f33-b35e-2fc78cb10df4/vc_redist.x64.exe -OutFile .\packaging\windows-innosetup\vc_redist.x64.exe

+ 9 - 4
assimpTargets-debug.cmake.in

@@ -35,6 +35,8 @@ if(MSVC)
   endif()
   set(ASSIMP_LIBRARY_SUFFIX "@ASSIMP_LIBRARY_SUFFIX@-${MSVC_PREFIX}-mt" CACHE STRING "the suffix for the assimp windows library" )
 
+  file(TO_NATIVE_PATH ${_IMPORT_PREFIX} _IMPORT_PREFIX)
+
   if(ASSIMP_BUILD_SHARED_LIBS)
     set(sharedLibraryName "assimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@@CMAKE_SHARED_LIBRARY_SUFFIX@")
     set(importLibraryName "assimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@@CMAKE_IMPORT_LIBRARY_SUFFIX@")
@@ -63,10 +65,13 @@ if(MSVC)
 else()
   set(ASSIMP_LIBRARY_SUFFIX "@ASSIMP_LIBRARY_SUFFIX@" CACHE STRING "the suffix for the assimp libraries" )
   if(ASSIMP_BUILD_SHARED_LIBS)
-    if(APPLE)
-        set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@.@ASSIMP_VERSION_MAJOR@@CMAKE_SHARED_LIBRARY_SUFFIX@")
-    else(APPLE)
-        set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@@CMAKE_SHARED_LIBRARY_SUFFIX@.@ASSIMP_VERSION_MAJOR@")
+    if(WIN32)
+      # Handle MinGW compiler.
+      set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@@CMAKE_SHARED_LIBRARY_SUFFIX@@CMAKE_STATIC_LIBRARY_SUFFIX@")
+    elseif(APPLE)
+      set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@.@ASSIMP_VERSION_MAJOR@@CMAKE_SHARED_LIBRARY_SUFFIX@")
+    else()
+      set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@@CMAKE_SHARED_LIBRARY_SUFFIX@.@ASSIMP_VERSION_MAJOR@")
     endif()
     set_target_properties(assimp::assimp PROPERTIES
       IMPORTED_SONAME_DEBUG "${sharedLibraryName}"

+ 10 - 4
assimpTargets-release.cmake.in

@@ -34,6 +34,8 @@ if(MSVC)
     endif()
   endif()
   set(ASSIMP_LIBRARY_SUFFIX "@ASSIMP_LIBRARY_SUFFIX@-${MSVC_PREFIX}-mt" CACHE STRING "the suffix for the assimp windows library" )
+  	
+  file(TO_NATIVE_PATH ${_IMPORT_PREFIX} _IMPORT_PREFIX)
 
   if(ASSIMP_BUILD_SHARED_LIBS)
     set(sharedLibraryName "assimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_SHARED_LIBRARY_SUFFIX@")
@@ -63,13 +65,17 @@ if(MSVC)
 else()
   set(ASSIMP_LIBRARY_SUFFIX "@ASSIMP_LIBRARY_SUFFIX@" CACHE STRING "the suffix for the assimp libraries" )
   if(ASSIMP_BUILD_SHARED_LIBS)
-    if(APPLE)
-        set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}.@ASSIMP_VERSION_MAJOR@@CMAKE_SHARED_LIBRARY_SUFFIX@")
-    else(APPLE)
-        set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_SHARED_LIBRARY_SUFFIX@.@ASSIMP_VERSION_MAJOR@")
+    if(WIN32)
+      # Handle MinGW compiler.
+      set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_DEBUG_POSTFIX@@CMAKE_SHARED_LIBRARY_SUFFIX@@CMAKE_STATIC_LIBRARY_SUFFIX@")
+    elseif(APPLE)
+      set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}.@ASSIMP_VERSION_MAJOR@@CMAKE_SHARED_LIBRARY_SUFFIX@")
+    else()
+      set(sharedLibraryName "libassimp${ASSIMP_LIBRARY_SUFFIX}@CMAKE_SHARED_LIBRARY_SUFFIX@.@ASSIMP_VERSION_MAJOR@")
     endif()
     set_target_properties(assimp::assimp PROPERTIES
       IMPORTED_SONAME_RELEASE "${sharedLibraryName}"
+
       IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/${sharedLibraryName}"
     )
     list(APPEND _IMPORT_CHECK_TARGETS assimp::assimp )

+ 1 - 5
assimpTargets.cmake.in

@@ -54,11 +54,7 @@ if(_IMPORT_PREFIX STREQUAL "/")
 endif()
 
 # Create imported target assimp::assimp
-if(@BUILD_SHARED_LIBS@)
-  add_library(assimp::assimp SHARED IMPORTED)
-else()
-  add_library(assimp::assimp STATIC IMPORTED)
-endif()
+add_library(assimp::assimp @BUILD_LIB_TYPE@ IMPORTED)
 
 set_target_properties(assimp::assimp PROPERTIES
   COMPATIBLE_INTERFACE_STRING "assimp_MAJOR_VERSION"

+ 1 - 1
code/3DS/3DSConverter.cpp

@@ -72,7 +72,7 @@ void Discreet3DSImporter::ReplaceDefaultMaterial()
     unsigned int idx( NotSet );
     for (unsigned int i = 0; i < mScene->mMaterials.size();++i)
     {
-        std::string &s = mScene->mMaterials[i].mName;
+        std::string s = mScene->mMaterials[i].mName;
         for ( std::string::iterator it = s.begin(); it != s.end(); ++it ) {
             *it = static_cast< char >( ::tolower( *it ) );
         }

+ 14 - 0
code/CMakeLists.txt

@@ -407,6 +407,18 @@ ADD_ASSIMP_IMPORTER( LWS
   LWS/LWSLoader.h
 )
 
+ADD_ASSIMP_IMPORTER( M3D
+  M3D/M3DMaterials.h
+  M3D/M3DImporter.h
+  M3D/M3DImporter.cpp
+  M3D/m3d.h
+)
+
+ADD_ASSIMP_EXPORTER( M3D
+  M3D/M3DExporter.h
+  M3D/M3DExporter.cpp
+)
+
 ADD_ASSIMP_IMPORTER( MD2
   MD2/MD2FileData.h
   MD2/MD2Loader.cpp
@@ -670,6 +682,8 @@ SET( PostProcessing_SRCS
   PostProcessing/MakeVerboseFormat.h
   PostProcessing/ScaleProcess.cpp
   PostProcessing/ScaleProcess.h
+  PostProcessing/ArmaturePopulate.cpp
+  PostProcessing/ArmaturePopulate.h
   PostProcessing/GenBoundingBoxesProcess.cpp
   PostProcessing/GenBoundingBoxesProcess.h
 )

+ 1 - 1
code/Collada/ColladaHelper.h

@@ -583,7 +583,7 @@ struct Image
     /** Embedded image data */
     std::vector<uint8_t> mImageData;
 
-    /** File format hint ofembedded image data */
+    /** File format hint of embedded image data */
     std::string mEmbeddedFormat;
 };
 

+ 3 - 53
code/Collada/ColladaLoader.cpp

@@ -1735,6 +1735,7 @@ void ColladaLoader::BuildMaterials(ColladaParser& pParser, aiScene* /*pScene*/)
 
 // ------------------------------------------------------------------------------------------------
 // Resolves the texture name for the given effect texture entry
+// and loads the texture data 
 aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParser,
     const Collada::Effect& pEffect, const std::string& pName)
 {
@@ -1762,7 +1763,7 @@ aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParse
 
         //set default texture file name
         result.Set(name + ".jpg");
-        ConvertPath(result);
+        ColladaParser::UriDecodePath(result);
         return result;
     }
 
@@ -1781,7 +1782,7 @@ aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParse
 
 
         // setup format hint
-        if (imIt->second.mEmbeddedFormat.length() > 3) {
+        if (imIt->second.mEmbeddedFormat.length() >= HINTMAXTEXTURELEN) {
             ASSIMP_LOG_WARN("Collada: texture format hint is too long, truncating to 3 characters");
         }
         strncpy(tex->achFormatHint, imIt->second.mEmbeddedFormat.c_str(), 3);
@@ -1802,61 +1803,10 @@ aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParse
         }
 
         result.Set(imIt->second.mFileName);
-        ConvertPath(result);
     }
     return result;
 }
 
-// ------------------------------------------------------------------------------------------------
-// Convert a path read from a collada file to the usual representation
-void ColladaLoader::ConvertPath(aiString& ss)
-{
-    // TODO: collada spec, p 22. Handle URI correctly.
-    // For the moment we're just stripping the file:// away to make it work.
-    // Windows doesn't seem to be able to find stuff like
-    // 'file://..\LWO\LWO2\MappingModes\earthSpherical.jpg'
-    if (0 == strncmp(ss.data, "file://", 7))
-    {
-        ss.length -= 7;
-        memmove(ss.data, ss.data + 7, ss.length);
-        ss.data[ss.length] = '\0';
-    }
-
-    // Maxon Cinema Collada Export writes "file:///C:\andsoon" with three slashes...
-    // I need to filter it without destroying linux paths starting with "/somewhere"
-#if defined( _MSC_VER )
-    if (ss.data[0] == '/' && isalpha((unsigned char)ss.data[1]) && ss.data[2] == ':') {
-#else
-    if (ss.data[0] == '/' && isalpha(ss.data[1]) && ss.data[2] == ':') {
-#endif
-        --ss.length;
-        ::memmove(ss.data, ss.data + 1, ss.length);
-        ss.data[ss.length] = 0;
-    }
-
-    // find and convert all %xy special chars
-    char* out = ss.data;
-    for (const char* it = ss.data; it != ss.data + ss.length; /**/)
-    {
-        if (*it == '%' && (it + 3) < ss.data + ss.length)
-        {
-            // separate the number to avoid dragging in chars from behind into the parsing
-            char mychar[3] = { it[1], it[2], 0 };
-            size_t nbr = strtoul16(mychar);
-            it += 3;
-            *out++ = (char)(nbr & 0xFF);
-        }
-        else
-        {
-            *out++ = *it++;
-        }
-    }
-
-    // adjust length and terminator of the shortened string
-    *out = 0;
-    ss.length = (ptrdiff_t)(out - ss.data);
-}
-
 // ------------------------------------------------------------------------------------------------
 // Reads a float value from an accessor and its data array.
 ai_real ColladaLoader::ReadFloat(const Collada::Accessor& pAccessor, const Collada::Data& pData, size_t pIndex, size_t pOffset) const

+ 1 - 4
code/Collada/ColladaLoader.h

@@ -94,7 +94,7 @@ public:
 public:
     /** Returns whether the class can handle the format of the given file.
      * See BaseImporter::CanRead() for details. */
-    bool CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const override;
+    bool CanRead(const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const override;
 
 protected:
     /** Return importer meta information.
@@ -184,9 +184,6 @@ protected:
     aiString FindFilenameForEffectTexture( const ColladaParser& pParser,
         const Collada::Effect& pEffect, const std::string& pName);
 
-    /** Converts a path read from a collada file to the usual representation */
-    void ConvertPath( aiString& ss);
-
     /** Reads a float value from an accessor and its data array.
      * @param pAccessor The accessor to use for reading
      * @param pData The data array to read from

+ 68 - 4
code/Collada/ColladaParser.cpp

@@ -183,13 +183,67 @@ std::string ColladaParser::ReadZaeManifest(ZipArchiveIOSystem &zip_archive) {
                 if (filepath == nullptr)
                     return std::string();
 
-                return std::string(filepath);
+                aiString ai_str(filepath);
+                UriDecodePath(ai_str);
+
+                return std::string(ai_str.C_Str());
             }
         }
     }
     return std::string();
 }
 
+// ------------------------------------------------------------------------------------------------
+// Convert a path read from a collada file to the usual representation
+void ColladaParser::UriDecodePath(aiString& ss)
+{
+    // TODO: collada spec, p 22. Handle URI correctly.
+    // For the moment we're just stripping the file:// away to make it work.
+    // Windows doesn't seem to be able to find stuff like
+    // 'file://..\LWO\LWO2\MappingModes\earthSpherical.jpg'
+    if (0 == strncmp(ss.data, "file://", 7))
+    {
+        ss.length -= 7;
+        memmove(ss.data, ss.data + 7, ss.length);
+        ss.data[ss.length] = '\0';
+    }
+
+    // Maxon Cinema Collada Export writes "file:///C:\andsoon" with three slashes...
+    // I need to filter it without destroying linux paths starting with "/somewhere"
+#if defined( _MSC_VER )
+    if (ss.data[0] == '/' && isalpha((unsigned char)ss.data[1]) && ss.data[2] == ':') {
+#else
+    if (ss.data[0] == '/' && isalpha(ss.data[1]) && ss.data[2] == ':') {
+#endif
+        --ss.length;
+        ::memmove(ss.data, ss.data + 1, ss.length);
+        ss.data[ss.length] = 0;
+    }
+
+    // find and convert all %xy special chars
+    char* out = ss.data;
+    for (const char* it = ss.data; it != ss.data + ss.length; /**/)
+    {
+        if (*it == '%' && (it + 3) < ss.data + ss.length)
+        {
+            // separate the number to avoid dragging in chars from behind into the parsing
+            char mychar[3] = { it[1], it[2], 0 };
+            size_t nbr = strtoul16(mychar);
+            it += 3;
+            *out++ = (char)(nbr & 0xFF);
+        }
+        else
+        {
+            *out++ = *it++;
+        }
+    }
+
+    // adjust length and terminator of the shortened string
+    *out = 0;
+    ai_assert(out > ss.data);
+    ss.length = static_cast<ai_uint32>(out - ss.data);
+}
+
 // ------------------------------------------------------------------------------------------------
 // Read bool from text contents of current element
 bool ColladaParser::ReadBoolFromTextContent()
@@ -1120,7 +1174,12 @@ void ColladaParser::ReadImage(Collada::Image& pImage)
                     if (!mReader->isEmptyElement()) {
                         // element content is filename - hopefully
                         const char* sz = TestTextContent();
-                        if (sz)pImage.mFileName = sz;
+                        if (sz)
+                        {
+                            aiString filepath(sz);
+                            UriDecodePath(filepath);
+                            pImage.mFileName = filepath.C_Str();
+                        }
                         TestClosing("init_from");
                     }
                     if (!pImage.mFileName.length()) {
@@ -1153,7 +1212,12 @@ void ColladaParser::ReadImage(Collada::Image& pImage)
                 {
                     // element content is filename - hopefully
                     const char* sz = TestTextContent();
-                    if (sz)pImage.mFileName = sz;
+                    if (sz)
+                    {
+                        aiString filepath(sz);
+                        UriDecodePath(filepath);
+                        pImage.mFileName = filepath.C_Str();
+                    }
                     TestClosing("ref");
                 }
                 else if (IsElement("hex") && !pImage.mFileName.length())
@@ -3056,7 +3120,7 @@ void ColladaParser::ReadMaterialVertexInputBinding(Collada::SemanticMappingTable
     }
 }
 
-void Assimp::ColladaParser::ReadEmbeddedTextures(ZipArchiveIOSystem& zip_archive)
+void ColladaParser::ReadEmbeddedTextures(ZipArchiveIOSystem& zip_archive)
 {
     // Attempt to load any undefined Collada::Image in ImageLibrary
     for (ImageLibrary::iterator it = mImageLibrary.begin(); it != mImageLibrary.end(); ++it) {

+ 4 - 1
code/Collada/ColladaParser.h

@@ -66,12 +66,15 @@ namespace Assimp
     {
         friend class ColladaLoader;
 
+        /** Converts a path read from a collada file to the usual representation */
+        static void UriDecodePath(aiString& ss);
+
     protected:
         /** Map for generic metadata as aiString */
         typedef std::map<std::string, aiString> StringMetaData;
 
         /** Constructor from XML file */
-        ColladaParser( IOSystem* pIOHandler, const std::string& pFile);
+        ColladaParser(IOSystem* pIOHandler, const std::string& pFile);
 
         /** Destructor */
         ~ColladaParser();

+ 31 - 2
code/Common/DefaultIOStream.cpp

@@ -52,6 +52,35 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 using namespace Assimp;
 
+namespace
+{
+    template<size_t sizeOfPointer>
+    size_t select_ftell(FILE* file)
+    {
+        return ::ftell(file);
+    }
+
+    template<size_t sizeOfPointer>
+    int select_fseek(FILE* file, int64_t offset, int origin)
+    {
+        return ::fseek(file, static_cast<long>(offset), origin);
+    }
+
+#if defined _WIN32 && (!defined __GNUC__ || __MSVCRT_VERSION__ >= 0x0601)
+    template<>
+    size_t select_ftell<8>(FILE* file)
+    {
+        return ::_ftelli64(file);
+    }
+
+    template<>
+    int select_fseek<8>(FILE* file, int64_t offset, int origin)
+    {
+        return ::_fseeki64(file, offset, origin);
+    }
+#endif
+}
+
 // ----------------------------------------------------------------------------------
 DefaultIOStream::~DefaultIOStream()
 {
@@ -93,7 +122,7 @@ aiReturn DefaultIOStream::Seek(size_t pOffset,
         aiOrigin_END == SEEK_END && aiOrigin_SET == SEEK_SET");
 
     // do the seek
-    return (0 == ::fseek(mFile, (long)pOffset,(int)pOrigin) ? AI_SUCCESS : AI_FAILURE);
+    return (0 == select_fseek<sizeof(void*)>(mFile, (int64_t)pOffset,(int)pOrigin) ? AI_SUCCESS : AI_FAILURE);
 }
 
 // ----------------------------------------------------------------------------------
@@ -102,7 +131,7 @@ size_t DefaultIOStream::Tell() const
     if (!mFile) {
         return 0;
     }
-    return ::ftell(mFile);
+    return select_ftell<sizeof(void*)>(mFile);
 }
 
 // ----------------------------------------------------------------------------------

+ 69 - 61
code/Common/Exporter.cpp

@@ -102,94 +102,92 @@ void ExportSceneX3D(const char*, IOSystem*, const aiScene*, const ExportProperti
 void ExportSceneFBX(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportSceneFBXA(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportScene3MF( const char*, IOSystem*, const aiScene*, const ExportProperties* );
+void ExportSceneM3D(const char*, IOSystem*, const aiScene*, const ExportProperties*);
+void ExportSceneA3D(const char*, IOSystem*, const aiScene*, const ExportProperties*);
 void ExportAssimp2Json(const char* , IOSystem*, const aiScene* , const Assimp::ExportProperties*);
 
-// ------------------------------------------------------------------------------------------------
-// global array of all export formats which Assimp supports in its current build
-Exporter::ExportFormatEntry gExporters[] =
-{
+
+static void setupExporterArray(std::vector<Exporter::ExportFormatEntry> &exporters) {
 #ifndef ASSIMP_BUILD_NO_COLLADA_EXPORTER
-    Exporter::ExportFormatEntry( "collada", "COLLADA - Digital Asset Exchange Schema", "dae", &ExportSceneCollada ),
+	exporters.push_back(Exporter::ExportFormatEntry("collada", "COLLADA - Digital Asset Exchange Schema", "dae", &ExportSceneCollada));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_X_EXPORTER
-    Exporter::ExportFormatEntry( "x", "X Files", "x", &ExportSceneXFile,
-        aiProcess_MakeLeftHanded | aiProcess_FlipWindingOrder | aiProcess_FlipUVs ),
+	exporters.push_back(Exporter::ExportFormatEntry("x", "X Files", "x", &ExportSceneXFile,
+			aiProcess_MakeLeftHanded | aiProcess_FlipWindingOrder | aiProcess_FlipUVs));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_STEP_EXPORTER
-    Exporter::ExportFormatEntry( "stp", "Step Files", "stp", &ExportSceneStep, 0 ),
+	exporters.push_back(Exporter::ExportFormatEntry("stp", "Step Files", "stp", &ExportSceneStep, 0));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_OBJ_EXPORTER
-    Exporter::ExportFormatEntry( "obj", "Wavefront OBJ format", "obj", &ExportSceneObj,
-        aiProcess_GenSmoothNormals /*| aiProcess_PreTransformVertices */ ),
-    Exporter::ExportFormatEntry( "objnomtl", "Wavefront OBJ format without material file", "obj", &ExportSceneObjNoMtl,
-        aiProcess_GenSmoothNormals /*| aiProcess_PreTransformVertices */ ),
+	exporters.push_back(Exporter::ExportFormatEntry("obj", "Wavefront OBJ format", "obj", &ExportSceneObj,
+			aiProcess_GenSmoothNormals /*| aiProcess_PreTransformVertices */));
+	exporters.push_back(Exporter::ExportFormatEntry("objnomtl", "Wavefront OBJ format without material file", "obj", &ExportSceneObjNoMtl,
+			aiProcess_GenSmoothNormals /*| aiProcess_PreTransformVertices */));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_STL_EXPORTER
-    Exporter::ExportFormatEntry( "stl", "Stereolithography", "stl" , &ExportSceneSTL,
-        aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_PreTransformVertices
-    ),
-    Exporter::ExportFormatEntry( "stlb", "Stereolithography (binary)", "stl" , &ExportSceneSTLBinary,
-        aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_PreTransformVertices
-    ),
+	exporters.push_back(Exporter::ExportFormatEntry("stl", "Stereolithography", "stl", &ExportSceneSTL,
+			aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_PreTransformVertices));
+	exporters.push_back(Exporter::ExportFormatEntry("stlb", "Stereolithography (binary)", "stl", &ExportSceneSTLBinary,
+			aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_PreTransformVertices));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_PLY_EXPORTER
-    Exporter::ExportFormatEntry( "ply", "Stanford Polygon Library", "ply" , &ExportScenePly,
-        aiProcess_PreTransformVertices
-    ),
-    Exporter::ExportFormatEntry( "plyb", "Stanford Polygon Library (binary)", "ply", &ExportScenePlyBinary,
-        aiProcess_PreTransformVertices
-    ),
+	exporters.push_back(Exporter::ExportFormatEntry("ply", "Stanford Polygon Library", "ply", &ExportScenePly,
+			aiProcess_PreTransformVertices));
+	exporters.push_back(Exporter::ExportFormatEntry("plyb", "Stanford Polygon Library (binary)", "ply", &ExportScenePlyBinary,
+			aiProcess_PreTransformVertices));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_3DS_EXPORTER
-    Exporter::ExportFormatEntry( "3ds", "Autodesk 3DS (legacy)", "3ds" , &ExportScene3DS,
-        aiProcess_Triangulate | aiProcess_SortByPType | aiProcess_JoinIdenticalVertices ),
+	exporters.push_back(Exporter::ExportFormatEntry("3ds", "Autodesk 3DS (legacy)", "3ds", &ExportScene3DS,
+			aiProcess_Triangulate | aiProcess_SortByPType | aiProcess_JoinIdenticalVertices));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_GLTF_EXPORTER
-    Exporter::ExportFormatEntry( "gltf2", "GL Transmission Format v. 2", "gltf", &ExportSceneGLTF2,
-        aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType ),
-    Exporter::ExportFormatEntry( "glb2", "GL Transmission Format v. 2 (binary)", "glb", &ExportSceneGLB2,
-        aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType ),
-    Exporter::ExportFormatEntry( "gltf", "GL Transmission Format", "gltf", &ExportSceneGLTF,
-        aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType ),
-    Exporter::ExportFormatEntry( "glb", "GL Transmission Format (binary)", "glb", &ExportSceneGLB,
-        aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType ),
+	exporters.push_back(Exporter::ExportFormatEntry("gltf2", "GL Transmission Format v. 2", "gltf", &ExportSceneGLTF2,
+			aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType));
+	exporters.push_back(Exporter::ExportFormatEntry("glb2", "GL Transmission Format v. 2 (binary)", "glb", &ExportSceneGLB2,
+			aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType));
+	exporters.push_back(Exporter::ExportFormatEntry("gltf", "GL Transmission Format", "gltf", &ExportSceneGLTF,
+			aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType));
+	exporters.push_back(Exporter::ExportFormatEntry("glb", "GL Transmission Format (binary)", "glb", &ExportSceneGLB,
+			aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_SortByPType));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_ASSBIN_EXPORTER
-    Exporter::ExportFormatEntry( "assbin", "Assimp Binary File", "assbin" , &ExportSceneAssbin, 0 ),
+	exporters.push_back(Exporter::ExportFormatEntry("assbin", "Assimp Binary File", "assbin", &ExportSceneAssbin, 0));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_ASSXML_EXPORTER
-    Exporter::ExportFormatEntry( "assxml", "Assimp XML Document", "assxml" , &ExportSceneAssxml, 0 ),
+	exporters.push_back(Exporter::ExportFormatEntry("assxml", "Assimp XML Document", "assxml", &ExportSceneAssxml, 0));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_X3D_EXPORTER
-    Exporter::ExportFormatEntry( "x3d", "Extensible 3D", "x3d" , &ExportSceneX3D, 0 ),
+	exporters.push_back(Exporter::ExportFormatEntry("x3d", "Extensible 3D", "x3d", &ExportSceneX3D, 0));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_FBX_EXPORTER
-    Exporter::ExportFormatEntry( "fbx", "Autodesk FBX (binary)", "fbx", &ExportSceneFBX, 0 ),
-    Exporter::ExportFormatEntry( "fbxa", "Autodesk FBX (ascii)", "fbx", &ExportSceneFBXA, 0 ),
+	exporters.push_back(Exporter::ExportFormatEntry("fbx", "Autodesk FBX (binary)", "fbx", &ExportSceneFBX, 0));
+	exporters.push_back(Exporter::ExportFormatEntry("fbxa", "Autodesk FBX (ascii)", "fbx", &ExportSceneFBXA, 0));
+#endif
+
+#ifndef ASSIMP_BUILD_NO_M3D_EXPORTER
+	exporters.push_back(Exporter::ExportFormatEntry("m3d", "Model 3D (binary)", "m3d", &ExportSceneM3D, 0));
+	exporters.push_back(Exporter::ExportFormatEntry("a3d", "Model 3D (ascii)", "m3d", &ExportSceneA3D, 0));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_3MF_EXPORTER
-    Exporter::ExportFormatEntry( "3mf", "The 3MF-File-Format", "3mf", &ExportScene3MF, 0 ),
+	exporters.push_back(Exporter::ExportFormatEntry("3mf", "The 3MF-File-Format", "3mf", &ExportScene3MF, 0));
 #endif
 
 #ifndef ASSIMP_BUILD_NO_ASSJSON_EXPORTER
-    Exporter::ExportFormatEntry( "assjson", "Assimp JSON Document", "json", &ExportAssimp2Json, 0)
+	exporters.push_back(Exporter::ExportFormatEntry("assjson", "Assimp JSON Document", "json", &ExportAssimp2Json, 0));
 #endif
-};
-
-#define ASSIMP_NUM_EXPORTERS (sizeof(gExporters)/sizeof(gExporters[0]))
-
+}
 
 class ExporterPimpl {
 public:
@@ -205,10 +203,7 @@ public:
         GetPostProcessingStepInstanceList(mPostProcessingSteps);
 
         // grab all built-in exporters
-        if ( 0 != ( ASSIMP_NUM_EXPORTERS ) ) {
-            mExporters.resize( ASSIMP_NUM_EXPORTERS );
-            std::copy( gExporters, gExporters + ASSIMP_NUM_EXPORTERS, mExporters.begin() );
-        }
+		setupExporterArray(mExporters);
     }
 
     ~ExporterPimpl() {
@@ -252,24 +247,28 @@ Exporter :: Exporter()
 
 // ------------------------------------------------------------------------------------------------
 Exporter::~Exporter() {
-    FreeBlob();
+	ai_assert(nullptr != pimpl);
+	FreeBlob();
     delete pimpl;
 }
 
 // ------------------------------------------------------------------------------------------------
 void Exporter::SetIOHandler( IOSystem* pIOHandler) {
-    pimpl->mIsDefaultIOHandler = !pIOHandler;
+	ai_assert(nullptr != pimpl);
+	pimpl->mIsDefaultIOHandler = !pIOHandler;
     pimpl->mIOSystem.reset(pIOHandler);
 }
 
 // ------------------------------------------------------------------------------------------------
 IOSystem* Exporter::GetIOHandler() const {
-    return pimpl->mIOSystem.get();
+	ai_assert(nullptr != pimpl);
+	return pimpl->mIOSystem.get();
 }
 
 // ------------------------------------------------------------------------------------------------
 bool Exporter::IsDefaultIOHandler() const {
-    return pimpl->mIsDefaultIOHandler;
+	ai_assert(nullptr != pimpl);
+	return pimpl->mIsDefaultIOHandler;
 }
 
 // ------------------------------------------------------------------------------------------------
@@ -295,6 +294,7 @@ void Exporter::SetProgressHandler(ProgressHandler* pHandler) {
 // ------------------------------------------------------------------------------------------------
 const aiExportDataBlob* Exporter::ExportToBlob( const aiScene* pScene, const char* pFormatId,
                                                 unsigned int pPreprocessing, const ExportProperties* pProperties) {
+	ai_assert(nullptr != pimpl);
     if (pimpl->blob) {
         delete pimpl->blob;
         pimpl->blob = nullptr;
@@ -319,7 +319,7 @@ const aiExportDataBlob* Exporter::ExportToBlob( const aiScene* pScene, const cha
 aiReturn Exporter::Export( const aiScene* pScene, const char* pFormatId, const char* pPath,
         unsigned int pPreprocessing, const ExportProperties* pProperties) {
     ASSIMP_BEGIN_EXCEPTION_REGION();
-
+	ai_assert(nullptr != pimpl);
     // when they create scenes from scratch, users will likely create them not in verbose
     // format. They will likely not be aware that there is a flag in the scene to indicate
     // this, however. To avoid surprises and bug reports, we check for duplicates in
@@ -466,11 +466,13 @@ aiReturn Exporter::Export( const aiScene* pScene, const char* pFormatId, const c
 
 // ------------------------------------------------------------------------------------------------
 const char* Exporter::GetErrorString() const {
+	ai_assert(nullptr != pimpl);
     return pimpl->mError.c_str();
 }
 
 // ------------------------------------------------------------------------------------------------
 void Exporter::FreeBlob() {
+	ai_assert(nullptr != pimpl);
     delete pimpl->blob;
     pimpl->blob = nullptr;
 
@@ -479,30 +481,34 @@ void Exporter::FreeBlob() {
 
 // ------------------------------------------------------------------------------------------------
 const aiExportDataBlob* Exporter::GetBlob() const {
-    return pimpl->blob;
+	ai_assert(nullptr != pimpl);
+	return pimpl->blob;
 }
 
 // ------------------------------------------------------------------------------------------------
 const aiExportDataBlob* Exporter::GetOrphanedBlob() const {
-    const aiExportDataBlob* tmp = pimpl->blob;
+	ai_assert(nullptr != pimpl);
+	const aiExportDataBlob *tmp = pimpl->blob;
     pimpl->blob = nullptr;
     return tmp;
 }
 
 // ------------------------------------------------------------------------------------------------
 size_t Exporter::GetExportFormatCount() const {
+	ai_assert(nullptr != pimpl);
     return pimpl->mExporters.size();
 }
 
 // ------------------------------------------------------------------------------------------------
 const aiExportFormatDesc* Exporter::GetExportFormatDescription( size_t index ) const {
-    if (index >= GetExportFormatCount()) {
+	ai_assert(nullptr != pimpl);
+	if (index >= GetExportFormatCount()) {
         return nullptr;
     }
 
     // Return from static storage if the requested index is built-in.
-    if (index < sizeof(gExporters) / sizeof(gExporters[0])) {
-        return &gExporters[index].mDescription;
+	if (index < pimpl->mExporters.size()) {
+		return &pimpl->mExporters[index].mDescription;
     }
 
     return &pimpl->mExporters[index].mDescription;
@@ -510,7 +516,8 @@ const aiExportFormatDesc* Exporter::GetExportFormatDescription( size_t index ) c
 
 // ------------------------------------------------------------------------------------------------
 aiReturn Exporter::RegisterExporter(const ExportFormatEntry& desc) {
-    for(const ExportFormatEntry& e : pimpl->mExporters) {
+	ai_assert(nullptr != pimpl);
+	for (const ExportFormatEntry &e : pimpl->mExporters) {
         if (!strcmp(e.mDescription.id,desc.mDescription.id)) {
             return aiReturn_FAILURE;
         }
@@ -522,7 +529,8 @@ aiReturn Exporter::RegisterExporter(const ExportFormatEntry& desc) {
 
 // ------------------------------------------------------------------------------------------------
 void Exporter::UnregisterExporter(const char* id) {
-    for(std::vector<ExportFormatEntry>::iterator it = pimpl->mExporters.begin();
+	ai_assert(nullptr != pimpl);
+	for (std::vector<ExportFormatEntry>::iterator it = pimpl->mExporters.begin();
             it != pimpl->mExporters.end(); ++it) {
         if (!strcmp((*it).mDescription.id,id)) {
             pimpl->mExporters.erase(it);

+ 6 - 0
code/Common/ImporterRegistry.cpp

@@ -197,6 +197,9 @@ corresponding preprocessor flag to selectively disable formats.
 #ifndef ASSIMP_BUILD_NO_MMD_IMPORTER
 #   include "MMD/MMDImporter.h"
 #endif
+#ifndef ASSIMP_BUILD_NO_M3D_IMPORTER
+#   include "M3D/M3DImporter.h"
+#endif
 #ifndef ASSIMP_BUILD_NO_STEP_IMPORTER
 #   include "Importer/StepFile/StepFileImporter.h"
 #endif
@@ -223,6 +226,9 @@ void GetImporterInstanceList(std::vector< BaseImporter* >& out)
 #if (!defined ASSIMP_BUILD_NO_3DS_IMPORTER)
     out.push_back( new Discreet3DSImporter());
 #endif
+#if (!defined ASSIMP_BUILD_NO_M3D_IMPORTER)
+    out.push_back( new M3DImporter());
+#endif
 #if (!defined ASSIMP_BUILD_NO_MD3_IMPORTER)
     out.push_back( new MD3Importer());
 #endif

+ 7 - 0
code/Common/PostStepRegistry.cpp

@@ -131,11 +131,15 @@ corresponding preprocessor flag to selectively disable steps.
 #if (!defined ASSIMP_BUILD_NO_GLOBALSCALE_PROCESS)
 #   include "PostProcessing/ScaleProcess.h"
 #endif
+#if (!defined ASSIMP_BUILD_NO_ARMATUREPOPULATE_PROCESS)
+#   include "PostProcessing/ArmaturePopulate.h"
+#endif
 #if (!defined ASSIMP_BUILD_NO_GENBOUNDINGBOXES_PROCESS)
 #   include "PostProcessing/GenBoundingBoxesProcess.h"
 #endif
 
 
+
 namespace Assimp {
 
 // ------------------------------------------------------------------------------------------------
@@ -180,6 +184,9 @@ void GetPostProcessingStepInstanceList(std::vector< BaseProcess* >& out)
 #if (!defined ASSIMP_BUILD_NO_GLOBALSCALE_PROCESS)
     out.push_back( new ScaleProcess());
 #endif
+#if (!defined ASSIMP_BUILD_NO_ARMATUREPOPULATE_PROCESS)
+    out.push_back( new ArmaturePopulate());
+#endif
 #if (!defined ASSIMP_BUILD_NO_PRETRANSFORMVERTICES_PROCESS)
     out.push_back( new PretransformVertices());
 #endif

+ 5 - 9
code/Common/Version.cpp

@@ -46,8 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <assimp/scene.h>
 #include "ScenePrivate.h"
 
-static const unsigned int MajorVersion = 5;
-static const unsigned int MinorVersion = 0;
+#include "revision.h"
 
 // --------------------------------------------------------------------------------
 // Legal information string - don't remove this.
@@ -56,9 +55,9 @@ static const char* LEGAL_INFORMATION =
 "Open Asset Import Library (Assimp).\n"
 "A free C/C++ library to import various 3D file formats into applications\n\n"
 
-"(c) 2008-2017, assimp team\n"
+"(c) 2006-2019, assimp team\n"
 "License under the terms and conditions of the 3-clause BSD license\n"
-"http://assimp.sourceforge.net\n"
+"http://assimp.org\n"
 ;
 
 // ------------------------------------------------------------------------------------------------
@@ -70,13 +69,13 @@ ASSIMP_API const char*  aiGetLegalString  ()    {
 // ------------------------------------------------------------------------------------------------
 // Get Assimp minor version
 ASSIMP_API unsigned int aiGetVersionMinor ()    {
-    return MinorVersion;
+    return VER_MINOR;
 }
 
 // ------------------------------------------------------------------------------------------------
 // Get Assimp major version
 ASSIMP_API unsigned int aiGetVersionMajor ()    {
-    return MajorVersion;
+    return VER_MAJOR;
 }
 
 // ------------------------------------------------------------------------------------------------
@@ -104,9 +103,6 @@ ASSIMP_API unsigned int aiGetCompileFlags ()    {
     return flags;
 }
 
-// include current build revision, which is even updated from time to time -- :-)
-#include "revision.h"
-
 // ------------------------------------------------------------------------------------------------
 ASSIMP_API unsigned int aiGetVersionRevision() {
     return GitVersion;

+ 9 - 9
code/Common/scene.cpp

@@ -44,23 +44,23 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 aiNode::aiNode()
 : mName("")
-, mParent(NULL)
+, mParent(nullptr)
 , mNumChildren(0)
-, mChildren(NULL)
+, mChildren(nullptr)
 , mNumMeshes(0)
-, mMeshes(NULL)
-, mMetaData(NULL) {
+, mMeshes(nullptr)
+, mMetaData(nullptr) {
     // empty
 }
 
 aiNode::aiNode(const std::string& name)
 : mName(name)
-, mParent(NULL)
+, mParent(nullptr)
 , mNumChildren(0)
-, mChildren(NULL)
+, mChildren(nullptr)
 , mNumMeshes(0)
-, mMeshes(NULL)
-, mMetaData(NULL) {
+, mMeshes(nullptr)
+, mMetaData(nullptr) {
     // empty
 }
 
@@ -68,7 +68,7 @@ aiNode::aiNode(const std::string& name)
 aiNode::~aiNode() {
     // delete all children recursively
     // to make sure we won't crash if the data is invalid ...
-    if (mChildren && mNumChildren)
+    if (mNumChildren && mChildren)
     {
         for (unsigned int a = 0; a < mNumChildren; a++)
             delete mChildren[a];

+ 4 - 3
code/FBX/FBXCommon.h

@@ -50,9 +50,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 namespace Assimp {
 namespace FBX
 {
-    const std::string NULL_RECORD = { // 13 null bytes
-        '\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0'
-    }; // who knows why
+    const std::string NULL_RECORD = { // 25 null bytes in 64-bit and 13 null bytes in 32-bit
+        '\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0',
+        '\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0','\0'
+    }; // who knows why, it looks like two integers 32/64 bit (compressed and uncompressed sizes?) + 1 byte (might be compression type?)
     const std::string SEPARATOR = {'\x00', '\x01'}; // for use inside strings
     const std::string MAGIC_NODE_TAG = "_$AssimpFbx$"; // from import
     const int64_t SECOND = 46186158000; // FBX's kTime unit

+ 8 - 0
code/FBX/FBXCompileConfig.h

@@ -47,6 +47,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #define INCLUDED_AI_FBX_COMPILECONFIG_H
 
 #include <map>
+#include <set>
 
 //
 #if _MSC_VER > 1500 || (defined __GNUC___)
@@ -54,16 +55,23 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #   else
 #   define fbx_unordered_map map
 #   define fbx_unordered_multimap multimap
+#   define fbx_unordered_set set
+#   define fbx_unordered_multiset multiset
 #endif
 
 #ifdef ASSIMP_FBX_USE_UNORDERED_MULTIMAP
 #   include <unordered_map>
+#   include <unordered_set>
 #   if _MSC_VER > 1600
 #       define fbx_unordered_map unordered_map
 #       define fbx_unordered_multimap unordered_multimap
+#       define fbx_unordered_set unordered_set
+#       define fbx_unordered_multiset unordered_multiset
 #   else
 #       define fbx_unordered_map tr1::unordered_map
 #       define fbx_unordered_multimap tr1::unordered_multimap
+#       define fbx_unordered_set tr1::unordered_set
+#       define fbx_unordered_multiset tr1::unordered_multiset
 #   endif
 #endif
 

+ 225 - 113
code/FBX/FBXConverter.cpp

@@ -68,7 +68,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <sstream>
 #include <iomanip>
 #include <cstdint>
-
+#include <iostream>
+#include <stdlib.h>
 
 namespace Assimp {
     namespace FBX {
@@ -77,7 +78,7 @@ namespace Assimp {
 
 #define MAGIC_NODE_TAG "_$AssimpFbx$"
 
-#define CONVERT_FBX_TIME(time) static_cast<double>(time) / 46186158000L
+#define CONVERT_FBX_TIME(time) static_cast<double>(time) / 46186158000LL
 
         FBXConverter::FBXConverter(aiScene* out, const Document& doc, bool removeEmptyBones )
         : defaultMaterialIndex()
@@ -96,6 +97,14 @@ namespace Assimp {
             // populate the node_anim_chain_bits map, which is needed
             // to determine which nodes need to be generated.
             ConvertAnimations();
+            // Embedded textures in FBX could be connected to nothing but to itself,
+            // for instance Texture -> Video connection only but not to the main graph,
+            // The idea here is to traverse all objects to find these Textures and convert them,
+            // so later during material conversion it will find converted texture in the textures_converted array.
+            if (doc.Settings().readTextures)
+            {
+                ConvertOrphantEmbeddedTextures();
+            }
             ConvertRootNode();
 
             if (doc.Settings().readAllMaterials) {
@@ -145,7 +154,7 @@ namespace Assimp {
             out->mRootNode->mName.Set(unique_name);
 
             // root has ID 0
-            ConvertNodes(0L, *out->mRootNode);
+            ConvertNodes(0L, out->mRootNode, out->mRootNode);
         }
 
         static std::string getAncestorBaseName(const aiNode* node)
@@ -179,8 +188,11 @@ namespace Assimp {
             GetUniqueName(original_name, unique_name);
             return unique_name;
         }
-
-        void FBXConverter::ConvertNodes(uint64_t id, aiNode& parent, const aiMatrix4x4& parent_transform) {
+        /// todo: pre-build node hierarchy
+        /// todo: get bone from stack
+        /// todo: make map of aiBone* to aiNode*
+        /// then update convert clusters to the new format
+        void FBXConverter::ConvertNodes(uint64_t id, aiNode *parent, aiNode *root_node) {
             const std::vector<const Connection*>& conns = doc.GetConnectionsByDestinationSequenced(id, "Model");
 
             std::vector<aiNode*> nodes;
@@ -191,62 +203,69 @@ namespace Assimp {
 
             try {
                 for (const Connection* con : conns) {
-
                     // ignore object-property links
                     if (con->PropertyName().length()) {
-                        continue;
+                        // really important we document why this is ignored.
+                        FBXImporter::LogInfo("ignoring property link - no docs on why this is ignored");
+                        continue; //?
                     }
 
+                    // convert connection source object into Object base class
                     const Object* const object = con->SourceObject();
                     if (nullptr == object) {
-                        FBXImporter::LogWarn("failed to convert source object for Model link");
+                        FBXImporter::LogError("failed to convert source object for Model link");
                         continue;
                     }
 
+                    // FBX Model::Cube, Model::Bone001, etc elements
+                    // This detects if we can cast the object into this model structure.
                     const Model* const model = dynamic_cast<const Model*>(object);
 
                     if (nullptr != model) {
                         nodes_chain.clear();
                         post_nodes_chain.clear();
 
-                        aiMatrix4x4 new_abs_transform = parent_transform;
-
-                        std::string unique_name = MakeUniqueNodeName(model, parent);
-
+                        aiMatrix4x4 new_abs_transform = parent->mTransformation;
+                        std::string node_name = FixNodeName(model->Name());
                         // even though there is only a single input node, the design of
                         // assimp (or rather: the complicated transformation chain that
                         // is employed by fbx) means that we may need multiple aiNode's
                         // to represent a fbx node's transformation.
-                        const bool need_additional_node = GenerateTransformationNodeChain(*model, unique_name, nodes_chain, post_nodes_chain);
 
+
+                        // generate node transforms - this includes pivot data
+                        // if need_additional_node is true then you t
+                        const bool need_additional_node = GenerateTransformationNodeChain(*model, node_name, nodes_chain, post_nodes_chain);
+
+                        // assert that for the current node we must have at least a single transform
                         ai_assert(nodes_chain.size());
 
                         if (need_additional_node) {
-                            nodes_chain.push_back(new aiNode(unique_name));
+                            nodes_chain.push_back(new aiNode(node_name));
                         }
 
                         //setup metadata on newest node
                         SetupNodeMetadata(*model, *nodes_chain.back());
 
                         // link all nodes in a row
-                        aiNode* last_parent = &parent;
-                        for (aiNode* prenode : nodes_chain) {
-                            ai_assert(prenode);
+                        aiNode* last_parent = parent;
+                        for (aiNode* child : nodes_chain) {
+                            ai_assert(child);
 
-                            if (last_parent != &parent) {
+                            if (last_parent != parent) {
                                 last_parent->mNumChildren = 1;
                                 last_parent->mChildren = new aiNode*[1];
-                                last_parent->mChildren[0] = prenode;
+                                last_parent->mChildren[0] = child;
                             }
 
-                            prenode->mParent = last_parent;
-                            last_parent = prenode;
+                            child->mParent = last_parent;
+                            last_parent = child;
 
-                            new_abs_transform *= prenode->mTransformation;
+                            new_abs_transform *= child->mTransformation;
                         }
 
                         // attach geometry
-                        ConvertModel(*model, *nodes_chain.back(), new_abs_transform);
+                        ConvertModel(*model, nodes_chain.back(), root_node, new_abs_transform);
 
                         // check if there will be any child nodes
                         const std::vector<const Connection*>& child_conns
@@ -258,7 +277,7 @@ namespace Assimp {
                             for (aiNode* postnode : post_nodes_chain) {
                                 ai_assert(postnode);
 
-                                if (last_parent != &parent) {
+                                if (last_parent != parent) {
                                     last_parent->mNumChildren = 1;
                                     last_parent->mChildren = new aiNode*[1];
                                     last_parent->mChildren[0] = postnode;
@@ -280,15 +299,15 @@ namespace Assimp {
                             );
                         }
 
-                        // attach sub-nodes (if any)
-                        ConvertNodes(model->ID(), *last_parent, new_abs_transform);
+                        // recursion call - child nodes
+                        ConvertNodes(model->ID(), last_parent, root_node);
 
                         if (doc.Settings().readLights) {
-                            ConvertLights(*model, unique_name);
+                            ConvertLights(*model, node_name);
                         }
 
                         if (doc.Settings().readCameras) {
-                            ConvertCameras(*model, unique_name);
+                            ConvertCameras(*model, node_name);
                         }
 
                         nodes.push_back(nodes_chain.front());
@@ -297,11 +316,17 @@ namespace Assimp {
                 }
 
                 if (nodes.size()) {
-                    parent.mChildren = new aiNode*[nodes.size()]();
-                    parent.mNumChildren = static_cast<unsigned int>(nodes.size());
+                    parent->mChildren = new aiNode*[nodes.size()]();
+                    parent->mNumChildren = static_cast<unsigned int>(nodes.size());
 
-                    std::swap_ranges(nodes.begin(), nodes.end(), parent.mChildren);
+                    std::swap_ranges(nodes.begin(), nodes.end(), parent->mChildren);
                 }
+                else
+                {
+                    parent->mNumChildren = 0;
+                    parent->mChildren = nullptr;
+                }
+                
             }
             catch (std::exception&) {
                 Util::delete_fun<aiNode> deleter;
@@ -803,7 +828,7 @@ namespace Assimp {
             // is_complex needs to be consistent with NeedsComplexTransformationChain()
             // or the interplay between this code and the animation converter would
             // not be guaranteed.
-            ai_assert(NeedsComplexTransformationChain(model) == ((chainBits & chainMaskComplex) != 0));
+            //ai_assert(NeedsComplexTransformationChain(model) == ((chainBits & chainMaskComplex) != 0));
 
             // now, if we have more than just Translation, Scaling and Rotation,
             // we need to generate a full node chain to accommodate for assimp's
@@ -905,7 +930,8 @@ namespace Assimp {
             }
         }
 
-        void FBXConverter::ConvertModel(const Model& model, aiNode& nd, const aiMatrix4x4& node_global_transform)
+        void FBXConverter::ConvertModel(const Model &model, aiNode *parent, aiNode *root_node,
+                                        const aiMatrix4x4 &absolute_transform)
         {
             const std::vector<const Geometry*>& geos = model.GetGeometry();
 
@@ -917,11 +943,12 @@ namespace Assimp {
                 const MeshGeometry* const mesh = dynamic_cast<const MeshGeometry*>(geo);
                 const LineGeometry* const line = dynamic_cast<const LineGeometry*>(geo);
                 if (mesh) {
-                    const std::vector<unsigned int>& indices = ConvertMesh(*mesh, model, node_global_transform, nd);
+                    const std::vector<unsigned int>& indices = ConvertMesh(*mesh, model, parent, root_node,
+                                                                           absolute_transform);
                     std::copy(indices.begin(), indices.end(), std::back_inserter(meshes));
                 }
                 else if (line) {
-                    const std::vector<unsigned int>& indices = ConvertLine(*line, model, node_global_transform, nd);
+                    const std::vector<unsigned int>& indices = ConvertLine(*line, model, parent, root_node);
                     std::copy(indices.begin(), indices.end(), std::back_inserter(meshes));
                 }
                 else {
@@ -930,15 +957,16 @@ namespace Assimp {
             }
 
             if (meshes.size()) {
-                nd.mMeshes = new unsigned int[meshes.size()]();
-                nd.mNumMeshes = static_cast<unsigned int>(meshes.size());
+                parent->mMeshes = new unsigned int[meshes.size()]();
+                parent->mNumMeshes = static_cast<unsigned int>(meshes.size());
 
-                std::swap_ranges(meshes.begin(), meshes.end(), nd.mMeshes);
+                std::swap_ranges(meshes.begin(), meshes.end(), parent->mMeshes);
             }
         }
 
-        std::vector<unsigned int> FBXConverter::ConvertMesh(const MeshGeometry& mesh, const Model& model,
-            const aiMatrix4x4& node_global_transform, aiNode& nd)
+        std::vector<unsigned int>
+        FBXConverter::ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node,
+                                  const aiMatrix4x4 &absolute_transform)
         {
             std::vector<unsigned int> temp;
 
@@ -962,18 +990,18 @@ namespace Assimp {
                 const MatIndexArray::value_type base = mindices[0];
                 for (MatIndexArray::value_type index : mindices) {
                     if (index != base) {
-                        return ConvertMeshMultiMaterial(mesh, model, node_global_transform, nd);
+                        return ConvertMeshMultiMaterial(mesh, model, parent, root_node, absolute_transform);
                     }
                 }
             }
 
             // faster code-path, just copy the data
-            temp.push_back(ConvertMeshSingleMaterial(mesh, model, node_global_transform, nd));
+            temp.push_back(ConvertMeshSingleMaterial(mesh, model, absolute_transform, parent, root_node));
             return temp;
         }
 
         std::vector<unsigned int> FBXConverter::ConvertLine(const LineGeometry& line, const Model& model,
-            const aiMatrix4x4& node_global_transform, aiNode& nd)
+                                                            aiNode *parent, aiNode *root_node)
         {
             std::vector<unsigned int> temp;
 
@@ -984,7 +1012,7 @@ namespace Assimp {
                 return temp;
             }
 
-            aiMesh* const out_mesh = SetupEmptyMesh(line, nd);
+            aiMesh* const out_mesh = SetupEmptyMesh(line, root_node);
             out_mesh->mPrimitiveTypes |= aiPrimitiveType_LINE;
 
             // copy vertices
@@ -1019,7 +1047,7 @@ namespace Assimp {
             return temp;
         }
 
-        aiMesh* FBXConverter::SetupEmptyMesh(const Geometry& mesh, aiNode& nd)
+        aiMesh* FBXConverter::SetupEmptyMesh(const Geometry& mesh, aiNode *parent)
         {
             aiMesh* const out_mesh = new aiMesh();
             meshes.push_back(out_mesh);
@@ -1036,17 +1064,18 @@ namespace Assimp {
             }
             else
             {
-                out_mesh->mName = nd.mName;
+                out_mesh->mName = parent->mName;
             }
 
             return out_mesh;
         }
 
-        unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry& mesh, const Model& model,
-            const aiMatrix4x4& node_global_transform, aiNode& nd)
+        unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model,
+                                                             const aiMatrix4x4 &absolute_transform, aiNode *parent,
+                                                             aiNode *root_node)
         {
             const MatIndexArray& mindices = mesh.GetMaterialIndices();
-            aiMesh* const out_mesh = SetupEmptyMesh(mesh, nd);
+            aiMesh* const out_mesh = SetupEmptyMesh(mesh, parent);
 
             const std::vector<aiVector3D>& vertices = mesh.GetVertices();
             const std::vector<unsigned int>& faces = mesh.GetFaceIndexCounts();
@@ -1113,7 +1142,7 @@ namespace Assimp {
                         binormals = &tempBinormals;
                     }
                     else {
-                        binormals = NULL;
+                        binormals = nullptr;
                     }
                 }
 
@@ -1163,8 +1192,9 @@ namespace Assimp {
                 ConvertMaterialForMesh(out_mesh, model, mesh, mindices[0]);
             }
 
-            if (doc.Settings().readWeights && mesh.DeformerSkin() != NULL) {
-                ConvertWeights(out_mesh, model, mesh, node_global_transform, NO_MATERIAL_SEPARATION);
+            if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr) {
+                ConvertWeights(out_mesh, model, mesh, absolute_transform, parent, root_node, NO_MATERIAL_SEPARATION,
+                               nullptr);
             }
 
             std::vector<aiAnimMesh*> animMeshes;
@@ -1209,8 +1239,10 @@ namespace Assimp {
             return static_cast<unsigned int>(meshes.size() - 1);
         }
 
-        std::vector<unsigned int> FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry& mesh, const Model& model,
-            const aiMatrix4x4& node_global_transform, aiNode& nd)
+        std::vector<unsigned int>
+        FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, aiNode *parent,
+                                               aiNode *root_node,
+                                               const aiMatrix4x4 &absolute_transform)
         {
             const MatIndexArray& mindices = mesh.GetMaterialIndices();
             ai_assert(mindices.size());
@@ -1221,7 +1253,7 @@ namespace Assimp {
             for (MatIndexArray::value_type index : mindices) {
                 if (had.find(index) == had.end()) {
 
-                    indices.push_back(ConvertMeshMultiMaterial(mesh, model, index, node_global_transform, nd));
+                    indices.push_back(ConvertMeshMultiMaterial(mesh, model, index, parent, root_node, absolute_transform));
                     had.insert(index);
                 }
             }
@@ -1229,18 +1261,18 @@ namespace Assimp {
             return indices;
         }
 
-        unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry& mesh, const Model& model,
-            MatIndexArray::value_type index,
-            const aiMatrix4x4& node_global_transform,
-            aiNode& nd)
+        unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model,
+                                                            MatIndexArray::value_type index,
+                                                            aiNode *parent, aiNode *root_node,
+                                                            const aiMatrix4x4 &absolute_transform)
         {
-            aiMesh* const out_mesh = SetupEmptyMesh(mesh, nd);
+            aiMesh* const out_mesh = SetupEmptyMesh(mesh, parent);
 
             const MatIndexArray& mindices = mesh.GetMaterialIndices();
             const std::vector<aiVector3D>& vertices = mesh.GetVertices();
             const std::vector<unsigned int>& faces = mesh.GetFaceIndexCounts();
 
-            const bool process_weights = doc.Settings().readWeights && mesh.DeformerSkin() != NULL;
+            const bool process_weights = doc.Settings().readWeights && mesh.DeformerSkin() != nullptr;
 
             unsigned int count_faces = 0;
             unsigned int count_vertices = 0;
@@ -1300,7 +1332,7 @@ namespace Assimp {
                         binormals = &tempBinormals;
                     }
                     else {
-                        binormals = NULL;
+                        binormals = nullptr;
                     }
                 }
 
@@ -1399,7 +1431,7 @@ namespace Assimp {
             ConvertMaterialForMesh(out_mesh, model, mesh, index);
 
             if (process_weights) {
-                ConvertWeights(out_mesh, model, mesh, node_global_transform, index, &reverseMapping);
+                ConvertWeights(out_mesh, model, mesh, absolute_transform, parent, root_node, index, &reverseMapping);
             }
 
             std::vector<aiAnimMesh*> animMeshes;
@@ -1449,10 +1481,10 @@ namespace Assimp {
             return static_cast<unsigned int>(meshes.size() - 1);
         }
 
-        void FBXConverter::ConvertWeights(aiMesh* out, const Model& model, const MeshGeometry& geo,
-            const aiMatrix4x4& node_global_transform,
-            unsigned int materialIndex,
-            std::vector<unsigned int>* outputVertStartIndices)
+        void FBXConverter::ConvertWeights(aiMesh *out, const Model &model, const MeshGeometry &geo,
+                                          const aiMatrix4x4 &absolute_transform,
+                                          aiNode *parent, aiNode *root_node, unsigned int materialIndex,
+                                          std::vector<unsigned int> *outputVertStartIndices)
         {
             ai_assert(geo.DeformerSkin());
 
@@ -1463,13 +1495,12 @@ namespace Assimp {
             const Skin& sk = *geo.DeformerSkin();
 
             std::vector<aiBone*> bones;
-            bones.reserve(sk.Clusters().size());
 
             const bool no_mat_check = materialIndex == NO_MATERIAL_SEPARATION;
             ai_assert(no_mat_check || outputVertStartIndices);
 
             try {
-
+                // iterate over the sub deformers
                 for (const Cluster* cluster : sk.Clusters()) {
                     ai_assert(cluster);
 
@@ -1483,15 +1514,16 @@ namespace Assimp {
                     index_out_indices.clear();
                     out_indices.clear();
 
+
                     // now check if *any* of these weights is contained in the output mesh,
                     // taking notes so we don't need to do it twice.
                     for (WeightIndexArray::value_type index : indices) {
 
                         unsigned int count = 0;
                         const unsigned int* const out_idx = geo.ToOutputVertexIndex(index, count);
-                        // ToOutputVertexIndex only returns NULL if index is out of bounds
+                        // ToOutputVertexIndex only returns nullptr if index is out of bounds
                         // which should never happen
-                        ai_assert(out_idx != NULL);
+                        ai_assert(out_idx != nullptr);
 
                         index_out_indices.push_back(no_index_sentinel);
                         count_out_indices.push_back(0);
@@ -1520,68 +1552,107 @@ namespace Assimp {
                             }
                         }
                     }
-                    
+
                     // if we found at least one, generate the output bones
                     // XXX this could be heavily simplified by collecting the bone
                     // data in a single step.
-                    ConvertCluster(bones, model, *cluster, out_indices, index_out_indices,
-                            count_out_indices, node_global_transform);
+                    ConvertCluster(bones, cluster, out_indices, index_out_indices,
+                                   count_out_indices, absolute_transform, parent, root_node);
                 }
+
+                bone_map.clear();
             }
-            catch (std::exception&) {
+            catch (std::exception&e) {
                 std::for_each(bones.begin(), bones.end(), Util::delete_fun<aiBone>());
                 throw;
             }
 
             if (bones.empty()) {
+                out->mBones = nullptr;
+                out->mNumBones = 0;
                 return;
-            }
-
-            out->mBones = new aiBone*[bones.size()]();
-            out->mNumBones = static_cast<unsigned int>(bones.size());
+            } else {
+                out->mBones = new aiBone *[bones.size()]();
+                out->mNumBones = static_cast<unsigned int>(bones.size());
 
-            std::swap_ranges(bones.begin(), bones.end(), out->mBones);
+                std::swap_ranges(bones.begin(), bones.end(), out->mBones);
+            }
         }
 
-        void FBXConverter::ConvertCluster(std::vector<aiBone*>& bones, const Model& /*model*/, const Cluster& cl,
-            std::vector<size_t>& out_indices,
-            std::vector<size_t>& index_out_indices,
-            std::vector<size_t>& count_out_indices,
-            const aiMatrix4x4& node_global_transform)
+        const aiNode* FBXConverter::GetNodeByName( const aiString& name, aiNode *current_node )
         {
+            aiNode * iter = current_node;
+            //printf("Child count: %d", iter->mNumChildren);
+            return iter;
+        }
 
-            aiBone* const bone = new aiBone();
-            bones.push_back(bone);
+        void FBXConverter::ConvertCluster(std::vector<aiBone *> &local_mesh_bones, const Cluster *cl,
+                                          std::vector<size_t> &out_indices, std::vector<size_t> &index_out_indices,
+                                          std::vector<size_t> &count_out_indices, const aiMatrix4x4 &absolute_transform,
+                                          aiNode *parent, aiNode *root_node) {
+            ai_assert(cl); // make sure cluster valid
+            std::string deformer_name = cl->TargetNode()->Name();
+            aiString bone_name = aiString(FixNodeName(deformer_name));
 
-            bone->mName = FixNodeName(cl.TargetNode()->Name());
+            aiBone *bone = nullptr;
 
-            bone->mOffsetMatrix = cl.TransformLink();
-            bone->mOffsetMatrix.Inverse();
+            if (bone_map.count(deformer_name)) {
+                std::cout << "retrieved bone from lookup " << bone_name.C_Str() << ". Deformer: " << deformer_name
+                          << std::endl;
+                bone = bone_map[deformer_name];
+            } else {
+                std::cout << "created new bone " << bone_name.C_Str() << ". Deformer: " << deformer_name << std::endl;
+                bone = new aiBone();
+                bone->mName = bone_name;
 
-            bone->mOffsetMatrix = bone->mOffsetMatrix * node_global_transform;
+                // store local transform link for post processing
+                bone->mOffsetMatrix = cl->TransformLink();
+                bone->mOffsetMatrix.Inverse();
 
-            bone->mNumWeights = static_cast<unsigned int>(out_indices.size());
-            aiVertexWeight* cursor = bone->mWeights = new aiVertexWeight[out_indices.size()];
+                aiMatrix4x4 matrix = (aiMatrix4x4)absolute_transform;
 
-            const size_t no_index_sentinel = std::numeric_limits<size_t>::max();
-            const WeightArray& weights = cl.GetWeights();
+                bone->mOffsetMatrix = bone->mOffsetMatrix * matrix; // * mesh_offset
 
-            const size_t c = index_out_indices.size();
-            for (size_t i = 0; i < c; ++i) {
-                const size_t index_index = index_out_indices[i];
 
-                if (index_index == no_index_sentinel) {
-                    continue;
-                }
+                //
+                // Now calculate the aiVertexWeights
+                //
+
+                aiVertexWeight *cursor = nullptr;
+
+                bone->mNumWeights = static_cast<unsigned int>(out_indices.size());
+                cursor = bone->mWeights = new aiVertexWeight[out_indices.size()];
 
-                const size_t cc = count_out_indices[i];
-                for (size_t j = 0; j < cc; ++j) {
-                    aiVertexWeight& out_weight = *cursor++;
+                const size_t no_index_sentinel = std::numeric_limits<size_t>::max();
+                const WeightArray& weights = cl->GetWeights();
 
-                    out_weight.mVertexId = static_cast<unsigned int>(out_indices[index_index + j]);
-                    out_weight.mWeight = weights[i];
+                const size_t c = index_out_indices.size();
+                for (size_t i = 0; i < c; ++i) {
+                    const size_t index_index = index_out_indices[i];
+
+                    if (index_index == no_index_sentinel) {
+                        continue;
+                    }
+
+                    const size_t cc = count_out_indices[i];
+                    for (size_t j = 0; j < cc; ++j) {
+                        // cursor runs from first element relative to the start
+                        // or relative to the start of the next indexes.
+                        aiVertexWeight& out_weight = *cursor++;
+
+                        out_weight.mVertexId = static_cast<unsigned int>(out_indices[index_index + j]);
+                        out_weight.mWeight = weights[i];
+                    }
                 }
+
+                bone_map.insert(std::pair<const std::string, aiBone *>(deformer_name, bone));
             }
+
+            std::cout << "bone research: Indicies size: " << out_indices.size() << std::endl;
+
+            // lookup must be populated in case something goes wrong
+            // this also allocates bones to mesh instance outside
+            local_mesh_bones.push_back(bone);
         }
 
         void FBXConverter::ConvertMaterialForMesh(aiMesh* out, const Model& model, const MeshGeometry& geo,
@@ -1711,7 +1782,7 @@ namespace Assimp {
                 bool textureReady = false; //tells if our texture is ready (if it was loaded or if it was found)
                 unsigned int index;
 
-                VideoMap::const_iterator it = textures_converted.find(media);
+                VideoMap::const_iterator it = textures_converted.find(*media);
                 if (it != textures_converted.end()) {
                     index = (*it).second;
                     textureReady = true;
@@ -1719,7 +1790,7 @@ namespace Assimp {
                 else {
                     if (media->ContentLength() > 0) {
                         index = ConvertVideo(*media);
-                        textures_converted[media] = index;
+                        textures_converted[*media] = index;
                         textureReady = true;
                     }
                 }
@@ -2243,13 +2314,13 @@ void FBXConverter::SetShadingPropertiesRaw(aiMaterial* out_mat, const PropertyTa
             if (media != nullptr && media->ContentLength() > 0) {
                 unsigned int index;
 
-                VideoMap::const_iterator it = textures_converted.find(media);
+                VideoMap::const_iterator it = textures_converted.find(*media);
                 if (it != textures_converted.end()) {
                     index = (*it).second;
                 }
                 else {
                     index = ConvertVideo(*media);
-                    textures_converted[media] = index;
+                    textures_converted[*media] = index;
                 }
 
                 // setup texture reference string (copied from ColladaLoader::FindFilenameForEffectTexture)
@@ -2677,7 +2748,7 @@ void FBXConverter::SetShadingPropertiesRaw(aiMaterial* out_mat, const PropertyTa
         // sanity check whether the input is ok
         static void validateAnimCurveNodes(const std::vector<const AnimationCurveNode*>& curves,
             bool strictMode) {
-            const Object* target(NULL);
+            const Object* target(nullptr);
             for (const AnimationCurveNode* node : curves) {
                 if (!target) {
                     target = node->Target();
@@ -2708,7 +2779,7 @@ void FBXConverter::SetShadingPropertiesRaw(aiMaterial* out_mat, const PropertyTa
 #ifdef ASSIMP_BUILD_DEBUG
             validateAnimCurveNodes(curves, doc.Settings().strictMode);
 #endif
-            const AnimationCurveNode* curve_node = NULL;
+            const AnimationCurveNode* curve_node = nullptr;
             for (const AnimationCurveNode* node : curves) {
                 ai_assert(node);
 
@@ -3556,7 +3627,7 @@ void FBXConverter::SetShadingPropertiesRaw(aiMaterial* out_mat, const PropertyTa
             ai_assert(!out->mMeshes);
             ai_assert(!out->mNumMeshes);
 
-            // note: the trailing () ensures initialization with NULL - not
+            // note: the trailing () ensures initialization with nullptr - not
             // many C++ users seem to know this, so pointing it out to avoid
             // confusion why this code works.
 
@@ -3603,6 +3674,47 @@ void FBXConverter::SetShadingPropertiesRaw(aiMaterial* out_mat, const PropertyTa
             }
         }
 
+        void FBXConverter::ConvertOrphantEmbeddedTextures()
+        {
+            // in C++14 it could be:
+            // for (auto&& [id, object] : objects)
+            for (auto&& id_and_object : doc.Objects())
+            {
+                auto&& id = std::get<0>(id_and_object);
+                auto&& object = std::get<1>(id_and_object);
+                // If an object doesn't have parent
+                if (doc.ConnectionsBySource().count(id) == 0)
+                {
+                    const Texture* realTexture = nullptr;
+                    try
+                    {
+                        const auto& element = object->GetElement();
+                        const Token& key = element.KeyToken();
+                        const char* obtype = key.begin();
+                        const size_t length = static_cast<size_t>(key.end() - key.begin());
+                        if (strncmp(obtype, "Texture", length) == 0)
+                        {
+                            const Texture* texture = static_cast<const Texture*>(object->Get());
+                            if (texture->Media() && texture->Media()->ContentLength() > 0)
+                            {
+                                realTexture = texture;
+                            }
+                        }
+                    }
+                    catch (...)
+                    {
+                        // do nothing
+                    }
+                    if (realTexture)
+                    {
+                        const Video* media = realTexture->Media();
+                        unsigned int index = ConvertVideo(*media);
+                        textures_converted[*media] = index;
+                    }
+                }
+            }
+        }
+
         // ------------------------------------------------------------------------------------------------
         void ConvertToAssimpScene(aiScene* out, const Document& doc, bool removeEmptyBones)
         {

+ 55 - 28
code/FBX/FBXConverter.h

@@ -123,7 +123,7 @@ private:
 
     // ------------------------------------------------------------------------------------------------
     // collect and assign child nodes
-    void ConvertNodes(uint64_t id, aiNode& parent, const aiMatrix4x4& parent_transform = aiMatrix4x4());
+    void ConvertNodes(uint64_t id, aiNode *parent, aiNode *root_node);
 
     // ------------------------------------------------------------------------------------------------
     void ConvertLights(const Model& model, const std::string &orig_name );
@@ -179,32 +179,35 @@ private:
     void SetupNodeMetadata(const Model& model, aiNode& nd);
 
     // ------------------------------------------------------------------------------------------------
-    void ConvertModel(const Model& model, aiNode& nd, const aiMatrix4x4& node_global_transform);
+    void ConvertModel(const Model &model, aiNode *parent, aiNode *root_node,
+                      const aiMatrix4x4 &absolute_transform);
     
     // ------------------------------------------------------------------------------------------------
     // MeshGeometry -> aiMesh, return mesh index + 1 or 0 if the conversion failed
-    std::vector<unsigned int> ConvertMesh(const MeshGeometry& mesh, const Model& model,
-        const aiMatrix4x4& node_global_transform, aiNode& nd);
+    std::vector<unsigned int>
+    ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node,
+                const aiMatrix4x4 &absolute_transform);
 
     // ------------------------------------------------------------------------------------------------
     std::vector<unsigned int> ConvertLine(const LineGeometry& line, const Model& model,
-        const aiMatrix4x4& node_global_transform, aiNode& nd);
+                                          aiNode *parent, aiNode *root_node);
 
     // ------------------------------------------------------------------------------------------------
-    aiMesh* SetupEmptyMesh(const Geometry& mesh, aiNode& nd);
+    aiMesh* SetupEmptyMesh(const Geometry& mesh, aiNode *parent);
 
     // ------------------------------------------------------------------------------------------------
-    unsigned int ConvertMeshSingleMaterial(const MeshGeometry& mesh, const Model& model,
-        const aiMatrix4x4& node_global_transform, aiNode& nd);
+    unsigned int ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model,
+                                           const aiMatrix4x4 &absolute_transform, aiNode *parent,
+                                           aiNode *root_node);
 
     // ------------------------------------------------------------------------------------------------
-    std::vector<unsigned int> ConvertMeshMultiMaterial(const MeshGeometry& mesh, const Model& model,
-        const aiMatrix4x4& node_global_transform, aiNode& nd);
+    std::vector<unsigned int>
+    ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node,
+                             const aiMatrix4x4 &absolute_transform);
 
     // ------------------------------------------------------------------------------------------------
-    unsigned int ConvertMeshMultiMaterial(const MeshGeometry& mesh, const Model& model,
-        MatIndexArray::value_type index,
-        const aiMatrix4x4& node_global_transform, aiNode& nd);
+    unsigned int ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, MatIndexArray::value_type index,
+                                          aiNode *parent, aiNode *root_node, const aiMatrix4x4 &absolute_transform);
 
     // ------------------------------------------------------------------------------------------------
     static const unsigned int NO_MATERIAL_SEPARATION = /* std::numeric_limits<unsigned int>::max() */
@@ -217,17 +220,17 @@ private:
     *  - outputVertStartIndices is only used when a material index is specified, it gives for
     *    each output vertex the DOM index it maps to.
     */
-    void ConvertWeights(aiMesh* out, const Model& model, const MeshGeometry& geo,
-        const aiMatrix4x4& node_global_transform = aiMatrix4x4(),
-        unsigned int materialIndex = NO_MATERIAL_SEPARATION,
-        std::vector<unsigned int>* outputVertStartIndices = NULL);
-
+    void ConvertWeights(aiMesh *out, const Model &model, const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform,
+                        aiNode *parent = NULL, aiNode *root_node = NULL,
+                        unsigned int materialIndex = NO_MATERIAL_SEPARATION,
+                        std::vector<unsigned int> *outputVertStartIndices = NULL);
+    // lookup
+    static const aiNode* GetNodeByName( const aiString& name, aiNode *current_node );
     // ------------------------------------------------------------------------------------------------
-    void ConvertCluster(std::vector<aiBone*>& bones, const Model& /*model*/, const Cluster& cl,
-        std::vector<size_t>& out_indices,
-        std::vector<size_t>& index_out_indices,
-        std::vector<size_t>& count_out_indices,
-        const aiMatrix4x4& node_global_transform);
+    void ConvertCluster(std::vector<aiBone *> &local_mesh_bones, const Cluster *cl,
+                        std::vector<size_t> &out_indices, std::vector<size_t> &index_out_indices,
+                        std::vector<size_t> &count_out_indices, const aiMatrix4x4 &absolute_transform,
+                        aiNode *parent, aiNode *root_node);
 
     // ------------------------------------------------------------------------------------------------
     void ConvertMaterialForMesh(aiMesh* out, const Model& model, const MeshGeometry& geo,
@@ -424,6 +427,10 @@ private:
     // copy generated meshes, animations, lights, cameras and textures to the output scene
     void TransferDataToScene();
 
+    // ------------------------------------------------------------------------------------------------
+    // FBX file could have embedded textures not connected to anything
+    void ConvertOrphantEmbeddedTextures();
+
 private:
     // 0: not assigned yet, others: index is value - 1
     unsigned int defaultMaterialIndex;
@@ -435,27 +442,47 @@ private:
     std::vector<aiCamera*> cameras;
     std::vector<aiTexture*> textures;
 
-    using MaterialMap = std::map<const Material*, unsigned int>;
+    using MaterialMap = std::fbx_unordered_map<const Material*, unsigned int>;
     MaterialMap materials_converted;
 
-    using VideoMap = std::map<const Video*, unsigned int>;
+    using VideoMap = std::fbx_unordered_map<const Video, unsigned int>;
     VideoMap textures_converted;
 
-    using MeshMap = std::map<const Geometry*, std::vector<unsigned int> >;
+    using MeshMap = std::fbx_unordered_map<const Geometry*, std::vector<unsigned int> >;
     MeshMap meshes_converted;
 
     // fixed node name -> which trafo chain components have animations?
-    using NodeAnimBitMap = std::map<std::string, unsigned int> ;
+    using NodeAnimBitMap = std::fbx_unordered_map<std::string, unsigned int> ;
     NodeAnimBitMap node_anim_chain_bits;
 
     // number of nodes with the same name
-    using NodeNameCache = std::unordered_map<std::string, unsigned int>;
+    using NodeNameCache = std::fbx_unordered_map<std::string, unsigned int>;
     NodeNameCache mNodeNames;
 
+    // Deformer name is not the same as a bone name - it does contain the bone name though :)
+    // Deformer names in FBX are always unique in an FBX file.
+    std::map<const std::string, aiBone *> bone_map;
+
     double anim_fps;
 
     aiScene* const out;
     const FBX::Document& doc;
+
+    static void BuildBoneList(aiNode *current_node, const aiNode *root_node, const aiScene *scene,
+                             std::vector<aiBone*>& bones);
+
+    void BuildBoneStack(aiNode *current_node, const aiNode *root_node, const aiScene *scene,
+                   const std::vector<aiBone *> &bones,
+                   std::map<aiBone *, aiNode *> &bone_stack,
+                   std::vector<aiNode*> &node_stack );
+
+    static void BuildNodeList(aiNode *current_node, std::vector<aiNode *> &nodes);
+
+    static aiNode *GetNodeFromStack(const aiString &node_name, std::vector<aiNode *> &nodes);
+
+    static aiNode *GetArmatureRoot(aiNode *bone_node, std::vector<aiBone*> &bone_list);
+
+    static bool IsBoneNode(const aiString &bone_name, std::vector<aiBone *> &bones);
 };
 
 }

+ 37 - 2
code/FBX/FBXDocument.h

@@ -637,6 +637,20 @@ public:
         return ptr;
     }
 
+    bool operator==(const Video& other) const
+    {
+        return (
+               type == other.type
+            && relativeFileName == other.relativeFileName
+            && fileName == other.fileName
+        );
+    }
+
+    bool operator<(const Video& other) const
+    {
+        return std::tie(type, relativeFileName, fileName) < std::tie(other.type, other.relativeFileName, other.fileName);
+    }
+
 private:
     std::string type;
     std::string relativeFileName;
@@ -1005,10 +1019,10 @@ public:
 // during their entire lifetime (Document). FBX files have
 // up to many thousands of objects (most of which we never use),
 // so the memory overhead for them should be kept at a minimum.
-typedef std::map<uint64_t, LazyObject*> ObjectMap;
+typedef std::fbx_unordered_map<uint64_t, LazyObject*> ObjectMap;
 typedef std::fbx_unordered_map<std::string, std::shared_ptr<const PropertyTable> > PropertyTemplateMap;
 
-typedef std::multimap<uint64_t, const Connection*> ConnectionMap;
+typedef std::fbx_unordered_multimap<uint64_t, const Connection*> ConnectionMap;
 
 /** DOM class for global document settings, a single instance per document can
  *  be accessed via Document.Globals(). */
@@ -1177,4 +1191,25 @@ private:
 } // Namespace FBX
 } // Namespace Assimp
 
+namespace std
+{
+    template <>
+    struct hash<const Assimp::FBX::Video>
+    {
+        std::size_t operator()(const Assimp::FBX::Video& video) const
+        {
+            using std::size_t;
+            using std::hash;
+            using std::string;
+
+            size_t res = 17;
+            res = res * 31 + hash<string>()(video.Name());
+            res = res * 31 + hash<string>()(video.RelativeFilename());
+            res = res * 31 + hash<string>()(video.Type());
+
+            return res;
+        }
+    };
+}
+
 #endif // INCLUDED_AI_FBX_DOCUMENT_H

+ 7 - 7
code/FBX/FBXExportNode.cpp

@@ -325,9 +325,9 @@ void FBX::Node::BeginBinary(Assimp::StreamWriterLE &s)
     this->start_pos = s.Tell();
 
     // placeholders for end pos and property section info
-    s.PutU4(0); // end pos
-    s.PutU4(0); // number of properties
-    s.PutU4(0); // total property section length
+    s.PutU8(0); // end pos
+    s.PutU8(0); // number of properties
+    s.PutU8(0); // total property section length
 
     // node name
     s.PutU1(uint8_t(name.size())); // length of node name
@@ -352,9 +352,9 @@ void FBX::Node::EndPropertiesBinary(
     size_t pos = s.Tell();
     ai_assert(pos > property_start);
     size_t property_section_size = pos - property_start;
-    s.Seek(start_pos + 4);
-    s.PutU4(uint32_t(num_properties));
-    s.PutU4(uint32_t(property_section_size));
+    s.Seek(start_pos + 8); // 8 bytes of uint64_t of end_pos
+    s.PutU8(num_properties);
+    s.PutU8(property_section_size);
     s.Seek(pos);
 }
 
@@ -375,7 +375,7 @@ void FBX::Node::EndBinary(
     // now go back and write initial pos
     this->end_pos = s.Tell();
     s.Seek(start_pos);
-    s.PutU4(uint32_t(end_pos));
+    s.PutU8(end_pos);
     s.Seek(end_pos);
 }
 

+ 2 - 2
code/FBX/FBXExporter.cpp

@@ -81,8 +81,8 @@ using namespace Assimp::FBX;
 // some constants that we'll use for writing metadata
 namespace Assimp {
 namespace FBX {
-    const std::string EXPORT_VERSION_STR = "7.4.0";
-    const uint32_t EXPORT_VERSION_INT = 7400; // 7.4 == 2014/2015
+    const std::string EXPORT_VERSION_STR = "7.5.0";
+    const uint32_t EXPORT_VERSION_INT = 7500; // 7.5 == 2016+
     // FBX files have some hashed values that depend on the creation time field,
     // but for now we don't actually know how to generate these.
     // what we can do is set them to a known-working version.

+ 102 - 110
code/FBX/FBXImporter.cpp

@@ -48,26 +48,26 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include "FBXImporter.h"
 
-#include "FBXTokenizer.h"
+#include "FBXConverter.h"
+#include "FBXDocument.h"
 #include "FBXParser.h"
+#include "FBXTokenizer.h"
 #include "FBXUtil.h"
-#include "FBXDocument.h"
-#include "FBXConverter.h"
 
-#include <assimp/StreamReader.h>
 #include <assimp/MemoryIOWrapper.h>
-#include <assimp/Importer.hpp>
+#include <assimp/StreamReader.h>
 #include <assimp/importerdesc.h>
+#include <assimp/Importer.hpp>
 
 namespace Assimp {
 
-template<>
-const char* LogFunctions<FBXImporter>::Prefix() {
-    static auto prefix = "FBX: ";
-    return prefix;
+template <>
+const char *LogFunctions<FBXImporter>::Prefix() {
+	static auto prefix = "FBX: ";
+	return prefix;
 }
 
-}
+} // namespace Assimp
 
 using namespace Assimp;
 using namespace Assimp::Formatter;
@@ -76,131 +76,123 @@ using namespace Assimp::FBX;
 namespace {
 
 static const aiImporterDesc desc = {
-    "Autodesk FBX Importer",
-    "",
-    "",
-    "",
-    aiImporterFlags_SupportTextFlavour,
-    0,
-    0,
-    0,
-    0,
-    "fbx"
+	"Autodesk FBX Importer",
+	"",
+	"",
+	"",
+	aiImporterFlags_SupportTextFlavour,
+	0,
+	0,
+	0,
+	0,
+	"fbx"
 };
 }
 
 // ------------------------------------------------------------------------------------------------
 // Constructor to be privately used by #Importer
-FBXImporter::FBXImporter()
-{
+FBXImporter::FBXImporter() {
 }
 
 // ------------------------------------------------------------------------------------------------
 // Destructor, private as well
-FBXImporter::~FBXImporter()
-{
+FBXImporter::~FBXImporter() {
 }
 
 // ------------------------------------------------------------------------------------------------
 // Returns whether the class can handle the format of the given file.
-bool FBXImporter::CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const
-{
-    const std::string& extension = GetExtension(pFile);
-    if (extension == std::string( desc.mFileExtensions ) ) {
-        return true;
-    }
-
-    else if ((!extension.length() || checkSig) && pIOHandler)   {
-        // at least ASCII-FBX files usually have a 'FBX' somewhere in their head
-        const char* tokens[] = {"fbx"};
-        return SearchFileHeaderForToken(pIOHandler,pFile,tokens,1);
-    }
-    return false;
+bool FBXImporter::CanRead(const std::string &pFile, IOSystem *pIOHandler, bool checkSig) const {
+	const std::string &extension = GetExtension(pFile);
+	if (extension == std::string(desc.mFileExtensions)) {
+		return true;
+	}
+
+	else if ((!extension.length() || checkSig) && pIOHandler) {
+		// at least ASCII-FBX files usually have a 'FBX' somewhere in their head
+		const char *tokens[] = { "fbx" };
+		return SearchFileHeaderForToken(pIOHandler, pFile, tokens, 1);
+	}
+	return false;
 }
 
 // ------------------------------------------------------------------------------------------------
 // List all extensions handled by this loader
-const aiImporterDesc* FBXImporter::GetInfo () const
-{
-    return &desc;
+const aiImporterDesc *FBXImporter::GetInfo() const {
+	return &desc;
 }
 
 // ------------------------------------------------------------------------------------------------
 // Setup configuration properties for the loader
-void FBXImporter::SetupProperties(const Importer* pImp)
-{
-    settings.readAllLayers = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true);
-    settings.readAllMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS, false);
-    settings.readMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true);
-    settings.readTextures = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true);
-    settings.readCameras = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, true);
-    settings.readLights = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, true);
-    settings.readAnimations = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, true);
-    settings.strictMode = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_STRICT_MODE, false);
-    settings.preservePivots = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, true);
-    settings.optimizeEmptyAnimationCurves = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
-    settings.useLegacyEmbeddedTextureNaming = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING, false);
-    settings.removeEmptyBones = pImp->GetPropertyBool(AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES, true);
-    settings.convertToMeters = pImp->GetPropertyBool(AI_CONFIG_FBX_CONVERT_TO_M, false);
+void FBXImporter::SetupProperties(const Importer *pImp) {
+	settings.readAllLayers = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true);
+	settings.readAllMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS, false);
+	settings.readMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true);
+	settings.readTextures = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true);
+	settings.readCameras = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, true);
+	settings.readLights = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, true);
+	settings.readAnimations = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, true);
+	settings.strictMode = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_STRICT_MODE, false);
+	settings.preservePivots = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, true);
+	settings.optimizeEmptyAnimationCurves = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
+	settings.useLegacyEmbeddedTextureNaming = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING, false);
+	settings.removeEmptyBones = pImp->GetPropertyBool(AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES, true);
+	settings.convertToMeters = pImp->GetPropertyBool(AI_CONFIG_FBX_CONVERT_TO_M, false);
 }
 
 // ------------------------------------------------------------------------------------------------
 // Imports the given file into the given scene structure.
-void FBXImporter::InternReadFile( const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler)
-{
-    std::unique_ptr<IOStream> stream(pIOHandler->Open(pFile,"rb"));
-    if (!stream) {
-        ThrowException("Could not open file for reading");
-    }
-
-    // read entire file into memory - no streaming for this, fbx
-    // files can grow large, but the assimp output data structure
-    // then becomes very large, too. Assimp doesn't support
-    // streaming for its output data structures so the net win with
-    // streaming input data would be very low.
-    std::vector<char> contents;
-    contents.resize(stream->FileSize()+1);
-    stream->Read( &*contents.begin(), 1, contents.size()-1 );
-    contents[ contents.size() - 1 ] = 0;
-    const char* const begin = &*contents.begin();
-
-    // broadphase tokenizing pass in which we identify the core
-    // syntax elements of FBX (brackets, commas, key:value mappings)
-    TokenList tokens;
-    try {
-
-        bool is_binary = false;
-        if (!strncmp(begin,"Kaydara FBX Binary",18)) {
-            is_binary = true;
-            TokenizeBinary(tokens,begin,contents.size());
-        }
-        else {
-            Tokenize(tokens,begin);
-        }
-
-        // use this information to construct a very rudimentary
-        // parse-tree representing the FBX scope structure
-        Parser parser(tokens, is_binary);
-
-        // take the raw parse-tree and convert it to a FBX DOM
-        Document doc(parser,settings);
-
-        // convert the FBX DOM to aiScene
-        ConvertToAssimpScene(pScene, doc, settings.removeEmptyBones);
-
-        // size relative to cm
-        float size_relative_to_cm = doc.GlobalSettings().UnitScaleFactor();
-
-        // Set FBX file scale is relative to CM must be converted to M for
-        // assimp universal format (M)
-        SetFileScale( size_relative_to_cm * 0.01f);
-
-        std::for_each(tokens.begin(),tokens.end(),Util::delete_fun<Token>());
-    }
-    catch(std::exception&) {
-        std::for_each(tokens.begin(),tokens.end(),Util::delete_fun<Token>());
-        throw;
-    }
+void FBXImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSystem *pIOHandler) {
+	std::unique_ptr<IOStream> stream(pIOHandler->Open(pFile, "rb"));
+	if (!stream) {
+		ThrowException("Could not open file for reading");
+	}
+
+	// read entire file into memory - no streaming for this, fbx
+	// files can grow large, but the assimp output data structure
+	// then becomes very large, too. Assimp doesn't support
+	// streaming for its output data structures so the net win with
+	// streaming input data would be very low.
+	std::vector<char> contents;
+	contents.resize(stream->FileSize() + 1);
+	stream->Read(&*contents.begin(), 1, contents.size() - 1);
+	contents[contents.size() - 1] = 0;
+	const char *const begin = &*contents.begin();
+
+	// broadphase tokenizing pass in which we identify the core
+	// syntax elements of FBX (brackets, commas, key:value mappings)
+	TokenList tokens;
+	try {
+
+		bool is_binary = false;
+		if (!strncmp(begin, "Kaydara FBX Binary", 18)) {
+			is_binary = true;
+			TokenizeBinary(tokens, begin, contents.size());
+		} else {
+			Tokenize(tokens, begin);
+		}
+
+		// use this information to construct a very rudimentary
+		// parse-tree representing the FBX scope structure
+		Parser parser(tokens, is_binary);
+
+		// take the raw parse-tree and convert it to a FBX DOM
+		Document doc(parser, settings);
+
+		// convert the FBX DOM to aiScene
+		ConvertToAssimpScene(pScene, doc, settings.removeEmptyBones);
+
+		// size relative to cm
+		float size_relative_to_cm = doc.GlobalSettings().UnitScaleFactor();
+
+		// Set FBX file scale is relative to CM must be converted to M for
+		// assimp universal format (M)
+		SetFileScale(size_relative_to_cm * 0.01f);
+
+		std::for_each(tokens.begin(), tokens.end(), Util::delete_fun<Token>());
+	} catch (std::exception &) {
+		std::for_each(tokens.begin(), tokens.end(), Util::delete_fun<Token>());
+		throw;
+	}
 }
 
 #endif // !ASSIMP_BUILD_NO_FBX_IMPORTER

+ 395 - 0
code/M3D/M3DExporter.cpp

@@ -0,0 +1,395 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+Copyright (c) 2019 bzt
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+#ifndef ASSIMP_BUILD_NO_EXPORT
+#ifndef ASSIMP_BUILD_NO_M3D_EXPORTER
+
+#define M3D_IMPLEMENTATION
+#define M3D_NOIMPORTER
+#define M3D_EXPORTER
+#define M3D_ASCII
+#ifndef ASSIMP_BUILD_NO_M3D_IMPORTER
+#define M3D_NODUP
+#endif
+
+// Header files, standard library.
+#include <memory> // shared_ptr
+#include <string>
+#include <vector>
+
+#include <assimp/version.h> // aiGetVersion
+#include <assimp/IOSystem.hpp>
+#include <assimp/Exporter.hpp>
+#include <assimp/DefaultLogger.hpp>
+#include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/Exceptional.h> // DeadlyExportError
+#include <assimp/material.h> // aiTextureType
+#include <assimp/scene.h>
+#include <assimp/mesh.h>
+#include "M3DExporter.h"
+#include "M3DMaterials.h"
+
+// RESOURCES:
+// https://gitlab.com/bztsrc/model3d/blob/master/docs/m3d_format.md
+// https://gitlab.com/bztsrc/model3d/blob/master/docs/a3d_format.md
+
+/*
+ * Currently supports static meshes, vertex colors, materials, textures
+ *
+ * For animation, it would require the following conversions:
+ *  - aiNode (bones) -> m3d_t.bone (with parent id, position vector and oriantation quaternion)
+ *  - aiMesh.aiBone -> m3d_t.skin (per vertex, with bone id, weight pairs)
+ *  - aiAnimation -> m3d_action (frame with timestamp and list of bone id, position, orientation
+ *      triplets, instead of per bone timestamp + lists)
+ */
+using namespace Assimp;
+
+namespace Assimp {
+
+    // ---------------------------------------------------------------------
+    // Worker function for exporting a scene to binary M3D.
+    // Prototyped and registered in Exporter.cpp
+    void ExportSceneM3D (
+        const char* pFile,
+        IOSystem* pIOSystem,
+        const aiScene* pScene,
+        const ExportProperties* pProperties
+    ){
+        // initialize the exporter
+        M3DExporter exporter(pScene, pProperties);
+
+        // perform binary export
+        exporter.doExport(pFile, pIOSystem, false);
+    }
+
+    // ---------------------------------------------------------------------
+    // Worker function for exporting a scene to ASCII A3D.
+    // Prototyped and registered in Exporter.cpp
+    void ExportSceneA3D (
+        const char* pFile,
+        IOSystem* pIOSystem,
+        const aiScene* pScene,
+        const ExportProperties* pProperties
+
+    ){
+        // initialize the exporter
+        M3DExporter exporter(pScene, pProperties);
+
+        // perform ascii export
+        exporter.doExport(pFile, pIOSystem, true);
+    }
+
+} // end of namespace Assimp
+
+// ------------------------------------------------------------------------------------------------
+M3DExporter::M3DExporter ( const aiScene* pScene, const ExportProperties* pProperties )
+: mScene(pScene)
+, mProperties(pProperties)
+, outfile()
+, m3d(nullptr) { }
+
+// ------------------------------------------------------------------------------------------------
+void M3DExporter::doExport (
+    const char* pFile,
+    IOSystem* pIOSystem,
+    bool toAscii
+){
+    // TODO: convert mProperties into M3D_EXP_* flags
+    (void)mProperties;
+
+    // open the indicated file for writing (in binary / ASCII mode)
+    outfile.reset(pIOSystem->Open(pFile, toAscii ? "wt" : "wb"));
+    if (!outfile) {
+        throw DeadlyExportError( "could not open output .m3d file: " + std::string(pFile) );
+    }
+
+    // use malloc() here because m3d_free() will call free()
+    m3d = (m3d_t*)calloc(1, sizeof(m3d_t));
+    if(!m3d) {
+        throw DeadlyExportError( "memory allocation error" );
+    }
+    m3d->name = _m3d_safestr((char*)&mScene->mRootNode->mName.data, 2);
+
+    // Create a model from assimp structures
+    aiMatrix4x4 m;
+    NodeWalk(mScene->mRootNode, m);
+
+    // serialize the structures
+    unsigned int size;
+    unsigned char *output = m3d_save(m3d, M3D_EXP_FLOAT,
+        M3D_EXP_EXTRA | (toAscii ? M3D_EXP_ASCII : 0), &size);
+    m3d_free(m3d);
+    if(!output || size < 8) {
+        throw DeadlyExportError( "unable to serialize into Model 3D" );
+    }
+
+    // Write out serialized model
+    outfile->Write(output, size, 1);
+
+    // explicitly release file pointer,
+    // so we don't have to rely on class destruction.
+    outfile.reset();
+}
+
+// ------------------------------------------------------------------------------------------------
+// recursive node walker
+void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m)
+{
+    aiMatrix4x4 nm = m * pNode->mTransformation;
+
+    for(unsigned int i = 0; i < pNode->mNumMeshes; i++) {
+        const aiMesh *mesh = mScene->mMeshes[pNode->mMeshes[i]];
+        unsigned int mi = (M3D_INDEX)-1U;
+        if(mScene->mMaterials) {
+            // get the material for this mesh
+            mi = addMaterial(mScene->mMaterials[mesh->mMaterialIndex]);
+        }
+        // iterate through the mesh faces
+        for(unsigned int j = 0; j < mesh->mNumFaces; j++) {
+            unsigned int n;
+            const aiFace* face = &(mesh->mFaces[j]);
+            // only triangle meshes supported for now
+            if(face->mNumIndices != 3) {
+                throw DeadlyExportError( "use aiProcess_Triangulate before export" );
+            }
+            // add triangle to the output
+            n = m3d->numface++;
+            m3d->face = (m3df_t*)M3D_REALLOC(m3d->face,
+                m3d->numface * sizeof(m3df_t));
+            if(!m3d->face) {
+                throw DeadlyExportError( "memory allocation error" );
+            }
+            /* set all index to -1 by default */
+            m3d->face[n].vertex[0] = m3d->face[n].vertex[1] = m3d->face[n].vertex[2] =
+            m3d->face[n].normal[0] = m3d->face[n].normal[1] = m3d->face[n].normal[2] =
+            m3d->face[n].texcoord[0] = m3d->face[n].texcoord[1] = m3d->face[n].texcoord[2] = -1U;
+            m3d->face[n].materialid = mi;
+            for(unsigned int k = 0; k < face->mNumIndices; k++) {
+                // get the vertex's index
+                unsigned int l = face->mIndices[k];
+                unsigned int idx;
+                m3dv_t vertex;
+                m3dti_t ti;
+                // multiply the position vector by the transformation matrix
+                aiVector3D v = mesh->mVertices[l];
+                v *= nm;
+                vertex.x = v.x;
+                vertex.y = v.y;
+                vertex.z = v.z;
+                vertex.w = 1.0;
+                vertex.color = 0;
+                vertex.skinid = -1U;
+                // add color if defined
+                if(mesh->HasVertexColors(0))
+                    vertex.color = mkColor(&mesh->mColors[0][l]);
+                // save the vertex to the output
+                m3d->vertex = _m3d_addvrtx(m3d->vertex, &m3d->numvertex,
+                    &vertex, &idx);
+                m3d->face[n].vertex[k] = (M3D_INDEX)idx;
+                // do we have texture coordinates?
+                if(mesh->HasTextureCoords(0)) {
+                    ti.u = mesh->mTextureCoords[0][l].x;
+                    ti.v = mesh->mTextureCoords[0][l].y;
+                    m3d->tmap = _m3d_addtmap(m3d->tmap, &m3d->numtmap, &ti,
+                        &idx);
+                    m3d->face[n].texcoord[k] = (M3D_INDEX)idx;
+                }
+                // do we have normal vectors?
+                if(mesh->HasNormals()) {
+                    vertex.color = 0;
+                    vertex.x = mesh->mNormals[l].x;
+                    vertex.y = mesh->mNormals[l].y;
+                    vertex.z = mesh->mNormals[l].z;
+                    m3d->vertex = _m3d_addnorm(m3d->vertex, &m3d->numvertex,
+                        &vertex, &idx);
+                    m3d->face[n].normal[k] = (M3D_INDEX)idx;
+                }
+            }
+        }
+    }
+    // repeat for the children nodes
+    for (unsigned int i = 0; i < pNode->mNumChildren; i++) {
+        NodeWalk(pNode->mChildren[i], nm);
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// convert aiColor4D into uint32_t
+uint32_t M3DExporter::mkColor(aiColor4D* c)
+{
+    return  ((uint8_t)(c->a*255) << 24L) |
+            ((uint8_t)(c->b*255) << 16L) |
+            ((uint8_t)(c->g*255) <<  8L) |
+            ((uint8_t)(c->r*255) <<  0L);
+}
+
+// ------------------------------------------------------------------------------------------------
+// add a material to the output
+M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat)
+{
+    unsigned int mi = -1U;
+    aiColor4D c;
+    aiString name;
+    ai_real f;
+    char *fn;
+
+    if(mat && mat->Get(AI_MATKEY_NAME, name) == AI_SUCCESS && name.length &&
+        strcmp((char*)&name.data, AI_DEFAULT_MATERIAL_NAME)) {
+        // check if we have saved a material by this name. This has to be done
+        // because only the referenced materials should be added to the output
+        for(unsigned int i = 0; i < m3d->nummaterial; i++)
+            if(!strcmp((char*)&name.data, m3d->material[i].name)) {
+                mi = i;
+                break;
+            }
+        // if not found, add the material to the output
+        if(mi == -1U) {
+            unsigned int k;
+            mi = m3d->nummaterial++;
+            m3d->material = (m3dm_t*)M3D_REALLOC(m3d->material, m3d->nummaterial
+                * sizeof(m3dm_t));
+            if(!m3d->material) {
+                throw DeadlyExportError( "memory allocation error" );
+            }
+            m3d->material[mi].name = _m3d_safestr((char*)&name.data, 0);
+            m3d->material[mi].numprop = 0;
+            m3d->material[mi].prop = NULL;
+            // iterate through the material property table and see what we got
+            for(k = 0; k < 15; k++) {
+                unsigned int j;
+                if(m3d_propertytypes[k].format == m3dpf_map)
+                    continue;
+                if(aiProps[k].pKey) {
+                    switch(m3d_propertytypes[k].format) {
+                        case m3dpf_color:
+                            if(mat->Get(aiProps[k].pKey, aiProps[k].type,
+                                aiProps[k].index, c) == AI_SUCCESS)
+                                addProp(&m3d->material[mi],
+                                    m3d_propertytypes[k].id, mkColor(&c));
+                        break;
+                        case m3dpf_float:
+                            if(mat->Get(aiProps[k].pKey, aiProps[k].type,
+                                aiProps[k].index, f) == AI_SUCCESS)
+                                    addProp(&m3d->material[mi],
+                                        m3d_propertytypes[k].id,
+                                        /* not (uint32_t)f, because we don't want to convert
+                                         * it, we want to see it as 32 bits of memory */
+                                        *((uint32_t*)&f));
+                        break;
+                        case m3dpf_uint8:
+                            if(mat->Get(aiProps[k].pKey, aiProps[k].type,
+                                aiProps[k].index, j) == AI_SUCCESS) {
+                                // special conversion for illumination model property
+                                if(m3d_propertytypes[k].id == m3dp_il) {
+                                    switch(j) {
+                                        case aiShadingMode_NoShading: j = 0; break;
+                                        case aiShadingMode_Phong: j = 2; break;
+                                        default: j = 1; break;
+                                    }
+                                }
+                                addProp(&m3d->material[mi],
+                                    m3d_propertytypes[k].id, j);
+                            }
+                        break;
+                        default:
+                            if(mat->Get(aiProps[k].pKey, aiProps[k].type,
+                                aiProps[k].index, j) == AI_SUCCESS)
+                                addProp(&m3d->material[mi],
+                                    m3d_propertytypes[k].id, j);
+                        break;
+                    }
+                }
+                if(aiTxProps[k].pKey &&
+                    mat->GetTexture((aiTextureType)aiTxProps[k].type,
+                        aiTxProps[k].index, &name, NULL, NULL, NULL,
+                        NULL, NULL) == AI_SUCCESS) {
+                        unsigned int i;
+                        for(j = name.length-1; j > 0 && name.data[j]!='.'; j++);
+                        if(j && name.data[j]=='.' &&
+                            (name.data[j+1]=='p' || name.data[j+1]=='P') &&
+                            (name.data[j+1]=='n' || name.data[j+1]=='N') &&
+                            (name.data[j+1]=='g' || name.data[j+1]=='G'))
+                                name.data[j]=0;
+                        // do we have this texture saved already?
+                        fn = _m3d_safestr((char*)&name.data, 0);
+                        for(j = 0, i = -1U; j < m3d->numtexture; j++)
+                            if(!strcmp(fn, m3d->texture[j].name)) {
+                                i = j;
+                                free(fn);
+                                break;
+                        }
+                        if(i == -1U) {
+                            i = m3d->numtexture++;
+                            m3d->texture = (m3dtx_t*)M3D_REALLOC(
+                                m3d->texture,
+                                m3d->numtexture * sizeof(m3dtx_t));
+                            if(!m3d->texture) {
+                                throw DeadlyExportError( "memory allocation error" );
+                            }
+                            // we don't need the texture itself, only its name
+                            m3d->texture[i].name = fn;
+                            m3d->texture[i].w = 0;
+                            m3d->texture[i].h = 0;
+                            m3d->texture[i].d = NULL;
+                        }
+                        addProp(&m3d->material[mi],
+                            m3d_propertytypes[k].id + 128, i);
+                }
+            }
+        }
+    }
+    return mi;
+}
+
+// ------------------------------------------------------------------------------------------------
+// add a material property to the output
+void M3DExporter::addProp(m3dm_t *m, uint8_t type, uint32_t value)
+{
+    unsigned int i;
+    i = m->numprop++;
+    m->prop = (m3dp_t*)M3D_REALLOC(m->prop, m->numprop * sizeof(m3dp_t));
+    if(!m->prop) { throw DeadlyExportError( "memory allocation error" ); }
+    m->prop[i].type = type;
+    m->prop[i].value.num = value;
+}
+
+#endif // ASSIMP_BUILD_NO_M3D_EXPORTER
+#endif // ASSIMP_BUILD_NO_EXPORT

+ 98 - 0
code/M3D/M3DExporter.h

@@ -0,0 +1,98 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+Copyright (c) 2019 bzt
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file M3DExporter.h
+*   @brief Declares the exporter class to write a scene to a Model 3D file
+*/
+#ifndef AI_M3DEXPORTER_H_INC
+#define AI_M3DEXPORTER_H_INC
+
+#ifndef ASSIMP_BUILD_NO_M3D_EXPORTER
+
+#include "m3d.h"
+
+#include <assimp/types.h>
+//#include <assimp/material.h>
+#include <assimp/StreamWriter.h> // StreamWriterLE
+#include <assimp/Exceptional.h> // DeadlyExportError
+
+#include <memory> // shared_ptr
+
+struct aiScene;
+struct aiNode;
+struct aiMaterial;
+struct aiFace;
+
+namespace Assimp
+{
+    class IOSystem;
+    class IOStream;
+    class ExportProperties;
+
+    // ---------------------------------------------------------------------
+    /** Helper class to export a given scene to an M3D file. */
+    // ---------------------------------------------------------------------
+    class M3DExporter
+    {
+    public:
+        /// Constructor for a specific scene to export
+        M3DExporter(const aiScene* pScene, const ExportProperties* pProperties);
+        // call this to do the actual export
+        void doExport(const char* pFile, IOSystem* pIOSystem, bool toAscii);
+
+    private:
+        const aiScene* mScene; // the scene to export
+        const ExportProperties* mProperties; // currently unused
+        std::shared_ptr<IOStream> outfile; // file to write to
+        m3d_t *m3d; // model for the C library to convert to
+
+        // helper to do the recursive walking
+        void NodeWalk(const aiNode* pNode, aiMatrix4x4 m);
+        uint32_t mkColor(aiColor4D* c);
+        M3D_INDEX addMaterial(const aiMaterial *mat);
+        void addProp(m3dm_t *m, uint8_t type, uint32_t value);
+    };
+}
+
+#endif // ASSIMP_BUILD_NO_M3D_EXPORTER
+
+#endif // AI_M3DEXPORTER_H_INC

+ 737 - 0
code/M3D/M3DImporter.cpp

@@ -0,0 +1,737 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+Copyright (c) 2019 bzt
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+#ifndef ASSIMP_BUILD_NO_M3D_IMPORTER
+
+#define M3D_IMPLEMENTATION
+#define M3D_ASCII
+
+#include <assimp/IOStreamBuffer.h>
+#include <memory>
+#include <assimp/DefaultIOSystem.h>
+#include <assimp/Importer.hpp>
+#include <assimp/scene.h>
+#include <assimp/ai_assert.h>
+#include <assimp/DefaultLogger.hpp>
+#include <assimp/importerdesc.h>
+#include "M3DImporter.h"
+#include "M3DMaterials.h"
+
+// RESOURCES:
+// https://gitlab.com/bztsrc/model3d/blob/master/docs/m3d_format.md
+// https://gitlab.com/bztsrc/model3d/blob/master/docs/a3d_format.md
+
+/*
+ Unfortunately aiNode has bone structures and meshes too, yet we can't assign
+ the mesh to a bone aiNode as a skin may refer to several aiNodes. Therefore
+ I've decided to import into this structure:
+
+   aiScene->mRootNode
+    |        |->mMeshes (all the meshes)
+    |        \->children (empty if there's no skeleton imported, no meshes)
+    |             \->skeleton root aiNode*
+    |                   |->bone aiNode
+    |                   |   \->subbone aiNode
+    |                   |->bone aiNode
+    |                   |   ...
+    |                   \->bone aiNode
+    \->mMeshes[]
+        \->aiBone, referencing mesh-less aiNodes from above
+
+  * - normally one, but if a model has several skeleton roots, then all of them
+      are listed in aiScene->mRootNode->children, but all without meshes
+*/
+
+static const aiImporterDesc desc = {
+    "Model 3D Importer",
+    "",
+    "",
+    "",
+    aiImporterFlags_SupportBinaryFlavour,
+    0,
+    0,
+    0,
+    0,
+    "m3d a3d"
+};
+
+// workaround: the SDK expects a C callback, but we want to use Assimp::IOSystem to implement that
+extern "C" {
+    void* m3dimporter_pIOHandler;
+
+    unsigned char *m3dimporter_readfile(char *fn, unsigned int *size) {
+        ai_assert( nullptr != fn );
+        ai_assert( nullptr != size );
+        std::string file(fn);
+        std::unique_ptr<Assimp::IOStream> pStream(
+            (reinterpret_cast<Assimp::IOSystem*>(m3dimporter_pIOHandler))->Open( file, "rb"));
+        size_t fileSize = pStream->FileSize();
+        // should be allocated with malloc(), because the library will call free() to deallocate
+        unsigned char *data = (unsigned char*)malloc(fileSize);
+        if( !data || !pStream.get() || !fileSize || fileSize != pStream->Read(data,1,fileSize)) {
+            pStream.reset();
+            *size = 0;
+            // don't throw a deadly exception, it's not fatal if we can't read an external asset
+            return nullptr;
+        }
+        pStream.reset();
+        *size = (int)fileSize;
+        return data;
+    }
+}
+
+namespace Assimp {
+
+using namespace std;
+
+// ------------------------------------------------------------------------------------------------
+//  Default constructor
+M3DImporter::M3DImporter()
+: mScene(nullptr)
+, m3d(nullptr) { }
+
+// ------------------------------------------------------------------------------------------------
+//  Destructor.
+M3DImporter::~M3DImporter() {}
+
+// ------------------------------------------------------------------------------------------------
+//  Returns true, if file is a binary or ASCII Model 3D file.
+bool M3DImporter::CanRead(const std::string& pFile, IOSystem*  pIOHandler , bool checkSig) const {
+    const std::string extension = GetExtension(pFile);
+
+    if (extension == "m3d" || extension == "a3d")
+        return true;
+    else if (!extension.length() || checkSig)   {
+        if (!pIOHandler) {
+            return true;
+        }
+        /*
+         * don't use CheckMagicToken because that checks with swapped bytes too, leading to false
+         * positives. This magic is not uint32_t, but char[4], so memcmp is the best way
+
+        const char* tokens[] = {"3DMO", "3dmo"};
+        return CheckMagicToken(pIOHandler,pFile,tokens,2,0,4);
+        */
+        std::unique_ptr<IOStream> pStream (pIOHandler->Open(pFile, "rb"));
+        unsigned char data[4];
+        if(4 != pStream->Read(data,1,4)) {
+            return false;
+        }
+        return !memcmp(data, "3DMO", 4) /* bin */ || !memcmp(data, "3dmo", 4) /* ASCII */;
+    }
+    return false;
+}
+
+// ------------------------------------------------------------------------------------------------
+const aiImporterDesc* M3DImporter::GetInfo() const {
+    return &desc;
+}
+
+// ------------------------------------------------------------------------------------------------
+//  Model 3D import implementation
+void M3DImporter::InternReadFile( const std::string &file, aiScene* pScene, IOSystem* pIOHandler) {
+    // Read file into memory
+    std::unique_ptr<IOStream> pStream( pIOHandler->Open( file, "rb"));
+    if( !pStream.get() ) {
+        throw DeadlyImportError( "Failed to open file " + file + "." );
+    }
+
+    // Get the file-size and validate it, throwing an exception when fails
+    size_t fileSize = pStream->FileSize();
+    if( fileSize < 8 ) {
+        throw DeadlyImportError( "M3D-file " + file + " is too small." );
+    }
+    std::unique_ptr<unsigned char[]> _buffer (new unsigned char[fileSize]);
+    unsigned char *data( _buffer.get() );
+    if(fileSize != pStream->Read(data,1,fileSize)) {
+        throw DeadlyImportError( "Failed to read the file " + file + "." );
+    }
+
+    // Get the path for external assets
+    std::string  folderName( "./" );
+    std::string::size_type pos = file.find_last_of( "\\/" );
+    if ( pos != std::string::npos ) {
+        folderName = file.substr( 0, pos );
+        if ( !folderName.empty() ) {
+            pIOHandler->PushDirectory( folderName );
+        }
+    }
+    // pass this IOHandler to the C callback
+    m3dimporter_pIOHandler = pIOHandler;
+
+    //DefaultLogger::create("/dev/stderr", Logger::VERBOSE);
+    ASSIMP_LOG_DEBUG_F("M3D: loading ", file);
+
+    // let the C SDK do the hard work for us
+    m3d = m3d_load(&data[0], m3dimporter_readfile, free, nullptr);
+    m3dimporter_pIOHandler = nullptr;
+    if( !m3d ) {
+        throw DeadlyImportError( "Unable to parse " + file + " as M3D." );
+    }
+
+    // create the root node
+    pScene->mRootNode = new aiNode;
+    pScene->mRootNode->mName = aiString(std::string(std::string(m3d->name)));
+    pScene->mRootNode->mTransformation = aiMatrix4x4();
+    pScene->mRootNode->mNumChildren = 0;
+    mScene = pScene;
+
+    ASSIMP_LOG_DEBUG("M3D: root node " + std::string(m3d->name));
+
+    // now we just have to fill up the Assimp structures in pScene
+    importMaterials();
+    importTextures();
+    importBones(-1U, pScene->mRootNode);
+    importMeshes();
+    importAnimations();
+
+    // we don't need the SDK's version any more
+    m3d_free(m3d);
+
+    // Pop directory stack
+    if ( pIOHandler->StackSize() > 0 ) {
+        pIOHandler->PopDirectory();
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// convert materials. properties are converted using a static table in M3DMaterials.h
+void M3DImporter::importMaterials()
+{
+    unsigned int i, j, k, l, n;
+    m3dm_t *m;
+    aiString name = aiString(AI_DEFAULT_MATERIAL_NAME);
+    aiColor4D c;
+    ai_real f;
+
+    ai_assert(mScene != nullptr);
+    ai_assert(m3d != nullptr);
+
+    mScene->mNumMaterials = m3d->nummaterial + 1;
+    mScene->mMaterials = new aiMaterial*[ m3d->nummaterial + 1 ];
+
+    ASSIMP_LOG_DEBUG_F("M3D: importMaterials ", mScene->mNumMaterials);
+
+    // add a default material as first
+    aiMaterial* mat = new aiMaterial;
+    mat->AddProperty( &name, AI_MATKEY_NAME );
+    c.a = 1.0; c.b = c.g = c.r = 0.6;
+    mat->AddProperty( &c, 1, AI_MATKEY_COLOR_DIFFUSE);
+    mScene->mMaterials[0] = mat;
+
+    for(i = 0; i < m3d->nummaterial; i++) {
+        m = &m3d->material[i];
+        aiMaterial* mat = new aiMaterial;
+        name.Set(std::string(m->name));
+        mat->AddProperty( &name, AI_MATKEY_NAME );
+        for(j = 0; j < m->numprop; j++) {
+            // look up property type
+            // 0 - 127 scalar values,
+            // 128 - 255 the same properties but for texture maps
+            k = 256;
+            for(l = 0; l < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); l++)
+                if(m->prop[j].type == m3d_propertytypes[l].id ||
+                    m->prop[j].type == m3d_propertytypes[l].id + 128) {
+                        k = l;
+                        break;
+                    }
+            // should never happen, but be safe than sorry
+            if(k == 256) continue;
+
+            // scalar properties
+            if(m->prop[j].type < 128 && aiProps[k].pKey) {
+                switch(m3d_propertytypes[k].format) {
+                    case m3dpf_color:
+                        c = mkColor(m->prop[j].value.color);
+                        mat->AddProperty(&c, 1, aiProps[k].pKey, aiProps[k].type, aiProps[k].index);
+                    break;
+                    case m3dpf_float:
+                        f = m->prop[j].value.fnum;
+                        mat->AddProperty(&f, 1, aiProps[k].pKey, aiProps[k].type, aiProps[k].index);
+                    break;
+                    default:
+                        n = m->prop[j].value.num;
+                        if(m->prop[j].type == m3dp_il) {
+                            switch(n) {
+                                case 0:  n = aiShadingMode_NoShading; break;
+                                case 2:  n = aiShadingMode_Phong; break;
+                                default: n = aiShadingMode_Gouraud; break;
+                            }
+                        }
+                        mat->AddProperty(&n, 1, aiProps[k].pKey, aiProps[k].type, aiProps[k].index);
+                    break;
+                }
+            }
+            // texture map properties
+            if(m->prop[j].type >= 128 && aiTxProps[k].pKey &&
+                // extra check, should never happen, do we have the refered texture?
+                m->prop[j].value.textureid < m3d->numtexture &&
+                m3d->texture[m->prop[j].value.textureid].name) {
+                    name.Set(std::string(std::string(m3d->texture[m->prop[j].value.textureid].name) + ".png"));
+                    mat->AddProperty(&name, aiProps[k].pKey, aiProps[k].type, aiProps[k].index);
+                    n = 0;
+                    mat->AddProperty(&n, 1, _AI_MATKEY_UVWSRC_BASE, aiProps[k].type, aiProps[k].index);
+            }
+        }
+        mScene->mMaterials[i + 1] = mat;
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// import textures, this is the simplest of all
+void M3DImporter::importTextures()
+{
+    unsigned int i;
+    m3dtx_t *t;
+
+    ai_assert(mScene != nullptr);
+    ai_assert(m3d != nullptr);
+
+    mScene->mNumTextures = m3d->numtexture;
+    ASSIMP_LOG_DEBUG_F("M3D: importTextures ", mScene->mNumTextures);
+
+    if(!m3d->numtexture)
+        return;
+
+    mScene->mTextures = new aiTexture*[m3d->numtexture];
+    for(i = 0; i < m3d->numtexture; i++) {
+        t = &m3d->texture[i];
+        aiTexture *tx = new aiTexture;
+        strcpy(tx->achFormatHint, "rgba8888");
+        tx->mFilename = aiString(std::string(t->name) + ".png");
+        tx->mWidth = t->w;
+        tx->mHeight = t->h;
+        tx->pcData = new aiTexel[ tx->mWidth*tx->mHeight ];
+        memcpy(tx->pcData, t->d, tx->mWidth*tx->mHeight*4);
+        mScene->mTextures[i] = tx;
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// this is tricky. M3D has a global vertex and UV list, and faces are indexing them
+// individually. In assimp there're per mesh vertex and UV lists, and they must be
+// indexed simultaneously.
+void M3DImporter::importMeshes()
+{
+    unsigned int i, j, k, l, numpoly = 3, lastMat = -2U;
+    std::vector<aiMesh*> *meshes = new std::vector<aiMesh*>();
+    std::vector<aiFace> *faces = nullptr;
+    std::vector<aiVector3D> *vertices = nullptr;
+    std::vector<aiVector3D> *normals = nullptr;
+    std::vector<aiVector3D> *texcoords = nullptr;
+    std::vector<aiColor4D> *colors = nullptr;
+    std::vector<unsigned int> *vertexids = nullptr;
+    aiMesh *pMesh = nullptr;
+
+    ai_assert(mScene != nullptr);
+    ai_assert(m3d != nullptr);
+    ai_assert(mScene->mRootNode != nullptr);
+
+    ASSIMP_LOG_DEBUG_F("M3D: importMeshes ", m3d->numface);
+
+    for(i = 0; i < m3d->numface; i++) {
+        // we must switch mesh if material changes
+        if(lastMat != m3d->face[i].materialid) {
+            lastMat = m3d->face[i].materialid;
+            if(pMesh && vertices->size() && faces->size()) {
+                populateMesh(pMesh, faces, vertices, normals, texcoords, colors, vertexids);
+                meshes->push_back(pMesh);
+                delete vertexids;   // this is not stored in pMesh, just to collect bone vertices
+            }
+            pMesh = new aiMesh;
+            pMesh->mPrimitiveTypes = aiPrimitiveType_TRIANGLE;
+            pMesh->mMaterialIndex = lastMat + 1;
+            faces = new std::vector<aiFace>();
+            vertices = new std::vector<aiVector3D>();
+            normals = new std::vector<aiVector3D>();
+            texcoords = new std::vector<aiVector3D>();
+            colors = new std::vector<aiColor4D>();
+            vertexids = new std::vector<unsigned int>();
+        }
+        // add a face to temporary vector
+        aiFace *pFace = new aiFace;
+        pFace->mNumIndices = numpoly;
+        pFace->mIndices = new unsigned int[numpoly];
+        for(j = 0; j < numpoly; j++) {
+            aiVector3D pos, uv, norm;
+            k = vertices->size();
+            pFace->mIndices[j] = k;
+            l = m3d->face[i].vertex[j];
+            pos.x = m3d->vertex[l].x;
+            pos.y = m3d->vertex[l].y;
+            pos.z = m3d->vertex[l].z;
+            vertices->push_back(pos);
+            colors->push_back(mkColor(m3d->vertex[l].color));
+            // add a bone to temporary vector
+            if(m3d->vertex[l].skinid != -1U &&m3d->vertex[l].skinid != -2U && m3d->skin && m3d->bone) {
+                // this is complicated, because M3D stores a list of bone id / weight pairs per
+                // vertex but assimp uses lists of local vertex id/weight pairs per local bone list
+                vertexids->push_back(l);
+            }
+            l = m3d->face[i].texcoord[j];
+            if(l != -1U) {
+                uv.x = m3d->tmap[l].u;
+                uv.y = m3d->tmap[l].v;
+                uv.z = 0.0;
+                texcoords->push_back(uv);
+            }
+            l = m3d->face[i].normal[j];
+            if(l != -1U) {
+                norm.x = m3d->vertex[l].x;
+                norm.y = m3d->vertex[l].y;
+                norm.z = m3d->vertex[l].z;
+                normals->push_back(norm);
+            }
+        }
+        faces->push_back(*pFace);
+        delete pFace;
+    }
+    // if there's data left in the temporary vectors, flush them
+    if(pMesh && vertices->size() && faces->size()) {
+        populateMesh(pMesh, faces, vertices, normals, texcoords, colors, vertexids);
+        meshes->push_back(pMesh);
+    }
+
+    // create global mesh list in scene
+    mScene->mNumMeshes = meshes->size();
+    mScene->mMeshes = new aiMesh*[mScene->mNumMeshes];
+    std::copy(meshes->begin(), meshes->end(), mScene->mMeshes);
+
+    // create mesh indeces in root node
+    mScene->mRootNode->mNumMeshes = meshes->size();
+    mScene->mRootNode->mMeshes = new unsigned int[meshes->size()];
+    for(i = 0; i < meshes->size(); i++) {
+        mScene->mRootNode->mMeshes[i] = i;
+    }
+
+    delete meshes;
+    if(faces)       delete faces;
+    if(vertices)    delete vertices;
+    if(normals)     delete normals;
+    if(texcoords)   delete texcoords;
+    if(colors)      delete colors;
+    if(vertexids)   delete vertexids;
+}
+
+// ------------------------------------------------------------------------------------------------
+// a reentrant node parser. Otherwise this is simple
+void M3DImporter::importBones(unsigned int parentid, aiNode *pParent)
+{
+    unsigned int i, n;
+
+    ai_assert(pParent != nullptr);
+    ai_assert(mScene != nullptr);
+    ai_assert(m3d != nullptr);
+
+    ASSIMP_LOG_DEBUG_F("M3D: importBones ", m3d->numbone, " parentid ", (int)parentid);
+
+    for(n = 0, i = parentid + 1; i < m3d->numbone; i++)
+        if(m3d->bone[i].parent == parentid) n++;
+    pParent->mChildren = new aiNode*[n];
+
+    for(i = parentid + 1; i < m3d->numbone; i++) {
+        if(m3d->bone[i].parent == parentid) {
+            aiNode *pChild = new aiNode;
+            pChild->mParent = pParent;
+            pChild->mName = aiString(std::string(m3d->bone[i].name));
+            convertPose(&pChild->mTransformation, m3d->bone[i].pos, m3d->bone[i].ori);
+            pChild->mNumChildren = 0;
+            pParent->mChildren[pParent->mNumChildren] = pChild;
+            pParent->mNumChildren++;
+            importBones(i, pChild);
+        }
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// this is another headache. M3D stores list of changed bone id/position/orientation triplets and
+// a timestamp per frame, but assimp needs timestamp and lists of position, orientation lists per
+// bone, so we have to convert between the two conceptually different representation forms
+void M3DImporter::importAnimations()
+{
+    unsigned int i, j, k, l, pos, ori;
+    double t;
+    m3da_t *a;
+
+    ai_assert(mScene != nullptr);
+    ai_assert(m3d != nullptr);
+
+    mScene->mNumAnimations = m3d->numaction;
+
+    ASSIMP_LOG_DEBUG_F("M3D: importAnimations ", mScene->mNumAnimations);
+
+    if(!m3d->numaction || !m3d->numbone)
+        return;
+
+    mScene->mAnimations = new aiAnimation*[m3d->numaction];
+    for(i = 0; i < m3d->numaction; i++) {
+        a = &m3d->action[i];
+        aiAnimation *pAnim = new aiAnimation;
+        pAnim->mName = aiString(std::string(a->name));
+        pAnim->mDuration = ((double)a->durationmsec) / 10;
+        pAnim->mTicksPerSecond = 100;
+        // now we know how many bones are referenced in this animation
+        pAnim->mNumChannels = m3d->numbone;
+        pAnim->mChannels = new aiNodeAnim*[pAnim->mNumChannels];
+        for(l = 0; l < m3d->numbone; l++) {
+            unsigned int n;
+            pAnim->mChannels[l] = new aiNodeAnim;
+            pAnim->mChannels[l]->mNodeName = aiString(std::string(m3d->bone[l].name));
+            // now n is the size of positions / orientations arrays
+            pAnim->mChannels[l]->mNumPositionKeys = pAnim->mChannels[l]->mNumRotationKeys = a->numframe;
+            pAnim->mChannels[l]->mPositionKeys = new aiVectorKey[a->numframe];
+            pAnim->mChannels[l]->mRotationKeys = new aiQuatKey[a->numframe];
+            pos = m3d->bone[l].pos;
+            ori = m3d->bone[l].ori;
+            for(j = n = 0; j < a->numframe; j++) {
+                t = ((double)a->frame[j].msec) / 10;
+                for(k = 0; k < a->frame[j].numtransform; k++) {
+                    if(a->frame[j].transform[k].boneid == l) {
+                        pos = a->frame[j].transform[k].pos;
+                        ori = a->frame[j].transform[k].ori;
+                    }
+                }
+                m3dv_t *v = &m3d->vertex[pos];
+                m3dv_t *q = &m3d->vertex[ori];
+                pAnim->mChannels[l]->mPositionKeys[j].mTime = t;
+                pAnim->mChannels[l]->mPositionKeys[j].mValue.x = v->x;
+                pAnim->mChannels[l]->mPositionKeys[j].mValue.y = v->y;
+                pAnim->mChannels[l]->mPositionKeys[j].mValue.z = v->z;
+                pAnim->mChannels[l]->mRotationKeys[j].mTime = t;
+                pAnim->mChannels[l]->mRotationKeys[j].mValue.w = q->w;
+                pAnim->mChannels[l]->mRotationKeys[j].mValue.x = q->x;
+                pAnim->mChannels[l]->mRotationKeys[j].mValue.y = q->y;
+                pAnim->mChannels[l]->mRotationKeys[j].mValue.z = q->z;
+            }// foreach frame
+        }// foreach bones
+        mScene->mAnimations[i] = pAnim;
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// convert uint32_t into aiColor4D
+aiColor4D M3DImporter::mkColor(uint32_t c) {
+    aiColor4D color;
+    color.a = ((float)((c >> 24)&0xff)) / 255;
+    color.b = ((float)((c >> 16)&0xff)) / 255;
+    color.g = ((float)((c >>  8)&0xff)) / 255;
+    color.r = ((float)((c >>  0)&0xff)) / 255;
+    return color;
+}
+
+// ------------------------------------------------------------------------------------------------
+// convert a position id and orientation id into a 4 x 4 transformation matrix
+void M3DImporter::convertPose(aiMatrix4x4 *m, unsigned int posid, unsigned int orientid)
+{
+    ai_assert(m != nullptr);
+    ai_assert(m3d != nullptr);
+    ai_assert(posid != -1U && posid < m3d->numvertex);
+    ai_assert(orientid != -1U && orientid < m3d->numvertex);
+    m3dv_t *p = &m3d->vertex[posid];
+    m3dv_t *q = &m3d->vertex[orientid];
+
+    /* quaternion to matrix. Do NOT use aiQuaternion to aiMatrix3x3, gives bad results */
+    if(q->x == 0.0 && q->y == 0.0 && q->z >= 0.7071065 && q->z <= 0.7071075 && q->w == 0.0) {
+        m->a2 = m->a3 = m->b1 = m->b3 = m->c1 = m->c2 = 0.0;
+        m->a1 = m->b2 = m->c3 = -1.0;
+    } else {
+        m->a1 = 1 - 2 * (q->y * q->y + q->z * q->z); if(m->a1 > -1e-7 && m->a1 < 1e-7) m->a1 = 0.0;
+        m->a2 = 2 * (q->x * q->y - q->z * q->w);     if(m->a2 > -1e-7 && m->a2 < 1e-7) m->a2 = 0.0;
+        m->a3 = 2 * (q->x * q->z + q->y * q->w);     if(m->a3 > -1e-7 && m->a3 < 1e-7) m->a3 = 0.0;
+        m->b1 = 2 * (q->x * q->y + q->z * q->w);     if(m->b1 > -1e-7 && m->b1 < 1e-7) m->b1 = 0.0;
+        m->b2 = 1 - 2 * (q->x * q->x + q->z * q->z); if(m->b2 > -1e-7 && m->b2 < 1e-7) m->b2 = 0.0;
+        m->b3 = 2 * (q->y * q->z - q->x * q->w);     if(m->b3 > -1e-7 && m->b3 < 1e-7) m->b3 = 0.0;
+        m->c1 = 2 * (q->x * q->z - q->y * q->w);     if(m->c1 > -1e-7 && m->c1 < 1e-7) m->c1 = 0.0;
+        m->c2 = 2 * (q->y * q->z + q->x * q->w);     if(m->c2 > -1e-7 && m->c2 < 1e-7) m->c2 = 0.0;
+        m->c3 = 1 - 2 * (q->x * q->x + q->y * q->y); if(m->c3 > -1e-7 && m->c3 < 1e-7) m->c3 = 0.0;
+    }
+
+    /* set translation */
+    m->a4 = p->x; m->b4 = p->y; m->c4 = p->z;
+
+    m->d1 = 0; m->d2 = 0; m->d3 = 0; m->d4 = 1;
+}
+
+// ------------------------------------------------------------------------------------------------
+// find a node by name
+aiNode *M3DImporter::findNode(aiNode *pNode, aiString name)
+{
+    unsigned int i;
+
+    ai_assert(pNode != nullptr);
+    ai_assert(mScene != nullptr);
+
+    if(pNode->mName == name)
+        return pNode;
+    for(i = 0; i < pNode->mNumChildren; i++) {
+        aiNode *pChild = findNode(pNode->mChildren[i], name);
+        if(pChild) return pChild;
+    }
+    return nullptr;
+}
+
+// ------------------------------------------------------------------------------------------------
+// fills up offsetmatrix in mBones
+void M3DImporter::calculateOffsetMatrix(aiNode *pNode, aiMatrix4x4 *m)
+{
+    ai_assert(pNode != nullptr);
+    ai_assert(mScene != nullptr);
+
+    if(pNode->mParent) {
+        calculateOffsetMatrix(pNode->mParent, m);
+        *m *= pNode->mTransformation;
+    } else {
+        *m = pNode->mTransformation;
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+// because M3D has a global mesh, global vertex ids and stores materialid on the face, we need
+// temporary lists to collect data for an aiMesh, which requires local arrays and local indeces
+// this function fills up an aiMesh with those temporary lists
+void M3DImporter::populateMesh(aiMesh *pMesh, std::vector<aiFace> *faces, std::vector<aiVector3D> *vertices,
+    std::vector<aiVector3D> *normals, std::vector<aiVector3D> *texcoords, std::vector<aiColor4D> *colors,
+    std::vector<unsigned int> *vertexids) {
+
+    ai_assert(pMesh != nullptr);
+    ai_assert(faces != nullptr);
+    ai_assert(vertices != nullptr);
+    ai_assert(normals != nullptr);
+    ai_assert(texcoords != nullptr);
+    ai_assert(colors != nullptr);
+    ai_assert(vertexids != nullptr);
+    ai_assert(m3d != nullptr);
+
+    ASSIMP_LOG_DEBUG_F("M3D: populateMesh numvertices ", vertices->size(), " numfaces ", faces->size(),
+        " numnormals ", normals->size(), " numtexcoord ", texcoords->size(), " numbones ", m3d->numbone);
+
+    if(vertices->size() && faces->size()) {
+        pMesh->mNumFaces = faces->size();
+        pMesh->mFaces = new aiFace[pMesh->mNumFaces];
+        std::copy(faces->begin(), faces->end(), pMesh->mFaces);
+        pMesh->mNumVertices = vertices->size();
+        pMesh->mVertices = new aiVector3D[pMesh->mNumVertices];
+        std::copy(vertices->begin(), vertices->end(), pMesh->mVertices);
+        if(normals->size() == vertices->size()) {
+            pMesh->mNormals = new aiVector3D[pMesh->mNumVertices];
+            std::copy(normals->begin(), normals->end(), pMesh->mNormals);
+        }
+        if(texcoords->size() == vertices->size()) {
+            pMesh->mTextureCoords[0] = new aiVector3D[pMesh->mNumVertices];
+            std::copy(texcoords->begin(), texcoords->end(), pMesh->mTextureCoords[0]);
+            pMesh->mNumUVComponents[0] = 2;
+        }
+        if(colors->size() == vertices->size()) {
+            pMesh->mColors[0] = new aiColor4D[pMesh->mNumVertices];
+            std::copy(colors->begin(), colors->end(), pMesh->mColors[0]);
+        }
+        // this is complicated, because M3D stores a list of bone id / weight pairs per
+        // vertex but assimp uses lists of local vertex id/weight pairs per local bone list
+        pMesh->mNumBones = m3d->numbone;
+        /* we need aiBone with mOffsetMatrix for bones without weights as well */
+        if(pMesh->mNumBones) {
+            pMesh->mBones = new aiBone*[pMesh->mNumBones];
+            for(unsigned int i = 0; i < m3d->numbone; i++) {
+                aiNode *pNode;
+                pMesh->mBones[i] = new aiBone;
+                pMesh->mBones[i]->mName = aiString(std::string(m3d->bone[i].name));
+                pMesh->mBones[i]->mNumWeights = 0;
+                pNode = findNode(mScene->mRootNode, pMesh->mBones[i]->mName);
+                if(pNode) {
+                    calculateOffsetMatrix(pNode, &pMesh->mBones[i]->mOffsetMatrix);
+                    pMesh->mBones[i]->mOffsetMatrix.Inverse();
+                } else
+                    pMesh->mBones[i]->mOffsetMatrix = aiMatrix4x4();
+            }
+            if(vertexids->size()) {
+                unsigned int i, j;
+                // first count how many vertices we have per bone
+                for(i = 0; i < vertexids->size(); i++) {
+                    unsigned int s = m3d->vertex[vertexids->at(i)].skinid;
+                    if(s != -1U && s!= -2U) {
+                        for(unsigned int k = 0; k < M3D_NUMBONE && m3d->skin[s].weight[k] > 0.0; k++) {
+                                aiString name = aiString(std::string(m3d->bone[m3d->skin[s].boneid[k]].name));
+                                for(j = 0; j < pMesh->mNumBones; j++) {
+                                    if(pMesh->mBones[j]->mName == name) {
+                                        pMesh->mBones[j]->mNumWeights++;
+                                        break;
+                                    }
+                                }
+                        }
+                    }
+                }
+                // allocate mWeights
+                for(j = 0; j < pMesh->mNumBones; j++) {
+                    aiBone *pBone = pMesh->mBones[j];
+                    if(pBone->mNumWeights) {
+                        pBone->mWeights = new aiVertexWeight[pBone->mNumWeights];
+                        pBone->mNumWeights = 0;
+                    }
+                }
+                // fill up with data
+                for(i = 0; i < vertexids->size(); i++) {
+                    unsigned int s = m3d->vertex[vertexids->at(i)].skinid;
+                    if(s != -1U && s!= -2U) {
+                        for(unsigned int k = 0; k < M3D_NUMBONE && m3d->skin[s].weight[k] > 0.0; k++) {
+                                aiString name = aiString(std::string(m3d->bone[m3d->skin[s].boneid[k]].name));
+                                for(j = 0; j < pMesh->mNumBones; j++) {
+                                    if(pMesh->mBones[j]->mName == name) {
+                                        aiBone *pBone = pMesh->mBones[j];
+                                        pBone->mWeights[pBone->mNumWeights].mVertexId = i;
+                                        pBone->mWeights[pBone->mNumWeights].mWeight = m3d->skin[s].weight[k];
+                                        pBone->mNumWeights++;
+                                        break;
+                                    }
+                                }
+                        } // foreach skin
+                    }
+                } // foreach vertexids
+            }
+        }
+    }
+}
+
+// ------------------------------------------------------------------------------------------------
+
+}   // Namespace Assimp
+
+#endif // !! ASSIMP_BUILD_NO_M3D_IMPORTER

+ 106 - 0
code/M3D/M3DImporter.h

@@ -0,0 +1,106 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+Copyright (c) 2019 bzt
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file M3DImporter.h
+*   @brief Declares the importer class to read a scene from a Model 3D file
+*/
+#ifndef AI_M3DIMPORTER_H_INC
+#define AI_M3DIMPORTER_H_INC
+
+#ifndef ASSIMP_BUILD_NO_M3D_IMPORTER
+
+#include "m3d.h"
+#include <assimp/BaseImporter.h>
+#include <assimp/material.h>
+#include <vector>
+
+struct aiMesh;
+struct aiNode;
+struct aiMaterial;
+struct aiFace;
+
+namespace Assimp {
+
+class M3DImporter : public BaseImporter {
+public:
+    /// \brief  Default constructor
+    M3DImporter();
+
+    /// \brief  Destructor
+    ~M3DImporter();
+
+public:
+    /// \brief  Returns whether the class can handle the format of the given file.
+    /// \remark See BaseImporter::CanRead() for details.
+    bool CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const;
+
+private:
+    aiScene* mScene; // the scene to import to
+    m3d_t *m3d; // model for the C library to convert from
+
+    //! \brief  Appends the supported extension.
+    const aiImporterDesc* GetInfo () const;
+
+    //! \brief  File import implementation.
+    void InternReadFile(const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler);
+
+    void importMaterials();
+    void importTextures();
+    void importMeshes();
+    void importBones(unsigned int parentid, aiNode *pParent);
+    void importAnimations();
+
+    // helper functions
+    aiColor4D mkColor(uint32_t c);
+    void convertPose(aiMatrix4x4 *m, unsigned int posid, unsigned int orientid);
+    aiNode *findNode(aiNode *pNode, aiString name);
+    void calculateOffsetMatrix(aiNode *pNode, aiMatrix4x4 *m);
+    void populateMesh(aiMesh *pMesh, std::vector<aiFace> *faces, std::vector<aiVector3D> *verteces,
+        std::vector<aiVector3D> *normals, std::vector<aiVector3D> *texcoords, std::vector<aiColor4D> *colors,
+        std::vector<unsigned int> *vertexids);
+};
+
+} // Namespace Assimp
+
+#endif // ASSIMP_BUILD_NO_M3D_IMPORTER
+
+#endif // AI_M3DIMPORTER_H_INC

+ 106 - 0
code/M3D/M3DMaterials.h

@@ -0,0 +1,106 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+Copyright (c) 2019 bzt
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+
+/** @file M3DMaterials.h
+*   @brief Declares the Assimp and Model 3D file material type relations
+*/
+#ifndef AI_M3DMATERIALS_H_INC
+#define AI_M3DMATERIALS_H_INC
+
+/*
+ * In the m3d.h header, there's a static array which defines the material
+ * properties, called m3d_propertytypes. These must have the same size, and
+ * list the matching Assimp materials for those properties. Used by both the
+ * M3DImporter and the M3DExporter, so you have to define these relations
+ * only once. D.R.Y. and K.I.S.S.
+ */
+typedef struct {
+    const char *pKey;
+    unsigned int type;
+    unsigned int index;
+} aiMatProp;
+
+/* --- Scalar Properties ---        !!!!! must match m3d_propertytypes !!!!! */
+static const aiMatProp aiProps[] = {
+    { AI_MATKEY_COLOR_DIFFUSE },                                /* m3dp_Kd */
+    { AI_MATKEY_COLOR_AMBIENT },                                /* m3dp_Ka */
+    { AI_MATKEY_COLOR_SPECULAR },                               /* m3dp_Ks */
+    { AI_MATKEY_SHININESS },                                    /* m3dp_Ns */
+    { AI_MATKEY_COLOR_EMISSIVE },                               /* m3dp_Ke */
+    { AI_MATKEY_COLOR_REFLECTIVE },                             /* m3dp_Tf */
+    { AI_MATKEY_BUMPSCALING },                                  /* m3dp_Km */
+    { AI_MATKEY_OPACITY },                                      /* m3dp_d */
+    { AI_MATKEY_SHADING_MODEL },                                /* m3dp_il */
+
+    { NULL, 0, 0 },                                             /* m3dp_Pr */
+    { AI_MATKEY_REFLECTIVITY },                                 /* m3dp_Pm */
+    { NULL, 0, 0 },                                             /* m3dp_Ps */
+    { AI_MATKEY_REFRACTI },                                     /* m3dp_Ni */
+    { NULL, 0, 0 },
+    { NULL, 0, 0 },
+    { NULL, 0, 0 },
+    { NULL, 0, 0 }
+};
+
+/* --- Texture Map Properties ---   !!!!! must match m3d_propertytypes !!!!! */
+static const aiMatProp aiTxProps[] = {
+    { AI_MATKEY_TEXTURE_DIFFUSE(0) },                        /* m3dp_map_Kd */
+    { AI_MATKEY_TEXTURE_AMBIENT(0) },                        /* m3dp_map_Ka */
+    { AI_MATKEY_TEXTURE_SPECULAR(0) },                       /* m3dp_map_Ks */
+    { AI_MATKEY_TEXTURE_SHININESS(0) },                      /* m3dp_map_Ns */
+    { AI_MATKEY_TEXTURE_EMISSIVE(0) },                       /* m3dp_map_Ke */
+    { NULL, 0, 0 },                                          /* m3dp_map_Tf */
+    { AI_MATKEY_TEXTURE_HEIGHT(0) },                         /* m3dp_bump */
+    { AI_MATKEY_TEXTURE_OPACITY(0) },                        /* m3dp_map_d */
+    { AI_MATKEY_TEXTURE_REFLECTION(0) },                     /* m3dp_refl */
+
+    { AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE_ROUGHNESS,0) },/* m3dp_map_Pr */
+    { AI_MATKEY_TEXTURE(aiTextureType_METALNESS,0) },        /* m3dp_map_Pm */
+    { NULL, 0, 0 },                                          /* m3dp_map_Ps */
+    { AI_MATKEY_TEXTURE(aiTextureType_AMBIENT_OCCLUSION,0) },/* m3dp_map_Ni */
+    { NULL, 0, 0 },
+    { NULL, 0, 0 },
+    { NULL, 0, 0 },
+    { NULL, 0, 0 }
+};
+
+#endif // AI_M3DMATERIALS_H_INC

+ 4565 - 0
code/M3D/m3d.h

@@ -0,0 +1,4565 @@
+/*
+ * m3d.h
+ *
+ * Copyright (C) 2019 bzt (bztsrc@gitlab)
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @brief ANSI C89 / C++11 single header importer / exporter SDK for the Model 3D (.M3D) format
+ * https://gitlab.com/bztsrc/model3d
+ *
+ * PNG decompressor included from (with minor modifications to make it C89 valid):
+ *  stb_image - v2.13 - public domain image loader - http://nothings.org/stb_image.h
+ *
+ * @version: 1.0.0
+ */
+
+#ifndef _M3D_H_
+#define _M3D_H_
+
+#ifdef  __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+
+/*** configuration ***/
+#ifndef M3D_MALLOC
+# define M3D_MALLOC(sz)     malloc(sz)
+#endif
+#ifndef M3D_REALLOC
+# define M3D_REALLOC(p,nsz) realloc(p,nsz)
+#endif
+#ifndef M3D_FREE
+# define M3D_FREE(p)        free(p)
+#endif
+#ifndef M3D_LOG
+# define M3D_LOG(x)
+#endif
+#ifndef M3D_APIVERSION
+#define M3D_APIVERSION      0x0100
+#ifndef M3D_DOUBLE
+typedef float M3D_FLOAT;
+#else
+typedef double M3D_FLOAT;
+#endif
+#if !defined(M3D_SMALLINDEX)
+typedef uint32_t M3D_INDEX;
+#define M3D_INDEXMAX 0xfffffffe
+#else
+typedef uint16_t M3D_INDEX;
+#define M3D_INDEXMAX 0xfffe
+#endif
+#ifndef M3D_NUMBONE
+#define M3D_NUMBONE 4
+#endif
+#ifndef M3D_BONEMAXLEVEL
+#define M3D_BONEMAXLEVEL 8
+#endif
+#ifndef _MSC_VER
+#define _inline __inline__
+#define _pack __attribute__((packed))
+#define _unused __attribute__((unused))
+#else
+#define _inline
+#define _pack
+#define _unused
+#endif
+#ifndef  __cplusplus
+#define _register register
+#else
+#define _register
+#endif
+
+/*** File format structures ***/
+
+/**
+ * M3D file format structure
+ *  3DMO m3dchunk_t file header chunk, may followed by compressed data
+ *  HEAD m3dhdr_t model header chunk
+ *  n x m3dchunk_t more chunks follow
+ *      CMAP color map chunk (optional)
+ *      TMAP texture map chunk (optional)
+ *      VRTS vertex data chunk (optional if it's a material library)
+ *      BONE bind-pose skeleton, bone hierarchy chunk (optional)
+ *          n x m3db_t contains propably more, but at least one bone
+ *      MTRL* material chunk(s), can be more (optional)
+ *          n x m3dp_t each material contains propapbly more, but at least one property
+ *                     the properties are configurable with a static array, see m3d_propertytypes
+ *      n x m3dchunk_t at least one, but maybe more face chunks
+ *          PROC* procedural face, or
+ *          MESH* triangle mesh (vertex index list)
+ *      ACTN* action chunk(s), animation-pose skeletons, can be more (optional)
+ *          n x m3dfr_t each action contains probably more, but at least one frame
+ *              n x m3dtr_t each frame contains probably more, but at least one transformation
+ *      ASET* inlined asset chunk(s), can be more (optional)
+ *  OMD3 end chunk
+ */
+typedef struct {
+    char magic[4];
+    uint32_t length;
+    float scale; /* deliberately not M3D_FLOAT */
+    uint32_t types;
+} _pack m3dhdr_t;
+
+typedef struct {
+    char magic[4];
+    uint32_t length;
+} _pack m3dchunk_t;
+
+/*** in-memory model structure ***/
+
+/* textmap entry */
+typedef struct {
+    M3D_FLOAT u;
+    M3D_FLOAT v;
+} m3dti_t;
+
+/* texture */
+typedef struct {
+    char *name;                 /* texture name */
+    uint32_t *d;                /* pixels data */
+    uint16_t w;                 /* width */
+    uint16_t h;                 /* height */
+} _pack m3dtx_t;
+
+typedef struct {
+    M3D_INDEX vertexid;
+    M3D_FLOAT weight;
+} m3dw_t;
+
+/* bone entry */
+typedef struct {
+    M3D_INDEX parent;           /* parent bone index */
+    char *name;                 /* name for this bone */
+    M3D_INDEX pos;              /* vertex index position */
+    M3D_INDEX ori;              /* vertex index orientation (quaternion) */
+    M3D_INDEX numweight;        /* number of controlled vertices */
+    m3dw_t *weight;             /* weights for those vertices */
+    M3D_FLOAT mat4[16];         /* transformation matrix */
+} m3db_t;
+
+/* skin: bone per vertex entry */
+typedef struct {
+    M3D_INDEX boneid[M3D_NUMBONE];
+    M3D_FLOAT weight[M3D_NUMBONE];
+} m3ds_t;
+
+/* vertex entry */
+typedef struct {
+    M3D_FLOAT x;                /* 3D coordinates and weight */
+    M3D_FLOAT y;
+    M3D_FLOAT z;
+    M3D_FLOAT w;
+    uint32_t color;             /* default vertex color */
+    M3D_INDEX skinid;           /* skin index */
+} m3dv_t;
+
+/* material property formats */
+enum {
+    m3dpf_color,
+    m3dpf_uint8,
+    m3dpf_uint16,
+    m3dpf_uint32,
+    m3dpf_float,
+    m3dpf_map
+};
+typedef struct {
+    uint8_t format;
+    uint8_t id;
+#ifdef M3D_ASCII
+#define M3D_PROPERTYDEF(f,i,n) { (f), (i), (char*)(n) }
+    char *key;
+#else
+#define M3D_PROPERTYDEF(f,i,n) { (f), (i) }
+#endif
+} m3dpd_t;
+
+/* material property types */
+/* You shouldn't change the first 8 display and first 4 physical property. Assign the rest as you like. */
+enum {
+    m3dp_Kd = 0,                /* scalar display properties */
+    m3dp_Ka,
+    m3dp_Ks,
+    m3dp_Ns,
+    m3dp_Ke,
+    m3dp_Tf,
+    m3dp_Km,
+    m3dp_d,
+    m3dp_il,
+
+    m3dp_Pr = 64,               /* scalar physical properties */
+    m3dp_Pm,
+    m3dp_Ps,
+    m3dp_Ni,
+
+    m3dp_map_Kd = 128,          /* textured display map properties */
+    m3dp_map_Ka,
+    m3dp_map_Ks,
+    m3dp_map_Ns,
+    m3dp_map_Ke,
+    m3dp_map_Tf,
+    m3dp_map_Km, /* bump map */
+    m3dp_map_D,
+    m3dp_map_il, /* reflection map */
+
+    m3dp_map_Pr = 192,          /* textured physical map properties */
+    m3dp_map_Pm,
+    m3dp_map_Ps,
+    m3dp_map_Ni
+};
+enum {                          /* aliases */
+    m3dp_bump = m3dp_map_Km,
+    m3dp_refl = m3dp_map_Pm
+};
+
+/* material property */
+typedef struct {
+    uint8_t type;               /* property type, see "m3dp_*" enumeration */
+    union {
+        uint32_t color;         /* if value is a color, m3dpf_color */
+        uint32_t num;           /* if value is a number, m3dpf_uint8, m3pf_uint16, m3dpf_uint32 */
+        float    fnum;          /* if value is a floating point number, m3dpf_float */
+        M3D_INDEX textureid;    /* if value is a texture, m3dpf_map */
+    } value;
+} m3dp_t;
+
+/* material entry */
+typedef struct {
+    char *name;                 /* name of the material */
+    uint8_t numprop;            /* number of properties */
+    m3dp_t *prop;               /* properties array */
+} m3dm_t;
+
+/* face entry */
+typedef struct {
+    M3D_INDEX materialid;       /* material index */
+    M3D_INDEX vertex[3];        /* 3D points of the triangle in CCW order */
+    M3D_INDEX normal[3];        /* normal vectors */
+    M3D_INDEX texcoord[3];      /* UV coordinates */
+} m3df_t;
+
+/* frame transformations / working copy skeleton entry */
+typedef struct {
+    M3D_INDEX boneid;           /* selects a node in bone hierarchy */
+    M3D_INDEX pos;              /* vertex index new position */
+    M3D_INDEX ori;              /* vertex index new orientation (quaternion) */
+} m3dtr_t;
+
+/* animation frame entry */
+typedef struct {
+    uint32_t msec;              /* frame's position on the timeline, timestamp */
+    M3D_INDEX numtransform;     /* number of transformations in this frame */
+    m3dtr_t *transform;         /* transformations */
+} m3dfr_t;
+
+/* model action entry */
+typedef struct {
+    char *name;                 /* name of the action */
+    uint32_t durationmsec;      /* duration in millisec (1/1000 sec) */
+    M3D_INDEX numframe;         /* number of frames in this animation */
+    m3dfr_t *frame;             /* frames array */
+} m3da_t;
+
+/* inlined asset */
+typedef struct {
+    char *name;                 /* asset name (same pointer as in texture[].name) */
+    uint8_t *data;              /* compressed asset data */
+    uint32_t length;            /* compressed data length */
+} m3di_t;
+
+/*** in-memory model structure ***/
+#define M3D_FLG_FREERAW     (1<<0)
+#define M3D_FLG_FREESTR     (1<<1)
+#define M3D_FLG_MTLLIB      (1<<2)
+
+typedef struct {
+    m3dhdr_t *raw;              /* pointer to raw data */
+    char flags;                 /* internal flags */
+    char errcode;               /* returned error code */
+    char vc_s, vi_s, si_s, ci_s, ti_s, bi_s, nb_s, sk_s, fi_s;  /* decoded sizes for types */
+    char *name;                 /* name of the model, like "Utah teapot" */
+    char *license;              /* usage condition or license, like "MIT", "LGPL" or "BSD-3clause" */
+    char *author;               /* nickname, email, homepage or github URL etc. */
+    char *desc;                 /* comments, descriptions. May contain '\n' newline character */
+    M3D_FLOAT scale;            /* the model's bounding cube's size in SI meters */
+    M3D_INDEX numcmap;
+    uint32_t *cmap;             /* color map */
+    M3D_INDEX numtmap;
+    m3dti_t *tmap;              /* texture map indices */
+    M3D_INDEX numtexture;
+    m3dtx_t *texture;           /* uncompressed textures */
+    M3D_INDEX numbone;
+    m3db_t *bone;               /* bone hierarchy */
+    M3D_INDEX numvertex;
+    m3dv_t *vertex;             /* vertex data */
+    M3D_INDEX numskin;
+    m3ds_t *skin;               /* skin data */
+    M3D_INDEX nummaterial;
+    m3dm_t *material;           /* material list */
+    M3D_INDEX numface;
+    m3df_t *face;               /* model face, triangle mesh */
+    M3D_INDEX numaction;
+    m3da_t *action;             /* action animations */
+    M3D_INDEX numinlined;
+    m3di_t *inlined;            /* inlined assets */
+    M3D_INDEX numunknown;
+    m3dchunk_t **unknown;       /* unknown chunks, application / engine specific data probably */
+} m3d_t;
+
+/*** export parameters ***/
+#define M3D_EXP_INT8        0
+#define M3D_EXP_INT16       1
+#define M3D_EXP_FLOAT       2
+#define M3D_EXP_DOUBLE      3
+
+#define M3D_EXP_NOCMAP      (1<<0)
+#define M3D_EXP_NOMATERIAL  (1<<1)
+#define M3D_EXP_NOFACE      (1<<2)
+#define M3D_EXP_NONORMAL    (1<<3)
+#define M3D_EXP_NOTXTCRD    (1<<4)
+#define M3D_EXP_FLIPTXTCRD  (1<<5)
+#define M3D_EXP_NORECALC    (1<<6)
+#define M3D_EXP_IDOSUCK     (1<<7)
+#define M3D_EXP_NOBONE      (1<<8)
+#define M3D_EXP_NOACTION    (1<<9)
+#define M3D_EXP_INLINE      (1<<10)
+#define M3D_EXP_EXTRA       (1<<11)
+#define M3D_EXP_NOZLIB      (1<<14)
+#define M3D_EXP_ASCII       (1<<15)
+
+/*** error codes ***/
+#define M3D_SUCCESS         0
+#define M3D_ERR_ALLOC       -1
+#define M3D_ERR_BADFILE     -2
+#define M3D_ERR_UNIMPL      -65
+#define M3D_ERR_UNKPROP     -66
+#define M3D_ERR_UNKMESH     -67
+#define M3D_ERR_UNKIMG      -68
+#define M3D_ERR_UNKFRAME    -69
+#define M3D_ERR_TRUNC       -70
+#define M3D_ERR_CMAP        -71
+#define M3D_ERR_TMAP        -72
+#define M3D_ERR_VRTS        -73
+#define M3D_ERR_BONE        -74
+#define M3D_ERR_MTRL        -75
+
+#define M3D_ERR_ISFATAL(x)  ((x) < 0 && (x) > -65)
+
+/* callbacks */
+typedef unsigned char *(*m3dread_t)(char *filename, unsigned int *size);                        /* read file contents into buffer */
+typedef void (*m3dfree_t)(void *buffer);                                                        /* free file contents buffer */
+typedef int (*m3dtxsc_t)(const char *name, const void *script, uint32_t len, m3dtx_t *output);  /* interpret texture script */
+typedef int (*m3dprsc_t)(const char *name, const void *script, uint32_t len, m3d_t *model);     /* interpret surface script */
+#endif /* ifndef M3D_APIVERSION */
+
+/*** C prototypes ***/
+/* import / export */
+m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d_t *mtllib);
+unsigned char *m3d_save(m3d_t *model, int quality, int flags, unsigned int *size);
+void m3d_free(m3d_t *model);
+/* generate animation pose skeleton */
+m3dtr_t *m3d_frame(m3d_t *model, M3D_INDEX actionid, M3D_INDEX frameid, m3dtr_t *skeleton);
+m3db_t *m3d_pose(m3d_t *model, M3D_INDEX actionid, uint32_t msec);
+
+/* private prototypes used by both importer and exporter */
+m3ds_t *_m3d_addskin(m3ds_t *skin, uint32_t *numskin, m3ds_t *s, uint32_t *idx);
+m3dv_t *_m3d_addnorm(m3dv_t *vrtx, uint32_t *numvrtx, m3dv_t *v, uint32_t *idx);
+char *_m3d_safestr(char *in, int morelines);
+
+/*** C implementation ***/
+#ifdef M3D_IMPLEMENTATION
+#if !defined(M3D_NOIMPORTER) || defined(M3D_EXPORTER)
+/* material property definitions */
+static m3dpd_t m3d_propertytypes[] = {
+    M3D_PROPERTYDEF(m3dpf_color, m3dp_Kd, "Kd"),    /* diffuse color */
+    M3D_PROPERTYDEF(m3dpf_color, m3dp_Ka, "Ka"),    /* ambient color */
+    M3D_PROPERTYDEF(m3dpf_color, m3dp_Ks, "Ks"),    /* specular color */
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_Ns, "Ns"),    /* specular exponent */
+    M3D_PROPERTYDEF(m3dpf_color, m3dp_Ke, "Ke"),    /* emissive (emitting light of this color) */
+    M3D_PROPERTYDEF(m3dpf_color, m3dp_Tf, "Tf"),    /* transmission color */
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_Km, "Km"),    /* bump strength */
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_d,  "d"),     /* dissolve (transparency) */
+    M3D_PROPERTYDEF(m3dpf_uint8, m3dp_il, "il"),    /* illumination model (informational, ignored by PBR-shaders) */
+
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_Pr, "Pr"),    /* roughness */
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_Pm, "Pm"),    /* metallic, also reflection */
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_Ps, "Ps"),    /* sheen */
+    M3D_PROPERTYDEF(m3dpf_float, m3dp_Ni, "Ni"),    /* index of refraction (optical density) */
+
+    /* aliases, note that "map_*" aliases are handled automatically */
+    M3D_PROPERTYDEF(m3dpf_map, m3dp_map_Km, "bump"),
+    M3D_PROPERTYDEF(m3dpf_map, m3dp_map_Pm, "refl")
+};
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+#if !defined(M3D_NOIMPORTER) && !defined(STBI_INCLUDE_STB_IMAGE_H)
+/* PNG decompressor from
+
+   stb_image - v2.23 - public domain image loader - http://nothings.org/stb_image.h
+*/
+static const char *stbi__g_failure_reason;
+
+enum
+{
+   STBI_default = 0,
+
+   STBI_grey       = 1,
+   STBI_grey_alpha = 2,
+   STBI_rgb        = 3,
+   STBI_rgb_alpha  = 4
+};
+
+enum
+{
+   STBI__SCAN_load=0,
+   STBI__SCAN_type,
+   STBI__SCAN_header
+};
+
+typedef unsigned char stbi_uc;
+typedef unsigned short stbi_us;
+
+typedef uint16_t stbi__uint16;
+typedef int16_t  stbi__int16;
+typedef uint32_t stbi__uint32;
+typedef int32_t  stbi__int32;
+
+typedef struct
+{
+   stbi__uint32 img_x, img_y;
+   int img_n, img_out_n;
+
+   void *io_user_data;
+
+   int read_from_callbacks;
+   int buflen;
+   stbi_uc buffer_start[128];
+
+   stbi_uc *img_buffer, *img_buffer_end;
+   stbi_uc *img_buffer_original, *img_buffer_original_end;
+} stbi__context;
+
+typedef struct
+{
+   int bits_per_channel;
+   int num_channels;
+   int channel_order;
+} stbi__result_info;
+
+#define STBI_ASSERT(v)
+#define STBI_NOTUSED(v)  (void)sizeof(v)
+#define STBI__BYTECAST(x)  ((stbi_uc) ((x) & 255))
+#define STBI_MALLOC(sz)           M3D_MALLOC(sz)
+#define STBI_REALLOC(p,newsz)     M3D_REALLOC(p,newsz)
+#define STBI_FREE(p)              M3D_FREE(p)
+#define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz)
+
+_inline static stbi_uc stbi__get8(stbi__context *s)
+{
+   if (s->img_buffer < s->img_buffer_end)
+      return *s->img_buffer++;
+   return 0;
+}
+
+_inline static int stbi__at_eof(stbi__context *s)
+{
+   return s->img_buffer >= s->img_buffer_end;
+}
+
+static void stbi__skip(stbi__context *s, int n)
+{
+   if (n < 0) {
+      s->img_buffer = s->img_buffer_end;
+      return;
+   }
+   s->img_buffer += n;
+}
+
+static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n)
+{
+   if (s->img_buffer+n <= s->img_buffer_end) {
+      memcpy(buffer, s->img_buffer, n);
+      s->img_buffer += n;
+      return 1;
+   } else
+      return 0;
+}
+
+static int stbi__get16be(stbi__context *s)
+{
+   int z = stbi__get8(s);
+   return (z << 8) + stbi__get8(s);
+}
+
+static stbi__uint32 stbi__get32be(stbi__context *s)
+{
+   stbi__uint32 z = stbi__get16be(s);
+   return (z << 16) + stbi__get16be(s);
+}
+
+#define stbi__err(x,y)  stbi__errstr(y)
+static int stbi__errstr(const char *str)
+{
+   stbi__g_failure_reason = str;
+   return 0;
+}
+
+_inline static void *stbi__malloc(size_t size)
+{
+    return STBI_MALLOC(size);
+}
+
+static int stbi__addsizes_valid(int a, int b)
+{
+   if (b < 0) return 0;
+   return a <= 2147483647 - b;
+}
+
+static int stbi__mul2sizes_valid(int a, int b)
+{
+   if (a < 0 || b < 0) return 0;
+   if (b == 0) return 1;
+   return a <= 2147483647/b;
+}
+
+static int stbi__mad2sizes_valid(int a, int b, int add)
+{
+   return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add);
+}
+
+static int stbi__mad3sizes_valid(int a, int b, int c, int add)
+{
+   return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) &&
+      stbi__addsizes_valid(a*b*c, add);
+}
+
+static void *stbi__malloc_mad2(int a, int b, int add)
+{
+   if (!stbi__mad2sizes_valid(a, b, add)) return NULL;
+   return stbi__malloc(a*b + add);
+}
+
+static void *stbi__malloc_mad3(int a, int b, int c, int add)
+{
+   if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL;
+   return stbi__malloc(a*b*c + add);
+}
+
+static stbi_uc stbi__compute_y(int r, int g, int b)
+{
+   return (stbi_uc) (((r*77) + (g*150) +  (29*b)) >> 8);
+}
+
+static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y)
+{
+   int i,j;
+   unsigned char *good;
+
+   if (req_comp == img_n) return data;
+   STBI_ASSERT(req_comp >= 1 && req_comp <= 4);
+
+   good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0);
+   if (good == NULL) {
+      STBI_FREE(data);
+      stbi__err("outofmem", "Out of memory");
+      return NULL;
+   }
+
+   for (j=0; j < (int) y; ++j) {
+      unsigned char *src  = data + j * x * img_n   ;
+      unsigned char *dest = good + j * x * req_comp;
+
+      #define STBI__COMBO(a,b)  ((a)*8+(b))
+      #define STBI__CASE(a,b)   case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b)
+      switch (STBI__COMBO(img_n, req_comp)) {
+         STBI__CASE(1,2) { dest[0]=src[0], dest[1]=255;                                     } break;
+         STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0];                                  } break;
+         STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0], dest[3]=255;                     } break;
+         STBI__CASE(2,1) { dest[0]=src[0];                                                  } break;
+         STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0];                                  } break;
+         STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0], dest[3]=src[1];                  } break;
+         STBI__CASE(3,4) { dest[0]=src[0],dest[1]=src[1],dest[2]=src[2],dest[3]=255;        } break;
+         STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]);                   } break;
+         STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]), dest[1] = 255;    } break;
+         STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]);                   } break;
+         STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]), dest[1] = src[3]; } break;
+         STBI__CASE(4,3) { dest[0]=src[0],dest[1]=src[1],dest[2]=src[2];                    } break;
+         default: STBI_ASSERT(0);
+      }
+      #undef STBI__CASE
+   }
+
+   STBI_FREE(data);
+   return good;
+}
+
+static stbi__uint16 stbi__compute_y_16(int r, int g, int b)
+{
+   return (stbi__uint16) (((r*77) + (g*150) +  (29*b)) >> 8);
+}
+
+static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y)
+{
+   int i,j;
+   stbi__uint16 *good;
+
+   if (req_comp == img_n) return data;
+   STBI_ASSERT(req_comp >= 1 && req_comp <= 4);
+
+   good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2);
+   if (good == NULL) {
+      STBI_FREE(data);
+      stbi__err("outofmem", "Out of memory");
+      return NULL;
+   }
+
+   for (j=0; j < (int) y; ++j) {
+      stbi__uint16 *src  = data + j * x * img_n   ;
+      stbi__uint16 *dest = good + j * x * req_comp;
+
+      #define STBI__COMBO(a,b)  ((a)*8+(b))
+      #define STBI__CASE(a,b)   case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b)
+      switch (STBI__COMBO(img_n, req_comp)) {
+         STBI__CASE(1,2) { dest[0]=src[0], dest[1]=0xffff;                                     } break;
+         STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0];                                     } break;
+         STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0], dest[3]=0xffff;                     } break;
+         STBI__CASE(2,1) { dest[0]=src[0];                                                     } break;
+         STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0];                                     } break;
+         STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0], dest[3]=src[1];                     } break;
+         STBI__CASE(3,4) { dest[0]=src[0],dest[1]=src[1],dest[2]=src[2],dest[3]=0xffff;        } break;
+         STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]);                   } break;
+         STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]), dest[1] = 0xffff; } break;
+         STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]);                   } break;
+         STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]), dest[1] = src[3]; } break;
+         STBI__CASE(4,3) { dest[0]=src[0],dest[1]=src[1],dest[2]=src[2];                       } break;
+         default: STBI_ASSERT(0);
+      }
+      #undef STBI__CASE
+   }
+
+   STBI_FREE(data);
+   return good;
+}
+
+#define STBI__ZFAST_BITS  9
+#define STBI__ZFAST_MASK  ((1 << STBI__ZFAST_BITS) - 1)
+
+typedef struct
+{
+   stbi__uint16 fast[1 << STBI__ZFAST_BITS];
+   stbi__uint16 firstcode[16];
+   int maxcode[17];
+   stbi__uint16 firstsymbol[16];
+   stbi_uc  size[288];
+   stbi__uint16 value[288];
+} stbi__zhuffman;
+
+_inline static int stbi__bitreverse16(int n)
+{
+  n = ((n & 0xAAAA) >>  1) | ((n & 0x5555) << 1);
+  n = ((n & 0xCCCC) >>  2) | ((n & 0x3333) << 2);
+  n = ((n & 0xF0F0) >>  4) | ((n & 0x0F0F) << 4);
+  n = ((n & 0xFF00) >>  8) | ((n & 0x00FF) << 8);
+  return n;
+}
+
+_inline static int stbi__bit_reverse(int v, int bits)
+{
+   STBI_ASSERT(bits <= 16);
+   return stbi__bitreverse16(v) >> (16-bits);
+}
+
+static int stbi__zbuild_huffman(stbi__zhuffman *z, stbi_uc *sizelist, int num)
+{
+   int i,k=0;
+   int code, next_code[16], sizes[17];
+
+   memset(sizes, 0, sizeof(sizes));
+   memset(z->fast, 0, sizeof(z->fast));
+   for (i=0; i < num; ++i)
+      ++sizes[sizelist[i]];
+   sizes[0] = 0;
+   for (i=1; i < 16; ++i)
+      if (sizes[i] > (1 << i))
+         return stbi__err("bad sizes", "Corrupt PNG");
+   code = 0;
+   for (i=1; i < 16; ++i) {
+      next_code[i] = code;
+      z->firstcode[i] = (stbi__uint16) code;
+      z->firstsymbol[i] = (stbi__uint16) k;
+      code = (code + sizes[i]);
+      if (sizes[i])
+         if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG");
+      z->maxcode[i] = code << (16-i);
+      code <<= 1;
+      k += sizes[i];
+   }
+   z->maxcode[16] = 0x10000;
+   for (i=0; i < num; ++i) {
+      int s = sizelist[i];
+      if (s) {
+         int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s];
+         stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i);
+         z->size [c] = (stbi_uc     ) s;
+         z->value[c] = (stbi__uint16) i;
+         if (s <= STBI__ZFAST_BITS) {
+            int j = stbi__bit_reverse(next_code[s],s);
+            while (j < (1 << STBI__ZFAST_BITS)) {
+               z->fast[j] = fastv;
+               j += (1 << s);
+            }
+         }
+         ++next_code[s];
+      }
+   }
+   return 1;
+}
+
+typedef struct
+{
+   stbi_uc *zbuffer, *zbuffer_end;
+   int num_bits;
+   stbi__uint32 code_buffer;
+
+   char *zout;
+   char *zout_start;
+   char *zout_end;
+   int   z_expandable;
+
+   stbi__zhuffman z_length, z_distance;
+} stbi__zbuf;
+
+_inline static stbi_uc stbi__zget8(stbi__zbuf *z)
+{
+   if (z->zbuffer >= z->zbuffer_end) return 0;
+   return *z->zbuffer++;
+}
+
+static void stbi__fill_bits(stbi__zbuf *z)
+{
+   do {
+      STBI_ASSERT(z->code_buffer < (1U << z->num_bits));
+      z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits;
+      z->num_bits += 8;
+   } while (z->num_bits <= 24);
+}
+
+_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n)
+{
+   unsigned int k;
+   if (z->num_bits < n) stbi__fill_bits(z);
+   k = z->code_buffer & ((1 << n) - 1);
+   z->code_buffer >>= n;
+   z->num_bits -= n;
+   return k;
+}
+
+static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z)
+{
+   int b,s,k;
+   k = stbi__bit_reverse(a->code_buffer, 16);
+   for (s=STBI__ZFAST_BITS+1; ; ++s)
+      if (k < z->maxcode[s])
+         break;
+   if (s == 16) return -1;
+   b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s];
+   STBI_ASSERT(z->size[b] == s);
+   a->code_buffer >>= s;
+   a->num_bits -= s;
+   return z->value[b];
+}
+
+_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)
+{
+   int b,s;
+   if (a->num_bits < 16) stbi__fill_bits(a);
+   b = z->fast[a->code_buffer & STBI__ZFAST_MASK];
+   if (b) {
+      s = b >> 9;
+      a->code_buffer >>= s;
+      a->num_bits -= s;
+      return b & 511;
+   }
+   return stbi__zhuffman_decode_slowpath(a, z);
+}
+
+static int stbi__zexpand(stbi__zbuf *z, char *zout, int n)
+{
+   char *q;
+   int cur, limit, old_limit;
+   z->zout = zout;
+   if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG");
+   cur   = (int) (z->zout     - z->zout_start);
+   limit = old_limit = (int) (z->zout_end - z->zout_start);
+   while (cur + n > limit)
+      limit *= 2;
+   q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit);
+   STBI_NOTUSED(old_limit);
+   if (q == NULL) return stbi__err("outofmem", "Out of memory");
+   z->zout_start = q;
+   z->zout       = q + cur;
+   z->zout_end   = q + limit;
+   return 1;
+}
+
+static int stbi__zlength_base[31] = {
+   3,4,5,6,7,8,9,10,11,13,
+   15,17,19,23,27,31,35,43,51,59,
+   67,83,99,115,131,163,195,227,258,0,0 };
+
+static int stbi__zlength_extra[31]=
+{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 };
+
+static int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,
+257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0};
+
+static int stbi__zdist_extra[32] =
+{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13};
+
+static int stbi__parse_huffman_block(stbi__zbuf *a)
+{
+   char *zout = a->zout;
+   for(;;) {
+      int z = stbi__zhuffman_decode(a, &a->z_length);
+      if (z < 256) {
+         if (z < 0) return stbi__err("bad huffman code","Corrupt PNG");
+         if (zout >= a->zout_end) {
+            if (!stbi__zexpand(a, zout, 1)) return 0;
+            zout = a->zout;
+         }
+         *zout++ = (char) z;
+      } else {
+         stbi_uc *p;
+         int len,dist;
+         if (z == 256) {
+            a->zout = zout;
+            return 1;
+         }
+         z -= 257;
+         len = stbi__zlength_base[z];
+         if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]);
+         z = stbi__zhuffman_decode(a, &a->z_distance);
+         if (z < 0) return stbi__err("bad huffman code","Corrupt PNG");
+         dist = stbi__zdist_base[z];
+         if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]);
+         if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG");
+         if (zout + len > a->zout_end) {
+            if (!stbi__zexpand(a, zout, len)) return 0;
+            zout = a->zout;
+         }
+         p = (stbi_uc *) (zout - dist);
+         if (dist == 1) {
+            stbi_uc v = *p;
+            if (len) { do *zout++ = v; while (--len); }
+         } else {
+            if (len) { do *zout++ = *p++; while (--len); }
+         }
+      }
+   }
+}
+
+static int stbi__compute_huffman_codes(stbi__zbuf *a)
+{
+   static stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 };
+   stbi__zhuffman z_codelength;
+   stbi_uc lencodes[286+32+137];
+   stbi_uc codelength_sizes[19];
+   int i,n;
+
+   int hlit  = stbi__zreceive(a,5) + 257;
+   int hdist = stbi__zreceive(a,5) + 1;
+   int hclen = stbi__zreceive(a,4) + 4;
+   int ntot  = hlit + hdist;
+
+   memset(codelength_sizes, 0, sizeof(codelength_sizes));
+   for (i=0; i < hclen; ++i) {
+      int s = stbi__zreceive(a,3);
+      codelength_sizes[length_dezigzag[i]] = (stbi_uc) s;
+   }
+   if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0;
+
+   n = 0;
+   while (n < ntot) {
+      int c = stbi__zhuffman_decode(a, &z_codelength);
+      if (c < 0 || c >= 19) return stbi__err("bad codelengths", "Corrupt PNG");
+      if (c < 16)
+         lencodes[n++] = (stbi_uc) c;
+      else {
+         stbi_uc fill = 0;
+         if (c == 16) {
+            c = stbi__zreceive(a,2)+3;
+            if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG");
+            fill = lencodes[n-1];
+         } else if (c == 17)
+            c = stbi__zreceive(a,3)+3;
+         else {
+            STBI_ASSERT(c == 18);
+            c = stbi__zreceive(a,7)+11;
+         }
+         if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG");
+         memset(lencodes+n, fill, c);
+         n += c;
+      }
+   }
+   if (n != ntot) return stbi__err("bad codelengths","Corrupt PNG");
+   if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0;
+   if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0;
+   return 1;
+}
+
+_inline static int stbi__parse_uncompressed_block(stbi__zbuf *a)
+{
+   stbi_uc header[4];
+   int len,nlen,k;
+   if (a->num_bits & 7)
+      stbi__zreceive(a, a->num_bits & 7);
+   k = 0;
+   while (a->num_bits > 0) {
+      header[k++] = (stbi_uc) (a->code_buffer & 255);
+      a->code_buffer >>= 8;
+      a->num_bits -= 8;
+   }
+   STBI_ASSERT(a->num_bits == 0);
+   while (k < 4)
+      header[k++] = stbi__zget8(a);
+   len  = header[1] * 256 + header[0];
+   nlen = header[3] * 256 + header[2];
+   if (nlen != (len ^ 0xffff)) return stbi__err("zlib corrupt","Corrupt PNG");
+   if (a->zbuffer + len > a->zbuffer_end) return stbi__err("read past buffer","Corrupt PNG");
+   if (a->zout + len > a->zout_end)
+      if (!stbi__zexpand(a, a->zout, len)) return 0;
+   memcpy(a->zout, a->zbuffer, len);
+   a->zbuffer += len;
+   a->zout += len;
+   return 1;
+}
+
+static int stbi__parse_zlib_header(stbi__zbuf *a)
+{
+   int cmf   = stbi__zget8(a);
+   int cm    = cmf & 15;
+   /* int cinfo = cmf >> 4; */
+   int flg   = stbi__zget8(a);
+   if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG");
+   if (flg & 32) return stbi__err("no preset dict","Corrupt PNG");
+   if (cm != 8) return stbi__err("bad compression","Corrupt PNG");
+   return 1;
+}
+
+static stbi_uc stbi__zdefault_length[288], stbi__zdefault_distance[32];
+static void stbi__init_zdefaults(void)
+{
+   int i;
+   for (i=0; i <= 143; ++i)     stbi__zdefault_length[i]   = 8;
+   for (   ; i <= 255; ++i)     stbi__zdefault_length[i]   = 9;
+   for (   ; i <= 279; ++i)     stbi__zdefault_length[i]   = 7;
+   for (   ; i <= 287; ++i)     stbi__zdefault_length[i]   = 8;
+
+   for (i=0; i <=  31; ++i)     stbi__zdefault_distance[i] = 5;
+}
+
+static int stbi__parse_zlib(stbi__zbuf *a, int parse_header)
+{
+   int final, type;
+   if (parse_header)
+      if (!stbi__parse_zlib_header(a)) return 0;
+   a->num_bits = 0;
+   a->code_buffer = 0;
+   do {
+      final = stbi__zreceive(a,1);
+      type = stbi__zreceive(a,2);
+      if (type == 0) {
+         if (!stbi__parse_uncompressed_block(a)) return 0;
+      } else if (type == 3) {
+         return 0;
+      } else {
+         if (type == 1) {
+            if (!stbi__zdefault_distance[31]) stbi__init_zdefaults();
+            if (!stbi__zbuild_huffman(&a->z_length  , stbi__zdefault_length  , 288)) return 0;
+            if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance,  32)) return 0;
+         } else {
+            if (!stbi__compute_huffman_codes(a)) return 0;
+         }
+         if (!stbi__parse_huffman_block(a)) return 0;
+      }
+   } while (!final);
+   return 1;
+}
+
+static int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header)
+{
+   a->zout_start = obuf;
+   a->zout       = obuf;
+   a->zout_end   = obuf + olen;
+   a->z_expandable = exp;
+
+   return stbi__parse_zlib(a, parse_header);
+}
+
+char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header)
+{
+   stbi__zbuf a;
+   char *p = (char *) stbi__malloc(initial_size);
+   if (p == NULL) return NULL;
+   a.zbuffer = (stbi_uc *) buffer;
+   a.zbuffer_end = (stbi_uc *) buffer + len;
+   if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) {
+      if (outlen) *outlen = (int) (a.zout - a.zout_start);
+      return a.zout_start;
+   } else {
+      STBI_FREE(a.zout_start);
+      return NULL;
+   }
+}
+
+typedef struct
+{
+   stbi__uint32 length;
+   stbi__uint32 type;
+} stbi__pngchunk;
+
+static stbi__pngchunk stbi__get_chunk_header(stbi__context *s)
+{
+   stbi__pngchunk c;
+   c.length = stbi__get32be(s);
+   c.type   = stbi__get32be(s);
+   return c;
+}
+
+_inline static int stbi__check_png_header(stbi__context *s)
+{
+   static stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 };
+   int i;
+   for (i=0; i < 8; ++i)
+      if (stbi__get8(s) != png_sig[i]) return stbi__err("bad png sig","Not a PNG");
+   return 1;
+}
+
+typedef struct
+{
+   stbi__context *s;
+   stbi_uc *idata, *expanded, *out;
+   int depth;
+} stbi__png;
+
+
+enum {
+   STBI__F_none=0,
+   STBI__F_sub=1,
+   STBI__F_up=2,
+   STBI__F_avg=3,
+   STBI__F_paeth=4,
+   STBI__F_avg_first,
+   STBI__F_paeth_first
+};
+
+static stbi_uc first_row_filter[5] =
+{
+   STBI__F_none,
+   STBI__F_sub,
+   STBI__F_none,
+   STBI__F_avg_first,
+   STBI__F_paeth_first
+};
+
+static int stbi__paeth(int a, int b, int c)
+{
+   int p = a + b - c;
+   int pa = abs(p-a);
+   int pb = abs(p-b);
+   int pc = abs(p-c);
+   if (pa <= pb && pa <= pc) return a;
+   if (pb <= pc) return b;
+   return c;
+}
+
+static stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 };
+
+static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color)
+{
+   int bytes = (depth == 16? 2 : 1);
+   stbi__context *s = a->s;
+   stbi__uint32 i,j,stride = x*out_n*bytes;
+   stbi__uint32 img_len, img_width_bytes;
+   int k;
+   int img_n = s->img_n;
+
+   int output_bytes = out_n*bytes;
+   int filter_bytes = img_n*bytes;
+   int width = x;
+
+   STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1);
+   a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0);
+   if (!a->out) return stbi__err("outofmem", "Out of memory");
+
+   img_width_bytes = (((img_n * x * depth) + 7) >> 3);
+   img_len = (img_width_bytes + 1) * y;
+   if (s->img_x == x && s->img_y == y) {
+      if (raw_len != img_len) return stbi__err("not enough pixels","Corrupt PNG");
+   } else {
+      if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG");
+   }
+
+   for (j=0; j < y; ++j) {
+      stbi_uc *cur = a->out + stride*j;
+      stbi_uc *prior = cur - stride;
+      int filter = *raw++;
+
+      if (filter > 4)
+         return stbi__err("invalid filter","Corrupt PNG");
+
+      if (depth < 8) {
+         STBI_ASSERT(img_width_bytes <= x);
+         cur += x*out_n - img_width_bytes;
+         filter_bytes = 1;
+         width = img_width_bytes;
+      }
+
+      if (j == 0) filter = first_row_filter[filter];
+
+      for (k=0; k < filter_bytes; ++k) {
+         switch (filter) {
+            case STBI__F_none       : cur[k] = raw[k]; break;
+            case STBI__F_sub        : cur[k] = raw[k]; break;
+            case STBI__F_up         : cur[k] = STBI__BYTECAST(raw[k] + prior[k]); break;
+            case STBI__F_avg        : cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); break;
+            case STBI__F_paeth      : cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(0,prior[k],0)); break;
+            case STBI__F_avg_first  : cur[k] = raw[k]; break;
+            case STBI__F_paeth_first: cur[k] = raw[k]; break;
+         }
+      }
+
+      if (depth == 8) {
+         if (img_n != out_n)
+            cur[img_n] = 255;
+         raw += img_n;
+         cur += out_n;
+         prior += out_n;
+      } else if (depth == 16) {
+         if (img_n != out_n) {
+            cur[filter_bytes]   = 255;
+            cur[filter_bytes+1] = 255;
+         }
+         raw += filter_bytes;
+         cur += output_bytes;
+         prior += output_bytes;
+      } else {
+         raw += 1;
+         cur += 1;
+         prior += 1;
+      }
+
+      if (depth < 8 || img_n == out_n) {
+         int nk = (width - 1)*filter_bytes;
+         #define STBI__CASE(f) \
+             case f:     \
+                for (k=0; k < nk; ++k)
+         switch (filter) {
+            case STBI__F_none:         memcpy(cur, raw, nk); break;
+            STBI__CASE(STBI__F_sub)          { cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); } break;
+            STBI__CASE(STBI__F_up)           { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break;
+            STBI__CASE(STBI__F_avg)          { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); } break;
+            STBI__CASE(STBI__F_paeth)        { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],prior[k],prior[k-filter_bytes])); } break;
+            STBI__CASE(STBI__F_avg_first)    { cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); } break;
+            STBI__CASE(STBI__F_paeth_first)  { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],0,0)); } break;
+         }
+         #undef STBI__CASE
+         raw += nk;
+      } else {
+         STBI_ASSERT(img_n+1 == out_n);
+         #define STBI__CASE(f) \
+             case f:     \
+                for (i=x-1; i >= 1; --i, cur[filter_bytes]=255,raw+=filter_bytes,cur+=output_bytes,prior+=output_bytes) \
+                   for (k=0; k < filter_bytes; ++k)
+         switch (filter) {
+            STBI__CASE(STBI__F_none)         { cur[k] = raw[k]; } break;
+            STBI__CASE(STBI__F_sub)          { cur[k] = STBI__BYTECAST(raw[k] + cur[k- output_bytes]); } break;
+            STBI__CASE(STBI__F_up)           { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break;
+            STBI__CASE(STBI__F_avg)          { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k- output_bytes])>>1)); } break;
+            STBI__CASE(STBI__F_paeth)        { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],prior[k],prior[k- output_bytes])); } break;
+            STBI__CASE(STBI__F_avg_first)    { cur[k] = STBI__BYTECAST(raw[k] + (cur[k- output_bytes] >> 1)); } break;
+            STBI__CASE(STBI__F_paeth_first)  { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],0,0)); } break;
+         }
+         #undef STBI__CASE
+
+         if (depth == 16) {
+            cur = a->out + stride*j;
+            for (i=0; i < x; ++i,cur+=output_bytes) {
+               cur[filter_bytes+1] = 255;
+            }
+         }
+      }
+   }
+
+   if (depth < 8) {
+      for (j=0; j < y; ++j) {
+         stbi_uc *cur = a->out + stride*j;
+         stbi_uc *in  = a->out + stride*j + x*out_n - img_width_bytes;
+         stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1;
+
+         if (depth == 4) {
+            for (k=x*img_n; k >= 2; k-=2, ++in) {
+               *cur++ = scale * ((*in >> 4)       );
+               *cur++ = scale * ((*in     ) & 0x0f);
+            }
+            if (k > 0) *cur++ = scale * ((*in >> 4)       );
+         } else if (depth == 2) {
+            for (k=x*img_n; k >= 4; k-=4, ++in) {
+               *cur++ = scale * ((*in >> 6)       );
+               *cur++ = scale * ((*in >> 4) & 0x03);
+               *cur++ = scale * ((*in >> 2) & 0x03);
+               *cur++ = scale * ((*in     ) & 0x03);
+            }
+            if (k > 0) *cur++ = scale * ((*in >> 6)       );
+            if (k > 1) *cur++ = scale * ((*in >> 4) & 0x03);
+            if (k > 2) *cur++ = scale * ((*in >> 2) & 0x03);
+         } else if (depth == 1) {
+            for (k=x*img_n; k >= 8; k-=8, ++in) {
+               *cur++ = scale * ((*in >> 7)       );
+               *cur++ = scale * ((*in >> 6) & 0x01);
+               *cur++ = scale * ((*in >> 5) & 0x01);
+               *cur++ = scale * ((*in >> 4) & 0x01);
+               *cur++ = scale * ((*in >> 3) & 0x01);
+               *cur++ = scale * ((*in >> 2) & 0x01);
+               *cur++ = scale * ((*in >> 1) & 0x01);
+               *cur++ = scale * ((*in     ) & 0x01);
+            }
+            if (k > 0) *cur++ = scale * ((*in >> 7)       );
+            if (k > 1) *cur++ = scale * ((*in >> 6) & 0x01);
+            if (k > 2) *cur++ = scale * ((*in >> 5) & 0x01);
+            if (k > 3) *cur++ = scale * ((*in >> 4) & 0x01);
+            if (k > 4) *cur++ = scale * ((*in >> 3) & 0x01);
+            if (k > 5) *cur++ = scale * ((*in >> 2) & 0x01);
+            if (k > 6) *cur++ = scale * ((*in >> 1) & 0x01);
+         }
+         if (img_n != out_n) {
+            int q;
+            cur = a->out + stride*j;
+            if (img_n == 1) {
+               for (q=x-1; q >= 0; --q) {
+                  cur[q*2+1] = 255;
+                  cur[q*2+0] = cur[q];
+               }
+            } else {
+               STBI_ASSERT(img_n == 3);
+               for (q=x-1; q >= 0; --q) {
+                  cur[q*4+3] = 255;
+                  cur[q*4+2] = cur[q*3+2];
+                  cur[q*4+1] = cur[q*3+1];
+                  cur[q*4+0] = cur[q*3+0];
+               }
+            }
+         }
+      }
+   } else if (depth == 16) {
+      stbi_uc *cur = a->out;
+      stbi__uint16 *cur16 = (stbi__uint16*)cur;
+
+      for(i=0; i < x*y*out_n; ++i,cur16++,cur+=2) {
+         *cur16 = (cur[0] << 8) | cur[1];
+      }
+   }
+
+   return 1;
+}
+
+static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced)
+{
+   int bytes = (depth == 16 ? 2 : 1);
+   int out_bytes = out_n * bytes;
+   stbi_uc *final;
+   int p;
+   if (!interlaced)
+      return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color);
+
+   final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0);
+   for (p=0; p < 7; ++p) {
+      int xorig[] = { 0,4,0,2,0,1,0 };
+      int yorig[] = { 0,0,4,0,2,0,1 };
+      int xspc[]  = { 8,8,4,4,2,2,1 };
+      int yspc[]  = { 8,8,8,4,4,2,2 };
+      int i,j,x,y;
+      x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p];
+      y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p];
+      if (x && y) {
+         stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y;
+         if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) {
+            STBI_FREE(final);
+            return 0;
+         }
+         for (j=0; j < y; ++j) {
+            for (i=0; i < x; ++i) {
+               int out_y = j*yspc[p]+yorig[p];
+               int out_x = i*xspc[p]+xorig[p];
+               memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes,
+                      a->out + (j*x+i)*out_bytes, out_bytes);
+            }
+         }
+         STBI_FREE(a->out);
+         image_data += img_len;
+         image_data_len -= img_len;
+      }
+   }
+   a->out = final;
+
+   return 1;
+}
+
+static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n)
+{
+   stbi__context *s = z->s;
+   stbi__uint32 i, pixel_count = s->img_x * s->img_y;
+   stbi_uc *p = z->out;
+
+   STBI_ASSERT(out_n == 2 || out_n == 4);
+
+   if (out_n == 2) {
+      for (i=0; i < pixel_count; ++i) {
+         p[1] = (p[0] == tc[0] ? 0 : 255);
+         p += 2;
+      }
+   } else {
+      for (i=0; i < pixel_count; ++i) {
+         if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2])
+            p[3] = 0;
+         p += 4;
+      }
+   }
+   return 1;
+}
+
+static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n)
+{
+   stbi__context *s = z->s;
+   stbi__uint32 i, pixel_count = s->img_x * s->img_y;
+   stbi__uint16 *p = (stbi__uint16*) z->out;
+
+   STBI_ASSERT(out_n == 2 || out_n == 4);
+
+   if (out_n == 2) {
+      for (i = 0; i < pixel_count; ++i) {
+         p[1] = (p[0] == tc[0] ? 0 : 65535);
+         p += 2;
+      }
+   } else {
+      for (i = 0; i < pixel_count; ++i) {
+         if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2])
+            p[3] = 0;
+         p += 4;
+      }
+   }
+   return 1;
+}
+
+static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n)
+{
+   stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y;
+   stbi_uc *p, *temp_out, *orig = a->out;
+
+   p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0);
+   if (p == NULL) return stbi__err("outofmem", "Out of memory");
+
+   temp_out = p;
+
+   if (pal_img_n == 3) {
+      for (i=0; i < pixel_count; ++i) {
+         int n = orig[i]*4;
+         p[0] = palette[n  ];
+         p[1] = palette[n+1];
+         p[2] = palette[n+2];
+         p += 3;
+      }
+   } else {
+      for (i=0; i < pixel_count; ++i) {
+         int n = orig[i]*4;
+         p[0] = palette[n  ];
+         p[1] = palette[n+1];
+         p[2] = palette[n+2];
+         p[3] = palette[n+3];
+         p += 4;
+      }
+   }
+   STBI_FREE(a->out);
+   a->out = temp_out;
+
+   STBI_NOTUSED(len);
+
+   return 1;
+}
+
+static int stbi__unpremultiply_on_load = 0;
+static int stbi__de_iphone_flag = 0;
+
+void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply)
+{
+   stbi__unpremultiply_on_load = flag_true_if_should_unpremultiply;
+}
+
+void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert)
+{
+   stbi__de_iphone_flag = flag_true_if_should_convert;
+}
+
+static void stbi__de_iphone(stbi__png *z)
+{
+   stbi__context *s = z->s;
+   stbi__uint32 i, pixel_count = s->img_x * s->img_y;
+   stbi_uc *p = z->out;
+
+   if (s->img_out_n == 3) {
+      for (i=0; i < pixel_count; ++i) {
+         stbi_uc t = p[0];
+         p[0] = p[2];
+         p[2] = t;
+         p += 3;
+      }
+   } else {
+      STBI_ASSERT(s->img_out_n == 4);
+      if (stbi__unpremultiply_on_load) {
+         for (i=0; i < pixel_count; ++i) {
+            stbi_uc a = p[3];
+            stbi_uc t = p[0];
+            if (a) {
+               p[0] = p[2] * 255 / a;
+               p[1] = p[1] * 255 / a;
+               p[2] =  t   * 255 / a;
+            } else {
+               p[0] = p[2];
+               p[2] = t;
+            }
+            p += 4;
+         }
+      } else {
+         for (i=0; i < pixel_count; ++i) {
+            stbi_uc t = p[0];
+            p[0] = p[2];
+            p[2] = t;
+            p += 4;
+         }
+      }
+   }
+}
+
+#define STBI__PNG_TYPE(a,b,c,d)  (((a) << 24) + ((b) << 16) + ((c) << 8) + (d))
+
+static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp)
+{
+   stbi_uc palette[1024], pal_img_n=0;
+   stbi_uc has_trans=0, tc[3];
+   stbi__uint16 tc16[3];
+   stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0;
+   int first=1,k,interlace=0, color=0, is_iphone=0;
+   stbi__context *s = z->s;
+
+   z->expanded = NULL;
+   z->idata = NULL;
+   z->out = NULL;
+
+   if (!stbi__check_png_header(s)) return 0;
+
+   if (scan == STBI__SCAN_type) return 1;
+
+   for (;;) {
+      stbi__pngchunk c = stbi__get_chunk_header(s);
+      switch (c.type) {
+         case STBI__PNG_TYPE('C','g','B','I'):
+            is_iphone = 1;
+            stbi__skip(s, c.length);
+            break;
+         case STBI__PNG_TYPE('I','H','D','R'): {
+            int comp,filter;
+            if (!first) return stbi__err("multiple IHDR","Corrupt PNG");
+            first = 0;
+            if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG");
+            s->img_x = stbi__get32be(s); if (s->img_x > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)");
+            s->img_y = stbi__get32be(s); if (s->img_y > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)");
+            z->depth = stbi__get8(s);  if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16)  return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only");
+            color = stbi__get8(s);  if (color > 6)         return stbi__err("bad ctype","Corrupt PNG");
+            if (color == 3 && z->depth == 16)                  return stbi__err("bad ctype","Corrupt PNG");
+            if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err("bad ctype","Corrupt PNG");
+            comp  = stbi__get8(s);  if (comp) return stbi__err("bad comp method","Corrupt PNG");
+            filter= stbi__get8(s);  if (filter) return stbi__err("bad filter method","Corrupt PNG");
+            interlace = stbi__get8(s); if (interlace>1) return stbi__err("bad interlace method","Corrupt PNG");
+            if (!s->img_x || !s->img_y) return stbi__err("0-pixel image","Corrupt PNG");
+            if (!pal_img_n) {
+               s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0);
+               if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode");
+               if (scan == STBI__SCAN_header) return 1;
+            } else {
+               s->img_n = 1;
+               if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG");
+            }
+            break;
+         }
+
+         case STBI__PNG_TYPE('P','L','T','E'):  {
+            if (first) return stbi__err("first not IHDR", "Corrupt PNG");
+            if (c.length > 256*3) return stbi__err("invalid PLTE","Corrupt PNG");
+            pal_len = c.length / 3;
+            if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG");
+            for (i=0; i < pal_len; ++i) {
+               palette[i*4+0] = stbi__get8(s);
+               palette[i*4+1] = stbi__get8(s);
+               palette[i*4+2] = stbi__get8(s);
+               palette[i*4+3] = 255;
+            }
+            break;
+         }
+
+         case STBI__PNG_TYPE('t','R','N','S'): {
+            if (first) return stbi__err("first not IHDR", "Corrupt PNG");
+            if (z->idata) return stbi__err("tRNS after IDAT","Corrupt PNG");
+            if (pal_img_n) {
+               if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; }
+               if (pal_len == 0) return stbi__err("tRNS before PLTE","Corrupt PNG");
+               if (c.length > pal_len) return stbi__err("bad tRNS len","Corrupt PNG");
+               pal_img_n = 4;
+               for (i=0; i < c.length; ++i)
+                  palette[i*4+3] = stbi__get8(s);
+            } else {
+               if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG");
+               if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG");
+               has_trans = 1;
+               if (z->depth == 16) {
+                  for (k = 0; k < s->img_n; ++k) tc16[k] = (stbi__uint16)stbi__get16be(s);
+               } else {
+                  for (k = 0; k < s->img_n; ++k) tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth];
+               }
+            }
+            break;
+         }
+
+         case STBI__PNG_TYPE('I','D','A','T'): {
+            if (first) return stbi__err("first not IHDR", "Corrupt PNG");
+            if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG");
+            if (scan == STBI__SCAN_header) { s->img_n = pal_img_n; return 1; }
+            if ((int)(ioff + c.length) < (int)ioff) return 0;
+            if (ioff + c.length > idata_limit) {
+               stbi__uint32 idata_limit_old = idata_limit;
+               stbi_uc *p;
+               if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096;
+               while (ioff + c.length > idata_limit)
+                  idata_limit *= 2;
+               STBI_NOTUSED(idata_limit_old);
+               p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err("outofmem", "Out of memory");
+               z->idata = p;
+            }
+            if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG");
+            ioff += c.length;
+            break;
+         }
+
+         case STBI__PNG_TYPE('I','E','N','D'): {
+            stbi__uint32 raw_len, bpl;
+            if (first) return stbi__err("first not IHDR", "Corrupt PNG");
+            if (scan != STBI__SCAN_load) return 1;
+            if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG");
+            bpl = (s->img_x * z->depth + 7) / 8;
+            raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */;
+            z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone);
+            if (z->expanded == NULL) return 0;
+            STBI_FREE(z->idata); z->idata = NULL;
+            if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans)
+               s->img_out_n = s->img_n+1;
+            else
+               s->img_out_n = s->img_n;
+            if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0;
+            if (has_trans) {
+               if (z->depth == 16) {
+                  if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0;
+               } else {
+                  if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0;
+               }
+            }
+            if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2)
+               stbi__de_iphone(z);
+            if (pal_img_n) {
+               s->img_n = pal_img_n;
+               s->img_out_n = pal_img_n;
+               if (req_comp >= 3) s->img_out_n = req_comp;
+               if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n))
+                  return 0;
+            }
+            STBI_FREE(z->expanded); z->expanded = NULL;
+            return 1;
+         }
+
+         default:
+            if (first) return stbi__err("first not IHDR", "Corrupt PNG");
+            if ((c.type & (1 << 29)) == 0) {
+               return stbi__err("invalid_chunk", "PNG not supported: unknown PNG chunk type");
+            }
+            stbi__skip(s, c.length);
+            break;
+      }
+      stbi__get32be(s);
+   }
+}
+
+static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri)
+{
+   void *result=NULL;
+   if (req_comp < 0 || req_comp > 4) { stbi__err("bad req_comp", "Internal error"); return NULL; }
+   if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) {
+      ri->bits_per_channel = p->depth;
+      result = p->out;
+      p->out = NULL;
+      if (req_comp && req_comp != p->s->img_out_n) {
+         if (ri->bits_per_channel == 8)
+            result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y);
+         else
+            result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y);
+         p->s->img_out_n = req_comp;
+         if (result == NULL) return result;
+      }
+      *x = p->s->img_x;
+      *y = p->s->img_y;
+      if (n) *n = p->s->img_n;
+   }
+   STBI_FREE(p->out);      p->out      = NULL;
+   STBI_FREE(p->expanded); p->expanded = NULL;
+   STBI_FREE(p->idata);    p->idata    = NULL;
+
+   return result;
+}
+
+static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)
+{
+   stbi__png p;
+   p.s = s;
+   return stbi__do_png(&p, x,y,comp,req_comp, ri);
+}
+#endif
+
+#if defined(M3D_EXPORTER) && !defined(INCLUDE_STB_IMAGE_WRITE_H)
+/* zlib_compressor from
+
+   stb_image_write - v1.13 - public domain - http://nothings.org/stb/stb_image_write.h
+*/
+typedef unsigned char stbiw_uc;
+typedef unsigned short stbiw_us;
+
+typedef uint16_t stbiw_uint16;
+typedef int16_t  stbiw_int16;
+typedef uint32_t stbiw_uint32;
+typedef int32_t  stbiw_int32;
+
+#define STBIW_MALLOC(s)     M3D_MALLOC(s)
+#define STBIW_REALLOC(p,ns) M3D_REALLOC(p,ns)
+#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz)
+#define STBIW_FREE          M3D_FREE
+#define STBIW_MEMMOVE       memmove
+#define STBIW_UCHAR         (uint8_t)
+#define STBIW_ASSERT(x)
+#define stbiw__sbraw(a)     ((int *) (a) - 2)
+#define stbiw__sbm(a)       stbiw__sbraw(a)[0]
+#define stbiw__sbn(a)       stbiw__sbraw(a)[1]
+
+#define stbiw__sbneedgrow(a,n)  ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a))
+#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0)
+#define stbiw__sbgrow(a,n)  stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a)))
+
+#define stbiw__sbpush(a, v)      (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v))
+#define stbiw__sbcount(a)        ((a) ? stbiw__sbn(a) : 0)
+#define stbiw__sbfree(a)         ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0)
+
+static void *stbiw__sbgrowf(void **arr, int increment, int itemsize)
+{
+   int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1;
+   void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2);
+   STBIW_ASSERT(p);
+   if (p) {
+      if (!*arr) ((int *) p)[1] = 0;
+      *arr = (void *) ((int *) p + 2);
+      stbiw__sbm(*arr) = m;
+   }
+   return *arr;
+}
+
+static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount)
+{
+   while (*bitcount >= 8) {
+      stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer));
+      *bitbuffer >>= 8;
+      *bitcount -= 8;
+   }
+   return data;
+}
+
+static int stbiw__zlib_bitrev(int code, int codebits)
+{
+   int res=0;
+   while (codebits--) {
+      res = (res << 1) | (code & 1);
+      code >>= 1;
+   }
+   return res;
+}
+
+static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit)
+{
+   int i;
+   for (i=0; i < limit && i < 258; ++i)
+      if (a[i] != b[i]) break;
+   return i;
+}
+
+static unsigned int stbiw__zhash(unsigned char *data)
+{
+   stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16);
+   hash ^= hash << 3;
+   hash += hash >> 5;
+   hash ^= hash << 4;
+   hash += hash >> 17;
+   hash ^= hash << 25;
+   hash += hash >> 6;
+   return hash;
+}
+
+#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount))
+#define stbiw__zlib_add(code,codebits) \
+      (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush())
+#define stbiw__zlib_huffa(b,c)  stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c)
+#define stbiw__zlib_huff1(n)  stbiw__zlib_huffa(0x30 + (n), 8)
+#define stbiw__zlib_huff2(n)  stbiw__zlib_huffa(0x190 + (n)-144, 9)
+#define stbiw__zlib_huff3(n)  stbiw__zlib_huffa(0 + (n)-256,7)
+#define stbiw__zlib_huff4(n)  stbiw__zlib_huffa(0xc0 + (n)-280,8)
+#define stbiw__zlib_huff(n)  ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n))
+#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n))
+
+#define stbiw__ZHASH   16384
+
+unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality)
+{
+   static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 };
+   static unsigned char  lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4,  4,  5,  5,  5,  5,  0 };
+   static unsigned short distc[]   = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 };
+   static unsigned char  disteb[]  = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 };
+   unsigned int bitbuf=0;
+   int i,j, bitcount=0;
+   unsigned char *out = NULL;
+   unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(char**));
+   if (quality < 5) quality = 5;
+
+   stbiw__sbpush(out, 0x78);
+   stbiw__sbpush(out, 0x5e);
+   stbiw__zlib_add(1,1);
+   stbiw__zlib_add(1,2);
+
+   for (i=0; i < stbiw__ZHASH; ++i)
+      hash_table[i] = NULL;
+
+   i=0;
+   while (i < data_len-3) {
+      int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3;
+      unsigned char *bestloc = 0;
+      unsigned char **hlist = hash_table[h];
+      int n = stbiw__sbcount(hlist);
+      for (j=0; j < n; ++j) {
+         if (hlist[j]-data > i-32768) {
+            int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i);
+            if (d >= best) best=d,bestloc=hlist[j];
+         }
+      }
+      if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) {
+         STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality);
+         stbiw__sbn(hash_table[h]) = quality;
+      }
+      stbiw__sbpush(hash_table[h],data+i);
+
+      if (bestloc) {
+         h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1);
+         hlist = hash_table[h];
+         n = stbiw__sbcount(hlist);
+         for (j=0; j < n; ++j) {
+            if (hlist[j]-data > i-32767) {
+               int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1);
+               if (e > best) {
+                  bestloc = NULL;
+                  break;
+               }
+            }
+         }
+      }
+
+      if (bestloc) {
+         int d = (int) (data+i - bestloc);
+         STBIW_ASSERT(d <= 32767 && best <= 258);
+         for (j=0; best > lengthc[j+1]-1; ++j);
+         stbiw__zlib_huff(j+257);
+         if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]);
+         for (j=0; d > distc[j+1]-1; ++j);
+         stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5);
+         if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]);
+         i += best;
+      } else {
+         stbiw__zlib_huffb(data[i]);
+         ++i;
+      }
+   }
+   for (;i < data_len; ++i)
+      stbiw__zlib_huffb(data[i]);
+   stbiw__zlib_huff(256);
+   while (bitcount)
+      stbiw__zlib_add(0,1);
+
+   for (i=0; i < stbiw__ZHASH; ++i)
+      (void) stbiw__sbfree(hash_table[i]);
+   STBIW_FREE(hash_table);
+
+   {
+      unsigned int s1=1, s2=0;
+      int blocklen = (int) (data_len % 5552);
+      j=0;
+      while (j < data_len) {
+         for (i=0; i < blocklen; ++i) s1 += data[j+i], s2 += s1;
+         s1 %= 65521, s2 %= 65521;
+         j += blocklen;
+         blocklen = 5552;
+      }
+      stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8));
+      stbiw__sbpush(out, STBIW_UCHAR(s2));
+      stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8));
+      stbiw__sbpush(out, STBIW_UCHAR(s1));
+   }
+   *out_len = stbiw__sbn(out);
+   STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len);
+   return (unsigned char *) stbiw__sbraw(out);
+}
+#endif
+
+#define M3D_CHUNKMAGIC(m, a,b,c,d) ((m)[0]==(a) && (m)[1]==(b) && (m)[2]==(c) && (m)[3]==(d))
+
+#ifdef M3D_ASCII
+#include <stdio.h>          /* get sprintf */
+#include <locale.h>         /* sprintf and strtod cares about number locale */
+#endif
+
+#if !defined(M3D_NOIMPORTER) && defined(M3D_ASCII)
+/* helper functions for the ASCII parser */
+static char *_m3d_findarg(char *s) {
+    while(s && *s && *s != ' ' && *s != '\t' && *s != '\r' && *s != '\n') s++;
+    while(s && *s && (*s == ' ' || *s == '\t')) s++;
+    return s;
+}
+static char *_m3d_findnl(char *s) {
+    while(s && *s && *s != '\r' && *s != '\n') s++;
+    if(*s == '\r') s++;
+    if(*s == '\n') s++;
+    return s;
+}
+static char *_m3d_gethex(char *s, uint32_t *ret)
+{
+    if(*s == '#') s++;
+    *ret = 0;
+    for(; *s; s++) {
+        if(*s >= '0' && *s <= '9') {      *ret <<= 4; *ret += (uint32_t)(*s-'0'); }
+        else if(*s >= 'a' && *s <= 'f') { *ret <<= 4; *ret += (uint32_t)(*s-'a'+10); }
+        else if(*s >= 'A' && *s <= 'F') { *ret <<= 4; *ret += (uint32_t)(*s-'A'+10); }
+        else break;
+    }
+    return _m3d_findarg(s);
+}
+static char *_m3d_getint(char *s, uint32_t *ret)
+{
+    char *e = s;
+    if(!s || !*s) return s;
+    for(; *e >= '0' && *e <= '9'; e++);
+    *ret = atoi(s);
+    return e;
+}
+static char *_m3d_getfloat(char *s, M3D_FLOAT *ret)
+{
+    char *e = s;
+    if(!s || !*s) return s;
+    for(; *e == '-' || *e == '+' || *e == '.' || (*e >= '0' && *e <= '9') || *e == 'e' || *e == 'E'; e++);
+    *ret = (M3D_FLOAT)strtod(s, NULL);
+    return _m3d_findarg(e);
+}
+#endif
+#if !defined(M3D_NODUP) && (!defined(M3D_NONORMALS) || defined(M3D_EXPORTER))
+/* add vertex to list, only compare x,y,z */
+m3dv_t *_m3d_addnorm(m3dv_t *vrtx, uint32_t *numvrtx, m3dv_t *v, uint32_t *idx)
+{
+    uint32_t i;
+    if(v->x == (M3D_FLOAT)-0.0) v->x = (M3D_FLOAT)0.0;
+    if(v->y == (M3D_FLOAT)-0.0) v->y = (M3D_FLOAT)0.0;
+    if(v->z == (M3D_FLOAT)-0.0) v->z = (M3D_FLOAT)0.0;
+    if(v->w == (M3D_FLOAT)-0.0) v->w = (M3D_FLOAT)0.0;
+    if(vrtx) {
+        for(i = 0; i < *numvrtx; i++)
+            if(vrtx[i].x == v->x && vrtx[i].y == v->y && vrtx[i].z == v->z) { *idx = i; return vrtx; }
+    }
+    vrtx = (m3dv_t*)M3D_REALLOC(vrtx, ((*numvrtx) + 1) * sizeof(m3dv_t));
+    memcpy(&vrtx[*numvrtx], v, sizeof(m3dv_t));
+    vrtx[*numvrtx].color = 0;
+    vrtx[*numvrtx].w = (M3D_FLOAT)1.0;
+    *idx = *numvrtx;
+    (*numvrtx)++;
+    return vrtx;
+}
+#endif
+#if !defined(M3D_NODUP) && (defined(M3D_ASCII) || defined(M3D_EXPORTER))
+m3ds_t *_m3d_addskin(m3ds_t *skin, uint32_t *numskin, m3ds_t *s, uint32_t *idx)
+{
+    uint32_t i;
+    M3D_FLOAT w = (M3D_FLOAT)0.0;
+    for(i = 0; i < M3D_NUMBONE && s->weight[i] > (M3D_FLOAT)0.0; i++)
+        w += s->weight[i];
+    if(w != (M3D_FLOAT)1.0 && w != (M3D_FLOAT)0.0)
+        for(i = 0; i < M3D_NUMBONE && s->weight[i] > (M3D_FLOAT)0.0; i++)
+            s->weight[i] /= w;
+    if(skin) {
+        for(i = 0; i < *numskin; i++)
+            if(!memcmp(&skin[i], s, sizeof(m3ds_t))) { *idx = i; return skin; }
+    }
+    skin = (m3ds_t*)M3D_REALLOC(skin, ((*numskin) + 1) * sizeof(m3ds_t));
+    memcpy(&skin[*numskin], s, sizeof(m3ds_t));
+    *idx = *numskin;
+    (*numskin)++;
+    return skin;
+}
+/* helper function to create safe strings */
+char *_m3d_safestr(char *in, int morelines)
+{
+    char *out, *o, *i = in;
+    int l;
+    if(!in || !*in) {
+        out = (char*)M3D_MALLOC(1);
+        if(!out) return NULL;
+        out[0] =0;
+    } else {
+        for(o = in, l = 0; *o && ((morelines & 1) || (*o != '\r' && *o != '\n')) && l < 256; o++, l++);
+        out = o = (char*)M3D_MALLOC(l+1);
+        if(!out) return NULL;
+        while(*i == ' ' || *i == '\t' || *i == '\r' || (morelines && *i == '\n')) i++;
+        for(; *i && (morelines || (*i != '\r' && *i != '\n')); i++) {
+            if(*i == '\r') continue;
+            if(*i == '\n') {
+                if(morelines >= 3 && o > out && *(o-1) == '\n') break;
+                if(i > in && *(i-1) == '\n') continue;
+                if(morelines & 1) {
+                    if(morelines == 1) *o++ = '\r';
+                    *o++ = '\n';
+                } else
+                    break;
+            } else
+            if(*i == ' ' || *i == '\t') {
+                *o++ = morelines? ' ' : '_';
+            } else
+                *o++ = !morelines && (*i == '/' || *i == '\\') ? '_' : *i;
+        }
+        for(; o > out && (*(o-1) == ' ' || *(o-1) == '\t' || *(o-1) == '\r' || *(o-1) == '\n'); o--);
+        *o = 0;
+        out = (char*)M3D_REALLOC(out, (uint64_t)o - (uint64_t)out + 1);
+    }
+    return out;
+}
+#endif
+#ifndef M3D_NOIMPORTER
+/* helper function to load and decode/generate a texture */
+M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char *fn)
+{
+    unsigned int i, len = 0, w, h;
+    unsigned char *buff = NULL;
+    char *fn2;
+    stbi__context s;
+    stbi__result_info ri;
+
+    for(i = 0; i < model->numtexture; i++)
+        if(!strcmp(fn, model->texture[i].name)) return i;
+    if(readfilecb) {
+        i = strlen(fn);
+        if(i < 5 || fn[i - 4] != '.') {
+            fn2 = (char*)M3D_MALLOC(i + 5);
+            if(!fn2) { model->errcode = M3D_ERR_ALLOC; return (M3D_INDEX)-1U; }
+            memcpy(fn2, fn, i);
+            memcpy(fn2+i, ".png", 5);
+            buff = (*readfilecb)(fn2, &len);
+            M3D_FREE(fn2);
+        }
+        if(!buff)
+            buff = (*readfilecb)(fn, &len);
+    }
+    if(!buff && model->inlined) {
+        for(i = 0; i < model->numinlined; i++)
+            if(!strcmp(fn, model->inlined[i].name)) {
+                buff = model->inlined[i].data;
+                len = model->inlined[i].length;
+                freecb = NULL;
+                break;
+            }
+    }
+    if(!buff) return (M3D_INDEX)-1U;
+    i = model->numtexture++;
+    model->texture = (m3dtx_t*)M3D_REALLOC(model->texture, model->numtexture * sizeof(m3dtx_t));
+    if(!model->texture) {
+        if(freecb) (*freecb)(buff);
+        model->errcode = M3D_ERR_ALLOC; return (M3D_INDEX)-1U;
+    }
+    model->texture[i].w = model->texture[i].h = 0; model->texture[i].d = NULL;
+    if(buff[0] == 0x89 && buff[1] == 'P' && buff[2] == 'N' && buff[3] == 'G') {
+        s.read_from_callbacks = 0;
+        s.img_buffer = s.img_buffer_original = (stbi_uc *) buff;
+        s.img_buffer_end = s.img_buffer_original_end = (stbi_uc *) buff+len;
+        /* don't use model->texture[i].w directly, it's a uint16_t */
+        w = h = 0;
+        model->texture[i].d = (uint32_t*)stbi__png_load(&s, (int*)&w, (int*)&h, (int*)&len, STBI_rgb_alpha, &ri);
+        model->texture[i].w = w;
+        model->texture[i].h = h;
+    } else {
+#ifdef M3D_TX_INTERP
+        if((model->errcode = M3D_TX_INTERP(fn, buff, len, &model->texture[i])) != M3D_SUCCESS) {
+            M3D_LOG("Unable to generate texture");
+            M3D_LOG(fn);
+        }
+#else
+        M3D_LOG("Unimplemented interpreter");
+        M3D_LOG(fn);
+#endif
+    }
+    if(freecb) (*freecb)(buff);
+    if(!model->texture[i].d) {
+        M3D_FREE(model->texture[i].d);
+        model->errcode = M3D_ERR_UNKIMG;
+        model->numtexture--;
+        return (M3D_INDEX)-1U;
+    }
+    model->texture[i].name = fn;
+    return i;
+}
+
+/* helper function to load and generate a procedural surface */
+void _m3d_getpr(m3d_t *model, _unused m3dread_t readfilecb, _unused  m3dfree_t freecb, _unused char *fn)
+{
+#ifdef M3D_PR_INTERP
+    unsigned int i, len = 0;
+    unsigned char *buff = readfilecb ? (*readfilecb)(fn, &len) : NULL;
+
+    if(!buff && model->inlined) {
+        for(i = 0; i < model->numinlined; i++)
+            if(!strcmp(fn, model->inlined[i].name)) {
+                buff = model->inlined[i].data;
+                len = model->inlined[i].length;
+                freecb = NULL;
+                break;
+            }
+    }
+    if(!buff || !len || (model->errcode = M3D_PR_INTERP(fn, buff, len, model)) != M3D_SUCCESS) {
+        M3D_LOG("Unable to generate procedural surface");
+        M3D_LOG(fn);
+        model->errcode = M3D_ERR_UNKIMG;
+    }
+    if(freecb && buff) (*freecb)(buff);
+#else
+    M3D_LOG("Unimplemented interpreter");
+    M3D_LOG(fn);
+    model->errcode = M3D_ERR_UNIMPL;
+#endif
+}
+/* helpers to read indices from data stream */
+#define M3D_GETSTR(x) do{offs=0;data=_m3d_getidx(data,model->si_s,&offs);x=offs?((char*)model->raw+16+offs):NULL;}while(0)
+_inline static unsigned char *_m3d_getidx(unsigned char *data, char type, M3D_INDEX *idx)
+{
+    switch(type) {
+        case 1: *idx = data[0] > 253 ? (int8_t)data[0] : data[0]; data++; break;
+        case 2: *idx = (uint16_t)((data[1]<<8)|data[0]) > 65533 ? (int16_t)((data[1]<<8)|data[0]) : (uint16_t)((data[1]<<8)|data[0]); data += 2; break;
+        case 4: *idx = (int32_t)((data[3]<<24)|(data[2]<<16)|(data[1]<<8)|data[0]); data += 4; break;
+    }
+    return data;
+}
+
+#ifndef M3D_NOANIMATION
+/* multiply 4 x 4 matrices. Do not use float *r[16] as argument, because some compilers misinterpret that as
+ * 16 pointers each pointing to a float, but we need a single pointer to 16 floats. */
+void _m3d_mul(M3D_FLOAT *r, M3D_FLOAT *a, M3D_FLOAT *b)
+{
+    r[ 0] = b[ 0] * a[ 0] + b[ 4] * a[ 1] + b[ 8] * a[ 2] + b[12] * a[ 3];
+    r[ 1] = b[ 1] * a[ 0] + b[ 5] * a[ 1] + b[ 9] * a[ 2] + b[13] * a[ 3];
+    r[ 2] = b[ 2] * a[ 0] + b[ 6] * a[ 1] + b[10] * a[ 2] + b[14] * a[ 3];
+    r[ 3] = b[ 3] * a[ 0] + b[ 7] * a[ 1] + b[11] * a[ 2] + b[15] * a[ 3];
+    r[ 4] = b[ 0] * a[ 4] + b[ 4] * a[ 5] + b[ 8] * a[ 6] + b[12] * a[ 7];
+    r[ 5] = b[ 1] * a[ 4] + b[ 5] * a[ 5] + b[ 9] * a[ 6] + b[13] * a[ 7];
+    r[ 6] = b[ 2] * a[ 4] + b[ 6] * a[ 5] + b[10] * a[ 6] + b[14] * a[ 7];
+    r[ 7] = b[ 3] * a[ 4] + b[ 7] * a[ 5] + b[11] * a[ 6] + b[15] * a[ 7];
+    r[ 8] = b[ 0] * a[ 8] + b[ 4] * a[ 9] + b[ 8] * a[10] + b[12] * a[11];
+    r[ 9] = b[ 1] * a[ 8] + b[ 5] * a[ 9] + b[ 9] * a[10] + b[13] * a[11];
+    r[10] = b[ 2] * a[ 8] + b[ 6] * a[ 9] + b[10] * a[10] + b[14] * a[11];
+    r[11] = b[ 3] * a[ 8] + b[ 7] * a[ 9] + b[11] * a[10] + b[15] * a[11];
+    r[12] = b[ 0] * a[12] + b[ 4] * a[13] + b[ 8] * a[14] + b[12] * a[15];
+    r[13] = b[ 1] * a[12] + b[ 5] * a[13] + b[ 9] * a[14] + b[13] * a[15];
+    r[14] = b[ 2] * a[12] + b[ 6] * a[13] + b[10] * a[14] + b[14] * a[15];
+    r[15] = b[ 3] * a[12] + b[ 7] * a[13] + b[11] * a[14] + b[15] * a[15];
+}
+/* calculate 4 x 4 matrix inverse */
+void _m3d_inv(M3D_FLOAT *m)
+{
+    M3D_FLOAT r[16];
+    M3D_FLOAT det =
+          m[ 0]*m[ 5]*m[10]*m[15] - m[ 0]*m[ 5]*m[11]*m[14] + m[ 0]*m[ 6]*m[11]*m[13] - m[ 0]*m[ 6]*m[ 9]*m[15]
+        + m[ 0]*m[ 7]*m[ 9]*m[14] - m[ 0]*m[ 7]*m[10]*m[13] - m[ 1]*m[ 6]*m[11]*m[12] + m[ 1]*m[ 6]*m[ 8]*m[15]
+        - m[ 1]*m[ 7]*m[ 8]*m[14] + m[ 1]*m[ 7]*m[10]*m[12] - m[ 1]*m[ 4]*m[10]*m[15] + m[ 1]*m[ 4]*m[11]*m[14]
+        + m[ 2]*m[ 7]*m[ 8]*m[13] - m[ 2]*m[ 7]*m[ 9]*m[12] + m[ 2]*m[ 4]*m[ 9]*m[15] - m[ 2]*m[ 4]*m[11]*m[13]
+        + m[ 2]*m[ 5]*m[11]*m[12] - m[ 2]*m[ 5]*m[ 8]*m[15] - m[ 3]*m[ 4]*m[ 9]*m[14] + m[ 3]*m[ 4]*m[10]*m[13]
+        - m[ 3]*m[ 5]*m[10]*m[12] + m[ 3]*m[ 5]*m[ 8]*m[14] - m[ 3]*m[ 6]*m[ 8]*m[13] + m[ 3]*m[ 6]*m[ 9]*m[12];
+    if(det == (M3D_FLOAT)0.0 || det == (M3D_FLOAT)-0.0) det = (M3D_FLOAT)1.0; else det = (M3D_FLOAT)1.0 / det;
+    r[ 0] = det *(m[ 5]*(m[10]*m[15] - m[11]*m[14]) + m[ 6]*(m[11]*m[13] - m[ 9]*m[15]) + m[ 7]*(m[ 9]*m[14] - m[10]*m[13]));
+    r[ 1] = -det*(m[ 1]*(m[10]*m[15] - m[11]*m[14]) + m[ 2]*(m[11]*m[13] - m[ 9]*m[15]) + m[ 3]*(m[ 9]*m[14] - m[10]*m[13]));
+    r[ 2] = det *(m[ 1]*(m[ 6]*m[15] - m[ 7]*m[14]) + m[ 2]*(m[ 7]*m[13] - m[ 5]*m[15]) + m[ 3]*(m[ 5]*m[14] - m[ 6]*m[13]));
+    r[ 3] = -det*(m[ 1]*(m[ 6]*m[11] - m[ 7]*m[10]) + m[ 2]*(m[ 7]*m[ 9] - m[ 5]*m[11]) + m[ 3]*(m[ 5]*m[10] - m[ 6]*m[ 9]));
+    r[ 4] = -det*(m[ 4]*(m[10]*m[15] - m[11]*m[14]) + m[ 6]*(m[11]*m[12] - m[ 8]*m[15]) + m[ 7]*(m[ 8]*m[14] - m[10]*m[12]));
+    r[ 5] = det *(m[ 0]*(m[10]*m[15] - m[11]*m[14]) + m[ 2]*(m[11]*m[12] - m[ 8]*m[15]) + m[ 3]*(m[ 8]*m[14] - m[10]*m[12]));
+    r[ 6] = -det*(m[ 0]*(m[ 6]*m[15] - m[ 7]*m[14]) + m[ 2]*(m[ 7]*m[12] - m[ 4]*m[15]) + m[ 3]*(m[ 4]*m[14] - m[ 6]*m[12]));
+    r[ 7] = det *(m[ 0]*(m[ 6]*m[11] - m[ 7]*m[10]) + m[ 2]*(m[ 7]*m[ 8] - m[ 4]*m[11]) + m[ 3]*(m[ 4]*m[10] - m[ 6]*m[ 8]));
+    r[ 8] = det *(m[ 4]*(m[ 9]*m[15] - m[11]*m[13]) + m[ 5]*(m[11]*m[12] - m[ 8]*m[15]) + m[ 7]*(m[ 8]*m[13] - m[ 9]*m[12]));
+    r[ 9] = -det*(m[ 0]*(m[ 9]*m[15] - m[11]*m[13]) + m[ 1]*(m[11]*m[12] - m[ 8]*m[15]) + m[ 3]*(m[ 8]*m[13] - m[ 9]*m[12]));
+    r[10] = det *(m[ 0]*(m[ 5]*m[15] - m[ 7]*m[13]) + m[ 1]*(m[ 7]*m[12] - m[ 4]*m[15]) + m[ 3]*(m[ 4]*m[13] - m[ 5]*m[12]));
+    r[11] = -det*(m[ 0]*(m[ 5]*m[11] - m[ 7]*m[ 9]) + m[ 1]*(m[ 7]*m[ 8] - m[ 4]*m[11]) + m[ 3]*(m[ 4]*m[ 9] - m[ 5]*m[ 8]));
+    r[12] = -det*(m[ 4]*(m[ 9]*m[14] - m[10]*m[13]) + m[ 5]*(m[10]*m[12] - m[ 8]*m[14]) + m[ 6]*(m[ 8]*m[13] - m[ 9]*m[12]));
+    r[13] = det *(m[ 0]*(m[ 9]*m[14] - m[10]*m[13]) + m[ 1]*(m[10]*m[12] - m[ 8]*m[14]) + m[ 2]*(m[ 8]*m[13] - m[ 9]*m[12]));
+    r[14] = -det*(m[ 0]*(m[ 5]*m[14] - m[ 6]*m[13]) + m[ 1]*(m[ 6]*m[12] - m[ 4]*m[14]) + m[ 2]*(m[ 4]*m[13] - m[ 5]*m[12]));
+    r[15] = det *(m[ 0]*(m[ 5]*m[10] - m[ 6]*m[ 9]) + m[ 1]*(m[ 6]*m[ 8] - m[ 4]*m[10]) + m[ 2]*(m[ 4]*m[ 9] - m[ 5]*m[ 8]));
+    memcpy(m, &r, sizeof(r));
+}
+/* compose a coloumn major 4 x 4 matrix from vec3 position and vec4 orientation/rotation quaternion */
+#ifndef M3D_EPSILON
+/* carefully choosen for IEEE 754 don't change */
+#define M3D_EPSILON ((M3D_FLOAT)1e-7)
+#endif
+void _m3d_mat(M3D_FLOAT *r, m3dv_t *p, m3dv_t *q)
+{
+    if(q->x == (M3D_FLOAT)0.0 && q->y == (M3D_FLOAT)0.0 && q->z >=(M3D_FLOAT) 0.7071065 && q->z <= (M3D_FLOAT)0.7071075 &&
+        q->w == (M3D_FLOAT)0.0) {
+        r[ 1] = r[ 2] = r[ 4] = r[ 6] = r[ 8] = r[ 9] = (M3D_FLOAT)0.0;
+        r[ 0] = r[ 5] = r[10] = (M3D_FLOAT)-1.0;
+    } else {
+        r[ 0] = 1 - 2 * (q->y * q->y + q->z * q->z); if(r[ 0]>-M3D_EPSILON && r[ 0]<M3D_EPSILON) r[ 0]=(M3D_FLOAT)0.0;
+        r[ 1] = 2 * (q->x * q->y - q->z * q->w);     if(r[ 1]>-M3D_EPSILON && r[ 1]<M3D_EPSILON) r[ 1]=(M3D_FLOAT)0.0;
+        r[ 2] = 2 * (q->x * q->z + q->y * q->w);     if(r[ 2]>-M3D_EPSILON && r[ 2]<M3D_EPSILON) r[ 2]=(M3D_FLOAT)0.0;
+        r[ 4] = 2 * (q->x * q->y + q->z * q->w);     if(r[ 4]>-M3D_EPSILON && r[ 4]<M3D_EPSILON) r[ 4]=(M3D_FLOAT)0.0;
+        r[ 5] = 1 - 2 * (q->x * q->x + q->z * q->z); if(r[ 5]>-M3D_EPSILON && r[ 5]<M3D_EPSILON) r[ 5]=(M3D_FLOAT)0.0;
+        r[ 6] = 2 * (q->y * q->z - q->x * q->w);     if(r[ 6]>-M3D_EPSILON && r[ 6]<M3D_EPSILON) r[ 6]=(M3D_FLOAT)0.0;
+        r[ 8] = 2 * (q->x * q->z - q->y * q->w);     if(r[ 8]>-M3D_EPSILON && r[ 8]<M3D_EPSILON) r[ 8]=(M3D_FLOAT)0.0;
+        r[ 9] = 2 * (q->y * q->z + q->x * q->w);     if(r[ 9]>-M3D_EPSILON && r[ 9]<M3D_EPSILON) r[ 9]=(M3D_FLOAT)0.0;
+        r[10] = 1 - 2 * (q->x * q->x + q->y * q->y); if(r[10]>-M3D_EPSILON && r[10]<M3D_EPSILON) r[10]=(M3D_FLOAT)0.0;
+    }
+    r[ 3] = p->x; r[ 7] = p->y; r[11] = p->z;
+    r[12] = 0; r[13] = 0; r[14] = 0; r[15] = 1;
+}
+#endif
+#if !defined(M3D_NOANIMATION) || !defined(M3D_NONORMALS)
+/* fast inverse square root calculation. returns 1/sqrt(x) */
+static M3D_FLOAT _m3d_rsq(M3D_FLOAT x)
+{
+#ifdef M3D_DOUBLE
+    return ((M3D_FLOAT)15.0/(M3D_FLOAT)8.0) + ((M3D_FLOAT)-5.0/(M3D_FLOAT)4.0)*x + ((M3D_FLOAT)3.0/(M3D_FLOAT)8.0)*x*x;
+#else
+    /* John Carmack's */
+    float x2 = x * 0.5f;
+    *((uint32_t*)&x) = (0x5f3759df - (*((uint32_t*)&x) >> 1));
+    return x * (1.5f - (x2 * x * x));
+#endif
+}
+#endif
+
+/**
+ * Function to decode a Model 3D into in-memory format
+ */
+m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d_t *mtllib)
+{
+    unsigned char *end, *chunk, *buff, weights[8];
+    unsigned int i, j, k, n, am, len = 0, reclen, offs;
+    char *material;
+#ifndef M3D_NONORMALS
+    unsigned int numnorm = 0;
+    m3dv_t *norm = NULL, *v0, *v1, *v2, va, vb, vn;
+    M3D_INDEX *ni = NULL, *vi = NULL;
+#endif
+    m3d_t *model;
+    M3D_INDEX mi;
+    M3D_FLOAT w;
+#ifndef M3D_NOANIMATION
+    M3D_FLOAT r[16];
+#endif
+    m3dtx_t *tx;
+    m3dm_t *m;
+    m3da_t *a;
+    m3db_t *b;
+    m3di_t *t;
+    m3ds_t *sk;
+#ifdef M3D_ASCII
+    m3ds_t s;
+    M3D_INDEX bi[M3D_BONEMAXLEVEL+1], level;
+    const char *ol;
+    char *ptr, *pe;
+#endif
+
+    if(!data || (!M3D_CHUNKMAGIC(data, '3','D','M','O')
+#ifdef M3D_ASCII
+        && !M3D_CHUNKMAGIC(data, '3','d','m','o')
+#endif
+        )) return NULL;
+    model = (m3d_t*)M3D_MALLOC(sizeof(m3d_t));
+    if(!model) {
+        M3D_LOG("Out of memory");
+        return NULL;
+    }
+    memset(model, 0, sizeof(m3d_t));
+
+    if(mtllib) {
+        model->nummaterial = mtllib->nummaterial;
+        model->material = mtllib->material;
+        model->numtexture = mtllib->numtexture;
+        model->texture = mtllib->texture;
+        model->flags |= M3D_FLG_MTLLIB;
+    }
+#ifdef M3D_ASCII
+    /* ASCII variant? */
+    if(M3D_CHUNKMAGIC(data, '3','d','m','o')) {
+        model->errcode = M3D_ERR_BADFILE;
+        model->flags |= M3D_FLG_FREESTR;
+        model->raw = (m3dhdr_t*)data;
+        ptr = (char*)data;
+        ol = setlocale(LC_NUMERIC, NULL);
+        setlocale(LC_NUMERIC, "C");
+        /* parse header. Don't use sscanf, that's incredibly slow */
+        ptr = _m3d_findarg(ptr);
+        if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+        pe = _m3d_findnl(ptr);
+        model->scale = (float)strtod(ptr, NULL); ptr = pe;
+        if(model->scale <= (M3D_FLOAT)0.0) model->scale = (M3D_FLOAT)1.0;
+        model->name = _m3d_safestr(ptr, 0); ptr = _m3d_findnl(ptr);
+        if(!*ptr) goto asciiend;
+        model->license = _m3d_safestr(ptr, 2); ptr = _m3d_findnl(ptr);
+        if(!*ptr) goto asciiend;
+        model->author = _m3d_safestr(ptr, 2); ptr = _m3d_findnl(ptr);
+        if(!*ptr) goto asciiend;
+        model->desc = _m3d_safestr(ptr, 3);
+        while(*ptr) {
+            while(*ptr && *ptr!='\n') ptr++;
+            ptr++; if(*ptr=='\r') ptr++;
+            if(*ptr == '\n') break;
+        }
+
+        /* the main chunk reader loop */
+        while(*ptr) {
+            while(*ptr && (*ptr == '\r' || *ptr == '\n')) ptr++;
+            if(!*ptr || (ptr[0]=='E' && ptr[1]=='n' && ptr[2]=='d')) break;
+            /* make sure there's at least one data row */
+            pe = ptr; ptr = _m3d_findnl(ptr);
+            if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+            /* texture map chunk */
+            if(!memcmp(pe, "Textmap", 7)) {
+                if(model->tmap) { M3D_LOG("More texture map chunks, should be unique"); goto asciiend; }
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    i = model->numtmap++;
+                    model->tmap = (m3dti_t*)M3D_REALLOC(model->tmap, model->numtmap * sizeof(m3dti_t));
+                    if(!model->tmap) goto memerr;
+                    ptr = _m3d_getfloat(ptr, &model->tmap[i].u);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    ptr = _m3d_getfloat(ptr, &model->tmap[i].v);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    ptr = _m3d_findnl(ptr);
+                }
+            } else
+            /* vertex chunk */
+            if(!memcmp(pe, "Vertex", 6)) {
+                if(model->vertex) { M3D_LOG("More vertex chunks, should be unique"); goto asciiend; }
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    i = model->numvertex++;
+                    model->vertex = (m3dv_t*)M3D_REALLOC(model->vertex, model->numvertex * sizeof(m3dv_t));
+                    if(!model->vertex) goto memerr;
+                    model->vertex[i].skinid = (M3D_INDEX)-1U;
+                    model->vertex[i].color = 0;
+                    ptr = _m3d_getfloat(ptr, &model->vertex[i].x);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    ptr = _m3d_getfloat(ptr, &model->vertex[i].y);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    ptr = _m3d_getfloat(ptr, &model->vertex[i].z);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    ptr = _m3d_getfloat(ptr, &model->vertex[i].w);
+                    if(model->vertex[i].w != 1.0) model->vertex[i].skinid = (M3D_INDEX)-2U;
+                    if(!*ptr) goto asciiend;
+                    if(*ptr == '#') {
+                        ptr = _m3d_gethex(ptr, &model->vertex[i].color);
+                        if(!*ptr) goto asciiend;
+                    }
+                    /* parse skin */
+                    memset(&s, 0, sizeof(m3ds_t));
+                    for(j = 0; j < M3D_NUMBONE && *ptr && *ptr != '\r' && *ptr != '\n'; j++) {
+                        ptr = _m3d_findarg(ptr);
+                        if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                        ptr = _m3d_getint(ptr, &k);
+                        s.boneid[j] = (M3D_INDEX)k;
+                        if(*ptr == ':') {
+                            ptr++;
+                            ptr = _m3d_getfloat(ptr, &s.weight[j]);
+                        } else if(!j)
+                            s.weight[j] = (M3D_FLOAT)1.0;
+                        if(!*ptr) goto asciiend;
+                    }
+                    if(s.boneid[0] != (M3D_INDEX)-1U && s.weight[0] > (M3D_FLOAT)0.0) {
+                        model->skin = _m3d_addskin(model->skin, &model->numskin, &s, &k);
+                        model->vertex[i].skinid = (M3D_INDEX)k;
+                    }
+                    ptr = _m3d_findnl(ptr);
+                }
+            } else
+            /* Skeleton, bone hierarchy */
+            if(!memcmp(pe, "Bones", 5)) {
+                if(model->bone) { M3D_LOG("More bones chunks, should be unique"); goto asciiend; }
+                bi[0] = (M3D_INDEX)-1U;
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    i = model->numbone++;
+                    model->bone = (m3db_t*)M3D_REALLOC(model->bone, model->numbone * sizeof(m3db_t));
+                    if(!model->bone) goto memerr;
+                    for(level = 0; *ptr == '/'; ptr++, level++);
+                    if(level > M3D_BONEMAXLEVEL || !*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    bi[level+1] = i;
+                    model->bone[i].numweight = 0;
+                    model->bone[i].weight = NULL;
+                    model->bone[i].parent = bi[level];
+                    ptr = _m3d_getint(ptr, &k);
+                    ptr = _m3d_findarg(ptr);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    model->bone[i].pos = (M3D_INDEX)k;
+                    ptr = _m3d_getint(ptr, &k);
+                    ptr = _m3d_findarg(ptr);
+                    if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                    model->bone[i].ori = (M3D_INDEX)k;
+                    pe = _m3d_safestr(ptr, 0);
+                    if(!pe || !*pe) goto asciiend;
+                    model->bone[i].name = pe;
+                    ptr = _m3d_findnl(ptr);
+                }
+            } else
+            /* material chunk */
+            if(!memcmp(pe, "Material", 8)) {
+                pe = _m3d_findarg(pe);
+                if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend;
+                pe = _m3d_safestr(pe, 0);
+                if(!pe || !*pe) goto asciiend;
+                for(i = 0; i < model->nummaterial; i++)
+                    if(!strcmp(pe, model->material[i].name)) {
+                        M3D_LOG("Multiple definitions for material");
+                        M3D_LOG(pe);
+                        M3D_FREE(pe);
+                        pe = NULL;
+                        while(*ptr && *ptr != '\r' && *ptr != '\n') ptr = _m3d_findnl(ptr);
+                        break;
+                    }
+                if(!pe) continue;
+                i = model->nummaterial++;
+                if(model->flags & M3D_FLG_MTLLIB) {
+                    m = model->material;
+                    model->material = (m3dm_t*)M3D_MALLOC(model->nummaterial * sizeof(m3dm_t));
+                    if(!model->material) goto memerr;
+                    memcpy(model->material, m, (model->nummaterial - 1) * sizeof(m3dm_t));
+                    if(model->texture) {
+                        tx = model->texture;
+                        model->texture = (m3dtx_t*)M3D_MALLOC(model->numtexture * sizeof(m3dtx_t));
+                        if(!model->texture) goto memerr;
+                        memcpy(model->texture, tx, model->numtexture * sizeof(m3dm_t));
+                    }
+                    model->flags &= ~M3D_FLG_MTLLIB;
+                } else {
+                    model->material = (m3dm_t*)M3D_REALLOC(model->material, model->nummaterial * sizeof(m3dm_t));
+                    if(!model->material) goto memerr;
+                }
+                m = &model->material[i];
+                m->name = pe;
+                m->numprop = 0;
+                m->prop = NULL;
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    k = n = 256;
+                    if(*ptr == 'm' && *(ptr+1) == 'a' && *(ptr+2) == 'p' && *(ptr+3) == '_') {
+                        k = m3dpf_map;
+                        ptr += 4;
+                    }
+                    for(j = 0; j < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); j++)
+                        if(!memcmp(ptr, m3d_propertytypes[j].key, strlen(m3d_propertytypes[j].key))) {
+                            n = m3d_propertytypes[j].id;
+                            if(k != m3dpf_map) k = m3d_propertytypes[j].format;
+                            break;
+                        }
+                    if(n != 256 && k != 256) {
+                        ptr = _m3d_findarg(ptr);
+                        if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                        j = m->numprop++;
+                        m->prop = (m3dp_t*)M3D_REALLOC(m->prop, m->numprop * sizeof(m3dp_t));
+                        if(!m->prop) goto memerr;
+                        m->prop[j].type = n;
+                        switch(k) {
+                            case m3dpf_color: ptr = _m3d_gethex(ptr, &m->prop[j].value.color); break;
+                            case m3dpf_uint8:
+                            case m3dpf_uint16:
+                            case m3dpf_uint32: ptr = _m3d_getint(ptr, &m->prop[j].value.num); break;
+                            case m3dpf_float:  ptr = _m3d_getfloat(ptr, &m->prop[j].value.fnum); break;
+                            case m3dpf_map:
+                                pe = _m3d_safestr(ptr, 0);
+                                if(!pe || !*pe) goto asciiend;
+                                m->prop[j].value.textureid = _m3d_gettx(model, readfilecb, freecb, pe);
+                                if(model->errcode == M3D_ERR_ALLOC) { M3D_FREE(pe); goto memerr; }
+                                if(m->prop[j].value.textureid == (M3D_INDEX)-1U) {
+                                    M3D_LOG("Texture not found");
+                                    M3D_LOG(pe);
+                                    m->numprop--;
+                                }
+                                M3D_FREE(pe);
+                            break;
+                        }
+                    } else {
+                        M3D_LOG("Unknown material property in");
+                        M3D_LOG(m->name);
+                        model->errcode = M3D_ERR_UNKPROP;
+                    }
+                    ptr = _m3d_findnl(ptr);
+                }
+                if(!m->numprop) model->nummaterial--;
+            } else
+            /* procedural, not implemented yet, skip chunk */
+            if(!memcmp(pe, "Procedural", 10)) {
+                pe = _m3d_safestr(ptr, 0);
+                _m3d_getpr(model, readfilecb, freecb, pe);
+                M3D_FREE(pe);
+                while(*ptr && *ptr != '\r' && *ptr != '\n') ptr = _m3d_findnl(ptr);
+            } else
+            /* mesh */
+            if(!memcmp(pe, "Mesh", 4)) {
+                mi = (M3D_INDEX)-1U;
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    if(*ptr == 'u') {
+                        ptr = _m3d_findarg(ptr);
+                        if(!*ptr) goto asciiend;
+                        mi = (M3D_INDEX)-1U;
+                        if(*ptr != '\r' && *ptr != '\n') {
+                            pe = _m3d_safestr(ptr, 0);
+                            if(!pe || !*pe) goto asciiend;
+                            for(j = 0; j < model->nummaterial; j++)
+                                if(!strcmp(pe, model->material[j].name)) {
+                                    mi = (M3D_INDEX)j;
+                                    break;
+                                }
+                            M3D_FREE(pe);
+                        }
+                    } else {
+                        i = model->numface++;
+                        model->face = (m3df_t*)M3D_REALLOC(model->face, model->numface * sizeof(m3df_t));
+                        if(!model->face) goto memerr;
+                        memset(&model->face[i], 255, sizeof(m3df_t)); /* set all index to -1 by default */
+                        model->face[i].materialid = mi;
+                        /* hardcoded triangles. */
+                        for(j = 0; j < 3; j++) {
+                            /* vertex */
+                            ptr = _m3d_getint(ptr, &k);
+                            model->face[i].vertex[j] = (M3D_INDEX)k;
+                            if(!*ptr) goto asciiend;
+                            if(*ptr == '/') {
+                                ptr++;
+                                if(*ptr != '/') {
+                                    /* texcoord */
+                                    ptr = _m3d_getint(ptr, &k);
+                                    model->face[i].texcoord[j] = (M3D_INDEX)k;
+                                    if(!*ptr) goto asciiend;
+                                }
+                                if(*ptr == '/') {
+                                    ptr++;
+                                    /* normal */
+                                    ptr = _m3d_getint(ptr, &k);
+                                    model->face[i].normal[j] = (M3D_INDEX)k;
+                                    if(!*ptr) goto asciiend;
+                                }
+                            }
+                            ptr = _m3d_findarg(ptr);
+                        }
+                    }
+                    ptr = _m3d_findnl(ptr);
+                }
+            } else
+            /* action */
+            if(!memcmp(pe, "Action", 6)) {
+                pe = _m3d_findarg(pe);
+                if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend;
+                pe = _m3d_getint(pe, &k);
+                pe = _m3d_findarg(pe);
+                if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend;
+                pe = _m3d_safestr(pe, 0);
+                if(!pe || !*pe) goto asciiend;
+                i = model->numaction++;
+                model->action = (m3da_t*)M3D_REALLOC(model->action, model->numaction * sizeof(m3da_t));
+                if(!model->action) goto memerr;
+                a = &model->action[i];
+                a->name = pe;
+                a->durationmsec = k;
+                /* skip the first frame marker as there's always at least one frame */
+                a->numframe = 1;
+                a->frame = (m3dfr_t*)M3D_MALLOC(sizeof(m3dfr_t));
+                if(!a->frame) goto memerr;
+                a->frame[0].msec = 0;
+                a->frame[0].numtransform = 0;
+                a->frame[0].transform = NULL;
+                i = 0;
+                if(*ptr == 'f')
+                    ptr = _m3d_findnl(ptr);
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    if(*ptr == 'f') {
+                        i = a->numframe++;
+                        a->frame = (m3dfr_t*)M3D_REALLOC(a->frame, a->numframe * sizeof(m3dfr_t));
+                        if(!a->frame) goto memerr;
+                        ptr = _m3d_findarg(ptr);
+                        ptr = _m3d_getint(ptr, &a->frame[i].msec);
+                        a->frame[i].numtransform = 0;
+                        a->frame[i].transform = NULL;
+                    } else {
+                        j = a->frame[i].numtransform++;
+                        a->frame[i].transform = (m3dtr_t*)M3D_REALLOC(a->frame[i].transform,
+                            a->frame[i].numtransform * sizeof(m3dtr_t));
+                        if(!a->frame[i].transform) goto memerr;
+                        ptr = _m3d_getint(ptr, &k);
+                        ptr = _m3d_findarg(ptr);
+                        if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                        a->frame[i].transform[j].boneid = (M3D_INDEX)k;
+                        ptr = _m3d_getint(ptr, &k);
+                        ptr = _m3d_findarg(ptr);
+                        if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                        a->frame[i].transform[j].pos = (M3D_INDEX)k;
+                        ptr = _m3d_getint(ptr, &k);
+                        if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend;
+                        a->frame[i].transform[j].ori = (M3D_INDEX)k;
+                    }
+                    ptr = _m3d_findnl(ptr);
+                }
+            } else
+            /* extra chunks */
+            if(!memcmp(pe, "Extra", 5)) {
+                pe = _m3d_findarg(pe);
+                if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend;
+                buff = (unsigned char*)_m3d_findnl(ptr);
+                k = ((uint32_t)((uint64_t)buff - (uint64_t)ptr) / 3) + 1;
+                i = model->numunknown++;
+                model->unknown = (m3dchunk_t**)M3D_REALLOC(model->unknown, model->numunknown * sizeof(m3dchunk_t*));
+                if(!model->unknown) goto memerr;
+                model->unknown[i] = (m3dchunk_t*)M3D_MALLOC(k + sizeof(m3dchunk_t));
+                if(!model->unknown[i]) goto memerr;
+                memcpy(&model->unknown[i]->magic, pe, 4);
+                model->unknown[i]->length = sizeof(m3dchunk_t);
+                pe = (char*)model->unknown[i] + sizeof(m3dchunk_t);
+                while(*ptr && *ptr != '\r' && *ptr != '\n') {
+                    ptr = _m3d_gethex(ptr, &k);
+                    *pe++ = (uint8_t)k;
+                    model->unknown[i]->length++;
+                }
+            } else
+                goto asciiend;
+        }
+        model->errcode = M3D_SUCCESS;
+asciiend:
+        setlocale(LC_NUMERIC, ol);
+        goto postprocess;
+    }
+    /* Binary variant */
+#endif
+    if(!M3D_CHUNKMAGIC(data + 8, 'H','E','A','D')) {
+        stbi__g_failure_reason = "Corrupt file";
+        buff = (unsigned char *)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)data+8, ((m3dchunk_t*)data)->length-8,
+            4096, (int*)&len, 1);
+        if(!buff || !len || !M3D_CHUNKMAGIC(buff, 'H','E','A','D')) {
+            M3D_LOG(stbi__g_failure_reason);
+            if(buff) M3D_FREE(buff);
+            M3D_FREE(model);
+            return NULL;
+        }
+        buff = (unsigned char*)M3D_REALLOC(buff, len);
+        model->flags |= M3D_FLG_FREERAW; /* mark that we have to free the raw buffer */
+        data = buff;
+    } else {
+        len = ((m3dhdr_t*)data)->length;
+        data += 8;
+    }
+    model->raw = (m3dhdr_t*)data;
+    end = data + len;
+
+    /* parse header */
+    data += sizeof(m3dhdr_t);
+    M3D_LOG(data);
+    model->name = (char*)data;
+    for(; data < end && *data; data++) {}; data++;
+    model->license = (char*)data;
+    for(; data < end && *data; data++) {}; data++;
+    model->author = (char*)data;
+    for(; data < end && *data; data++) {}; data++;
+    model->desc = (char*)data;
+    chunk = (unsigned char*)model->raw + model->raw->length;
+    model->scale = (M3D_FLOAT)model->raw->scale;
+    if(model->scale <= (M3D_FLOAT)0.0) model->scale = (M3D_FLOAT)1.0;
+    model->vc_s = 1 << ((model->raw->types >> 0) & 3);  /* vertex coordinate size */
+    model->vi_s = 1 << ((model->raw->types >> 2) & 3);  /* vertex index size */
+    model->si_s = 1 << ((model->raw->types >> 4) & 3);  /* string offset size */
+    model->ci_s = 1 << ((model->raw->types >> 6) & 3);  /* color index size */
+    model->ti_s = 1 << ((model->raw->types >> 8) & 3);  /* tmap index size */
+    model->bi_s = 1 << ((model->raw->types >>10) & 3);  /* bone index size */
+    model->nb_s = 1 << ((model->raw->types >>12) & 3);  /* number of bones per vertex */
+    model->sk_s = 1 << ((model->raw->types >>14) & 3);  /* skin index size */
+    model->fi_s = 1 << ((model->raw->types >>16) & 3);  /* frame counter size */
+    if(model->ci_s == 8) model->ci_s = 0;               /* optional indices */
+    if(model->ti_s == 8) model->ti_s = 0;
+    if(model->bi_s == 8) model->bi_s = 0;
+    if(model->sk_s == 8) model->sk_s = 0;
+    if(model->fi_s == 8) model->fi_s = 0;
+
+    /* variable limit checks */
+    if(sizeof(M3D_FLOAT) == 4 && model->vc_s > 4) {
+        M3D_LOG("Double precision coordinates not supported, truncating to float...");
+        model->errcode = M3D_ERR_TRUNC;
+    }
+    if(sizeof(M3D_INDEX) == 2 && (model->vi_s > 2 || model->si_s > 2 || model->ci_s > 2 || model->ti_s > 2 ||
+        model->bi_s > 2 || model->sk_s > 2 || model->fi_s > 2)) {
+        M3D_LOG("32 bit indices not supported, unable to load model");
+        M3D_FREE(model);
+        return NULL;
+    }
+    if(model->vi_s > 4 || model->si_s > 4) {
+        M3D_LOG("Invalid index size, unable to load model");
+        M3D_FREE(model);
+        return NULL;
+    }
+    if(model->nb_s > M3D_NUMBONE) {
+        M3D_LOG("Model has more bones per vertex than importer supports");
+        model->errcode = M3D_ERR_TRUNC;
+    }
+
+    /* look for inlined assets in advance, material and procedural chunks may need them */
+    buff = chunk;
+    while(buff < end && !M3D_CHUNKMAGIC(buff, 'O','M','D','3')) {
+        data = buff;
+        len = ((m3dchunk_t*)data)->length;
+        if(len < sizeof(m3dchunk_t)) {
+            M3D_LOG("Invalid chunk size");
+            break;
+        }
+        buff += len;
+        len -= sizeof(m3dchunk_t) + model->si_s;
+
+        /* inlined assets */
+        if(M3D_CHUNKMAGIC(data, 'A','S','E','T') && len > 0) {
+            M3D_LOG("Inlined asset");
+            i = model->numinlined++;
+            model->inlined = (m3di_t*)M3D_REALLOC(model->inlined, model->numinlined * sizeof(m3di_t));
+            if(!model->inlined) {
+memerr:         M3D_LOG("Out of memory");
+                model->errcode = M3D_ERR_ALLOC;
+                return model;
+            }
+            data += sizeof(m3dchunk_t);
+            t = &model->inlined[i];
+            M3D_GETSTR(t->name);
+            M3D_LOG(t->name);
+            t->data = (uint8_t*)data;
+            t->length = len;
+        }
+    }
+
+    /* parse chunks */
+    while(chunk < end && !M3D_CHUNKMAGIC(chunk, 'O','M','D','3')) {
+        data = chunk;
+        len = ((m3dchunk_t*)chunk)->length;
+        if(len < sizeof(m3dchunk_t)) {
+            M3D_LOG("Invalid chunk size");
+            break;
+        }
+        chunk += len;
+        len -= sizeof(m3dchunk_t);
+
+        /* color map */
+        if(M3D_CHUNKMAGIC(data, 'C','M','A','P')) {
+            M3D_LOG("Color map");
+            if(model->cmap) { M3D_LOG("More color map chunks, should be unique"); model->errcode = M3D_ERR_CMAP; continue; }
+            if(!model->ci_s) { M3D_LOG("Color map chunk, shouldn't be any"); model->errcode = M3D_ERR_CMAP; continue; }
+            model->numcmap = len / sizeof(uint32_t);
+            model->cmap = (uint32_t*)(data + sizeof(m3dchunk_t));
+        } else
+        /* texture map */
+        if(M3D_CHUNKMAGIC(data, 'T','M','A','P')) {
+            M3D_LOG("Texture map");
+            if(model->tmap) { M3D_LOG("More texture map chunks, should be unique"); model->errcode = M3D_ERR_TMAP; continue; }
+            if(!model->ti_s) { M3D_LOG("Texture map chunk, shouldn't be any"); model->errcode = M3D_ERR_TMAP; continue; }
+            reclen = model->vc_s + model->vc_s;
+            model->numtmap = len / reclen;
+            model->tmap = (m3dti_t*)M3D_MALLOC(model->numtmap * sizeof(m3dti_t));
+            if(!model->tmap) goto memerr;
+            for(i = 0, data += sizeof(m3dchunk_t); data < chunk; i++) {
+                switch(model->vc_s) {
+                    case 1:
+                        model->tmap[i].u = (M3D_FLOAT)(data[0]) / 255;
+                        model->tmap[i].v = (M3D_FLOAT)(data[1]) / 255;
+                    break;
+                    case 2:
+                        model->tmap[i].u = (M3D_FLOAT)(*((int16_t*)(data+0))) / 65535;
+                        model->tmap[i].v = (M3D_FLOAT)(*((int16_t*)(data+2))) / 65535;
+                    break;
+                    case 4:
+                        model->tmap[i].u = (M3D_FLOAT)(*((float*)(data+0)));
+                        model->tmap[i].v = (M3D_FLOAT)(*((float*)(data+4)));
+                    break;
+                    case 8:
+                        model->tmap[i].u = (M3D_FLOAT)(*((double*)(data+0)));
+                        model->tmap[i].v = (M3D_FLOAT)(*((double*)(data+8)));
+                    break;
+                }
+                data += reclen;
+            }
+        } else
+        /* vertex list */
+        if(M3D_CHUNKMAGIC(data, 'V','R','T','S')) {
+            M3D_LOG("Vertex list");
+            if(model->vertex) { M3D_LOG("More vertex chunks, should be unique"); model->errcode = M3D_ERR_VRTS; continue; }
+            if(model->ci_s && model->ci_s < 4 && !model->cmap) model->errcode = M3D_ERR_CMAP;
+            reclen = model->ci_s + model->sk_s + 4 * model->vc_s;
+            model->numvertex = len / reclen;
+            model->vertex = (m3dv_t*)M3D_MALLOC(model->numvertex * sizeof(m3dv_t));
+            if(!model->vertex) goto memerr;
+            memset(model->vertex, 0, model->numvertex * sizeof(m3dv_t));
+            for(i = 0, data += sizeof(m3dchunk_t); data < chunk && i < model->numvertex; i++) {
+                switch(model->vc_s) {
+                    case 1:
+                        model->vertex[i].x = (M3D_FLOAT)((int8_t)data[0]) / 127;
+                        model->vertex[i].y = (M3D_FLOAT)((int8_t)data[1]) / 127;
+                        model->vertex[i].z = (M3D_FLOAT)((int8_t)data[2]) / 127;
+                        model->vertex[i].w = (M3D_FLOAT)((int8_t)data[3]) / 127;
+                        data += 4;
+                    break;
+                    case 2:
+                        model->vertex[i].x = (M3D_FLOAT)((int16_t)((data[1]<<8)|data[0])) / 32767;
+                        model->vertex[i].y = (M3D_FLOAT)((int16_t)((data[3]<<8)|data[2])) / 32767;
+                        model->vertex[i].z = (M3D_FLOAT)((int16_t)((data[5]<<8)|data[4])) / 32767;
+                        model->vertex[i].w = (M3D_FLOAT)((int16_t)((data[7]<<8)|data[6])) / 32767;
+                        data += 8;
+                    break;
+                    case 4:
+                        model->vertex[i].x = (M3D_FLOAT)(*((float*)(data+0)));
+                        model->vertex[i].y = (M3D_FLOAT)(*((float*)(data+4)));
+                        model->vertex[i].z = (M3D_FLOAT)(*((float*)(data+8)));
+                        model->vertex[i].w = (M3D_FLOAT)(*((float*)(data+12)));
+                        data += 16;
+                    break;
+                    case 8:
+                        model->vertex[i].x = (M3D_FLOAT)(*((double*)(data+0)));
+                        model->vertex[i].y = (M3D_FLOAT)(*((double*)(data+8)));
+                        model->vertex[i].z = (M3D_FLOAT)(*((double*)(data+16)));
+                        model->vertex[i].w = (M3D_FLOAT)(*((double*)(data+24)));
+                        data += 32;
+                    break;
+                }
+                switch(model->ci_s) {
+                    case 1: model->vertex[i].color = model->cmap ? model->cmap[data[0]] : 0; data++; break;
+                    case 2: model->vertex[i].color = model->cmap ? model->cmap[*((uint16_t*)data)] : 0; data += 2; break;
+                    case 4: model->vertex[i].color = *((uint32_t*)data); data += 4; break;
+                    /* case 8: break; */
+                }
+                model->vertex[i].skinid = (M3D_INDEX)-1U;
+                data = _m3d_getidx(data, model->sk_s, &model->vertex[i].skinid);
+            }
+        } else
+        /* skeleton: bone hierarchy and skin */
+        if(M3D_CHUNKMAGIC(data, 'B','O','N','E')) {
+            M3D_LOG("Skeleton");
+            if(model->bone) { M3D_LOG("More bone chunks, should be unique"); model->errcode = M3D_ERR_BONE; continue; }
+            if(!model->bi_s) { M3D_LOG("Bone chunk, shouldn't be any"); model->errcode=M3D_ERR_BONE; continue; }
+            if(!model->vertex) { M3D_LOG("No vertex chunk before bones"); model->errcode = M3D_ERR_VRTS; break; }
+            data += sizeof(m3dchunk_t);
+            model->numbone = 0;
+            data = _m3d_getidx(data, model->bi_s, &model->numbone);
+            if(model->numbone) {
+                model->bone = (m3db_t*)M3D_MALLOC(model->numbone * sizeof(m3db_t));
+                if(!model->bone) goto memerr;
+            }
+            model->numskin = 0;
+            data = _m3d_getidx(data, model->sk_s, &model->numskin);
+            if(model->numskin) {
+                model->skin = (m3ds_t*)M3D_MALLOC(model->numskin * sizeof(m3ds_t));
+                if(!model->skin) goto memerr;
+                for(i = 0; i < model->numskin; i++)
+                    for(j = 0; j < M3D_NUMBONE; j++) {
+                        model->skin[i].boneid[j] = (M3D_INDEX)-1U;
+                        model->skin[i].weight[j] = (M3D_FLOAT)0.0;
+                    }
+            }
+            /* read bone hierarchy */
+            for(i = 0; i < model->numbone; i++) {
+                data = _m3d_getidx(data, model->bi_s, &model->bone[i].parent);
+                M3D_GETSTR(model->bone[i].name);
+                data = _m3d_getidx(data, model->vi_s, &model->bone[i].pos);
+                data = _m3d_getidx(data, model->vi_s, &model->bone[i].ori);
+                model->bone[i].numweight = 0;
+                model->bone[i].weight = NULL;
+            }
+            /* read skin definitions */
+            for(i = 0; data < chunk && i < model->numskin; i++) {
+                memset(&weights, 0, sizeof(weights));
+                if(model->nb_s == 1) weights[0] = 255;
+                else {
+                    memcpy(&weights, data, model->nb_s);
+                    data += model->nb_s;
+                }
+                for(j = 0; j < (unsigned int)model->nb_s; j++) {
+                    if(weights[j]) {
+                        if(j >= M3D_NUMBONE)
+                            data += model->bi_s;
+                        else {
+                            model->skin[i].weight[j] = (M3D_FLOAT)(weights[j]) / 255;
+                            data = _m3d_getidx(data, model->bi_s, &model->skin[i].boneid[j]);
+                        }
+                    }
+                }
+            }
+        } else
+        /* material */
+        if(M3D_CHUNKMAGIC(data, 'M','T','R','L')) {
+            data += sizeof(m3dchunk_t);
+            M3D_GETSTR(material);
+            M3D_LOG("Material");
+            M3D_LOG(material);
+            if(model->ci_s < 4 && !model->numcmap) model->errcode = M3D_ERR_CMAP;
+            for(i = 0; i < model->nummaterial; i++)
+                if(!strcmp(material, model->material[i].name)) {
+                    model->errcode = M3D_ERR_MTRL;
+                    M3D_LOG("Multiple definitions for material");
+                    M3D_LOG(material);
+                    material = NULL;
+                    break;
+                }
+            if(material) {
+                i = model->nummaterial++;
+                if(model->flags & M3D_FLG_MTLLIB) {
+                    m = model->material;
+                    model->material = (m3dm_t*)M3D_MALLOC(model->nummaterial * sizeof(m3dm_t));
+                    if(!model->material) goto memerr;
+                    memcpy(model->material, m, (model->nummaterial - 1) * sizeof(m3dm_t));
+                    if(model->texture) {
+                        tx = model->texture;
+                        model->texture = (m3dtx_t*)M3D_MALLOC(model->numtexture * sizeof(m3dtx_t));
+                        if(!model->texture) goto memerr;
+                        memcpy(model->texture, tx, model->numtexture * sizeof(m3dm_t));
+                    }
+                    model->flags &= ~M3D_FLG_MTLLIB;
+                } else {
+                    model->material = (m3dm_t*)M3D_REALLOC(model->material, model->nummaterial * sizeof(m3dm_t));
+                    if(!model->material) goto memerr;
+                }
+                m = &model->material[i];
+                m->numprop = 0;
+                m->prop = NULL;
+                m->name = material;
+                m->prop = (m3dp_t*)M3D_REALLOC(m->prop, (len / 2) * sizeof(m3dp_t));
+                if(!m->prop) goto memerr;
+                while(data < chunk) {
+                    i = m->numprop++;
+                    m->prop[i].type = *data++;
+                    m->prop[i].value.num = 0;
+                    if(m->prop[i].type >= 128)
+                        k = m3dpf_map;
+                    else {
+                        for(k = 256, j = 0; j < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); j++)
+                            if(m->prop[i].type == m3d_propertytypes[j].id) { k = m3d_propertytypes[j].format; break; }
+                    }
+                    switch(k) {
+                        case m3dpf_color:
+                            switch(model->ci_s) {
+                                case 1: m->prop[i].value.color = model->cmap ? model->cmap[data[0]] : 0; data++; break;
+                                case 2: m->prop[i].value.color = model->cmap ? model->cmap[*((uint16_t*)data)] : 0; data += 2; break;
+                                case 4: m->prop[i].value.color = *((uint32_t*)data); data += 4; break;
+                            }
+                        break;
+
+                        case m3dpf_uint8: m->prop[i].value.num = *data++; break;
+                        case m3dpf_uint16:m->prop[i].value.num = *((uint16_t*)data); data += 2; break;
+                        case m3dpf_uint32:m->prop[i].value.num = *((uint32_t*)data); data += 4; break;
+                        case m3dpf_float: m->prop[i].value.fnum = *((float*)data); data += 4; break;
+
+                        case m3dpf_map:
+                            M3D_GETSTR(material);
+                            m->prop[i].value.textureid = _m3d_gettx(model, readfilecb, freecb, material);
+                            if(model->errcode == M3D_ERR_ALLOC) goto memerr;
+                            if(m->prop[i].value.textureid == (M3D_INDEX)-1U) {
+                                M3D_LOG("Texture not found");
+                                M3D_LOG(material);
+                                m->numprop--;
+                            }
+                        break;
+
+                        default:
+                            M3D_LOG("Unknown material property in");
+                            M3D_LOG(m->name);
+                            model->errcode = M3D_ERR_UNKPROP;
+                            data = chunk;
+                        break;
+                    }
+                }
+                m->prop = (m3dp_t*)M3D_REALLOC(m->prop, m->numprop * sizeof(m3dp_t));
+                if(!m->prop) goto memerr;
+            }
+        } else
+        /* face */
+        if(M3D_CHUNKMAGIC(data, 'P','R','O','C')) {
+            /* procedural surface */
+            M3D_GETSTR(material);
+            M3D_LOG("Procedural surface");
+            M3D_LOG(material);
+            _m3d_getpr(model, readfilecb, freecb, material);
+        } else
+        if(M3D_CHUNKMAGIC(data, 'M','E','S','H')) {
+            M3D_LOG("Mesh data");
+            /* mesh */
+            data += sizeof(m3dchunk_t);
+            mi = (M3D_INDEX)-1U;
+            am = model->numface;
+            while(data < chunk) {
+                k = *data++;
+                n = k >> 4;
+                k &= 15;
+                if(!n) {
+                    /* use material */
+                    mi = (M3D_INDEX)-1U;
+                    M3D_GETSTR(material);
+                    if(material) {
+                        for(j = 0; j < model->nummaterial; j++)
+                            if(!strcmp(material, model->material[j].name)) {
+                                mi = (M3D_INDEX)j;
+                                break;
+                            }
+                        if(mi == (M3D_INDEX)-1U) model->errcode = M3D_ERR_MTRL;
+                    }
+                    continue;
+                }
+                if(n != 3) { M3D_LOG("Only triangle mesh supported for now"); model->errcode = M3D_ERR_UNKMESH; return model; }
+                i = model->numface++;
+                if(model->numface > am) {
+                    am = model->numface + 4095;
+                    model->face = (m3df_t*)M3D_REALLOC(model->face, am * sizeof(m3df_t));
+                    if(!model->face) goto memerr;
+                }
+                memset(&model->face[i], 255, sizeof(m3df_t)); /* set all index to -1 by default */
+                model->face[i].materialid = mi;
+                for(j = 0; j < n; j++) {
+                    /* vertex */
+                    data = _m3d_getidx(data, model->vi_s, &model->face[i].vertex[j]);
+                    /* texcoord */
+                    if(k & 1)
+                        data = _m3d_getidx(data, model->ti_s, &model->face[i].texcoord[j]);
+                    /* normal */
+                    if(k & 2)
+                        data = _m3d_getidx(data, model->vi_s, &model->face[i].normal[j]);
+                }
+            }
+            model->face = (m3df_t*)M3D_REALLOC(model->face, model->numface * sizeof(m3df_t));
+        } else
+        /* action */
+        if(M3D_CHUNKMAGIC(data, 'A','C','T','N')) {
+            M3D_LOG("Action");
+            i = model->numaction++;
+            model->action = (m3da_t*)M3D_REALLOC(model->action, model->numaction * sizeof(m3da_t));
+            if(!model->action) goto memerr;
+            a = &model->action[i];
+            data += sizeof(m3dchunk_t);
+            M3D_GETSTR(a->name);
+            M3D_LOG(a->name);
+            a->numframe = *((uint16_t*)data); data += 2;
+            if(a->numframe < 1) {
+                model->numaction--;
+            } else {
+                a->durationmsec = *((uint32_t*)data); data += 4;
+                a->frame = (m3dfr_t*)M3D_MALLOC(a->numframe * sizeof(m3dfr_t));
+                if(!a->frame) goto memerr;
+                for(i = 0; data < chunk && i < a->numframe; i++) {
+                    a->frame[i].msec = *((uint32_t*)data); data += 4;
+                    a->frame[i].numtransform = 0; a->frame[i].transform = NULL;
+                    data = _m3d_getidx(data, model->fi_s, &a->frame[i].numtransform);
+                    if(a->frame[i].numtransform > 0) {
+                        a->frame[i].transform = (m3dtr_t*)M3D_MALLOC(a->frame[i].numtransform * sizeof(m3dtr_t));
+                        for(j = 0; j < a->frame[i].numtransform; j++) {
+                            data = _m3d_getidx(data, model->bi_s, &a->frame[i].transform[j].boneid);
+                            data = _m3d_getidx(data, model->vi_s, &a->frame[i].transform[j].pos);
+                            data = _m3d_getidx(data, model->vi_s, &a->frame[i].transform[j].ori);
+                        }
+                    }
+                }
+            }
+        } else {
+            i = model->numunknown++;
+            model->unknown = (m3dchunk_t**)M3D_REALLOC(model->unknown, model->numunknown * sizeof(m3dchunk_t*));
+            if(!model->unknown) goto memerr;
+            model->unknown[i] = (m3dchunk_t*)data;
+        }
+    }
+    /* calculate normals, normalize skin weights, create bone/vertex cross-references and calculate transform matrices */
+#ifdef M3D_ASCII
+postprocess:
+#endif
+    if(model) {
+#ifndef M3D_NONORMALS
+        if(model->numface && model->face) {
+            memset(&vn, 0, sizeof(m3dv_t));
+            /* if they are missing, calculate triangle normals into a temporary buffer */
+            for(i = numnorm = 0; i < model->numface; i++)
+                if(model->face[i].normal[0] == -1U) {
+                    v0 = &model->vertex[model->face[i].vertex[0]]; v1 = &model->vertex[model->face[i].vertex[1]];
+                    v2 = &model->vertex[model->face[i].vertex[2]];
+                    va.x = v1->x - v0->x; va.y = v1->y - v0->y; va.z = v1->z - v0->z;
+                    vb.x = v2->x - v0->x; vb.y = v2->y - v0->y; vb.z = v2->z - v0->z;
+                    vn.x = (va.y * vb.z) - (va.z * vb.y);
+                    vn.y = (va.z * vb.x) - (va.x * vb.z);
+                    vn.z = (va.x * vb.y) - (va.y * vb.x);
+                    w = _m3d_rsq((vn.x * vn.x) + (vn.y * vn.y) + (vn.z * vn.z));
+                    vn.x *= w; vn.y *= w; vn.z *= w;
+                    norm = _m3d_addnorm(norm, &numnorm, &vn, &j);
+                    if(!ni) {
+                        ni = (M3D_INDEX*)M3D_MALLOC(model->numface * sizeof(M3D_INDEX));
+                        if(!ni) goto memerr;
+                    }
+                    ni[i] = j;
+                }
+            if(ni && norm) {
+                vi = (M3D_INDEX*)M3D_MALLOC(model->numvertex * sizeof(M3D_INDEX));
+                if(!vi) goto memerr;
+                /* for each vertex, take the average of the temporary normals and use that */
+                for(i = 0, n = model->numvertex; i < n; i++) {
+                    memset(&vn, 0, sizeof(m3dv_t));
+                    for(j = 0; j < model->numface; j++)
+                        for(k = 0; k < 3; k++)
+                            if(model->face[j].vertex[k] == i) {
+                                vn.x += norm[ni[j]].x;
+                                vn.y += norm[ni[j]].y;
+                                vn.z += norm[ni[j]].z;
+                            }
+                    w = _m3d_rsq((vn.x * vn.x) + (vn.y * vn.y) + (vn.z * vn.z));
+                    vn.x *= w; vn.y *= w; vn.z *= w;
+                    vn.skinid = -1U;
+                    model->vertex = _m3d_addnorm(model->vertex, &model->numvertex, &vn, &vi[i]);
+                }
+                for(j = 0; j < model->numface; j++)
+                    for(k = 0; k < 3; k++)
+                        model->face[j].normal[k] = vi[model->face[j].vertex[k]];
+                M3D_FREE(norm);
+                M3D_FREE(ni);
+                M3D_FREE(vi);
+            }
+        }
+#endif
+        if(model->numbone && model->bone && model->numskin && model->skin && model->numvertex && model->vertex) {
+#ifndef M3D_NOWEIGHTS
+            for(i = 0; i < model->numvertex; i++) {
+                if(model->vertex[i].skinid < M3D_INDEXMAX) {
+                    sk = &model->skin[model->vertex[i].skinid];
+                    w = (M3D_FLOAT)0.0;
+                    for(j = 0; j < M3D_NUMBONE && sk->boneid[j] != (M3D_INDEX)-1U && sk->weight[j] > (M3D_FLOAT)0.0; j++)
+                        w += sk->weight[j];
+                    for(j = 0; j < M3D_NUMBONE && sk->boneid[j] != (M3D_INDEX)-1U && sk->weight[j] > (M3D_FLOAT)0.0; j++) {
+                        sk->weight[j] /= w;
+                        b = &model->bone[sk->boneid[j]];
+                        k = b->numweight++;
+                        b->weight = (m3dw_t*)M3D_REALLOC(b->weight, b->numweight * sizeof(m3da_t));
+                        if(!b->weight) goto memerr;
+                        b->weight[k].vertexid = i;
+                        b->weight[k].weight = sk->weight[j];
+                    }
+                }
+            }
+#endif
+#ifndef M3D_NOANIMATION
+            for(i = 0; i < model->numbone; i++) {
+                b = &model->bone[i];
+                if(model->bone[i].parent == (M3D_INDEX)-1U) {
+                    _m3d_mat((M3D_FLOAT*)&b->mat4, &model->vertex[b->pos], &model->vertex[b->ori]);
+                } else {
+                    _m3d_mat((M3D_FLOAT*)&r, &model->vertex[b->pos], &model->vertex[b->ori]);
+                    _m3d_mul((M3D_FLOAT*)&b->mat4, (M3D_FLOAT*)&model->bone[b->parent].mat4, (M3D_FLOAT*)&r);
+                }
+            }
+            for(i = 0; i < model->numbone; i++)
+                _m3d_inv((M3D_FLOAT*)&model->bone[i].mat4);
+#endif
+        }
+    }
+    return model;
+}
+
+/**
+ * Calculates skeletons for animation frames, returns a working copy (should be freed after use)
+ */
+m3dtr_t *m3d_frame(m3d_t *model, M3D_INDEX actionid, M3D_INDEX frameid, m3dtr_t *skeleton)
+{
+    unsigned int i;
+    M3D_INDEX s = frameid;
+    m3dfr_t *fr;
+
+    if(!model || !model->numbone || !model->bone || (actionid != (M3D_INDEX)-1U && (!model->action ||
+        actionid >= model->numaction || frameid >= model->action[actionid].numframe))) {
+            model->errcode = M3D_ERR_UNKFRAME;
+            return skeleton;
+    }
+    model->errcode = M3D_SUCCESS;
+    if(!skeleton) {
+        skeleton = (m3dtr_t*)M3D_MALLOC(model->numbone * sizeof(m3dtr_t));
+        if(!skeleton) {
+            model->errcode = M3D_ERR_ALLOC;
+            return NULL;
+        }
+        goto gen;
+    }
+    if(actionid == (M3D_INDEX)-1U || !frameid) {
+gen:    s = 0;
+        for(i = 0; i < model->numbone; i++) {
+            skeleton[i].boneid = i;
+            skeleton[i].pos = model->bone[i].pos;
+            skeleton[i].ori = model->bone[i].ori;
+        }
+    }
+    if(actionid < model->numaction && (frameid || !model->action[actionid].frame[0].msec)) {
+        for(; s <= frameid; s++) {
+            fr = &model->action[actionid].frame[s];
+            for(i = 0; i < fr->numtransform; i++) {
+                skeleton[fr->transform[i].boneid].pos = fr->transform[i].pos;
+                skeleton[fr->transform[i].boneid].ori = fr->transform[i].ori;
+            }
+        }
+    }
+    return skeleton;
+}
+
+#ifndef M3D_NOANIMATION
+/**
+ * Returns interpolated animation-pose, a working copy (should be freed after use)
+ */
+m3db_t *m3d_pose(m3d_t *model, M3D_INDEX actionid, uint32_t msec)
+{
+    unsigned int i, j, l;
+    M3D_FLOAT r[16], t, d;
+    m3db_t *ret;
+    m3dv_t *v, *p, *f;
+    m3dtr_t *tmp;
+    m3dfr_t *fr;
+
+    if(!model || !model->numbone || !model->bone) {
+        model->errcode = M3D_ERR_UNKFRAME;
+        return NULL;
+    }
+    ret = (m3db_t*)M3D_MALLOC(model->numbone * sizeof(m3db_t));
+    if(!ret) {
+        model->errcode = M3D_ERR_ALLOC;
+        return NULL;
+    }
+    memcpy(ret, model->bone, model->numbone * sizeof(m3db_t));
+    for(i = 0; i < model->numbone; i++)
+        _m3d_inv((M3D_FLOAT*)&ret[i].mat4);
+    if(!model->action || actionid >= model->numaction) {
+        model->errcode = M3D_ERR_UNKFRAME;
+        return ret;
+    }
+    msec %= model->action[actionid].durationmsec;
+    model->errcode = M3D_SUCCESS;
+    fr = &model->action[actionid].frame[0];
+    for(j = l = 0; j < model->action[actionid].numframe && model->action[actionid].frame[j].msec <= msec; j++) {
+        fr = &model->action[actionid].frame[j];
+        l = fr->msec;
+        for(i = 0; i < fr->numtransform; i++) {
+            ret[fr->transform[i].boneid].pos = fr->transform[i].pos;
+            ret[fr->transform[i].boneid].ori = fr->transform[i].ori;
+        }
+    }
+    if(l != msec) {
+        model->vertex = (m3dv_t*)M3D_REALLOC(model->vertex, (model->numvertex + 2 * model->numbone) * sizeof(m3dv_t));
+        if(!model->vertex) {
+            free(ret);
+            model->errcode = M3D_ERR_ALLOC;
+            return NULL;
+        }
+        tmp = (m3dtr_t*)M3D_MALLOC(model->numbone * sizeof(m3dtr_t));
+        if(tmp) {
+            for(i = 0; i < model->numbone; i++) {
+                tmp[i].pos = ret[i].pos;
+                tmp[i].ori = ret[i].ori;
+            }
+            fr = &model->action[actionid].frame[j % model->action[actionid].numframe];
+            t = l >= fr->msec ? (M3D_FLOAT)1.0 : (M3D_FLOAT)(msec - l) / (M3D_FLOAT)(fr->msec - l);
+            for(i = 0; i < fr->numtransform; i++) {
+                tmp[fr->transform[i].boneid].pos = fr->transform[i].pos;
+                tmp[fr->transform[i].boneid].ori = fr->transform[i].ori;
+            }
+            for(i = 0, j = model->numvertex; i < model->numbone; i++) {
+                /* LERP interpolation of position */
+                if(ret[i].pos != tmp[i].pos) {
+                    p = &model->vertex[ret[i].pos];
+                    f = &model->vertex[tmp[i].pos];
+                    v = &model->vertex[j];
+                    v->x = p->x + t * (f->x - p->x);
+                    v->y = p->y + t * (f->y - p->y);
+                    v->z = p->z + t * (f->z - p->z);
+                    ret[i].pos = j++;
+                }
+                /* NLERP interpolation of orientation (could have used SLERP, that's nicer, but slower) */
+                if(ret[i].ori != tmp[i].ori) {
+                    p = &model->vertex[ret[i].ori];
+                    f = &model->vertex[tmp[i].ori];
+                    v = &model->vertex[j];
+                    d = (p->w * f->w + p->x * f->x + p->y * f->y + p->z * f->z < 0) ? (M3D_FLOAT)-1.0 : (M3D_FLOAT)1.0;
+                    v->x = p->x + t * (d*f->x - p->x);
+                    v->y = p->y + t * (d*f->y - p->y);
+                    v->z = p->z + t * (d*f->z - p->z);
+                    v->w = p->w + t * (d*f->w - p->w);
+                    d = _m3d_rsq(v->w * v->w + v->x * v->x + v->y * v->y + v->z * v->z);
+                    v->x *= d; v->y *= d; v->z *= d; v->w *= d;
+                    ret[i].ori = j++;
+                }
+            }
+            M3D_FREE(tmp);
+        }
+    }
+    for(i = 0; i < model->numbone; i++) {
+        if(ret[i].parent == (M3D_INDEX)-1U) {
+            _m3d_mat((M3D_FLOAT*)&ret[i].mat4, &model->vertex[ret[i].pos], &model->vertex[ret[i].ori]);
+        } else {
+            _m3d_mat((M3D_FLOAT*)&r, &model->vertex[ret[i].pos], &model->vertex[ret[i].ori]);
+            _m3d_mul((M3D_FLOAT*)&ret[i].mat4, (M3D_FLOAT*)&ret[ret[i].parent].mat4, (M3D_FLOAT*)&r);
+        }
+    }
+    return ret;
+}
+
+#endif /* M3D_NOANIMATION */
+
+#endif /* M3D_IMPLEMENTATION */
+
+#if !defined(M3D_NODUP) && (!defined(M3D_NOIMPORTER) || defined(M3D_EXPORTER))
+/**
+ * Free the in-memory model
+ */
+void m3d_free(m3d_t *model)
+{
+    unsigned int i, j;
+
+    if(!model) return;
+#ifdef M3D_ASCII
+    /* if model imported from ASCII, we have to free all strings as well */
+    if(model->flags & M3D_FLG_FREESTR) {
+        if(model->name) M3D_FREE(model->name);
+        if(model->license) M3D_FREE(model->license);
+        if(model->author) M3D_FREE(model->author);
+        if(model->desc) M3D_FREE(model->desc);
+        if(model->bone)
+            for(i = 0; i < model->numbone; i++)
+                if(model->bone[i].name)
+                    M3D_FREE(model->bone[i].name);
+        if(model->material)
+            for(i = 0; i < model->nummaterial; i++)
+                if(model->material[i].name)
+                    M3D_FREE(model->material[i].name);
+        if(model->action)
+            for(i = 0; i < model->numaction; i++)
+                if(model->action[i].name)
+                    M3D_FREE(model->action[i].name);
+        if(model->texture)
+            for(i = 0; i < model->numtexture; i++)
+                if(model->texture[i].name)
+                    M3D_FREE(model->texture[i].name);
+        if(model->unknown)
+            for(i = 0; i < model->numunknown; i++)
+                if(model->unknown[i])
+                    M3D_FREE(model->unknown[i]);
+    }
+#endif
+    if(model->flags & M3D_FLG_FREERAW) M3D_FREE(model->raw);
+
+    if(model->tmap) M3D_FREE(model->tmap);
+    if(model->bone) {
+        for(i = 0; i < model->numbone; i++)
+            if(model->bone[i].weight)
+                M3D_FREE(model->bone[i].weight);
+        M3D_FREE(model->bone);
+    }
+    if(model->skin) M3D_FREE(model->skin);
+    if(model->vertex) M3D_FREE(model->vertex);
+    if(model->face) M3D_FREE(model->face);
+    if(model->material && !(model->flags & M3D_FLG_MTLLIB)) {
+        for(i = 0; i < model->nummaterial; i++)
+            if(model->material[i].prop) M3D_FREE(model->material[i].prop);
+        M3D_FREE(model->material);
+    }
+    if(model->texture) {
+        for(i = 0; i < model->numtexture; i++)
+            if(model->texture[i].d) M3D_FREE(model->texture[i].d);
+        M3D_FREE(model->texture);
+    }
+    if(model->action) {
+        for(i = 0; i < model->numaction; i++) {
+            if(model->action[i].frame) {
+                for(j = 0; j < model->action[i].numframe; j++)
+                    if(model->action[i].frame[j].transform) M3D_FREE(model->action[i].frame[j].transform);
+                M3D_FREE(model->action[i].frame);
+            }
+        }
+        M3D_FREE(model->action);
+    }
+    if(model->inlined) M3D_FREE(model->inlined);
+    if(model->unknown) M3D_FREE(model->unknown);
+    free(model);
+}
+#endif
+
+#ifdef M3D_EXPORTER
+typedef struct {
+    char *str;
+    uint32_t offs;
+} m3dstr_t;
+
+/* create unique list of strings */
+static m3dstr_t *_m3d_addstr(m3dstr_t *str, uint32_t *numstr, char *s)
+{
+    uint32_t i;
+    if(!s || !*s) return str;
+    if(str) {
+        for(i = 0; i < *numstr; i++)
+            if(str[i].str == s || !strcmp(str[i].str, s)) return str;
+    }
+    str = (m3dstr_t*)M3D_REALLOC(str, ((*numstr) + 1) * sizeof(m3dstr_t));
+    str[*numstr].str = s;
+    str[*numstr].offs = 0;
+    (*numstr)++;
+    return str;
+}
+
+/* add strings to header */
+m3dhdr_t *_m3d_addhdr(m3dhdr_t *h, m3dstr_t *s)
+{
+    int i;
+    char *safe = _m3d_safestr(s->str, 0);
+    i = strlen(safe);
+    h = (m3dhdr_t*)M3D_REALLOC(h, h->length + i+1);
+    if(!h) { M3D_FREE(safe); return NULL; }
+    memcpy((uint8_t*)h + h->length, safe, i+1);
+    s->offs = h->length - 16;
+    h->length += i+1;
+    M3D_FREE(safe);
+    return h;
+}
+
+/* return offset of string */
+static uint32_t _m3d_stridx(m3dstr_t *str, uint32_t numstr, char *s)
+{
+    uint32_t i;
+    char *safe;
+    if(!s || !*s) return 0;
+    if(str) {
+        safe = _m3d_safestr(s, 0);
+        if(!safe) return 0;
+        if(!*safe) {
+            free(safe);
+            return 0;
+        }
+        for(i = 0; i < numstr; i++)
+            if(!strcmp(str[i].str, s)) {
+                free(safe);
+                return str[i].offs;
+            }
+        free(safe);
+    }
+    return 0;
+}
+
+/* compare two colors by HSV value */
+_inline static int _m3d_cmapcmp(const void *a, const void *b)
+{
+    uint8_t *A = (uint8_t*)a,  *B = (uint8_t*)b;
+    _register int m, vA, vB;
+    /* get HSV value for A */
+    m = A[2] < A[1]? A[2] : A[1]; if(A[0] < m) m = A[0];
+    vA = A[2] > A[1]? A[2] : A[1]; if(A[0] > vA) vA = A[0];
+    /* get HSV value for B */
+    m = B[2] < B[1]? B[2] : B[1]; if(B[0] < m) m = B[0];
+    vB = B[2] > B[1]? B[2] : B[1]; if(B[0] > vB) vB = B[0];
+    return vA - vB;
+}
+
+/* create sorted list of colors */
+static uint32_t *_m3d_addcmap(uint32_t *cmap, uint32_t *numcmap, uint32_t color)
+{
+    uint32_t i;
+    if(cmap) {
+        for(i = 0; i < *numcmap; i++)
+            if(cmap[i] == color) return cmap;
+    }
+    cmap = (uint32_t*)M3D_REALLOC(cmap, ((*numcmap) + 1) * sizeof(uint32_t));
+    for(i = 0; i < *numcmap && _m3d_cmapcmp(&color, &cmap[i]) > 0; i++);
+    if(i < *numcmap) memmove(&cmap[i+1], &cmap[i], ((*numcmap) - i)*sizeof(uint32_t));
+    cmap[i] = color;
+    (*numcmap)++;
+    return cmap;
+}
+
+/* look up a color and return its index */
+static uint32_t _m3d_cmapidx(uint32_t *cmap, uint32_t numcmap, uint32_t color)
+{
+    uint32_t i;
+    for(i = 0; i < numcmap; i++)
+        if(cmap[i] == color) return i;
+    return 0;
+}
+
+/* add vertex to list */
+static m3dv_t *_m3d_addvrtx(m3dv_t *vrtx, uint32_t *numvrtx, m3dv_t *v, uint32_t *idx)
+{
+    uint32_t i;
+    if(v->x == (M3D_FLOAT)-0.0) v->x = (M3D_FLOAT)0.0;
+    if(v->y == (M3D_FLOAT)-0.0) v->y = (M3D_FLOAT)0.0;
+    if(v->z == (M3D_FLOAT)-0.0) v->z = (M3D_FLOAT)0.0;
+    if(v->w == (M3D_FLOAT)-0.0) v->w = (M3D_FLOAT)0.0;
+    if(vrtx) {
+        for(i = 0; i < *numvrtx; i++)
+            if(!memcmp(&vrtx[i], v, sizeof(m3dv_t))) { *idx = i; return vrtx; }
+    }
+    vrtx = (m3dv_t*)M3D_REALLOC(vrtx, ((*numvrtx) + 1) * sizeof(m3dv_t));
+    memcpy(&vrtx[*numvrtx], v, sizeof(m3dv_t));
+    *idx = *numvrtx;
+    (*numvrtx)++;
+    return vrtx;
+}
+
+/* add texture map to list */
+static m3dti_t *_m3d_addtmap(m3dti_t *tmap, uint32_t *numtmap, m3dti_t *t, uint32_t *idx)
+{
+    uint32_t i;
+    if(tmap) {
+        for(i = 0; i < *numtmap; i++)
+            if(!memcmp(&tmap[i], t, sizeof(m3dti_t))) { *idx = i; return tmap; }
+    }
+    tmap = (m3dti_t*)M3D_REALLOC(tmap, ((*numtmap) + 1) * sizeof(m3dti_t));
+    memcpy(&tmap[*numtmap], t, sizeof(m3dti_t));
+    *idx = *numtmap;
+    (*numtmap)++;
+    return tmap;
+}
+
+/* add material to list */
+static m3dm_t **_m3d_addmtrl(m3dm_t **mtrl, uint32_t *nummtrl, m3dm_t *m, uint32_t *idx)
+{
+    uint32_t i;
+    if(mtrl) {
+        for(i = 0; i < *nummtrl; i++)
+            if(mtrl[i]->name == m->name || !strcmp(mtrl[i]->name, m->name)) { *idx = i; return mtrl; }
+    }
+    mtrl = (m3dm_t**)M3D_REALLOC(mtrl, ((*nummtrl) + 1) * sizeof(m3dm_t*));
+    mtrl[*nummtrl] = m;
+    *idx = *nummtrl;
+    (*nummtrl)++;
+    return mtrl;
+}
+
+/* add index to output */
+static unsigned char *_m3d_addidx(unsigned char *out, char type, uint32_t idx) {
+    switch(type) {
+        case 1: *out++ = (uint8_t)(idx); break;
+        case 2: *((uint16_t*)out) = (uint16_t)(idx); out += 2; break;
+        case 4: *((uint32_t*)out) = (uint32_t)(idx); out += 4; break;
+        /* case 0: case 8: break; */
+    }
+    return out;
+}
+
+/* round a vertex position */
+static void _m3d_round(int quality, m3dv_t *src, m3dv_t *dst)
+{
+    _register int t;
+    /* copy additional attributes */
+    if(src != dst) memcpy(dst, src, sizeof(m3dv_t));
+    /* round according to quality */
+    switch(quality) {
+        case M3D_EXP_INT8:
+            t = src->x * 127; dst->x = (M3D_FLOAT)t / 127;
+            t = src->y * 127; dst->y = (M3D_FLOAT)t / 127;
+            t = src->z * 127; dst->z = (M3D_FLOAT)t / 127;
+            t = src->w * 127; dst->w = (M3D_FLOAT)t / 127;
+        break;
+        case M3D_EXP_INT16:
+            t = src->x * 32767; dst->x = (M3D_FLOAT)t / 32767;
+            t = src->y * 32767; dst->y = (M3D_FLOAT)t / 32767;
+            t = src->z * 32767; dst->z = (M3D_FLOAT)t / 32767;
+            t = src->w * 32767; dst->w = (M3D_FLOAT)t / 32767;
+        break;
+    }
+}
+
+#ifdef M3D_ASCII
+/* add a bone to ascii output */
+static char *_m3d_prtbone(char *ptr, m3db_t *bone, M3D_INDEX numbone, M3D_INDEX parent, uint32_t level)
+{
+    uint32_t i, j;
+    char *sn;
+
+    if(level > M3D_BONEMAXLEVEL || !bone) return ptr;
+    for(i = 0; i < numbone; i++) {
+        if(bone[i].parent == parent) {
+            for(j = 0; j < level; j++) *ptr++ = '/';
+            sn = _m3d_safestr(bone[i].name, 0);
+            ptr += sprintf(ptr, "%d %d %s\r\n", bone[i].pos, bone[i].ori, sn);
+            M3D_FREE(sn);
+            ptr = _m3d_prtbone(ptr, bone, numbone, i, level + 1);
+        }
+    }
+    return ptr;
+}
+#endif
+
+/**
+ * Function to encode an in-memory model into on storage Model 3D format
+ */
+unsigned char *m3d_save(m3d_t *model, int quality, int flags, unsigned int *size)
+{
+#ifdef M3D_ASCII
+    const char *ol;
+    char *ptr;
+#endif
+    char vc_s, vi_s, si_s, ci_s, ti_s, bi_s, nb_s, sk_s, fi_s;
+    char *sn = NULL, *sl = NULL, *sa = NULL, *sd = NULL;
+    unsigned char *out = NULL, *z = NULL, weights[M3D_NUMBONE];
+    unsigned int i, j, k, l, len, chunklen, *length;
+    float scale = 0.0f, min_x, max_x, min_y, max_y, min_z, max_z;
+    uint32_t idx, numcmap = 0, *cmap = NULL, numvrtx = 0, numtmap = 0, numbone = 0;
+    uint32_t numskin = 0, numactn = 0, *actn = NULL, numstr = 0, nummtrl = 0, maxt = 0;
+    m3dstr_t *str = NULL;
+    m3dv_t *vrtx = NULL, vertex;
+    m3dti_t *tmap = NULL, tcoord;
+    m3db_t *bone = NULL;
+    m3ds_t *skin = NULL;
+    m3df_t *face = NULL;
+    m3dhdr_t *h = NULL;
+    m3dm_t *m, **mtrl = NULL;
+    m3da_t *a;
+    M3D_INDEX last;
+
+    if(!model) {
+        if(size) *size = 0;
+        return NULL;
+    }
+    model->errcode = M3D_SUCCESS;
+#ifdef M3D_ASCII
+    if(flags & M3D_EXP_ASCII) quality = M3D_EXP_DOUBLE;
+#endif
+    /* collect array elements that are actually referenced */
+    if(model->numface && model->face && !(flags & M3D_EXP_NOFACE)) {
+        face = (m3df_t*)M3D_MALLOC(model->numface * sizeof(m3df_t));
+        if(!face) goto memerr;
+        memset(face, 255, model->numface * sizeof(m3df_t));
+        last = (M3D_INDEX)-1U;
+        for(i = 0; i < model->numface; i++) {
+            face[i].materialid = (M3D_INDEX)-1U;
+            if(!(flags & M3D_EXP_NOMATERIAL) && model->face[i].materialid != last) {
+                last = model->face[i].materialid;
+                if(last < model->nummaterial) {
+                    mtrl = _m3d_addmtrl(mtrl, &nummtrl, &model->material[last], &face[i].materialid);
+                    if(!mtrl) goto memerr;
+                }
+            }
+            for(j = 0; j < 3; j++) {
+                k = model->face[i].vertex[j];
+                if(quality < M3D_EXP_FLOAT) {
+                    _m3d_round(quality, &model->vertex[k], &vertex);
+                    vrtx = _m3d_addvrtx(vrtx, &numvrtx, &vertex, &idx);
+                } else
+                    vrtx = _m3d_addvrtx(vrtx, &numvrtx, &model->vertex[k], &idx);
+                if(!vrtx) goto memerr;
+                face[i].vertex[j] = (M3D_INDEX)idx;
+                if(!(flags & M3D_EXP_NOCMAP)) {
+                    cmap = _m3d_addcmap(cmap, &numcmap, model->vertex[k].color);
+                    if(!cmap) goto memerr;
+                }
+                k = model->face[i].normal[j];
+                if(k < model->numvertex && !(flags & M3D_EXP_NONORMAL)) {
+                    if(quality < M3D_EXP_FLOAT) {
+                        _m3d_round(quality, &model->vertex[k], &vertex);
+                        vrtx = _m3d_addnorm(vrtx, &numvrtx, &vertex, &idx);
+                    } else
+                        vrtx = _m3d_addnorm(vrtx, &numvrtx, &model->vertex[k], &idx);
+                    if(!vrtx) goto memerr;
+                    face[i].normal[j] = (M3D_INDEX)idx;
+                }
+                k = model->face[i].texcoord[j];
+                if(k < model->numtmap) {
+                    switch(quality) {
+                        case M3D_EXP_INT8:
+                            l = model->tmap[k].u * 255; tcoord.u = (M3D_FLOAT)l / 255;
+                            l = model->tmap[k].v * 255; tcoord.v = (M3D_FLOAT)l / 255;
+                        break;
+                        case M3D_EXP_INT16:
+                            l = model->tmap[k].u * 65535; tcoord.u = (M3D_FLOAT)l / 65535;
+                            l = model->tmap[k].v * 65535; tcoord.v = (M3D_FLOAT)l / 65535;
+                        break;
+                        default:
+                            tcoord.u = model->tmap[k].u;
+                            tcoord.v = model->tmap[k].v;
+                        break;
+                    }
+                    if(flags & M3D_EXP_FLIPTXTCRD)
+                        tcoord.v = (M3D_FLOAT)1.0 - tcoord.v;
+                    tmap = _m3d_addtmap(tmap, &numtmap, &tcoord, &idx);
+                    if(!tmap) goto memerr;
+                    face[i].texcoord[j] = (M3D_INDEX)idx;
+                }
+            }
+            /* convert from CW to CCW */
+            if(flags & M3D_EXP_IDOSUCK) {
+                j = face[i].vertex[1];
+                face[i].vertex[1] = face[i].vertex[2];
+                face[i].vertex[2] = face[i].vertex[1];
+                j = face[i].normal[1];
+                face[i].normal[1] = face[i].normal[2];
+                face[i].normal[2] = face[i].normal[1];
+                j = face[i].texcoord[1];
+                face[i].texcoord[1] = face[i].texcoord[2];
+                face[i].texcoord[2] = face[i].texcoord[1];
+            }
+        }
+    } else if(!(flags & M3D_EXP_NOMATERIAL)) {
+        /* without a face, simply add all materials, because it can be an mtllib */
+        nummtrl = model->nummaterial;
+    }
+    /* add colors to color map and texture names to string table */
+    for(i = 0; i < nummtrl; i++) {
+        m = !mtrl ? &model->material[i] : mtrl[i];
+        str = _m3d_addstr(str, &numstr, m->name);
+        if(!str) goto memerr;
+        for(j = 0; j < mtrl[i]->numprop; j++) {
+            if(!(flags & M3D_EXP_NOCMAP) && m->prop[j].type < 128) {
+                for(l = 0; l < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); l++) {
+                    if(m->prop[j].type == m3d_propertytypes[l].id && m3d_propertytypes[l].format == m3dpf_color) {
+                        cmap = _m3d_addcmap(cmap, &numcmap, m->prop[j].value.color);
+                        if(!cmap) goto memerr;
+                        break;
+                    }
+                }
+            }
+            if(m->prop[j].type >= 128 && m->prop[j].value.textureid < model->numtexture &&
+                model->texture[m->prop[j].value.textureid].name) {
+                str = _m3d_addstr(str, &numstr, model->texture[m->prop[j].value.textureid].name);
+                if(!str) goto memerr;
+            }
+        }
+    }
+    /* get bind-pose skeleton and skin */
+    if(model->numbone && model->bone && !(flags & M3D_EXP_NOBONE)) {
+        numbone = model->numbone;
+        bone = (m3db_t*)M3D_MALLOC(model->numbone * sizeof(m3db_t));
+        if(!bone) goto memerr;
+        memset(bone, 0, model->numbone * sizeof(m3db_t));
+        for(i = 0; i < model->numbone; i++) {
+            bone[i].parent = model->bone[i].parent;
+            bone[i].name = model->bone[i].name;
+            str = _m3d_addstr(str, &numstr, bone[i].name);
+            if(!str) goto memerr;
+            if(quality < M3D_EXP_FLOAT) {
+                _m3d_round(quality, &model->vertex[model->bone[i].pos], &vertex);
+                vrtx = _m3d_addvrtx(vrtx, &numvrtx, &vertex, &k);
+            } else
+                vrtx = _m3d_addvrtx(vrtx, &numvrtx, &model->vertex[model->bone[i].pos], &k);
+            if(!vrtx) goto memerr;
+            bone[i].pos = (M3D_INDEX)k;
+            if(quality < M3D_EXP_FLOAT) {
+                _m3d_round(quality, &model->vertex[model->bone[i].ori], &vertex);
+                vrtx = _m3d_addvrtx(vrtx, &numvrtx, &vertex, &k);
+            } else
+                vrtx = _m3d_addvrtx(vrtx, &numvrtx, &model->vertex[model->bone[i].ori], &k);
+            if(!vrtx) goto memerr;
+            bone[i].ori = (M3D_INDEX)k;
+        }
+    }
+    /* actions, animated skeleton poses */
+    if(model->numaction && model->action && !(flags & M3D_EXP_NOACTION)) {
+        for(j = 0; j < model->numaction; j++) {
+            a = &model->action[j];
+            str = _m3d_addstr(str, &numstr, a->name);
+            if(!str) goto memerr;
+            if(a->numframe > 65535) a->numframe = 65535;
+            for(i = 0; i < a->numframe; i++) {
+                l = numactn;
+                numactn += (a->frame[i].numtransform * 2);
+                if(a->frame[i].numtransform > maxt)
+                    maxt = a->frame[i].numtransform;
+                actn = (uint32_t*)M3D_REALLOC(actn, numactn * sizeof(uint32_t));
+                if(!actn) goto memerr;
+                for(k = 0; k < a->frame[i].numtransform; k++) {
+                    if(quality < M3D_EXP_FLOAT) {
+                        _m3d_round(quality, &model->vertex[a->frame[i].transform[k].pos], &vertex);
+                        vrtx = _m3d_addvrtx(vrtx, &numvrtx, &vertex, &actn[l++]);
+                        if(!vrtx) goto memerr;
+                        _m3d_round(quality, &model->vertex[a->frame[i].transform[k].ori], &vertex);
+                        vrtx = _m3d_addvrtx(vrtx, &numvrtx, &vertex, &actn[l++]);
+                    } else {
+                        vrtx = _m3d_addvrtx(vrtx, &numvrtx, &model->vertex[a->frame[i].transform[k].pos], &actn[l++]);
+                        if(!vrtx) goto memerr;
+                        vrtx = _m3d_addvrtx(vrtx, &numvrtx, &model->vertex[a->frame[i].transform[k].ori], &actn[l++]);
+                    }
+                    if(!vrtx) goto memerr;
+                }
+            }
+        }
+    }
+    /* normalize bounding cube and collect referenced skin records */
+    if(numvrtx) {
+        min_x = min_y = min_z = 1e10;
+        max_x = max_y = max_z = -1e10;
+        j = model->numskin && model->skin && !(flags & M3D_EXP_NOBONE);
+        for(i = 0; i < numvrtx; i++) {
+            if(j && model->numskin && model->skin && vrtx[i].skinid < M3D_INDEXMAX) {
+                skin = _m3d_addskin(skin, &numskin, &model->skin[vrtx[i].skinid], &idx);
+                if(!skin) goto memerr;
+                vrtx[i].skinid = idx;
+            }
+            if(vrtx[i].skinid == (M3D_INDEX)-2U) continue;
+            if(vrtx[i].x > max_x) max_x = vrtx[i].x;
+            if(vrtx[i].x < min_x) min_x = vrtx[i].x;
+            if(vrtx[i].y > max_y) max_y = vrtx[i].y;
+            if(vrtx[i].y < min_y) min_y = vrtx[i].y;
+            if(vrtx[i].z > max_z) max_z = vrtx[i].z;
+            if(vrtx[i].z < min_z) min_z = vrtx[i].z;
+        }
+        if(min_x < 0.0f) min_x = -min_x;
+        if(max_x < 0.0f) max_x = -max_x;
+        if(min_y < 0.0f) min_y = -min_y;
+        if(max_y < 0.0f) max_y = -max_y;
+        if(min_z < 0.0f) min_z = -min_z;
+        if(max_z < 0.0f) max_z = -max_z;
+        scale = min_x;
+        if(max_x > scale) scale = max_x;
+        if(min_y > scale) scale = min_y;
+        if(max_y > scale) scale = max_y;
+        if(min_z > scale) scale = min_z;
+        if(max_z > scale) scale = max_z;
+        if(scale == 0.0f) scale = 1.0f;
+        if(scale != 1.0f && !(flags & M3D_EXP_NORECALC)) {
+            for(i = 0; i < numvrtx; i++) {
+                if(vrtx[i].skinid == (M3D_INDEX)-2U) continue;
+                vrtx[i].x /= scale;
+                vrtx[i].y /= scale;
+                vrtx[i].z /= scale;
+            }
+        }
+    }
+    /* if there's only one black color, don't store it */
+    if(numcmap == 1 && cmap && !cmap[0]) numcmap = 0;
+    /* at least 3 UV coordinate required for texture mapping */
+    if(numtmap < 3 && tmap) numtmap = 0;
+    /* meta info */
+    sn = _m3d_safestr(model->name && *model->name ? model->name : (char*)"(noname)", 2);
+    sl = _m3d_safestr(model->license ? model->license : (char*)"MIT", 2);
+    sa = _m3d_safestr(model->author ? model->author : getenv("LOGNAME"), 2);
+    if(!sn || !sl || !sa) {
+memerr: if(face) M3D_FREE(face);
+        if(cmap) M3D_FREE(cmap);
+        if(tmap) M3D_FREE(tmap);
+        if(mtrl) M3D_FREE(mtrl);
+        if(vrtx) M3D_FREE(vrtx);
+        if(bone) M3D_FREE(bone);
+        if(skin) M3D_FREE(skin);
+        if(actn) M3D_FREE(actn);
+        if(sn) M3D_FREE(sn);
+        if(sl) M3D_FREE(sl);
+        if(sa) M3D_FREE(sa);
+        if(sd) M3D_FREE(sd);
+        if(out) M3D_FREE(out);
+        if(str) M3D_FREE(str);
+        if(h) M3D_FREE(h);
+        M3D_LOG("Out of memory");
+        model->errcode = M3D_ERR_ALLOC;
+        return NULL;
+    }
+    if(model->scale > (M3D_FLOAT)0.0) scale = (float)model->scale;
+    if(scale <= 0.0f) scale = 1.0f;
+#ifdef M3D_ASCII
+    if(flags & M3D_EXP_ASCII) {
+        /* use CRLF to make model creators on Win happy... */
+        sd = _m3d_safestr(model->desc, 1);
+        if(!sd) goto memerr;
+        ol = setlocale(LC_NUMERIC, NULL);
+        setlocale(LC_NUMERIC, "C");
+        /* header */
+        len = 64 + strlen(sn) + strlen(sl) + strlen(sa) + strlen(sd);
+        out = (unsigned char*)M3D_MALLOC(len);
+        if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+        ptr = (char*)out;
+        ptr += sprintf(ptr, "3dmodel %g\r\n%s\r\n%s\r\n%s\r\n%s\r\n\r\n", scale,
+            sn, sl, sa, sd);
+        M3D_FREE(sn); M3D_FREE(sl); M3D_FREE(sa); M3D_FREE(sd);
+        sn = sl = sa = sd = NULL;
+        /* texture map */
+        if(numtmap && tmap && !(flags & M3D_EXP_NOTXTCRD) && !(flags & M3D_EXP_NOFACE)) {
+            ptr -= (uint64_t)out; len = (uint64_t)ptr + numtmap * 32 + 12;
+            out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+            if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+            ptr += sprintf(ptr, "Textmap\r\n");
+            for(i = 0; i < numtmap; i++)
+                ptr += sprintf(ptr, "%g %g\r\n", tmap[i].u, tmap[i].v);
+            ptr += sprintf(ptr, "\r\n");
+        }
+        /* vertex chunk */
+        if(numvrtx && vrtx && !(flags & M3D_EXP_NOFACE)) {
+            ptr -= (uint64_t)out; len = (uint64_t)ptr + numvrtx * 128 + 10;
+            out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+            if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+            ptr += sprintf(ptr, "Vertex\r\n");
+            for(i = 0; i < numvrtx; i++) {
+                ptr += sprintf(ptr, "%g %g %g %g", vrtx[i].x, vrtx[i].y, vrtx[i].z, vrtx[i].w);
+                if(!(flags & M3D_EXP_NOCMAP) && vrtx[i].color)
+                    ptr += sprintf(ptr, " #%08x", vrtx[i].color);
+                if(!(flags & M3D_EXP_NOBONE) && numbone && numskin && vrtx[i].skinid != (M3D_INDEX)-1U &&
+                    vrtx[i].skinid != (M3D_INDEX)-2U) {
+                    if(skin[vrtx[i].skinid].weight[0] == (M3D_FLOAT)1.0)
+                        ptr += sprintf(ptr, " %d", skin[vrtx[i].skinid].boneid[0]);
+                    else
+                        for(j = 0; j < M3D_NUMBONE && skin[vrtx[i].skinid].boneid[j] != (M3D_INDEX)-1U &&
+                            skin[vrtx[i].skinid].weight[j] > (M3D_FLOAT)0.0; j++)
+                            ptr += sprintf(ptr, " %d:%g", skin[vrtx[i].skinid].boneid[j],
+                                skin[vrtx[i].skinid].weight[j]);
+                }
+                ptr += sprintf(ptr, "\r\n");
+            }
+            ptr += sprintf(ptr, "\r\n");
+        }
+        /* bones chunk */
+        if(numbone && bone && !(flags & M3D_EXP_NOBONE)) {
+            ptr -= (uint64_t)out; len = (uint64_t)ptr + 9;
+            for(i = 0; i < numbone; i++) {
+                len += strlen(bone[i].name) + 128;
+            }
+            out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+            if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+            ptr += sprintf(ptr, "Bones\r\n");
+            ptr = _m3d_prtbone(ptr, bone, numbone, (M3D_INDEX)-1U, 0);
+            ptr += sprintf(ptr, "\r\n");
+        }
+        /* materials */
+        if(nummtrl && !(flags & M3D_EXP_NOMATERIAL)) {
+            for(j = 0; j < nummtrl; j++) {
+                m = !mtrl ? &model->material[j] : mtrl[j];
+                sn = _m3d_safestr(m->name, 0);
+                if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                ptr -= (uint64_t)out; len = (uint64_t)ptr + strlen(sn) + 12;
+                for(i = 0; i < m->numprop; i++) {
+                    if(m->prop[i].type < 128)
+                        len += 32;
+                    else if(m->prop[i].value.textureid < model->numtexture && model->texture[m->prop[i].value.textureid].name)
+                        len += strlen(model->texture[m->prop[i].value.textureid].name) + 16;
+                }
+                out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+                if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                ptr += sprintf(ptr, "Material %s\r\n", sn);
+                M3D_FREE(sn); sn = NULL;
+                for(i = 0; i < m->numprop; i++) {
+                    k = 256;
+                    if(m->prop[i].type >= 128) {
+                        for(l = 0; l < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); l++)
+                            if(m->prop[i].type == m3d_propertytypes[l].id) {
+                                sn = m3d_propertytypes[l].key;
+                                break;
+                            }
+                        if(!sn)
+                            for(l = 0; l < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); l++)
+                                if(m->prop[i].type - 128 == m3d_propertytypes[l].id) {
+                                    sn = m3d_propertytypes[l].key;
+                                    break;
+                                }
+                        k = sn ? m3dpf_map : 256;
+                    } else {
+                        for(l = 0; l < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); l++)
+                            if(m->prop[i].type == m3d_propertytypes[l].id) {
+                                sn = m3d_propertytypes[l].key;
+                                k = m3d_propertytypes[l].format;
+                                break;
+                            }
+                    }
+                    switch(k) {
+                        case m3dpf_color: ptr += sprintf(ptr, "%s #%08x\r\n", sn, m->prop[i].value.color); break;
+                        case m3dpf_uint8:
+                        case m3dpf_uint16:
+                        case m3dpf_uint32: ptr += sprintf(ptr, "%s %d\r\n", sn, m->prop[i].value.num); break;
+                        case m3dpf_float:  ptr += sprintf(ptr, "%s %g\r\n", sn, m->prop[i].value.fnum); break;
+                        case m3dpf_map:
+                            if(m->prop[i].value.textureid < model->numtexture &&
+                                model->texture[m->prop[i].value.textureid].name) {
+                                sl = _m3d_safestr(model->texture[m->prop[i].value.textureid].name, 0);
+                                if(!sl) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                                if(*sl)
+                                    ptr += sprintf(ptr, "map_%s %s\r\n", sn, sl);
+                                M3D_FREE(sn); M3D_FREE(sl); sl = NULL;
+                            }
+                        break;
+                    }
+                    sn = NULL;
+                }
+                ptr += sprintf(ptr, "\r\n");
+            }
+        }
+        /* mesh face */
+        if(model->numface && face && !(flags & M3D_EXP_NOFACE)) {
+            ptr -= (uint64_t)out; len = (uint64_t)ptr + model->numface * 128 + 6;
+            last = (M3D_INDEX)-1U;
+            if(!(flags & M3D_EXP_NOMATERIAL))
+                for(i = 0; i < model->numface; i++) {
+                    if(face[i].materialid != last) {
+                        last = face[i].materialid;
+                        if(last < nummtrl)
+                            len += strlen(mtrl[last]->name);
+                        len += 6;
+                    }
+                }
+            out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+            if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+            ptr += sprintf(ptr, "Mesh\r\n");
+            last = (M3D_INDEX)-1U;
+            for(i = 0; i < model->numface; i++) {
+                if(!(flags & M3D_EXP_NOMATERIAL) && face[i].materialid != last) {
+                    last = face[i].materialid;
+                    if(last < nummtrl) {
+                        sn = _m3d_safestr(mtrl[last]->name, 0);
+                        if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                        ptr += sprintf(ptr, "use %s\r\n", sn);
+                        M3D_FREE(sn); sn = NULL;
+                    } else
+                        ptr += sprintf(ptr, "use\r\n");
+                }
+                /* hardcoded triangles. Should be repeated as many times as the number of edges in polygon */
+                for(j = 0; j < 3; j++) {
+                    ptr += sprintf(ptr, "%s%d", j?" ":"", face[i].vertex[j]);
+                    if(!(flags & M3D_EXP_NOTXTCRD) && (face[i].texcoord[j] != (M3D_INDEX)-1U))
+                        ptr += sprintf(ptr, "/%d", face[i].texcoord[j]);
+                    if(!(flags & M3D_EXP_NONORMAL) && (face[i].normal[j] != (M3D_INDEX)-1U))
+                        ptr += sprintf(ptr, "%s/%d",
+                            (flags & M3D_EXP_NOTXTCRD) || (face[i].texcoord[j] == (M3D_INDEX)-1U)? "/" : "",
+                            face[i].normal[j]);
+                }
+                ptr += sprintf(ptr, "\r\n");
+            }
+            ptr += sprintf(ptr, "\r\n");
+        }
+        /* actions */
+        if(model->numaction && model->action && numactn && actn && !(flags & M3D_EXP_NOACTION)) {
+            l = 0;
+            for(j = 0; j < model->numaction; j++) {
+                a = &model->action[j];
+                sn = _m3d_safestr(a->name, 0);
+                if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                ptr -= (uint64_t)out; len = (uint64_t)ptr + strlen(sn) + 48;
+                for(i = 0; i < a->numframe; i++)
+                    len += a->frame[i].numtransform * 128 + 8;
+                out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+                if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                ptr += sprintf(ptr, "Action %d %s\r\n", a->durationmsec, sn);
+                M3D_FREE(sn); sn = NULL;
+                if(a->numframe > 65535) a->numframe = 65535;
+                for(i = 0; i < a->numframe; i++) {
+                    ptr += sprintf(ptr, "frame %d\r\n", a->frame[i].msec);
+                    for(k = 0; k < a->frame[i].numtransform; k++) {
+                        ptr += sprintf(ptr, "%d %d %d\r\n", a->frame[i].transform[k].boneid, actn[l], actn[l + 1]);
+                        l += 2;
+                    }
+                }
+                ptr += sprintf(ptr, "\r\n");
+            }
+        }
+        /* extra info */
+        if(model->numunknown && (flags & M3D_EXP_EXTRA)) {
+            for(i = 0; i < model->numunknown; i++) {
+                if(model->unknown[i]->length < 9) continue;
+                ptr -= (uint64_t)out; len = (uint64_t)ptr + 17 + model->unknown[i]->length * 3;
+                out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out;
+                if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; }
+                ptr += sprintf(ptr, "Extra %c%c%c%c\r\n",
+                    model->unknown[i]->magic[0] > ' ' ? model->unknown[i]->magic[0] : '_',
+                    model->unknown[i]->magic[1] > ' ' ? model->unknown[i]->magic[1] : '_',
+                    model->unknown[i]->magic[2] > ' ' ? model->unknown[i]->magic[2] : '_',
+                    model->unknown[i]->magic[3] > ' ' ? model->unknown[i]->magic[3] : '_');
+                for(j = 0; j < model->unknown[i]->length; j++)
+                    ptr += sprintf(ptr, "%02x ", *((unsigned char *)model->unknown + sizeof(m3dchunk_t) + j));
+                ptr--;
+                ptr += sprintf(ptr, "\r\n\r\n");
+            }
+        }
+        setlocale(LC_NUMERIC, ol);
+        len = (uint64_t)ptr - (uint64_t)out;
+        out = (unsigned char*)M3D_REALLOC(out, len + 1);
+        if(!out) goto memerr;
+        out[len] = 0;
+    } else
+#endif
+    {
+        /* stricly only use LF (newline) in binary */
+        sd = _m3d_safestr(model->desc, 3);
+        if(!sd) goto memerr;
+        /* header */
+        h = (m3dhdr_t*)M3D_MALLOC(sizeof(m3dhdr_t) + strlen(sn) + strlen(sl) + strlen(sa) + strlen(sd) + 4);
+        if(!h) goto memerr;
+        memcpy((uint8_t*)h, "HEAD", 4);
+        h->length = sizeof(m3dhdr_t);
+        h->scale = scale;
+        i = strlen(sn); memcpy((uint8_t*)h + h->length, sn, i+1); h->length += i+1; M3D_FREE(sn);
+        i = strlen(sl); memcpy((uint8_t*)h + h->length, sl, i+1); h->length += i+1; M3D_FREE(sl);
+        i = strlen(sa); memcpy((uint8_t*)h + h->length, sa, i+1); h->length += i+1; M3D_FREE(sa);
+        i = strlen(sd); memcpy((uint8_t*)h + h->length, sd, i+1); h->length += i+1; M3D_FREE(sd);
+        sn = sl = sa = sd = NULL;
+        len = 0;
+        if(!bone) numbone = 0;
+        if(skin)
+            for(i = 0; i < numskin; i++) {
+                for(j = k = 0; j < M3D_NUMBONE; j++)
+                    if(skin[i].boneid[j] != (M3D_INDEX)-1U && skin[i].weight[j] > (M3D_FLOAT)0.0) k++;
+                if(k > len) len = k;
+            }
+        else
+            numskin = 0;
+        if(model->inlined)
+            for(i = 0; i < model->numinlined; i++) {
+                if(model->inlined[i].name && *model->inlined[i].name && model->inlined[i].length > 0) {
+                    str = _m3d_addstr(str, &numstr, model->inlined[i].name);
+                    if(!str) goto memerr;
+                }
+            }
+        if(str)
+            for(i = 0; i < numstr; i++) {
+                h = _m3d_addhdr(h, &str[i]);
+                if(!h) goto memerr;
+            }
+        vc_s = quality == M3D_EXP_INT8? 1 : (quality == M3D_EXP_INT16? 2 : (quality == M3D_EXP_DOUBLE? 8 : 4));
+        vi_s = numvrtx < 254 ? 1 : (numvrtx < 65534 ? 2 : 4);
+        si_s = h->length - 16 < 254 ? 1 : (h->length - 16 < 65534 ? 2 : 4);
+        ci_s = !numcmap || !cmap ? 0 : (numcmap < 254 ? 1 : (numcmap < 65534 ? 2 : 4));
+        ti_s = !numtmap || !tmap ? 0 : (numtmap < 254 ? 1 : (numtmap < 65534 ? 2 : 4));
+        bi_s = !numbone || !bone ? 0 : (numbone < 254 ? 1 : (numbone < 65534 ? 2 : 4));
+        nb_s = len < 2 ? 1 : (len == 2 ? 2 : (len <= 4 ? 4 : 8));
+        sk_s = !numbone || !numskin ? 0 : (numskin < 254 ? 1 : (numskin < 65534 ? 2 : 4));
+        fi_s = maxt < 254 ? 1 : (maxt < 65534 ? 2 : 4);
+        h->types =  (vc_s == 8 ? (3<<0) : (vc_s == 2 ? (1<<0) : (vc_s == 1 ? (0<<0) : (2<<0)))) |
+                    (vi_s == 2 ? (1<<2) : (vi_s == 1 ? (0<<2) : (2<<2))) |
+                    (si_s == 2 ? (1<<4) : (si_s == 1 ? (0<<4) : (2<<4))) |
+                    (ci_s == 2 ? (1<<6) : (ci_s == 1 ? (0<<6) : (ci_s == 4 ? (2<<6) : (3<<6)))) |
+                    (ti_s == 2 ? (1<<8) : (ti_s == 1 ? (0<<8) : (ti_s == 4 ? (2<<8) : (3<<8)))) |
+                    (bi_s == 2 ? (1<<10): (bi_s == 1 ? (0<<10): (bi_s == 4 ? (2<<10) : (3<<10)))) |
+                    (nb_s == 2 ? (1<<12): (nb_s == 1 ? (0<<12): (2<<12))) |
+                    (sk_s == 2 ? (1<<14): (sk_s == 1 ? (0<<14): (sk_s == 4 ? (2<<14) : (3<<14)))) |
+                    (fi_s == 2 ? (1<<16): (fi_s == 1 ? (0<<16): (2<<16))) ;
+        len = h->length;
+        /* color map */
+        if(numcmap && cmap && ci_s < 4 && !(flags & M3D_EXP_NOCMAP)) {
+            chunklen = 8 + numcmap * sizeof(uint32_t);
+            h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+            if(!h) goto memerr;
+            memcpy((uint8_t*)h + len, "CMAP", 4);
+            *((uint32_t*)((uint8_t*)h + len + 4)) = chunklen;
+            memcpy((uint8_t*)h + len + 8, cmap, chunklen - 8);
+            len += chunklen;
+        } else numcmap = 0;
+        /* texture map */
+        if(numtmap && tmap && !(flags & M3D_EXP_NOTXTCRD) && !(flags & M3D_EXP_NOFACE)) {
+            chunklen = 8 + numtmap * vc_s * 2;
+            h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+            if(!h) goto memerr;
+            memcpy((uint8_t*)h + len, "TMAP", 4);
+            *((uint32_t*)((uint8_t*)h + len + 4)) = chunklen;
+            out = (uint8_t*)h + len + 8;
+            for(i = 0; i < numtmap; i++) {
+                switch(vc_s) {
+                    case 1: *out++ = (uint8_t)(tmap[i].u * 255); *out++ = (uint8_t)(tmap[i].v * 255); break;
+                    case 2:
+                        *((uint16_t*)out) = (uint16_t)(tmap[i].u * 65535); out += 2;
+                        *((uint16_t*)out) = (uint16_t)(tmap[i].v * 65535); out += 2;
+                    break;
+                    case 4:  *((float*)out) = tmap[i].u; out += 4;  *((float*)out) = tmap[i].v; out += 4; break;
+                    case 8: *((double*)out) = tmap[i].u; out += 8; *((double*)out) = tmap[i].v; out += 8; break;
+                }
+            }
+            out = NULL;
+            len += chunklen;
+        }
+        /* vertex */
+        if(numvrtx && vrtx) {
+            chunklen = 8 + numvrtx * (ci_s + sk_s + 4 * vc_s);
+            h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+            if(!h) goto memerr;
+            memcpy((uint8_t*)h + len, "VRTS", 4);
+            *((uint32_t*)((uint8_t*)h + len + 4)) = chunklen;
+            out = (uint8_t*)h + len + 8;
+            for(i = 0; i < numvrtx; i++) {
+                switch(vc_s) {
+                    case 1:
+                        *out++ = (int8_t)(vrtx[i].x * 127);
+                        *out++ = (int8_t)(vrtx[i].y * 127);
+                        *out++ = (int8_t)(vrtx[i].z * 127);
+                        *out++ = (int8_t)(vrtx[i].w * 127);
+                    break;
+                    case 2:
+                        *((int16_t*)out) = (int16_t)(vrtx[i].x * 32767); out += 2;
+                        *((int16_t*)out) = (int16_t)(vrtx[i].y * 32767); out += 2;
+                        *((int16_t*)out) = (int16_t)(vrtx[i].z * 32767); out += 2;
+                        *((int16_t*)out) = (int16_t)(vrtx[i].w * 32767); out += 2;
+                    break;
+                    case 4:
+                        *((float*)out) = vrtx[i].x; out += 4;
+                        *((float*)out) = vrtx[i].y; out += 4;
+                        *((float*)out) = vrtx[i].z; out += 4;
+                        *((float*)out) = vrtx[i].w; out += 4;
+                    break;
+                    case 8:
+                        *((double*)out) = vrtx[i].x; out += 8;
+                        *((double*)out) = vrtx[i].y; out += 8;
+                        *((double*)out) = vrtx[i].z; out += 8;
+                        *((double*)out) = vrtx[i].w; out += 8;
+                    break;
+                }
+                idx = _m3d_cmapidx(cmap, numcmap, vrtx[i].color);
+                switch(ci_s) {
+                    case 1: *out++ = (uint8_t)(idx); break;
+                    case 2: *((uint16_t*)out) = (uint16_t)(idx); out += 2; break;
+                    case 4: *((uint32_t*)out) = vrtx[i].color; out += 4; break;
+                }
+                out = _m3d_addidx(out, sk_s, numbone && numskin ? vrtx[i].skinid : -1U);
+            }
+            out = NULL;
+            len += chunklen;
+        }
+        /* bones chunk */
+        if(numbone && bone && !(flags & M3D_EXP_NOBONE)) {
+            i = 8 + bi_s + sk_s + numbone * (bi_s + si_s + 2*vi_s);
+            chunklen = i + numskin * nb_s * (bi_s + 1);
+            h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+            if(!h) goto memerr;
+            memcpy((uint8_t*)h + len, "BONE", 4);
+            length = (uint32_t*)((uint8_t*)h + len + 4);
+            out = (uint8_t*)h + len + 8;
+            out = _m3d_addidx(out, bi_s, numbone);
+            out = _m3d_addidx(out, sk_s, numskin);
+            for(i = 0; i < numbone; i++) {
+                out = _m3d_addidx(out, bi_s, bone[i].parent);
+                out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, bone[i].name));
+                out = _m3d_addidx(out, vi_s, bone[i].pos);
+                out = _m3d_addidx(out, vi_s, bone[i].ori);
+            }
+            if(numskin && skin && sk_s) {
+                for(i = 0; i < numskin; i++) {
+                    memset(&weights, 0, nb_s);
+                    for(j = 0; j < (uint32_t)nb_s && skin[i].boneid[j] != (M3D_INDEX)-1U &&
+                        skin[i].weight[j] > (M3D_FLOAT)0.0; j++)
+                            weights[j] = (uint8_t)(skin[i].weight[j] * 255);
+                    switch(nb_s) {
+                        case 1: weights[0] = 255; break;
+                        case 2: *((uint16_t*)out) = *((uint16_t*)&weights[0]); out += 2; break;
+                        case 4: *((uint32_t*)out) = *((uint32_t*)&weights[0]); out += 4; break;
+                        case 8: *((uint64_t*)out) = *((uint64_t*)&weights[0]); out += 8; break;
+                    }
+                    for(j = 0; j < (uint32_t)nb_s && skin[i].boneid[j] != (M3D_INDEX)-1U &&
+                        skin[i].weight[j] > (M3D_FLOAT)0.0; j++) {
+                        out = _m3d_addidx(out, bi_s, skin[i].boneid[j]);
+                        *length += bi_s;
+                    }
+                }
+            }
+            *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len);
+            out = NULL;
+            len += *length;
+        }
+        /* materials */
+        if(nummtrl && !(flags & M3D_EXP_NOMATERIAL)) {
+            for(j = 0; j < nummtrl; j++) {
+                m = !mtrl ? &model->material[j] : mtrl[j];
+                chunklen = 12 + si_s + m->numprop * 5;
+                h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+                if(!h) goto memerr;
+                memcpy((uint8_t*)h + len, "MTRL", 4);
+                length = (uint32_t*)((uint8_t*)h + len + 4);
+                out = (uint8_t*)h + len + 8;
+                out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, m->name));
+                for(i = 0; i < m->numprop; i++) {
+                    if(m->prop[i].type >= 128) {
+                        if(m->prop[i].value.textureid >= model->numtexture ||
+                            !model->texture[m->prop[i].value.textureid].name) continue;
+                        k = m3dpf_map;
+                    } else {
+                        for(k = 256, l = 0; l < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); l++)
+                            if(m->prop[i].type == m3d_propertytypes[l].id) { k = m3d_propertytypes[l].format; break; }
+                    }
+                    if(k == 256) continue;
+                    *out++ = m->prop[i].type;
+                    switch(k) {
+                        case m3dpf_color:
+                            if(!(flags & M3D_EXP_NOCMAP)) {
+                                idx = _m3d_cmapidx(cmap, numcmap, m->prop[i].value.color);
+                                switch(ci_s) {
+                                    case 1: *out++ = (uint8_t)(idx); break;
+                                    case 2: *((uint16_t*)out) = (uint16_t)(idx); out += 2; break;
+                                    case 4: *((uint32_t*)out) = (uint32_t)(m->prop[i].value.color); out += 4; break;
+                                }
+                            } else out--;
+                        break;
+                        case m3dpf_uint8:  *out++ = m->prop[i].value.num; break;
+                        case m3dpf_uint16: *((uint16_t*)out) = m->prop[i].value.num; out += 2; break;
+                        case m3dpf_uint32: *((uint32_t*)out) = m->prop[i].value.num; out += 4; break;
+                        case m3dpf_float:  *((float*)out) = m->prop[i].value.fnum; out += 4; break;
+
+                        case m3dpf_map:
+                            idx = _m3d_stridx(str, numstr, model->texture[m->prop[i].value.textureid].name);
+                            out = _m3d_addidx(out, si_s, idx);
+                        break;
+                    }
+                }
+                *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len);
+                len += *length;
+                out = NULL;
+            }
+        }
+        /* mesh face */
+        if(model->numface && face && !(flags & M3D_EXP_NOFACE)) {
+            chunklen = 8 + si_s + model->numface * (6 * vi_s + 3 * ti_s + si_s + 1);
+            h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+            if(!h) goto memerr;
+            memcpy((uint8_t*)h + len, "MESH", 4);
+            length = (uint32_t*)((uint8_t*)h + len + 4);
+            out = (uint8_t*)h + len + 8;
+            last = (M3D_INDEX)-1U;
+            for(i = 0; i < model->numface; i++) {
+                if(!(flags & M3D_EXP_NOMATERIAL) && face[i].materialid != last) {
+                    last = face[i].materialid;
+                    if(last < nummtrl) {
+                        idx = _m3d_stridx(str, numstr, !mtrl ? model->material[last].name : mtrl[last]->name);
+                        if(idx) {
+                            *out++ = 0;
+                            out = _m3d_addidx(out, si_s, idx);
+                        }
+                    }
+                }
+                /* hardcoded triangles. */
+                k = (3 << 4) |
+                    (((flags & M3D_EXP_NOTXTCRD) || ti_s == 8 || (face[i].texcoord[0] == (M3D_INDEX)-1U &&
+                    face[i].texcoord[1] == (M3D_INDEX)-1U && face[i].texcoord[2] == (M3D_INDEX)-1U)) ? 0 : 1) |
+                    (((flags & M3D_EXP_NONORMAL) || (face[i].normal[0] == (M3D_INDEX)-1U &&
+                    face[i].normal[1] == (M3D_INDEX)-1U && face[i].normal[2] == (M3D_INDEX)-1U)) ? 0 : 2);
+                *out++ = k;
+                for(j = 0; j < 3; j++) {
+                    out = _m3d_addidx(out, vi_s, face[i].vertex[j]);
+                    if(k & 1)
+                        out = _m3d_addidx(out, ti_s, face[i].texcoord[j]);
+                    if(k & 2)
+                        out = _m3d_addidx(out, vi_s, face[i].normal[j]);
+                }
+            }
+            *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len);
+            len += *length;
+            out = NULL;
+        }
+        /* actions */
+        if(model->numaction && model->action && numactn && actn && numbone && bone && !(flags & M3D_EXP_NOACTION)) {
+            l = 0;
+            for(j = 0; j < model->numaction; j++) {
+                a = &model->action[j];
+                chunklen = 14 + si_s + a->numframe * (4 + fi_s + maxt * (bi_s + 2 * vi_s));
+                h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+                if(!h) goto memerr;
+                memcpy((uint8_t*)h + len, "ACTN", 4);
+                length = (uint32_t*)((uint8_t*)h + len + 4);
+                out = (uint8_t*)h + len + 8;
+                out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, a->name));
+                *((uint16_t*)out) = (uint16_t)(a->numframe); out += 2;
+                *((uint32_t*)out) = (uint32_t)(a->durationmsec); out += 4;
+                for(i = 0; i < a->numframe; i++) {
+                    *((uint32_t*)out) = (uint32_t)(a->frame[i].msec); out += 4;
+                    out = _m3d_addidx(out, fi_s, a->frame[i].numtransform);
+                    for(k = 0; k < a->frame[i].numtransform; k++) {
+                        out = _m3d_addidx(out, bi_s, a->frame[i].transform[k].boneid);
+                        out = _m3d_addidx(out, vi_s, actn[l++]);
+                        out = _m3d_addidx(out, vi_s, actn[l++]);
+                    }
+                }
+                *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len);
+                len += *length;
+                out = NULL;
+            }
+        }
+        /* inlined assets */
+        if(model->numinlined && model->inlined && (flags & M3D_EXP_INLINE)) {
+            for(j = 0; j < model->numinlined; j++) {
+                if(!model->inlined[j].name || !*model->inlined[j].name || !model->inlined[j].length)
+                    continue;
+                chunklen = 8 + si_s + model->inlined[j].length;
+                h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+                if(!h) goto memerr;
+                memcpy((uint8_t*)h + len, "ASET", 4);
+                *((uint32_t*)((uint8_t*)h + len + 4)) = chunklen;
+                out = (uint8_t*)h + len + 8;
+                out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, model->inlined[j].name));
+                memcpy(out, model->inlined[j].data, model->inlined[j].length);
+                out = NULL;
+                len += chunklen;
+            }
+        }
+        /* extra chunks */
+        if(model->numunknown && model->unknown && (flags & M3D_EXP_EXTRA)) {
+            for(j = 0; j < model->numunknown; j++) {
+                if(!model->unknown[j] || model->unknown[j]->length < 8)
+                    continue;
+                chunklen = model->unknown[j]->length;
+                h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen);
+                if(!h) goto memerr;
+                memcpy((uint8_t*)h + len, model->unknown[j], chunklen);
+                len += chunklen;
+            }
+        }
+        /* add end chunk */
+        h = (m3dhdr_t*)M3D_REALLOC(h, len + 4);
+        if(!h) goto memerr;
+        memcpy((uint8_t*)h + len, "OMD3", 4);
+        len += 4;
+        /* zlib compress */
+        if(!(flags & M3D_EXP_NOZLIB)) {
+            z = stbi_zlib_compress((unsigned char *)h, len, (int*)&l, 9);
+            if(z && l > 0 && l < len) { len = l; M3D_FREE(h); h = (m3dhdr_t*)z; }
+        }
+        /* add file header at the begining */
+        len += 8;
+        out = (unsigned char*)M3D_MALLOC(len);
+        if(!out) goto memerr;
+        memcpy(out, "3DMO", 4);
+        *((uint32_t*)(out + 4)) = len;
+        memcpy(out + 8, h, len - 8);
+    }
+    if(size) *size = out ? len : 0;
+    if(face) M3D_FREE(face);
+    if(cmap) M3D_FREE(cmap);
+    if(tmap) M3D_FREE(tmap);
+    if(mtrl) M3D_FREE(mtrl);
+    if(vrtx) M3D_FREE(vrtx);
+    if(bone) M3D_FREE(bone);
+    if(skin) M3D_FREE(skin);
+    if(actn) M3D_FREE(actn);
+    if(str) M3D_FREE(str);
+    if(h) M3D_FREE(h);
+    return out;
+}
+#endif
+
+#endif
+
+#ifdef  __cplusplus
+}
+#ifdef M3D_CPPWRAPPER
+#include <vector>
+#include <string>
+#include <memory>
+
+/*** C++ wrapper class ***/
+namespace M3D {
+#ifdef M3D_IMPLEMENTATION
+
+    class Model {
+        public:
+            m3d_t *model;
+
+        public:
+            Model() {
+                this->model = (m3d_t*)malloc(sizeof(m3d_t)); memset(this->model, 0, sizeof(m3d_t));
+            }
+            Model(_unused const std::string &data, _unused m3dread_t ReadFileCB,
+                _unused m3dfree_t FreeCB, _unused M3D::Model mtllib) {
+#ifndef M3D_NOIMPORTER
+                this->model = m3d_load((unsigned char *)data.data(), ReadFileCB, FreeCB, mtllib.model);
+#else
+                Model();
+#endif
+            }
+            Model(_unused const std::vector<unsigned char> data, _unused m3dread_t ReadFileCB,
+                _unused m3dfree_t FreeCB, _unused M3D::Model mtllib) {
+#ifndef M3D_NOIMPORTER
+                this->model = m3d_load((unsigned char *)&data[0], ReadFileCB, FreeCB, mtllib.model);
+#else
+                Model();
+#endif
+            }
+            ~Model() { m3d_free(this->model); }
+
+        public:
+            m3d_t *getCStruct() { return this->model; }
+            std::string getName() { return std::string(this->model->name); }
+            void setName(std::string name) { this->model->name = (char*)name.c_str(); }
+            std::string getLicense() { return std::string(this->model->license); }
+            void setLicense(std::string license) { this->model->license = (char*)license.c_str(); }
+            std::string getAuthor() { return std::string(this->model->author); }
+            void setAuthor(std::string author) { this->model->author = (char*)author.c_str(); }
+            std::string getDescription() { return std::string(this->model->desc); }
+            void setDescription(std::string desc) { this->model->desc = (char*)desc.c_str(); }
+            float getScale() { return this->model->scale; }
+            void setScale(float scale) { this->model->scale = scale; }
+            std::vector<uint32_t> getColorMap() { return this->model->cmap ? std::vector<uint32_t>(this->model->cmap,
+                this->model->cmap + this->model->numcmap) : std::vector<uint32_t>(); }
+            std::vector<m3dti_t> getTextureMap() { return this->model->tmap ? std::vector<m3dti_t>(this->model->tmap,
+                this->model->tmap + this->model->numtmap) : std::vector<m3dti_t>(); }
+            std::vector<m3dtx_t> getTextures() { return this->model->texture ? std::vector<m3dtx_t>(this->model->texture,
+                this->model->texture + this->model->numtexture) : std::vector<m3dtx_t>(); }
+            std::string getTextureName(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numtexture ?
+                std::string(this->model->texture[idx].name) : nullptr; }
+            std::vector<m3db_t> getBones() { return this->model->bone ? std::vector<m3db_t>(this->model->bone, this->model->bone +
+                this->model->numbone) : std::vector<m3db_t>(); }
+            std::string getBoneName(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numbone ?
+                std::string(this->model->bone[idx].name) : nullptr; }
+            std::vector<m3dm_t> getMaterials() { return this->model->material ? std::vector<m3dm_t>(this->model->material,
+                this->model->material + this->model->nummaterial) : std::vector<m3dm_t>(); }
+            std::string getMaterialName(int idx) { return idx >= 0 && (unsigned int)idx < this->model->nummaterial ?
+                std::string(this->model->material[idx].name) : nullptr; }
+            int getMaterialPropertyInt(int idx, int type) {
+                    if (idx < 0 || (unsigned int)idx >= this->model->nummaterial || type < 0 || type >= 127 ||
+                        !this->model->material[idx].prop) return -1;
+                    for (int i = 0; i < this->model->material[idx].numprop; i++) {
+                        if (this->model->material[idx].prop[i].type == type)
+                            return this->model->material[idx].prop[i].value.num;
+                    }
+                    return -1;
+                }
+            uint32_t getMaterialPropertyColor(int idx, int type) { return this->getMaterialPropertyInt(idx, type); }
+            float getMaterialPropertyFloat(int idx, int type) {
+                    if (idx < 0 || (unsigned int)idx >= this->model->nummaterial || type < 0 || type >= 127 ||
+                        !this->model->material[idx].prop) return -1.0f;
+                    for (int i = 0; i < this->model->material[idx].numprop; i++) {
+                        if (this->model->material[idx].prop[i].type == type)
+                            return this->model->material[idx].prop[i].value.fnum;
+                    }
+                    return -1.0f;
+                }
+            m3dtx_t* getMaterialPropertyMap(int idx, int type) {
+                    if (idx < 0 || (unsigned int)idx >= this->model->nummaterial || type < 128 || type > 255 ||
+                        !this->model->material[idx].prop) return nullptr;
+                    for (int i = 0; i < this->model->material[idx].numprop; i++) {
+                        if (this->model->material[idx].prop[i].type == type)
+                            return this->model->material[idx].prop[i].value.textureid < this->model->numtexture ?
+                                &this->model->texture[this->model->material[idx].prop[i].value.textureid] : nullptr;
+                    }
+                    return nullptr;
+                }
+            std::vector<m3dv_t> getVertices() { return this->model->vertex ? std::vector<m3dv_t>(this->model->vertex,
+                this->model->vertex + this->model->numvertex) : std::vector<m3dv_t>(); }
+            std::vector<m3df_t> getFace() { return this->model->face ? std::vector<m3df_t>(this->model->face, this->model->face +
+                this->model->numface) : std::vector<m3df_t>(); }
+            std::vector<m3ds_t> getSkin() { return this->model->skin ? std::vector<m3ds_t>(this->model->skin, this->model->skin +
+                this->model->numskin) : std::vector<m3ds_t>(); }
+            std::vector<m3da_t> getActions() { return this->model->action ? std::vector<m3da_t>(this->model->action,
+                this->model->action + this->model->numaction) : std::vector<m3da_t>(); }
+            std::string getActionName(int aidx) { return aidx >= 0 && (unsigned int)aidx < this->model->numaction ?
+                std::string(this->model->action[aidx].name) : nullptr; }
+            unsigned int getActionDuration(int aidx) { return aidx >= 0 && (unsigned int)aidx < this->model->numaction ?
+                this->model->action[aidx].durationmsec : 0; }
+            std::vector<m3dfr_t> getActionFrames(int aidx) { return aidx >= 0 && (unsigned int)aidx < this->model->numaction ?
+                std::vector<m3dfr_t>(this->model->action[aidx].frame, this->model->action[aidx].frame +
+                this->model->action[aidx].numframe) : std::vector<m3dfr_t>(); }
+            unsigned int getActionFrameTimestamp(int aidx, int fidx) { return aidx >= 0 && (unsigned int)aidx < this->model->numaction?
+                    (fidx >= 0 && (unsigned int)fidx < this->model->action[aidx].numframe ?
+                    this->model->action[aidx].frame[fidx].msec : 0) : 0; }
+            std::vector<m3dtr_t> getActionFrameTransforms(int aidx, int fidx) {
+                return aidx >= 0 && (unsigned int)aidx < this->model->numaction ? (
+                    fidx >= 0 && (unsigned int)fidx < this->model->action[aidx].numframe ?
+                    std::vector<m3dtr_t>(this->model->action[aidx].frame[fidx].transform,
+                    this->model->action[aidx].frame[fidx].transform + this->model->action[aidx].frame[fidx].numtransform) :
+                    std::vector<m3dtr_t>()) : std::vector<m3dtr_t>(); }
+            std::vector<m3dtr_t> getActionFrame(int aidx, int fidx, std::vector<m3dtr_t> skeleton) {
+                m3dtr_t *pose = m3d_frame(this->model, (unsigned int)aidx, (unsigned int)fidx,
+                    skeleton.size() ? &skeleton[0] : nullptr);
+                return std::vector<m3dtr_t>(pose, pose + this->model->numbone); }
+            std::vector<m3db_t> getActionPose(int aidx, unsigned int msec) {
+                m3db_t *pose = m3d_pose(this->model, (unsigned int)aidx, (unsigned int)msec);
+                return std::vector<m3db_t>(pose, pose + this->model->numbone); }
+            std::vector<m3di_t> getInlinedAssets() { return this->model->inlined ? std::vector<m3di_t>(this->model->inlined,
+                this->model->inlined + this->model->numinlined) : std::vector<m3di_t>(); }
+            std::vector<std::unique_ptr<m3dchunk_t>> getUnknowns() { return this->model->unknown ?
+                std::vector<std::unique_ptr<m3dchunk_t>>(this->model->unknown,
+                this->model->unknown + this->model->numunknown) : std::vector<std::unique_ptr<m3dchunk_t>>(); }
+            std::vector<unsigned char> Save(_unused int quality, _unused int flags) {
+#ifdef M3D_EXPORTER
+                unsigned int size;
+                unsigned char *ptr = m3d_save(this->model, quality, flags, &size);
+                return ptr && size ? std::vector<unsigned char>(ptr, ptr + size) : std::vector<unsigned char>();
+#else
+                return std::vector<unsigned char>();
+#endif
+            }
+    };
+
+#else
+    class Model {
+        public:
+            m3d_t *model;
+
+        public:
+            Model(const std::string &data, m3dread_t ReadFileCB, m3dfree_t FreeCB);
+            Model(const std::vector<unsigned char> data, m3dread_t ReadFileCB, m3dfree_t FreeCB);
+            Model();
+            ~Model();
+
+        public:
+            m3d_t *getCStruct();
+            std::string getName();
+            void setName(std::string name);
+            std::string getLicense();
+            void setLicense(std::string license);
+            std::string getAuthor();
+            void setAuthor(std::string author);
+            std::string getDescription();
+            void setDescription(std::string desc);
+            float getScale();
+            void setScale(float scale);
+            std::vector<uint32_t> getColorMap();
+            std::vector<m3dti_t> getTextureMap();
+            std::vector<m3dtx_t> getTextures();
+            std::string getTextureName(int idx);
+            std::vector<m3db_t> getBones();
+            std::string getBoneName(int idx);
+            std::vector<m3dm_t> getMaterials();
+            std::string getMaterialName(int idx);
+            int getMaterialPropertyInt(int idx, int type);
+            uint32_t getMaterialPropertyColor(int idx, int type);
+            float getMaterialPropertyFloat(int idx, int type);
+            m3dtx_t* getMaterialPropertyMap(int idx, int type);
+            std::vector<m3dv_t> getVertices();
+            std::vector<m3df_t> getFace();
+            std::vector<m3ds_t> getSkin();
+            std::vector<m3da_t> getActions();
+            std::string getActionName(int aidx);
+            unsigned int getActionDuration(int aidx);
+            std::vector<m3dfr_t> getActionFrames(int aidx);
+            unsigned int getActionFrameTimestamp(int aidx, int fidx);
+            std::vector<m3dtr_t> getActionFrameTransforms(int aidx, int fidx);
+            std::vector<m3dtr_t> getActionFrame(int aidx, int fidx, std::vector<m3dtr_t> skeleton);
+            std::vector<m3db_t> getActionPose(int aidx, unsigned int msec);
+            std::vector<m3di_t> getInlinedAssets();
+            std::vector<std::unique_ptr<m3dchunk_t>> getUnknowns();
+            std::vector<unsigned char> Save(int quality, int flags);
+    };
+
+#endif /* impl */
+}
+#endif
+
+#endif /* __cplusplus */
+
+#endif

+ 268 - 0
code/PostProcessing/ArmaturePopulate.cpp

@@ -0,0 +1,268 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+#include "ArmaturePopulate.h"
+
+#include <assimp/BaseImporter.h>
+#include <assimp/DefaultLogger.hpp>
+#include <assimp/postprocess.h>
+#include <assimp/scene.h>
+#include <iostream>
+
+namespace Assimp {
+
+/// The default class constructor.
+ArmaturePopulate::ArmaturePopulate() : BaseProcess()
+{}
+
+/// The class destructor.
+ArmaturePopulate::~ArmaturePopulate() 
+{}
+
+bool ArmaturePopulate::IsActive(unsigned int pFlags) const {
+  return (pFlags & aiProcess_PopulateArmatureData) != 0;
+}
+
+void ArmaturePopulate::SetupProperties(const Importer *pImp) {
+  // do nothing
+}
+
+void ArmaturePopulate::Execute(aiScene *out) {
+
+  // Now convert all bone positions to the correct mOffsetMatrix
+  std::vector<aiBone *> bones;
+  std::vector<aiNode *> nodes;
+  std::map<aiBone *, aiNode *> bone_stack;
+  BuildBoneList(out->mRootNode, out->mRootNode, out, bones);
+  BuildNodeList(out->mRootNode, nodes);
+
+  BuildBoneStack(out->mRootNode, out->mRootNode, out, bones, bone_stack, nodes);
+
+  ASSIMP_LOG_DEBUG_F("Bone stack size: ", bone_stack.size());
+
+  for (std::pair<aiBone *, aiNode *> kvp : bone_stack) {
+    aiBone *bone = kvp.first;
+    aiNode *bone_node = kvp.second;
+    ASSIMP_LOG_DEBUG_F("active node lookup: ", bone->mName.C_Str());
+    // lcl transform grab - done in generate_nodes :)
+
+    // bone->mOffsetMatrix = bone_node->mTransformation;
+    aiNode *armature = GetArmatureRoot(bone_node, bones);
+
+    ai_assert(armature);
+
+    // set up bone armature id
+    bone->mArmature = armature;
+
+    // set this bone node to be referenced properly
+    ai_assert(bone_node);
+    bone->mNode = bone_node;
+  }
+}
+
+
+/* Reprocess all nodes to calculate bone transforms properly based on the REAL
+ * mOffsetMatrix not the local. */
+/* Before this would use mesh transforms which is wrong for bone transforms */
+/* Before this would work for simple character skeletons but not complex meshes
+ * with multiple origins */
+/* Source: sketch fab log cutter fbx */
+void ArmaturePopulate::BuildBoneList(aiNode *current_node,
+                                     const aiNode *root_node,
+                                     const aiScene *scene,
+                                     std::vector<aiBone *> &bones) {
+  ai_assert(scene);
+  for (unsigned int nodeId = 0; nodeId < current_node->mNumChildren; ++nodeId) {
+    aiNode *child = current_node->mChildren[nodeId];
+    ai_assert(child);
+
+    // check for bones
+    for (unsigned int meshId = 0; meshId < child->mNumMeshes; ++meshId) {
+      ai_assert(child->mMeshes);
+      unsigned int mesh_index = child->mMeshes[meshId];
+      aiMesh *mesh = scene->mMeshes[mesh_index];
+      ai_assert(mesh);
+
+      for (unsigned int boneId = 0; boneId < mesh->mNumBones; ++boneId) {
+        aiBone *bone = mesh->mBones[boneId];
+        ai_assert(bone);
+
+        // duplicate meshes exist with the same bones sometimes :)
+        // so this must be detected
+        if (std::find(bones.begin(), bones.end(), bone) == bones.end()) {
+          // add the element once
+          bones.push_back(bone);
+        }
+      }
+
+      // find mesh and get bones
+      // then do recursive lookup for bones in root node hierarchy
+    }
+
+    BuildBoneList(child, root_node, scene, bones);
+  }
+}
+
+/* Prepare flat node list which can be used for non recursive lookups later */
+void ArmaturePopulate::BuildNodeList(const aiNode *current_node,
+                                     std::vector<aiNode *> &nodes) {
+  ai_assert(current_node);
+
+  for (unsigned int nodeId = 0; nodeId < current_node->mNumChildren; ++nodeId) {
+    aiNode *child = current_node->mChildren[nodeId];
+    ai_assert(child);
+
+    nodes.push_back(child);
+
+    BuildNodeList(child, nodes);
+  }
+}
+
+/* A bone stack allows us to have multiple armatures, with the same bone names
+ * A bone stack allows us also to retrieve bones true transform even with
+ * duplicate names :)
+ */
+void ArmaturePopulate::BuildBoneStack(aiNode *current_node,
+                                      const aiNode *root_node,
+                                      const aiScene *scene,
+                                      const std::vector<aiBone *> &bones,
+                                      std::map<aiBone *, aiNode *> &bone_stack,
+                                      std::vector<aiNode *> &node_stack) {
+  ai_assert(scene);
+  ai_assert(root_node);
+  ai_assert(!node_stack.empty());
+
+  for (aiBone *bone : bones) {
+    ai_assert(bone);
+    aiNode *node = GetNodeFromStack(bone->mName, node_stack);
+    if (node == nullptr) {
+      node_stack.clear();
+      BuildNodeList(root_node, node_stack);
+      ASSIMP_LOG_DEBUG_F("Resetting bone stack: nullptr element ", bone->mName.C_Str());
+
+      node = GetNodeFromStack(bone->mName, node_stack);
+
+      if (!node) {
+        ASSIMP_LOG_ERROR("serious import issue node for bone was not detected");
+        continue;
+      }
+    }
+
+    ASSIMP_LOG_DEBUG_F("Successfully added bone[", bone->mName.C_Str(), "] to stack and bone node is: ", node->mName.C_Str());
+
+    bone_stack.insert(std::pair<aiBone *, aiNode *>(bone, node));
+  }
+}
+
+
+/* Returns the armature root node */
+/* This is required to be detected for a bone initially, it will recurse up
+ * until it cannot find another bone and return the node No known failure
+ * points. (yet)
+ */
+aiNode *ArmaturePopulate::GetArmatureRoot(aiNode *bone_node,
+                                          std::vector<aiBone *> &bone_list) {
+  while (bone_node) {
+    if (!IsBoneNode(bone_node->mName, bone_list)) {
+      ASSIMP_LOG_DEBUG_F("GetArmatureRoot() Found valid armature: ", bone_node->mName.C_Str());
+      return bone_node;
+    }
+
+    bone_node = bone_node->mParent;
+  }
+  
+  ASSIMP_LOG_ERROR("GetArmatureRoot() can't find armature!");
+  
+  return nullptr;
+}
+
+
+
+/* Simple IsBoneNode check if this could be a bone */
+bool ArmaturePopulate::IsBoneNode(const aiString &bone_name,
+                                  std::vector<aiBone *> &bones) {
+  for (aiBone *bone : bones) {
+    if (bone->mName == bone_name) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/* Pop this node by name from the stack if found */
+/* Used in multiple armature situations with duplicate node / bone names */
+/* Known flaw: cannot have nodes with bone names, will be fixed in later release
+ */
+/* (serious to be fixed) Known flaw: nodes which have more than one bone could
+ * be prematurely dropped from stack */
+aiNode *ArmaturePopulate::GetNodeFromStack(const aiString &node_name,
+                                           std::vector<aiNode *> &nodes) {
+  std::vector<aiNode *>::iterator iter;
+  aiNode *found = nullptr;
+  for (iter = nodes.begin(); iter < nodes.end(); ++iter) {
+    aiNode *element = *iter;
+    ai_assert(element);
+    // node valid and node name matches
+    if (element->mName == node_name) {
+      found = element;
+      break;
+    }
+  }
+
+  if (found != nullptr) {
+    ASSIMP_LOG_INFO_F("Removed node from stack: ", found->mName.C_Str());
+    // now pop the element from the node list
+    nodes.erase(iter);
+
+    return found;
+  }
+
+  // unique names can cause this problem
+  ASSIMP_LOG_ERROR("[Serious] GetNodeFromStack() can't find node from stack!");
+
+  return nullptr;
+}
+
+
+
+
+} // Namespace Assimp

+ 112 - 0
code/PostProcessing/ArmaturePopulate.h

@@ -0,0 +1,112 @@
+/*
+Open Asset Import Library (assimp)
+----------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the
+following conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+*/
+#ifndef ARMATURE_POPULATE_H_
+#define ARMATURE_POPULATE_H_
+
+#include "Common/BaseProcess.h"
+#include <assimp/BaseImporter.h>
+#include <vector>
+#include <map>
+
+
+struct aiNode;
+struct aiBone;
+
+namespace Assimp {
+
+// ---------------------------------------------------------------------------
+/** Armature Populate: This is a post process designed
+ * To save you time when importing models into your game engines
+ * This was originally designed only for fbx but will work with other formats
+ * it is intended to auto populate aiBone data with armature and the aiNode
+ * This is very useful when dealing with skinned meshes
+ * or when dealing with many different skeletons
+ * It's off by default but recommend that you try it and use it
+ * It should reduce down any glue code you have in your
+ * importers
+ * You can contact RevoluPowered <[email protected]>
+ * For more info about this
+*/
+class ASSIMP_API ArmaturePopulate : public BaseProcess {
+public:
+    /// The default class constructor.
+    ArmaturePopulate();
+
+    /// The class destructor.
+    virtual ~ArmaturePopulate();
+
+    /// Overwritten, @see BaseProcess
+    virtual bool IsActive( unsigned int pFlags ) const;
+
+    /// Overwritten, @see BaseProcess
+    virtual void SetupProperties( const Importer* pImp );
+
+    /// Overwritten, @see BaseProcess
+    virtual void Execute( aiScene* pScene );
+
+    static aiNode *GetArmatureRoot(aiNode *bone_node,
+                                      std::vector<aiBone *> &bone_list);
+
+    static bool IsBoneNode(const aiString &bone_name,
+                              std::vector<aiBone *> &bones);
+
+    static aiNode *GetNodeFromStack(const aiString &node_name,
+                                       std::vector<aiNode *> &nodes);
+
+    static void BuildNodeList(const aiNode *current_node,
+                                 std::vector<aiNode *> &nodes);
+
+    static void BuildBoneList(aiNode *current_node, const aiNode *root_node,
+                                 const aiScene *scene,
+                                 std::vector<aiBone *> &bones);                        
+
+    static void BuildBoneStack(aiNode *current_node, const aiNode *root_node,
+                                  const aiScene *scene,
+                                  const std::vector<aiBone *> &bones,
+                                  std::map<aiBone *, aiNode *> &bone_stack,
+                                  std::vector<aiNode *> &node_stack);
+};
+
+} // Namespace Assimp
+
+
+#endif // SCALE_PROCESS_H_

+ 1 - 1
code/glTF2/glTF2AssetWriter.inl

@@ -358,7 +358,7 @@ namespace glTF2 {
             WriteVec(pbrSpecularGlossiness, pbrSG.specularFactor, "specularFactor", defaultSpecularFactor, w.mAl);
 
             if (pbrSG.glossinessFactor != 1) {
-                WriteFloat(obj, pbrSG.glossinessFactor, "glossinessFactor", w.mAl);
+                WriteFloat(pbrSpecularGlossiness, pbrSG.glossinessFactor, "glossinessFactor", w.mAl);
             }
 
             WriteTex(pbrSpecularGlossiness, pbrSG.diffuseTexture, "diffuseTexture", w.mAl);

+ 3 - 1
code/glTF2/glTF2Exporter.cpp

@@ -320,7 +320,9 @@ void glTF2Exporter::GetMatTex(const aiMaterial* mat, Ref<Texture>& texture, aiTe
                     if (path[0] == '*') { // embedded
                         aiTexture* tex = mScene->mTextures[atoi(&path[1])];
 
-                        uint8_t* data = reinterpret_cast<uint8_t*>(tex->pcData);
+                        // copy data since lifetime control is handed over to the asset
+                        uint8_t* data = new uint8_t[tex->mWidth];
+                        memcpy(data, tex->pcData, tex->mWidth);
                         texture->source->SetData(data, tex->mWidth, *mAsset);
 
                         if (tex->achFormatHint[0]) {

+ 8 - 2
contrib/irrXML/CXMLReaderImpl.h

@@ -15,6 +15,9 @@
 #include <cstdint>
 //using namespace Assimp;
 
+// For locale independent number conversion
+#include <sstream>
+#include <locale>
 
 #ifdef _DEBUG
 #define IRR_DEBUGPRINT(x) printf((x));
@@ -178,8 +181,11 @@ public:
 			return 0;
 
 		core::stringc c = attrvalue;
-        return static_cast<float>(atof(c.c_str()));
-		//return fast_atof(c.c_str());
+		std::istringstream sstr(c.c_str());
+		sstr.imbue(std::locale("C")); // Locale free number convert
+		float fNum;
+		sstr >> fNum;
+		return fNum;
 	}
 
 

+ 0 - 2
include/assimp/Exporter.hpp

@@ -5,8 +5,6 @@ Open Asset Import Library (assimp)
 
 Copyright (c) 2006-2019, assimp team
 
-
-
 All rights reserved.
 
 Redistribution and use of this software in source and binary forms,

+ 13 - 8
include/assimp/defs.h

@@ -128,16 +128,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  * GENBOUNDINGBOXES */
 //////////////////////////////////////////////////////////////////////////
 
-#ifdef _MSC_VER
+#ifdef _WIN32
 #   undef ASSIMP_API
-
     //////////////////////////////////////////////////////////////////////////
     /* Define 'ASSIMP_BUILD_DLL_EXPORT' to build a DLL of the library */
     //////////////////////////////////////////////////////////////////////////
 #   ifdef ASSIMP_BUILD_DLL_EXPORT
 #       define ASSIMP_API __declspec(dllexport)
 #       define ASSIMP_API_WINONLY __declspec(dllexport)
-#       pragma warning (disable : 4251)
 
     //////////////////////////////////////////////////////////////////////////
     /* Define 'ASSIMP_DLL' before including Assimp to link to ASSIMP in
@@ -150,7 +148,19 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #       define ASSIMP_API
 #       define ASSIMP_API_WINONLY
 #   endif
+#elif defined(SWIG)
+
+    /* Do nothing, the relevant defines are all in AssimpSwigPort.i */
+
+#else
+#   define ASSIMP_API __attribute__ ((visibility("default")))
+#   define ASSIMP_API_WINONLY
+#endif
 
+#ifdef _MSC_VER
+#   ifdef ASSIMP_BUILD_DLL_EXPORT
+#       pragma warning (disable : 4251)
+#   endif
     /* Force the compiler to inline a function, if possible
      */
 #   define AI_FORCE_INLINE __forceinline
@@ -158,17 +168,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     /* Tells the compiler that a function never returns. Used in code analysis
      * to skip dead paths (e.g. after an assertion evaluated to false). */
 #   define AI_WONT_RETURN __declspec(noreturn)
-
 #elif defined(SWIG)
 
     /* Do nothing, the relevant defines are all in AssimpSwigPort.i */
 
 #else
-
 #   define AI_WONT_RETURN
-
-#   define ASSIMP_API __attribute__ ((visibility("default")))
-#   define ASSIMP_API_WINONLY
 #   define AI_FORCE_INLINE inline
 #endif // (defined _MSC_VER)
 

+ 26 - 10
include/assimp/mesh.h

@@ -252,6 +252,9 @@ struct aiVertexWeight {
 };
 
 
+// Forward declare aiNode (pointer use only)
+struct aiNode;
+
 // ---------------------------------------------------------------------------
 /** @brief A single bone of a mesh.
  *
@@ -268,6 +271,16 @@ struct aiBone {
     //! The maximum value for this member is #AI_MAX_BONE_WEIGHTS.
     unsigned int mNumWeights;
 
+#ifndef ASSIMP_BUILD_NO_ARMATUREPOPULATE_PROCESS
+    // The bone armature node - used for skeleton conversion
+    // you must enable aiProcess_PopulateArmatureData to populate this
+    C_STRUCT aiNode* mArmature;
+
+    // The bone node in the scene - used for skeleton conversion
+    // you must enable aiProcess_PopulateArmatureData to populate this
+    C_STRUCT aiNode* mNode;
+
+#endif
     //! The influence weights of this bone, by vertex index.
     C_STRUCT aiVertexWeight* mWeights;
 
@@ -422,11 +435,11 @@ struct aiAnimMesh
     /**Anim Mesh name */
     C_STRUCT aiString mName;
 
-    /** Replacement for aiMesh::mVertices. If this array is non-NULL,
+    /** Replacement for aiMesh::mVertices. If this array is non-nullptr,
      *  it *must* contain mNumVertices entries. The corresponding
-     *  array in the host mesh must be non-NULL as well - animation
+     *  array in the host mesh must be non-nullptr as well - animation
      *  meshes may neither add or nor remove vertex components (if
-     *  a replacement array is NULL and the corresponding source
+     *  a replacement array is nullptr and the corresponding source
      *  array is not, the source data is taken instead)*/
     C_STRUCT aiVector3D* mVertices;
 
@@ -600,7 +613,7 @@ struct aiMesh
     C_STRUCT aiVector3D* mVertices;
 
     /** Vertex normals.
-    * The array contains normalized vectors, NULL if not present.
+    * The array contains normalized vectors, nullptr if not present.
     * The array is mNumVertices in size. Normals are undefined for
     * point and line primitives. A mesh consisting of points and
     * lines only may not have normal vectors. Meshes with mixed
@@ -623,7 +636,7 @@ struct aiMesh
 
     /** Vertex tangents.
     * The tangent of a vertex points in the direction of the positive
-    * X texture axis. The array contains normalized vectors, NULL if
+    * X texture axis. The array contains normalized vectors, nullptr if
     * not present. The array is mNumVertices in size. A mesh consisting
     * of points and lines only may not have normal vectors. Meshes with
     * mixed primitive types (i.e. lines and triangles) may have
@@ -637,7 +650,7 @@ struct aiMesh
 
     /** Vertex bitangents.
     * The bitangent of a vertex points in the direction of the positive
-    * Y texture axis. The array contains normalized vectors, NULL if not
+    * Y texture axis. The array contains normalized vectors, nullptr if not
     * present. The array is mNumVertices in size.
     * @note If the mesh contains tangents, it automatically also contains
     * bitangents.
@@ -646,14 +659,14 @@ struct aiMesh
 
     /** Vertex color sets.
     * A mesh may contain 0 to #AI_MAX_NUMBER_OF_COLOR_SETS vertex
-    * colors per vertex. NULL if not present. Each array is
+    * colors per vertex. nullptr if not present. Each array is
     * mNumVertices in size if present.
     */
     C_STRUCT aiColor4D* mColors[AI_MAX_NUMBER_OF_COLOR_SETS];
 
     /** Vertex texture coords, also known as UV channels.
     * A mesh may contain 0 to AI_MAX_NUMBER_OF_TEXTURECOORDS per
-    * vertex. NULL if not present. The array is mNumVertices in size.
+    * vertex. nullptr if not present. The array is mNumVertices in size.
     */
     C_STRUCT aiVector3D* mTextureCoords[AI_MAX_NUMBER_OF_TEXTURECOORDS];
 
@@ -675,7 +688,7 @@ struct aiMesh
     C_STRUCT aiFace* mFaces;
 
     /** The number of bones this mesh contains.
-    * Can be 0, in which case the mBones array is NULL.
+    * Can be 0, in which case the mBones array is nullptr.
     */
     unsigned int mNumBones;
 
@@ -773,7 +786,10 @@ struct aiMesh
         // DO NOT REMOVE THIS ADDITIONAL CHECK
         if (mNumBones && mBones)    {
             for( unsigned int a = 0; a < mNumBones; a++) {
-                delete mBones[a];
+                if(mBones[a])
+                {
+                    delete mBones[a];
+                }
             }
             delete [] mBones;
         }

+ 15 - 0
include/assimp/postprocess.h

@@ -320,6 +320,19 @@ enum aiPostProcessSteps
     */
     aiProcess_FixInfacingNormals = 0x2000,
 
+
+
+    // -------------------------------------------------------------------------
+    /** 
+     * This step generically populates aiBone->mArmature and aiBone->mNode generically
+     * The point of these is it saves you later having to calculate these elements
+     * This is useful when handling rest information or skin information
+     * If you have multiple armatures on your models we strongly recommend enabling this 
+     * Instead of writing your own multi-root, multi-armature lookups we have done the 
+     * hard work for you :)
+   */
+    aiProcess_PopulateArmatureData = 0x4000,
+
     // -------------------------------------------------------------------------
     /** <hr>This step splits meshes with more than one primitive type in
      *  homogeneous sub-meshes.
@@ -537,6 +550,8 @@ enum aiPostProcessSteps
     */
     aiProcess_Debone  = 0x4000000,
 
+
+
     // -------------------------------------------------------------------------
     /** <hr>This step will perform a global scale of the model.
     *

+ 11 - 11
include/assimp/scene.h

@@ -110,13 +110,13 @@ struct ASSIMP_API aiNode
     /** The transformation relative to the node's parent. */
     C_STRUCT aiMatrix4x4 mTransformation;
 
-    /** Parent node. NULL if this node is the root node. */
+    /** Parent node. nullptr if this node is the root node. */
     C_STRUCT aiNode* mParent;
 
     /** The number of child nodes of this node. */
     unsigned int mNumChildren;
 
-    /** The child nodes of this node. NULL if mNumChildren is 0. */
+    /** The child nodes of this node. nullptr if mNumChildren is 0. */
     C_STRUCT aiNode** mChildren;
 
     /** The number of meshes of this node. */
@@ -127,7 +127,7 @@ struct ASSIMP_API aiNode
       */
     unsigned int* mMeshes;
 
-    /** Metadata associated with this node or NULL if there is no metadata.
+    /** Metadata associated with this node or nullptr if there is no metadata.
       *  Whether any metadata is generated depends on the source file format. See the
       * @link importer_notes @endlink page for more information on every source file
       * format. Importers that don't document any metadata don't write any.
@@ -149,7 +149,7 @@ struct ASSIMP_API aiNode
      *  of the scene.
      *
      *  @param name Name to search for
-     *  @return NULL or a valid Node if the search was successful.
+     *  @return nullptr or a valid Node if the search was successful.
      */
     inline 
     const aiNode* FindNode(const aiString& name) const {
@@ -344,7 +344,7 @@ struct aiScene
 
 #ifdef __cplusplus
 
-    //! Default constructor - set everything to 0/NULL
+    //! Default constructor - set everything to 0/nullptr
     ASSIMP_API aiScene();
 
     //! Destructor
@@ -353,33 +353,33 @@ struct aiScene
     //! Check whether the scene contains meshes
     //! Unless no special scene flags are set this will always be true.
     inline bool HasMeshes() const { 
-        return mMeshes != NULL && mNumMeshes > 0; 
+        return mMeshes != nullptr && mNumMeshes > 0; 
     }
 
     //! Check whether the scene contains materials
     //! Unless no special scene flags are set this will always be true.
     inline bool HasMaterials() const { 
-        return mMaterials != NULL && mNumMaterials > 0; 
+        return mMaterials != nullptr && mNumMaterials > 0; 
     }
 
     //! Check whether the scene contains lights
     inline bool HasLights() const { 
-        return mLights != NULL && mNumLights > 0; 
+        return mLights != nullptr && mNumLights > 0; 
     }
 
     //! Check whether the scene contains textures
     inline bool HasTextures() const {
-        return mTextures != NULL && mNumTextures > 0; 
+        return mTextures != nullptr && mNumTextures > 0; 
     }
 
     //! Check whether the scene contains cameras
     inline bool HasCameras() const {
-        return mCameras != NULL && mNumCameras > 0; 
+        return mCameras != nullptr && mNumCameras > 0; 
     }
 
     //! Check whether the scene contains animations
     inline bool HasAnimations() const { 
-        return mAnimations != NULL && mNumAnimations > 0; 
+        return mAnimations != nullptr && mNumAnimations > 0; 
     }
 
     //! Returns a short filename from a full path

+ 2 - 0
test/CMakeLists.txt

@@ -118,6 +118,7 @@ SET( IMPORTERS
   unit/utColladaImportExport.cpp
   unit/utCSMImportExport.cpp
   unit/utB3DImportExport.cpp
+  unit/utM3DImportExport.cpp
   unit/utMDCImportExport.cpp
   unit/utAssbinImportExport.cpp
   unit/ImportExport/utAssjsonImportExport.cpp
@@ -148,6 +149,7 @@ SET( POST_PROCESSES
   unit/utRemoveRedundantMaterials.cpp
   unit/utRemoveVCProcess.cpp
   unit/utScaleProcess.cpp
+  unit/utArmaturePopulate.cpp
   unit/utJoinVertices.cpp
   unit/utRemoveComments.cpp
   unit/utRemoveComponent.cpp

BIN
test/models/Collada/duck.zae


File diff suppressed because it is too large
+ 544 - 0
test/models/FBX/box_orphant_embedded_texture.fbx


BIN
test/models/FBX/huesitos.fbx


BIN
test/models/M3D/WusonBlitz0.m3d


BIN
test/models/M3D/WusonBlitz1.m3d


BIN
test/models/M3D/WusonBlitz2.m3d


BIN
test/models/M3D/cube_normals.m3d


BIN
test/models/M3D/cube_usemtl.m3d


+ 33 - 0
test/models/M3D/cube_with_vertexcolors.a3d

@@ -0,0 +1,33 @@
+3dmodel 1
+cube_with_vertexcolors.obj
+MIT
+bzt
+comment
+
+Vertex
+0 0 0 1 #ff786d7b
+0 0 -1 1
+1 1 0 1 #ff320a4d
+1 0 0 1 #ff19c718
+0 1 0 1 #ff2c0004
+-1 0 0 1
+0 1 1 1 #ff0a00df
+0 0 1 1 #ff790018
+1 1 1 1 #ffc70017
+1 0 1 1 #ff380a7b
+0 -1 0 1
+
+Mesh
+0//1 2//1 3//1
+0//1 4//1 2//1
+0//5 6//5 4//5
+0//5 7//5 6//5
+4//4 8//4 2//4
+4//4 6//4 8//4
+3//3 2//3 8//3
+3//3 8//3 9//3
+0//10 3//10 9//10
+0//10 9//10 7//10
+7//7 9//7 8//7
+7//7 8//7 6//7
+

BIN
test/models/M3D/cube_with_vertexcolors.m3d


BIN
test/models/M3D/suzanne.m3d


+ 1 - 1
test/unit/Main.cpp

@@ -16,7 +16,7 @@ int main(int argc, char* argv[])
 
     // create a logger from both CPP
     Assimp::DefaultLogger::create("AssimpLog_Cpp.txt",Assimp::Logger::VERBOSE,
-        aiDefaultLogStream_DEBUGGER | aiDefaultLogStream_FILE);
+        aiDefaultLogStream_STDOUT | aiDefaultLogStream_DEBUGGER | aiDefaultLogStream_FILE);
 
     // .. and C. They should smoothly work together
     aiEnableVerboseLogging(AI_TRUE);

+ 83 - 0
test/unit/utArmaturePopulate.cpp

@@ -0,0 +1,83 @@
+/*
+---------------------------------------------------------------------------
+Open Asset Import Library (assimp)
+---------------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+
+
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the following
+conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+---------------------------------------------------------------------------
+*/
+#include "UnitTestPCH.h"
+#include "TestModelFactory.h"
+
+
+#include "SceneDiffer.h"
+#include "AbstractImportExportBase.h"
+
+#include <assimp/Importer.hpp>
+#include <assimp/postprocess.h>
+#include <assimp/material.h>
+#include <assimp/scene.h>
+#include <assimp/types.h>
+
+#include "PostProcessing/ArmaturePopulate.h"
+
+namespace Assimp {
+namespace UnitTest {
+
+class utArmaturePopulate : public ::testing::Test {
+    // empty
+};
+
+TEST_F( utArmaturePopulate, importCheckForArmatureTest) {
+    Assimp::Importer importer;
+    unsigned int mask = aiProcess_PopulateArmatureData | aiProcess_ValidateDataStructure;
+    const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/FBX/huesitos.fbx", mask);
+    EXPECT_NE( nullptr, scene );
+    EXPECT_EQ(scene->mNumMeshes, 1u);
+    aiMesh* mesh = scene->mMeshes[0];
+    EXPECT_EQ(mesh->mNumFaces, 68u);
+    EXPECT_EQ(mesh->mNumVertices, 256u);
+    EXPECT_GT(mesh->mNumBones, 0u);
+
+    aiBone* exampleBone = mesh->mBones[0];
+    EXPECT_NE(exampleBone, nullptr);
+    EXPECT_NE(exampleBone->mArmature, nullptr);
+    EXPECT_NE(exampleBone->mNode, nullptr);
+}
+
+} // Namespace UnitTest
+} // Namespace Assimp

+ 45 - 5
test/unit/utColladaImportExport.cpp

@@ -44,6 +44,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include "AbstractImportExportBase.h"
 
 #include <assimp/Importer.hpp>
+#include <assimp/scene.h>
 #include <assimp/postprocess.h>
 
 using namespace Assimp;
@@ -52,8 +53,19 @@ class utColladaImportExport : public AbstractImportExportBase {
 public:
     virtual bool importerTest() {
         Assimp::Importer importer;
-        const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/Collada/duck.dae", aiProcess_ValidateDataStructure );
-        return nullptr != scene;
+        const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck.dae", aiProcess_ValidateDataStructure);
+        if (scene == nullptr)
+            return false;
+
+        // Expected number of items
+        EXPECT_EQ(scene->mNumMeshes, 1u);
+        EXPECT_EQ(scene->mNumMaterials, 1u);
+        EXPECT_EQ(scene->mNumAnimations, 0u);
+        EXPECT_EQ(scene->mNumTextures, 0u);
+        EXPECT_EQ(scene->mNumLights, 1u);
+        EXPECT_EQ(scene->mNumCameras, 1u);
+
+        return true;
     }
 };
 
@@ -64,9 +76,37 @@ TEST_F(utColladaImportExport, importBlenFromFileTest) {
 class utColladaZaeImportExport : public AbstractImportExportBase {
 public:
     virtual bool importerTest() {
-        Assimp::Importer importer;
-        const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck.zae", aiProcess_ValidateDataStructure);
-        return nullptr != scene;
+        {
+            Assimp::Importer importer;
+            const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck.zae", aiProcess_ValidateDataStructure);
+            if (scene == nullptr)
+                return false;
+
+            // Expected number of items
+            EXPECT_EQ(scene->mNumMeshes, 1u);
+            EXPECT_EQ(scene->mNumMaterials, 1u);
+            EXPECT_EQ(scene->mNumAnimations, 0u);
+            EXPECT_EQ(scene->mNumTextures, 1u);
+            EXPECT_EQ(scene->mNumLights, 1u);
+            EXPECT_EQ(scene->mNumCameras, 1u);
+        }
+
+        {
+            Assimp::Importer importer;
+            const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck_nomanifest.zae", aiProcess_ValidateDataStructure);
+            if (scene == nullptr)
+                return false;
+
+            // Expected number of items
+            EXPECT_EQ(scene->mNumMeshes, 1u);
+            EXPECT_EQ(scene->mNumMaterials, 1u);
+            EXPECT_EQ(scene->mNumAnimations, 0u);
+            EXPECT_EQ(scene->mNumTextures, 1u);
+            EXPECT_EQ(scene->mNumLights, 1u);
+            EXPECT_EQ(scene->mNumCameras, 1u);
+        }
+
+        return true;
     }
 };
 

+ 30 - 29
test/unit/utFBXImporterExporter.cpp

@@ -76,6 +76,7 @@ TEST_F( utFBXImporterExporter, importBareBoxWithoutColorsAndTextureCoords ) {
     EXPECT_EQ(mesh->mNumVertices, 36u);
 }
 
+
 TEST_F(utFBXImporterExporter, importCubesWithNoNames) {
     Assimp::Importer importer;
     const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/FBX/cubes_nonames.fbx", aiProcess_ValidateDataStructure);
@@ -86,26 +87,6 @@ TEST_F(utFBXImporterExporter, importCubesWithNoNames) {
     ASSERT_STREQ(root->mName.C_Str(), "RootNode");
     ASSERT_TRUE(root->mChildren);
     ASSERT_EQ(root->mNumChildren, 2u);
-
-    const auto child0 = root->mChildren[0];
-    ASSERT_TRUE(child0);
-    ASSERT_STREQ(child0->mName.C_Str(), "RootNode001");
-    ASSERT_TRUE(child0->mChildren);
-    ASSERT_EQ(child0->mNumChildren, 1u);
-
-    const auto child00 = child0->mChildren[0];
-    ASSERT_TRUE(child00);
-    ASSERT_STREQ(child00->mName.C_Str(), "RootNode001001");
-
-    const auto child1 = root->mChildren[1];
-    ASSERT_TRUE(child1);
-    ASSERT_STREQ(child1->mName.C_Str(), "RootNode002");
-    ASSERT_TRUE(child1->mChildren);
-    ASSERT_EQ(child1->mNumChildren, 1u);
-
-    const auto child10 = child1->mChildren[0];
-    ASSERT_TRUE(child10);
-    ASSERT_STREQ(child10->mName.C_Str(), "RootNode002001");
 }
 
 TEST_F(utFBXImporterExporter, importCubesWithUnicodeDuplicatedNames) {
@@ -137,7 +118,7 @@ TEST_F(utFBXImporterExporter, importCubesWithUnicodeDuplicatedNames) {
 
     const auto child10 = child1->mChildren[0];
     ASSERT_TRUE(child10);
-    ASSERT_STREQ(child10->mName.C_Str(), "\xd0\x9a\xd1\x83\xd0\xb1\x31""001");
+    ASSERT_STREQ(child10->mName.C_Str(), "\xd0\x9a\xd1\x83\xd0\xb1\x31");
 }
 
 TEST_F(utFBXImporterExporter, importCubesComplexTransform) {
@@ -168,14 +149,14 @@ TEST_F(utFBXImporterExporter, importCubesComplexTransform) {
     auto parent = child1;
     const size_t chain_length = 8u;
     const char* chainStr[chain_length] = {
-        "Cube1001_$AssimpFbx$_Translation",
-        "Cube1001_$AssimpFbx$_RotationPivot",
-        "Cube1001_$AssimpFbx$_RotationPivotInverse",
-        "Cube1001_$AssimpFbx$_ScalingOffset",
-        "Cube1001_$AssimpFbx$_ScalingPivot",
-        "Cube1001_$AssimpFbx$_Scaling",
-        "Cube1001_$AssimpFbx$_ScalingPivotInverse",
-        "Cube1001"
+        "Cube1_$AssimpFbx$_Translation",
+        "Cube1_$AssimpFbx$_RotationPivot",
+        "Cube1_$AssimpFbx$_RotationPivotInverse",
+        "Cube1_$AssimpFbx$_ScalingOffset",
+        "Cube1_$AssimpFbx$_ScalingPivot",
+        "Cube1_$AssimpFbx$_Scaling",
+        "Cube1_$AssimpFbx$_ScalingPivotInverse",
+        "Cube1"
     };
     for (size_t i = 0; i < chain_length; ++i) {
         ASSERT_TRUE(parent->mChildren);
@@ -282,3 +263,23 @@ TEST_F(utFBXImporterExporter, fbxTokenizeTestTest) {
     //const aiScene* scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/FBX/transparentTest2.fbx", aiProcess_ValidateDataStructure);
     //EXPECT_NE(nullptr, scene);
 }
+
+TEST_F(utFBXImporterExporter, importOrphantEmbeddedTextureTest) {
+    // see https://github.com/assimp/assimp/issues/1957
+    Assimp::Importer importer;
+    const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/FBX/box_orphant_embedded_texture.fbx", aiProcess_ValidateDataStructure);
+    EXPECT_NE(nullptr, scene);
+
+    EXPECT_EQ(1u, scene->mNumMaterials);
+    aiMaterial *mat = scene->mMaterials[0];
+    ASSERT_NE(nullptr, mat);
+
+    aiString path;
+    aiTextureMapMode modes[2];
+    ASSERT_EQ(aiReturn_SUCCESS, mat->GetTexture(aiTextureType_DIFFUSE, 0, &path, nullptr, nullptr, nullptr, nullptr, modes));
+    ASSERT_STREQ(path.C_Str(), "..\\Primitives\\GridGrey.tga");
+
+    ASSERT_EQ(1u, scene->mNumTextures);
+    ASSERT_TRUE(scene->mTextures[0]->pcData);
+    ASSERT_EQ(9026u, scene->mTextures[0]->mWidth) << "FBX ASCII base64 compression used for a texture.";
+}

+ 68 - 0
test/unit/utM3DImportExport.cpp

@@ -0,0 +1,68 @@
+/*
+---------------------------------------------------------------------------
+Open Asset Import Library (assimp)
+---------------------------------------------------------------------------
+
+Copyright (c) 2006-2019, assimp team
+
+
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the following
+conditions are met:
+
+* Redistributions of source code must retain the above
+copyright notice, this list of conditions and the
+following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the
+following disclaimer in the documentation and/or other
+materials provided with the distribution.
+
+* Neither the name of the assimp team, nor the names of its
+contributors may be used to endorse or promote products
+derived from this software without specific prior
+written permission of the assimp team.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+---------------------------------------------------------------------------
+*/
+
+#include "UnitTestPCH.h"
+#include "SceneDiffer.h"
+#include "AbstractImportExportBase.h"
+
+#include <assimp/Importer.hpp>
+#include <assimp/postprocess.h>
+
+using namespace Assimp;
+
+class utM3DImportExport : public AbstractImportExportBase {
+public:
+    virtual bool importerTest() {
+        Assimp::Importer importer;
+        const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/M3D/WusonBlitz0.m3d", aiProcess_ValidateDataStructure );
+#ifndef ASSIMP_BUILD_NO_M3D_IMPORTER
+        return nullptr != scene;
+#else
+        return nullptr == scene;
+#endif // ASSIMP_BUILD_NO_M3D_IMPORTER
+    }
+};
+
+TEST_F( utM3DImportExport, importM3DFromFileTest ) {
+    EXPECT_TRUE( importerTest() );
+}

+ 1 - 1
test/unit/utVersion.cpp

@@ -48,7 +48,7 @@ TEST_F( utVersion, aiGetLegalStringTest ) {
     EXPECT_NE( lv, nullptr );
     std::string text( lv );
 
-    size_t pos( text.find( std::string( "2017" ) ) );
+    size_t pos( text.find( std::string( "2019" ) ) );
     EXPECT_NE( pos, std::string::npos );
 }
 

Some files were not shown because too many files changed in this diff