From 32781a566db2cd6022c7020ff2ac591d753c11ab Mon Sep 17 00:00:00 2001 From: Fabrice Fontaine Date: Sun, 6 Oct 2019 15:42:11 +0200 Subject: [PATCH 01/74] Fix FBXConverter: use proper 64-bit constant Use proper 64-bit constant for CONVERT_FBX_TIME(time) conversion, fixes: code/FBXConverter.cpp:2025: error: integer constant is too large for 'long' type code/FBXConverter.cpp:2026: error: integer constant is too large for 'long' type code/FBXConverter.cpp:2794: error: integer constant is too large for 'long' type code/FBXConverter.cpp:2868: error: integer constant is too large for 'long' type code/FBXConverter.cpp:2878: error: integer constant is too large for 'long' type code/FBXConverter.cpp:2888: error: integer constant is too large for 'long' type Signed-off-by: Peter Seiderer [Retrieved from: https://git.buildroot.net/buildroot/tree/package/assimp/0001-Fix-FBXConverter-use-proper-64-bit-constant.patch] Signed-off-by: Fabrice Fontaine --- code/FBX/FBXConverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index 152be32772..b8601a2a50 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -77,7 +77,7 @@ namespace Assimp { #define MAGIC_NODE_TAG "_$AssimpFbx$" -#define CONVERT_FBX_TIME(time) static_cast(time) / 46186158000L +#define CONVERT_FBX_TIME(time) static_cast(time) / 46186158000LL FBXConverter::FBXConverter(aiScene* out, const Document& doc, bool removeEmptyBones ) : defaultMaterialIndex() From 81d125a2cca748ef3f79f1f0a6731d7e6d694ec1 Mon Sep 17 00:00:00 2001 From: Robikz Date: Sun, 6 Oct 2019 18:46:35 +0200 Subject: [PATCH 02/74] dllexport ASSIMP_API in all Windows compilers, not just MSVC This fixes a problem mentioned in issue #2685 where the libassimp.dll compiled with MinGW doesn't export any symbols. --- include/assimp/defs.h | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/include/assimp/defs.h b/include/assimp/defs.h index 05a5e3fd4b..e32b95ddef 100644 --- a/include/assimp/defs.h +++ b/include/assimp/defs.h @@ -126,16 +126,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 @@ -148,7 +146,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 @@ -156,17 +166,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) From 0761530e17cb7638be3e8267a9128bea7a2dae51 Mon Sep 17 00:00:00 2001 From: Robikz Date: Sun, 6 Oct 2019 18:46:47 +0200 Subject: [PATCH 03/74] Set proper sharedLibraryName for MinGW in installed assimpTargets-*.cmake The shared library name for MinGW is the .a library with which we need to link the built binary. This problem is described in issue #2685. --- assimpTargets-debug.cmake.in | 7 ++++++- assimpTargets-release.cmake.in | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/assimpTargets-debug.cmake.in b/assimpTargets-debug.cmake.in index 1ebe2a6081..ed39359706 100644 --- a/assimpTargets-debug.cmake.in +++ b/assimpTargets-debug.cmake.in @@ -63,7 +63,12 @@ if(MSVC) else() set(ASSIMP_LIBRARY_SUFFIX "@ASSIMP_LIBRARY_SUFFIX@" CACHE STRING "the suffix for the assimp libraries" ) if(ASSIMP_BUILD_SHARED_LIBS) - 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@") + 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}" IMPORTED_LOCATION_DEBUG "${_IMPORT_PREFIX}/lib/${sharedLibraryName}" diff --git a/assimpTargets-release.cmake.in b/assimpTargets-release.cmake.in index b09b881f73..f00710d854 100644 --- a/assimpTargets-release.cmake.in +++ b/assimpTargets-release.cmake.in @@ -63,7 +63,12 @@ if(MSVC) else() set(ASSIMP_LIBRARY_SUFFIX "@ASSIMP_LIBRARY_SUFFIX@" CACHE STRING "the suffix for the assimp libraries" ) if(ASSIMP_BUILD_SHARED_LIBS) - 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@") + 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_RELEASE "${sharedLibraryName}" IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/${sharedLibraryName}" From 6ac8279977c3a54118551e549d77329497116f66 Mon Sep 17 00:00:00 2001 From: Robikz Date: Sun, 6 Oct 2019 18:46:51 +0200 Subject: [PATCH 04/74] assimpTargets.cmake: define add_library() with lib type directly Instead of using if(ON)/if(OFF) to determine which of the "hardcoded" add_library(... SHARED ...) or (... STATIC ...) should be used, specify a new BUILD_LIB_TYPE variable that is set directly to either SHARED or STATIC and substituted in the `add_library()` statement when assimpTargets.cmake.in is configured. This removes a CMP0012 collision with prior `cmake_policy(VERSION 2.6)` statement and makes the CMP0012 warning not appear in users' projects. This problem is mentioned in issue #2685. --- CMakeLists.txt | 5 +++++ assimpTargets.cmake.in | 6 +----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dcafb649f5..5a716daa0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/assimpTargets.cmake.in b/assimpTargets.cmake.in index ab1a8d2c7b..ea63ba2a50 100644 --- a/assimpTargets.cmake.in +++ b/assimpTargets.cmake.in @@ -51,11 +51,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" From 059ee0e091f1c658c20202a9123bdf90fc7fa307 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 11 Oct 2019 16:28:14 +0100 Subject: [PATCH 05/74] Update assimp legal and version Will now report the major and minor versions specified in cmakelists --- code/Common/Version.cpp | 14 +++++--------- test/unit/utVersion.cpp | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/code/Common/Version.cpp b/code/Common/Version.cpp index 868cfb06af..cf1da7d5ba 100644 --- a/code/Common/Version.cpp +++ b/code/Common/Version.cpp @@ -46,8 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #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; diff --git a/test/unit/utVersion.cpp b/test/unit/utVersion.cpp index 233b2fb0b2..66e832baae 100644 --- a/test/unit/utVersion.cpp +++ b/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 ); } From ce5c71d2e7d993823c674aa71f05b33d281a4e00 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Fri, 11 Oct 2019 18:57:38 +0100 Subject: [PATCH 06/74] Collada ZAE import must convert manifest and image paths Moved ConvertPath into ColladaParser and use it when reading all filenames from the XML Added more EXPECTS to the Collada tests --- code/Collada/ColladaHelper.h | 2 +- code/Collada/ColladaLoader.cpp | 56 ++--------------------- code/Collada/ColladaLoader.h | 5 +-- code/Collada/ColladaParser.cpp | 69 +++++++++++++++++++++++++++-- code/Collada/ColladaParser.h | 5 ++- test/unit/utColladaImportExport.cpp | 27 ++++++++++- 6 files changed, 100 insertions(+), 64 deletions(-) diff --git a/code/Collada/ColladaHelper.h b/code/Collada/ColladaHelper.h index 66cff2d207..b7d47da155 100644 --- a/code/Collada/ColladaHelper.h +++ b/code/Collada/ColladaHelper.h @@ -583,7 +583,7 @@ struct Image /** Embedded image data */ std::vector mImageData; - /** File format hint ofembedded image data */ + /** File format hint of embedded image data */ std::string mEmbeddedFormat; }; diff --git a/code/Collada/ColladaLoader.cpp b/code/Collada/ColladaLoader.cpp index 0fbc7d599d..8327142bf8 100644 --- a/code/Collada/ColladaLoader.cpp +++ b/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::ConvertPath(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 diff --git a/code/Collada/ColladaLoader.h b/code/Collada/ColladaLoader.h index dce46e06f6..d8d3f4f52e 100644 --- a/code/Collada/ColladaLoader.h +++ b/code/Collada/ColladaLoader.h @@ -94,7 +94,7 @@ class ColladaLoader : public BaseImporter 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 @@ class ColladaLoader : public BaseImporter 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 diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index 646ec473db..f80ed14174 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -183,13 +183,66 @@ std::string ColladaParser::ReadZaeManifest(ZipArchiveIOSystem &zip_archive) { if (filepath == nullptr) return std::string(); - return std::string(filepath); + aiString ai_str(filepath); + ConvertPath(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::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); +} + // ------------------------------------------------------------------------------------------------ // Read bool from text contents of current element bool ColladaParser::ReadBoolFromTextContent() @@ -1120,7 +1173,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); + ConvertPath(filepath); + pImage.mFileName = filepath.C_Str(); + } TestClosing("init_from"); } if (!pImage.mFileName.length()) { @@ -1153,7 +1211,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); + ConvertPath(filepath); + pImage.mFileName = filepath.C_Str(); + } TestClosing("ref"); } else if (IsElement("hex") && !pImage.mFileName.length()) diff --git a/code/Collada/ColladaParser.h b/code/Collada/ColladaParser.h index a2c9a4ff2d..2c4adaead9 100644 --- a/code/Collada/ColladaParser.h +++ b/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 ConvertPath(aiString& ss); + protected: /** Map for generic metadata as aiString */ typedef std::map StringMetaData; /** Constructor from XML file */ - ColladaParser( IOSystem* pIOHandler, const std::string& pFile); + ColladaParser(IOSystem* pIOHandler, const std::string& pFile); /** Destructor */ ~ColladaParser(); diff --git a/test/unit/utColladaImportExport.cpp b/test/unit/utColladaImportExport.cpp index 1a2a6bc9dc..8a413d52ba 100644 --- a/test/unit/utColladaImportExport.cpp +++ b/test/unit/utColladaImportExport.cpp @@ -44,6 +44,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "AbstractImportExportBase.h" #include +#include #include using namespace Assimp; @@ -53,7 +54,18 @@ class utColladaImportExport : public AbstractImportExportBase { virtual bool importerTest() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/Collada/duck.dae", aiProcess_ValidateDataStructure ); - return nullptr != scene; + if (scene == nullptr) + return false; + + // Expected number of items + EXPECT_EQ(scene->mNumMeshes, 1); + EXPECT_EQ(scene->mNumMaterials, 1); + EXPECT_EQ(scene->mNumAnimations, 0); + EXPECT_EQ(scene->mNumTextures, 0); + EXPECT_EQ(scene->mNumLights, 1); + EXPECT_EQ(scene->mNumCameras, 1); + + return true; } }; @@ -66,7 +78,18 @@ class utColladaZaeImportExport : public AbstractImportExportBase { virtual bool importerTest() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck.zae", aiProcess_ValidateDataStructure); - return nullptr != scene; + if (scene == nullptr) + return false; + + // Expected number of items + EXPECT_EQ(scene->mNumMeshes, 1); + EXPECT_EQ(scene->mNumMaterials, 1); + EXPECT_EQ(scene->mNumAnimations, 0); + EXPECT_EQ(scene->mNumTextures, 1); + EXPECT_EQ(scene->mNumLights, 1); + EXPECT_EQ(scene->mNumCameras, 1); + + return true; } }; From 6a6ccc0fb0d537f9bbafd841428afe908773767d Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:27:34 +0100 Subject: [PATCH 07/74] Rename ConvertPath() to UriDecodePath() --- code/Collada/ColladaLoader.cpp | 2 +- code/Collada/ColladaParser.cpp | 8 ++++---- code/Collada/ColladaParser.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/code/Collada/ColladaLoader.cpp b/code/Collada/ColladaLoader.cpp index 8327142bf8..37529bc986 100644 --- a/code/Collada/ColladaLoader.cpp +++ b/code/Collada/ColladaLoader.cpp @@ -1763,7 +1763,7 @@ aiString ColladaLoader::FindFilenameForEffectTexture(const ColladaParser& pParse //set default texture file name result.Set(name + ".jpg"); - ColladaParser::ConvertPath(result); + ColladaParser::UriDecodePath(result); return result; } diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index f80ed14174..4eece8327c 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -184,7 +184,7 @@ std::string ColladaParser::ReadZaeManifest(ZipArchiveIOSystem &zip_archive) { return std::string(); aiString ai_str(filepath); - ConvertPath(ai_str); + UriDecodePath(ai_str); return std::string(ai_str.C_Str()); } @@ -195,7 +195,7 @@ std::string ColladaParser::ReadZaeManifest(ZipArchiveIOSystem &zip_archive) { // ------------------------------------------------------------------------------------------------ // Convert a path read from a collada file to the usual representation -void ColladaParser::ConvertPath(aiString& ss) +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. @@ -1176,7 +1176,7 @@ void ColladaParser::ReadImage(Collada::Image& pImage) if (sz) { aiString filepath(sz); - ConvertPath(filepath); + UriDecodePath(filepath); pImage.mFileName = filepath.C_Str(); } TestClosing("init_from"); @@ -1214,7 +1214,7 @@ void ColladaParser::ReadImage(Collada::Image& pImage) if (sz) { aiString filepath(sz); - ConvertPath(filepath); + UriDecodePath(filepath); pImage.mFileName = filepath.C_Str(); } TestClosing("ref"); diff --git a/code/Collada/ColladaParser.h b/code/Collada/ColladaParser.h index 2c4adaead9..4e8c8fccfd 100644 --- a/code/Collada/ColladaParser.h +++ b/code/Collada/ColladaParser.h @@ -67,7 +67,7 @@ namespace Assimp friend class ColladaLoader; /** Converts a path read from a collada file to the usual representation */ - static void ConvertPath(aiString& ss); + static void UriDecodePath(aiString& ss); protected: /** Map for generic metadata as aiString */ From d371a113fa4d9bb7815db347fcb4bfa70b019352 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Mon, 14 Oct 2019 12:10:47 +0100 Subject: [PATCH 08/74] /DEBUG:FULL is not a compiler flag --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dcafb649f5..c4d8cf4e28 100644 --- a/CMakeLists.txt +++ b/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}") From 198bc428fafff40cf3814d1a771173f8f19bcf91 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Mon, 14 Oct 2019 12:15:02 +0100 Subject: [PATCH 09/74] Fix some warnings in Collada import --- code/Collada/ColladaParser.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index 4eece8327c..1a7b961895 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -240,7 +240,8 @@ void ColladaParser::UriDecodePath(aiString& ss) // adjust length and terminator of the shortened string *out = 0; - ss.length = (ptrdiff_t)(out - ss.data); + ai_assert(out > ss.data); + ss.length = static_cast(out - ss.data); } // ------------------------------------------------------------------------------------------------ @@ -3119,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) { From f4bd11279fbae18b1151dfc4b4c25c9cca9ddb04 Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Mon, 14 Oct 2019 12:25:42 +0100 Subject: [PATCH 10/74] Update Collada ZAE tests to include encoded URIs --- test/models/Collada/duck.zae | Bin 132801 -> 132811 bytes test/unit/utColladaImportExport.cpp | 43 +++++++++++++++++++--------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/test/models/Collada/duck.zae b/test/models/Collada/duck.zae index 97823535522cba8aee8db7b75b0e99bcc39c3d73..7e6748cb0092e498298802d23d1d7608bb474b04 100644 GIT binary patch delta 100935 zcmV)6K*+zrjR?z)2!BvZ0|XQR000O8!(vWP_#8sCV}=0$kXHl%3;+NCWOZX}AbMeC zE@WY4w7qMuUB{88`Q3p3!-g6&m;ve>uDoYf$!fUmsu|DthLXFxs=o*{bz~7mkt&k1 zt;YQL$>)hY6y=Mo#`L(*O`UyNd#$`gyz#~xkstoYKYsDz_J8|_*KeM`dikRd(*5

fN^6#HK`NJRnaR2Ppix*EneR}`u_3xhK z>uOI@dU8Jf@w3k#zIgh<`4SZ}+y38&Pv5+Gczgcvg=eo`zJ2}tw_m+|_4>V+ z_`kr(pgCEtDdPcHrJ)fZnpynOrSqsK3Ksek zPyg-VH?N<*eDm3>*I&H((e)1xe&E-yUVQz#S1*6@@PFp>hc`b;|G>w7{`_~JzumvK zKlJ>~$4|fb@hdnKx?(;Xd&z`?{xIO&w*~6D_Z_i)e_Vet8@^nA^?GMl2etz?E=R)_le?3=t z^;_=v_VmVM-d=s?EuK%^p1%G2AO7I%?bi=)9-h8__WAvrXAe*JF8|^Q|MAHiALNra zKFB98&gDOUyRF6#fAF7Q?ltq~_WI%9zkhnp?7HHwe7xS#v|5A3mf`tlaz^y80j zKl?cT^Y)*9{PE8|{<|Ok;J^OtH~;6a{_}5c|MkcJ{LerB+S#k*)MabNezR}$(;xoe#q%$o zzm3iI=g+o(gjM;?>igERJR|<;(|@;5KVs(h6g~OL-#_{N8;$N8tVF!~ga!KI;qAjG zzkd4c-}Vi?dBPI>OjXM6m=KYO#Z@>4IuUnH#D*B?H4{OkChC+|C-4_`e0?dzwnzy2f)#G7wDnJ?X*f4YCj|J*zI z%ZG1%{QcMdbjn_BxA5#w?SG5!ynT4~`OD}3{?)@LSfN)h|H5bd++IMeueZ;C|L|8n z;>W)J?A7l-D&v0EwH=v%eaEMtddW{-vkpH>_l?1L@0IWP=6`>=O~OapcE*2v*C#*Y zgV+Aw{_tmi^|K#7`R*tD@X6m8sSlt0*&y%r=A^uv>c2d@?Sj z&)BX{U+7(9qVM)s9~Z!1{=}axh3_%pAHH{~zcyUY@S-23+x|cH;)_3nKz{1UU%%3K z4k$zQjpg}f;J!6N@5XJThHYEk|GKX@ZWyTiI7H`sMcV zbpPbjub%ze|B|o&{(to0C%?G=^5yS7eDck^@876D^=CKwty_9`qZ{2kY(VqkTlf0- zqPP3o4X|F8$NN2h{rPRad5d@bCV@_79}yT?Dh zn|HSvm;KgFo?rUr7Wth&y{|7{zhZO>4@v&%U;CgR{$`H6_+Os1Kl>IK`RUt_emwtq{vY!{ z%@3cP7yg>5IJfi{SN!86e(%TEU%vk9k00;npMKW^gp>T}TLb#&I=}0CUp#-gzq6OY zM?e4BFaPn!|9||+FMjsxU;OyjpTu8#?Yw*cyDswdci*}QOYV2y`Lz$9eAiQb_ZYsj z8Zn4rI{xbDxmdsJ<{u6$`i{@|xiE3pjsN=&i}&vHPrk>6KYaSdZ=Y|>@IU>x|Mqpa ziT~`gjo<&)Pw#l??N_h&=8J#-bkXwqGlHxieej!q{C}70TbAd~uJz<^|K^8y;ICeM zr$_g5|HDJ_Judv=o6onkc)9WR-}?2>UcB1Y;kqx|Quv?W`ODwqsvquf@x|XnSAOz; z_{qQ5rN6m>=WoCMTetA|`0e{V{`b7_hi@S(_OAc`V?TVqYrl63&%Wmvr0+8V-}AED zcYXTs$A3>=efG&7ve!>VP5%?`;`?9y!zX{uYQBfr{^Z>apZ}Wk-=n=9V=nST#%&tC07_eUS}lIsT_m3DvS+;4N; zmpSS!m-{kGzNK~F`tbFUM!DsDU*}l2b)WOP_P;xFs(IYfxKCFZw|Za7Rp)Ka`*B@! z|J}ye*RA{dZ6vN!)0kX$=6bs;dnc#WZzBD?^5Ci$JN@c^nHU@zV&uL^0hBC_l@uCjQeZ$d~1Ej$Z2?)QXBW*S1-36oA2w@ z!sk<+`$p6KRr9*t)$4xcGZ_TzZyPr3=RCv8C#$ms{UmpAU+8VdE$NMULI@dmLz3$=NkG)^6J3PLX zd7t~9roKmS?x)`W*3=ur=ELvLwX~vpCM=?3MD%X{^nZcHm>{070RBn(s^SKS%2C$G4o!B zdkO62o$H><{R_Fa%t&2huj{=-%HGg>zomWCx4AdyNIrG1`_A@2wzl8jA20LTb6xiU zm)o@mkNfm0`)?+;?1|bR-|J|){}P*ZFSE+#-2+wlxAfr_*nfBaoxR4gTQ#1x4_EV= zdoK1mOMTze9>+1ab!l90&wl`$gXO%ZsurGU&mD8g-n(Yw^<4Hk-Fy1lm))aVuvL3A zq&+<>qP;oXs>8wFm+cGdw5PPx&iD7j*SLPl`;EPv`X2Shn%INN4ej0SrS=_mHq^ZK z_S{d|0}nPHrdfb;cOy(Gp9&Z;O9x`*GI3 z-Mjs}7Xz_}u9Q8Oynk^ybeShy-1tlk_FkbpC~uT~3k*@odlvUzn3(Llv|rfw#9FQU znYh##Y3V$!v^T-Nr=fA*^PVnk_WrGe-Rgxe>{Z#P_9t_dHMY&<#kD;Qf0yK>r@gj) zvbJwyZ={j;m-awRcG~{ceVdJeo>;w(vGVRyVt7{HD+2d1=YL-E`}KNlE7Dmo+g;2( z%$cRSAAamx-P4};0P_0U19#icnCc#`?J%#s8?mtaH}kbuQ5fcoH{jEwxq0`3U)=ND zhLF#vSofoCC%TQ_c6R%wE?&-Fyl}bwC{^Fjt#0GGpSNI*TiIKG@nd=NqiyU5+?#Rb zzyu8w#1g*tzkhqQHwN(P*9H|g#-Z<NK?A@}Reor~pLfvZU1#qIaDGv8haw6zCnJN?0WtohOnZ#x&Li@mws zcEc~;u}Qa6@(UeMru|4DYg{j>iz#7)^z6E9?=a@6L(q8qsZa7rx84F&&^b@lK1T#~~p69a7rum=KHmN1_i8|?@24m&e7FhLur?0?f>BA69zQdzcd+;-1p1_m>R#l zm%`l3{MK0l+`3R*$g+{wVz~DJ`-SZp;*G?<+fcenk84??Y( z{lfy>dwv`L4M?^H(!F-qc_JCk24iL36& wtseB-q9*h9l$0_RW_DhTTqTyJ|i8 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 delta 100924 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 diff --git a/test/unit/utColladaImportExport.cpp b/test/unit/utColladaImportExport.cpp index 8a413d52ba..f07f9a274a 100644 --- a/test/unit/utColladaImportExport.cpp +++ b/test/unit/utColladaImportExport.cpp @@ -53,7 +53,7 @@ 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 ); + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/Collada/duck.dae", aiProcess_ValidateDataStructure); if (scene == nullptr) return false; @@ -76,18 +76,35 @@ 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); - if (scene == nullptr) - return false; - - // Expected number of items - EXPECT_EQ(scene->mNumMeshes, 1); - EXPECT_EQ(scene->mNumMaterials, 1); - EXPECT_EQ(scene->mNumAnimations, 0); - EXPECT_EQ(scene->mNumTextures, 1); - EXPECT_EQ(scene->mNumLights, 1); - EXPECT_EQ(scene->mNumCameras, 1); + { + 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, 1); + EXPECT_EQ(scene->mNumMaterials, 1); + EXPECT_EQ(scene->mNumAnimations, 0); + EXPECT_EQ(scene->mNumTextures, 1); + EXPECT_EQ(scene->mNumLights, 1); + EXPECT_EQ(scene->mNumCameras, 1); + } + + { + 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, 1); + EXPECT_EQ(scene->mNumMaterials, 1); + EXPECT_EQ(scene->mNumAnimations, 0); + EXPECT_EQ(scene->mNumTextures, 1); + EXPECT_EQ(scene->mNumLights, 1); + EXPECT_EQ(scene->mNumCameras, 1); + } return true; } From 17eb292d73e817d49a51b3e98a5cedbfdd923fdc Mon Sep 17 00:00:00 2001 From: RichardTea <31507749+RichardTea@users.noreply.github.com> Date: Mon, 14 Oct 2019 14:49:57 +0100 Subject: [PATCH 11/74] Tests: Fix signed/unsigned warnings --- test/unit/utColladaImportExport.cpp | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/unit/utColladaImportExport.cpp b/test/unit/utColladaImportExport.cpp index f07f9a274a..8bce3a3ddf 100644 --- a/test/unit/utColladaImportExport.cpp +++ b/test/unit/utColladaImportExport.cpp @@ -58,12 +58,12 @@ class utColladaImportExport : public AbstractImportExportBase { return false; // Expected number of items - EXPECT_EQ(scene->mNumMeshes, 1); - EXPECT_EQ(scene->mNumMaterials, 1); - EXPECT_EQ(scene->mNumAnimations, 0); - EXPECT_EQ(scene->mNumTextures, 0); - EXPECT_EQ(scene->mNumLights, 1); - EXPECT_EQ(scene->mNumCameras, 1); + 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; } @@ -83,12 +83,12 @@ class utColladaZaeImportExport : public AbstractImportExportBase { return false; // Expected number of items - EXPECT_EQ(scene->mNumMeshes, 1); - EXPECT_EQ(scene->mNumMaterials, 1); - EXPECT_EQ(scene->mNumAnimations, 0); - EXPECT_EQ(scene->mNumTextures, 1); - EXPECT_EQ(scene->mNumLights, 1); - EXPECT_EQ(scene->mNumCameras, 1); + 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); } { @@ -98,12 +98,12 @@ class utColladaZaeImportExport : public AbstractImportExportBase { return false; // Expected number of items - EXPECT_EQ(scene->mNumMeshes, 1); - EXPECT_EQ(scene->mNumMaterials, 1); - EXPECT_EQ(scene->mNumAnimations, 0); - EXPECT_EQ(scene->mNumTextures, 1); - EXPECT_EQ(scene->mNumLights, 1); - EXPECT_EQ(scene->mNumCameras, 1); + 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; From 9aa9238e7e97e702c7dd152598449e1518692d5b Mon Sep 17 00:00:00 2001 From: Paul Arden Date: Fri, 18 Oct 2019 14:40:41 +1100 Subject: [PATCH 12/74] Copy texture data before handing on to asset which then handles freeing of the memory. This prevents the memory being released twice which was throwing an error. Fixes issue #2714. --- code/glTF2/glTF2Exporter.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/glTF2/glTF2Exporter.cpp b/code/glTF2/glTF2Exporter.cpp index 4724c2ef48..dc0b76443b 100644 --- a/code/glTF2/glTF2Exporter.cpp +++ b/code/glTF2/glTF2Exporter.cpp @@ -320,7 +320,9 @@ void glTF2Exporter::GetMatTex(const aiMaterial* mat, Ref& texture, aiTe if (path[0] == '*') { // embedded aiTexture* tex = mScene->mTextures[atoi(&path[1])]; - uint8_t* data = reinterpret_cast(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]) { From 31e885a47ae59c161e2d8faebb3568d9b4b98f79 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Tue, 22 Oct 2019 20:24:23 +0200 Subject: [PATCH 13/74] Update appveyor.yml Reenable VS2013 --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index df16431bd9..39525f59d8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,6 +14,7 @@ matrix: fast_finish: true image: + - Visual Studio 2013 - Visual Studio 2015 - Visual Studio 2017 - Visual Studio 2019 From 41ab019cbf14137f2105c46c47adf49011f9bb2b Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Wed, 23 Oct 2019 22:42:19 +0200 Subject: [PATCH 14/74] Update appveyor.yml Add missing cmake build target. --- appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 39525f59d8..fa0a125f04 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,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 From 822be33408533b4f861d69dbc9aa9abd6c237f45 Mon Sep 17 00:00:00 2001 From: Paul Arden Date: Fri, 25 Oct 2019 12:17:43 +1100 Subject: [PATCH 15/74] Move pbrSG glossinessFactor into the correct material extension object. Fixes issue #2724. --- code/glTF2/glTF2AssetWriter.inl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/glTF2/glTF2AssetWriter.inl b/code/glTF2/glTF2AssetWriter.inl index 5d1b220645..b3148f798e 100644 --- a/code/glTF2/glTF2AssetWriter.inl +++ b/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); From cbd4e8bc220b1c79bed6aa788c758a69853b0376 Mon Sep 17 00:00:00 2001 From: Paul Arden Date: Fri, 25 Oct 2019 12:19:28 +1100 Subject: [PATCH 16/74] Fix inconsistent indentation in previous commit. --- code/glTF2/glTF2AssetWriter.inl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/glTF2/glTF2AssetWriter.inl b/code/glTF2/glTF2AssetWriter.inl index b3148f798e..44a1d3e73d 100644 --- a/code/glTF2/glTF2AssetWriter.inl +++ b/code/glTF2/glTF2AssetWriter.inl @@ -358,7 +358,7 @@ namespace glTF2 { WriteVec(pbrSpecularGlossiness, pbrSG.specularFactor, "specularFactor", defaultSpecularFactor, w.mAl); if (pbrSG.glossinessFactor != 1) { - WriteFloat(pbrSpecularGlossiness, pbrSG.glossinessFactor, "glossinessFactor", w.mAl); + WriteFloat(pbrSpecularGlossiness, pbrSG.glossinessFactor, "glossinessFactor", w.mAl); } WriteTex(pbrSpecularGlossiness, pbrSG.diffuseTexture, "diffuseTexture", w.mAl); From 5f58ef82b93c7f6fd9c0c08c3f8d739db0a29047 Mon Sep 17 00:00:00 2001 From: Rem Date: Fri, 25 Oct 2019 10:18:27 +0300 Subject: [PATCH 17/74] prevent accidental lower casing material names in ReplaceDefaultMaterial --- code/3DS/3DSConverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/3DS/3DSConverter.cpp b/code/3DS/3DSConverter.cpp index 2176b75fc2..3c3da36a3a 100644 --- a/code/3DS/3DSConverter.cpp +++ b/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 ) ); } From 2d086fd23627289fc5fecf87ffd0ecb22640be86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20D=C3=BCmig?= Date: Fri, 25 Oct 2019 15:10:54 +0200 Subject: [PATCH 18/74] ColladaParser: fix handling of empty XML-elements --- code/Collada/ColladaParser.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index 646ec473db..fc7ab8c2f8 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -3236,6 +3236,10 @@ void ColladaParser::TestOpening(const char* pName) // Tests for the closing tag of the given element, throws an exception if not found void ColladaParser::TestClosing(const char* pName) { + // check if we have an empty (self-closing) element + if (mReader->isEmptyElement()) + return; + // check if we're already on the closing tag and return right away if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END && strcmp(mReader->getNodeName(), pName) == 0) return; From 5baff40edc754efbf0c98120aa08cf1d67b771a6 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Wed, 2 Oct 2019 19:44:30 +0100 Subject: [PATCH 19/74] Git ignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 9dcb6623d9..65a54aaebf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ build .project *.kdev4* +# build artefacts +*.o +*.a + # Visual Studio *.sln *.ncb From b5f659c98d86c82ff29ff4c4a8faf92271ff9618 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Wed, 2 Oct 2019 19:44:58 +0100 Subject: [PATCH 20/74] Added clang format --- .clang-format | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..eba6d586f0 --- /dev/null +++ b/.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 +... From 168ae22ad4dc3f079b4efff7d77be02b1f1b99c5 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sat, 26 Oct 2019 16:28:51 +0100 Subject: [PATCH 21/74] Implemented easy armature lookup This lets you directly retrieve the node a bone links to and informs you of the armature directly This also fixes a bug with bone name being made unique which causes them to become not 1:1 what the modeller has imported. --- code/FBX/FBXConverter.cpp | 456 +++++++++++++++++++++++++++++--------- code/FBX/FBXConverter.h | 69 ++++-- code/FBX/FBXImporter.cpp | 212 +++++++++--------- include/assimp/mesh.h | 19 +- 4 files changed, 523 insertions(+), 233 deletions(-) diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index b8601a2a50..f8225f2e5c 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -68,7 +68,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include - +#include +#include namespace Assimp { namespace FBX { @@ -120,6 +121,46 @@ namespace Assimp { ConvertGlobalSettings(); TransferDataToScene(); + // Now convert all bone positions to the correct mOffsetMatrix + std::vector bones; + std::vector nodes; + std::map bone_stack; + BuildBoneList(out->mRootNode, out->mRootNode, out, bones); + BuildNodeList(out->mRootNode, nodes ); + + + BuildBoneStack(out->mRootNode, out->mRootNode, out, bones, bone_stack, nodes); + + std::cout << "Bone stack size: " << bone_stack.size() << std::endl; + + for( std::pair kvp : bone_stack ) + { + aiBone *bone = kvp.first; + aiNode *bone_node = kvp.second; + std::cout << "active node lookup: " << bone->mName.C_Str() << std::endl; + // 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; + + // apply full hierarchy to transform for basic offset + while( bone_node->mParent ) + { + bone->mRestMatrix = bone_node->mTransformation * bone->mRestMatrix; + bone_node = bone_node->mParent; + } + } + + // if we didn't read any meshes set the AI_SCENE_FLAGS_INCOMPLETE // to make sure the scene passes assimp's validation. FBX files // need not contain geometry (i.e. camera animations, raw armatures). @@ -138,6 +179,167 @@ namespace Assimp { std::for_each(textures.begin(), textures.end(), Util::delete_fun()); } + /* 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 * FBXConverter::GetArmatureRoot(aiNode *bone_node, std::vector &bone_list) + { + while(bone_node) + { + if(!IsBoneNode(bone_node->mName, bone_list)) + { + std::cout << "Found valid armature: " << bone_node->mName.C_Str() << std::endl; + return bone_node; + } + + bone_node = bone_node->mParent; + } + + std::cout << "can't find armature! node: " << bone_node << std::endl; + + return NULL; + } + + /* Simple IsBoneNode check if this could be a bone */ + bool FBXConverter::IsBoneNode(const aiString &bone_name, std::vector& 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* FBXConverter::GetNodeFromStack(const aiString &node_name, std::vector &nodes) + { + std::vector::iterator iter; + aiNode *found = NULL; + 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 != NULL) { + // now pop the element from the node list + nodes.erase(iter); + + return found; + } + return NULL; + } + + /* Prepare flat node list which can be used for non recursive lookups later */ + void FBXConverter::BuildNodeList(aiNode *current_node, std::vector &nodes) + { + assert(current_node); + + for( unsigned int nodeId = 0; nodeId < current_node->mNumChildren; ++nodeId) + { + aiNode *child = current_node->mChildren[nodeId]; + assert(child); + + nodes.push_back(child); + + BuildNodeList(child, nodes); + } + } + + /* 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 FBXConverter::BuildBoneList(aiNode *current_node, const aiNode * root_node, const aiScene *scene, std::vector &bones ) + { + assert(scene); + for( unsigned int nodeId = 0; nodeId < current_node->mNumChildren; ++nodeId) + { + aiNode *child = current_node->mChildren[nodeId]; + assert(child); + + // check for bones + for( unsigned int meshId = 0; meshId < child->mNumMeshes; ++meshId) + { + assert(child->mMeshes); + unsigned int mesh_index = child->mMeshes[meshId]; + aiMesh *mesh = scene->mMeshes[ mesh_index ]; + 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); + } + } + + /* 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 FBXConverter::BuildBoneStack(aiNode *current_node, const aiNode *root_node, const aiScene *scene, + const std::vector &bones, + std::map &bone_stack, + std::vector &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 == NULL) + { + node_stack.clear(); + BuildNodeList(out->mRootNode, node_stack ); + std::cout << "Resetting bone stack: null element " << bone->mName.C_Str() << std::endl; + + node = GetNodeFromStack(bone->mName, node_stack); + + if(!node) { + std::cout << "serious import issue armature failed to be detected?" << std::endl; + continue; + } + } + + std::cout << "Successfully added bone to stack and have valid armature: " << bone->mName.C_Str() << std::endl; + + bone_stack.insert(std::pair(bone, node)); + } + } + void FBXConverter::ConvertRootNode() { out->mRootNode = new aiNode(); std::string unique_name; @@ -145,7 +347,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 +381,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& conns = doc.GetConnectionsByDestinationSequenced(id, "Model"); std::vector nodes; @@ -191,62 +396,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(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& child_conns @@ -258,7 +470,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 +492,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,10 +509,10 @@ namespace Assimp { } if (nodes.size()) { - parent.mChildren = new aiNode*[nodes.size()](); - parent.mNumChildren = static_cast(nodes.size()); + parent->mChildren = new aiNode*[nodes.size()](); + parent->mNumChildren = static_cast(nodes.size()); - std::swap_ranges(nodes.begin(), nodes.end(), parent.mChildren); + std::swap_ranges(nodes.begin(), nodes.end(), parent->mChildren); } } catch (std::exception&) { @@ -803,7 +1015,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 +1117,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& geos = model.GetGeometry(); @@ -917,11 +1130,12 @@ namespace Assimp { const MeshGeometry* const mesh = dynamic_cast(geo); const LineGeometry* const line = dynamic_cast(geo); if (mesh) { - const std::vector& indices = ConvertMesh(*mesh, model, node_global_transform, nd); + const std::vector& 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& indices = ConvertLine(*line, model, node_global_transform, nd); + const std::vector& indices = ConvertLine(*line, model, parent, root_node); std::copy(indices.begin(), indices.end(), std::back_inserter(meshes)); } else { @@ -930,15 +1144,16 @@ namespace Assimp { } if (meshes.size()) { - nd.mMeshes = new unsigned int[meshes.size()](); - nd.mNumMeshes = static_cast(meshes.size()); + parent->mMeshes = new unsigned int[meshes.size()](); + parent->mNumMeshes = static_cast(meshes.size()); - std::swap_ranges(meshes.begin(), meshes.end(), nd.mMeshes); + std::swap_ranges(meshes.begin(), meshes.end(), parent->mMeshes); } } - std::vector FBXConverter::ConvertMesh(const MeshGeometry& mesh, const Model& model, - const aiMatrix4x4& node_global_transform, aiNode& nd) + std::vector + FBXConverter::ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node, + const aiMatrix4x4 &absolute_transform) { std::vector temp; @@ -962,18 +1177,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 FBXConverter::ConvertLine(const LineGeometry& line, const Model& model, - const aiMatrix4x4& node_global_transform, aiNode& nd) + aiNode *parent, aiNode *root_node) { std::vector temp; @@ -984,7 +1199,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 +1234,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 +1251,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& vertices = mesh.GetVertices(); const std::vector& faces = mesh.GetFaceIndexCounts(); @@ -1164,7 +1380,8 @@ namespace Assimp { } if (doc.Settings().readWeights && mesh.DeformerSkin() != NULL) { - ConvertWeights(out_mesh, model, mesh, node_global_transform, NO_MATERIAL_SEPARATION); + ConvertWeights(out_mesh, model, mesh, absolute_transform, parent, root_node, NO_MATERIAL_SEPARATION, + nullptr); } std::vector animMeshes; @@ -1209,8 +1426,10 @@ namespace Assimp { return static_cast(meshes.size() - 1); } - std::vector FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry& mesh, const Model& model, - const aiMatrix4x4& node_global_transform, aiNode& nd) + std::vector + 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 +1440,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,12 +1448,12 @@ 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& vertices = mesh.GetVertices(); @@ -1399,7 +1618,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 animMeshes; @@ -1449,10 +1668,10 @@ namespace Assimp { return static_cast(meshes.size() - 1); } - void FBXConverter::ConvertWeights(aiMesh* out, const Model& model, const MeshGeometry& geo, - const aiMatrix4x4& node_global_transform, - unsigned int materialIndex, - std::vector* 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 *outputVertStartIndices) { ai_assert(geo.DeformerSkin()); @@ -1463,13 +1682,12 @@ namespace Assimp { const Skin& sk = *geo.DeformerSkin(); std::vector 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,6 +1701,7 @@ 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) { @@ -1520,68 +1739,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()); throw; } if (bones.empty()) { + out->mBones = nullptr; + out->mNumBones = 0; return; - } - - out->mBones = new aiBone*[bones.size()](); - out->mNumBones = static_cast(bones.size()); + } else { + out->mBones = new aiBone *[bones.size()](); + out->mNumBones = static_cast(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& bones, const Model& /*model*/, const Cluster& cl, - std::vector& out_indices, - std::vector& index_out_indices, - std::vector& 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 &local_mesh_bones, const Cluster *cl, + std::vector &out_indices, std::vector &index_out_indices, + std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, + aiNode *parent, aiNode *root_node) { + 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 = NULL; - 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(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::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(out_indices.size()); + cursor = bone->mWeights = new aiVertexWeight[out_indices.size()]; + + const size_t no_index_sentinel = std::numeric_limits::max(); + const WeightArray& weights = cl->GetWeights(); + + 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]; - const size_t cc = count_out_indices[i]; - for (size_t j = 0; j < cc; ++j) { - aiVertexWeight& out_weight = *cursor++; + if (index_index == no_index_sentinel) { + continue; + } - out_weight.mVertexId = static_cast(out_indices[index_index + j]); - out_weight.mWeight = weights[i]; + 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(out_indices[index_index + j]); + out_weight.mWeight = weights[i]; + } } + + bone_map.insert(std::pair(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, diff --git a/code/FBX/FBXConverter.h b/code/FBX/FBXConverter.h index 77ced1950c..619da92c17 100644 --- a/code/FBX/FBXConverter.h +++ b/code/FBX/FBXConverter.h @@ -123,7 +123,7 @@ class FBXConverter { // ------------------------------------------------------------------------------------------------ // 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 @@ class FBXConverter { 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 ConvertMesh(const MeshGeometry& mesh, const Model& model, - const aiMatrix4x4& node_global_transform, aiNode& nd); + std::vector + ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node, + const aiMatrix4x4 &absolute_transform); // ------------------------------------------------------------------------------------------------ std::vector 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 ConvertMeshMultiMaterial(const MeshGeometry& mesh, const Model& model, - const aiMatrix4x4& node_global_transform, aiNode& nd); + std::vector + 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::max() */ @@ -217,17 +220,17 @@ class FBXConverter { * - 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* 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 *outputVertStartIndices = NULL); + // lookup + static const aiNode* GetNodeByName( const aiString& name, aiNode *current_node ); // ------------------------------------------------------------------------------------------------ - void ConvertCluster(std::vector& bones, const Model& /*model*/, const Cluster& cl, - std::vector& out_indices, - std::vector& index_out_indices, - std::vector& count_out_indices, - const aiMatrix4x4& node_global_transform); + void ConvertCluster(std::vector &local_mesh_bones, const Cluster *cl, + std::vector &out_indices, std::vector &index_out_indices, + std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, + aiNode *parent, aiNode *root_node); // ------------------------------------------------------------------------------------------------ void ConvertMaterialForMesh(aiMesh* out, const Model& model, const MeshGeometry& geo, @@ -452,10 +455,30 @@ class FBXConverter { using NodeNameCache = std::unordered_map; 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 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& bones); + + void BuildBoneStack(aiNode *current_node, const aiNode *root_node, const aiScene *scene, + const std::vector &bones, + std::map &bone_stack, + std::vector &node_stack ); + + static void BuildNodeList(aiNode *current_node, std::vector &nodes); + + static aiNode *GetNodeFromStack(const aiString &node_name, std::vector &nodes); + + static aiNode *GetArmatureRoot(aiNode *bone_node, std::vector &bone_list); + + static bool IsBoneNode(const aiString &bone_name, std::vector &bones); }; } diff --git a/code/FBX/FBXImporter.cpp b/code/FBX/FBXImporter.cpp index c8c1a6853d..afcc1ddc78 100644 --- a/code/FBX/FBXImporter.cpp +++ b/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 #include -#include +#include #include +#include namespace Assimp { -template<> -const char* LogFunctions::Prefix() { - static auto prefix = "FBX: "; - return prefix; +template <> +const char *LogFunctions::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 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 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()); - } - catch(std::exception&) { - std::for_each(tokens.begin(),tokens.end(),Util::delete_fun()); - throw; - } +void FBXImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSystem *pIOHandler) { + std::unique_ptr 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 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()); + } catch (std::exception &) { + std::for_each(tokens.begin(), tokens.end(), Util::delete_fun()); + throw; + } } #endif // !ASSIMP_BUILD_NO_FBX_IMPORTER diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index eb30ad5df9..e652a69275 100644 --- a/include/assimp/mesh.h +++ b/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,12 @@ struct aiBone { //! The maximum value for this member is #AI_MAX_BONE_WEIGHTS. unsigned int mNumWeights; + // The bone armature node - used for skeleton conversion + C_STRUCT aiNode* mArmature; + + // The bone node in the scene - used for skeleton conversion + C_STRUCT aiNode* mNode; + //! The influence weights of this bone, by vertex index. C_STRUCT aiVertexWeight* mWeights; @@ -284,6 +293,11 @@ struct aiBone { */ C_STRUCT aiMatrix4x4 mOffsetMatrix; + /** Matrix used for the global rest transform + * This tells you directly the rest without extending as required in most game engine implementations + * */ + C_STRUCT aiMatrix4x4 mRestMatrix; + #ifdef __cplusplus //! Default constructor @@ -773,7 +787,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; } From 6ea97a1282835ef43e25044a5053882bbf285d7a Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sat, 26 Oct 2019 17:27:07 +0100 Subject: [PATCH 22/74] Updated test cases to test import of names This now doesn't overwrite names anymore as this would cause nasty bugs application side. We can now support these by default without having to handle them as edge cases. --- test/unit/utFBXImporterExporter.cpp | 39 ++++++++--------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/test/unit/utFBXImporterExporter.cpp b/test/unit/utFBXImporterExporter.cpp index 1445e3c3e8..e73733a0df 100644 --- a/test/unit/utFBXImporterExporter.cpp +++ b/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); From 93efe4197aff5f46358545dd6b890ca454e8bfcd Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sat, 26 Oct 2019 18:09:26 +0100 Subject: [PATCH 23/74] Removed redundant rest matrix and fixed assert compile error --- code/FBX/FBXConverter.cpp | 21 +++++++-------------- include/assimp/mesh.h | 5 ----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index f8225f2e5c..0d754a6381 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -151,13 +151,6 @@ namespace Assimp { // set this bone node to be referenced properly ai_assert(bone_node); bone->mNode = bone_node; - - // apply full hierarchy to transform for basic offset - while( bone_node->mParent ) - { - bone->mRestMatrix = bone_node->mTransformation * bone->mRestMatrix; - bone_node = bone_node->mParent; - } } @@ -249,12 +242,12 @@ namespace Assimp { /* Prepare flat node list which can be used for non recursive lookups later */ void FBXConverter::BuildNodeList(aiNode *current_node, std::vector &nodes) { - assert(current_node); + ai_assert(current_node); for( unsigned int nodeId = 0; nodeId < current_node->mNumChildren; ++nodeId) { aiNode *child = current_node->mChildren[nodeId]; - assert(child); + ai_assert(child); nodes.push_back(child); @@ -268,19 +261,19 @@ namespace Assimp { /* Source: sketch fab log cutter fbx */ void FBXConverter::BuildBoneList(aiNode *current_node, const aiNode * root_node, const aiScene *scene, std::vector &bones ) { - assert(scene); + ai_assert(scene); for( unsigned int nodeId = 0; nodeId < current_node->mNumChildren; ++nodeId) { aiNode *child = current_node->mChildren[nodeId]; - assert(child); + ai_assert(child); // check for bones for( unsigned int meshId = 0; meshId < child->mNumMeshes; ++meshId) { - assert(child->mMeshes); + ai_assert(child->mMeshes); unsigned int mesh_index = child->mMeshes[meshId]; aiMesh *mesh = scene->mMeshes[ mesh_index ]; - assert(mesh); + ai_assert(mesh); for( unsigned int boneId = 0; boneId < mesh->mNumBones; ++boneId) { @@ -1777,7 +1770,7 @@ namespace Assimp { std::vector &out_indices, std::vector &index_out_indices, std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, aiNode *parent, aiNode *root_node) { - assert(cl); // make sure cluster valid + ai_assert(cl); // make sure cluster valid std::string deformer_name = cl->TargetNode()->Name(); aiString bone_name = aiString(FixNodeName(deformer_name)); diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index e652a69275..617835e51a 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -293,11 +293,6 @@ struct aiBone { */ C_STRUCT aiMatrix4x4 mOffsetMatrix; - /** Matrix used for the global rest transform - * This tells you directly the rest without extending as required in most game engine implementations - * */ - C_STRUCT aiMatrix4x4 mRestMatrix; - #ifdef __cplusplus //! Default constructor From 46cdd81d754bb602dabd3a3a026607bc2759e37e Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sat, 26 Oct 2019 18:47:48 +0100 Subject: [PATCH 24/74] Added ArmaturePopulate scale process for all formats --- code/CMakeLists.txt | 2 + code/FBX/FBXConverter.cpp | 194 ------------------ code/PostProcessing/ArmaturePopulate.cpp | 247 +++++++++++++++++++++++ code/PostProcessing/ArmaturePopulate.h | 111 ++++++++++ include/assimp/postprocess.h | 12 ++ 5 files changed, 372 insertions(+), 194 deletions(-) create mode 100644 code/PostProcessing/ArmaturePopulate.cpp create mode 100644 code/PostProcessing/ArmaturePopulate.h diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index eec805b544..2c030abbbc 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -670,6 +670,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 ) diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index 0d754a6381..88abd6721b 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -121,39 +121,6 @@ namespace Assimp { ConvertGlobalSettings(); TransferDataToScene(); - // Now convert all bone positions to the correct mOffsetMatrix - std::vector bones; - std::vector nodes; - std::map bone_stack; - BuildBoneList(out->mRootNode, out->mRootNode, out, bones); - BuildNodeList(out->mRootNode, nodes ); - - - BuildBoneStack(out->mRootNode, out->mRootNode, out, bones, bone_stack, nodes); - - std::cout << "Bone stack size: " << bone_stack.size() << std::endl; - - for( std::pair kvp : bone_stack ) - { - aiBone *bone = kvp.first; - aiNode *bone_node = kvp.second; - std::cout << "active node lookup: " << bone->mName.C_Str() << std::endl; - // 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; - } - - // if we didn't read any meshes set the AI_SCENE_FLAGS_INCOMPLETE // to make sure the scene passes assimp's validation. FBX files // need not contain geometry (i.e. camera animations, raw armatures). @@ -172,167 +139,6 @@ namespace Assimp { std::for_each(textures.begin(), textures.end(), Util::delete_fun()); } - /* 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 * FBXConverter::GetArmatureRoot(aiNode *bone_node, std::vector &bone_list) - { - while(bone_node) - { - if(!IsBoneNode(bone_node->mName, bone_list)) - { - std::cout << "Found valid armature: " << bone_node->mName.C_Str() << std::endl; - return bone_node; - } - - bone_node = bone_node->mParent; - } - - std::cout << "can't find armature! node: " << bone_node << std::endl; - - return NULL; - } - - /* Simple IsBoneNode check if this could be a bone */ - bool FBXConverter::IsBoneNode(const aiString &bone_name, std::vector& 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* FBXConverter::GetNodeFromStack(const aiString &node_name, std::vector &nodes) - { - std::vector::iterator iter; - aiNode *found = NULL; - 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 != NULL) { - // now pop the element from the node list - nodes.erase(iter); - - return found; - } - return NULL; - } - - /* Prepare flat node list which can be used for non recursive lookups later */ - void FBXConverter::BuildNodeList(aiNode *current_node, std::vector &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); - } - } - - /* 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 FBXConverter::BuildBoneList(aiNode *current_node, const aiNode * root_node, const aiScene *scene, std::vector &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); - } - } - - /* 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 FBXConverter::BuildBoneStack(aiNode *current_node, const aiNode *root_node, const aiScene *scene, - const std::vector &bones, - std::map &bone_stack, - std::vector &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 == NULL) - { - node_stack.clear(); - BuildNodeList(out->mRootNode, node_stack ); - std::cout << "Resetting bone stack: null element " << bone->mName.C_Str() << std::endl; - - node = GetNodeFromStack(bone->mName, node_stack); - - if(!node) { - std::cout << "serious import issue armature failed to be detected?" << std::endl; - continue; - } - } - - std::cout << "Successfully added bone to stack and have valid armature: " << bone->mName.C_Str() << std::endl; - - bone_stack.insert(std::pair(bone, node)); - } - } - void FBXConverter::ConvertRootNode() { out->mRootNode = new aiNode(); std::string unique_name; diff --git a/code/PostProcessing/ArmaturePopulate.cpp b/code/PostProcessing/ArmaturePopulate.cpp new file mode 100644 index 0000000000..1892645a9b --- /dev/null +++ b/code/PostProcessing/ArmaturePopulate.cpp @@ -0,0 +1,247 @@ +/* +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 +#include +#include +#include + +namespace Assimp { + +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 bones; + std::vector nodes; + std::map 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: %ld\n", bone_stack.size()); + + for (std::pair kvp : bone_stack) { + aiBone *bone = kvp.first; + aiNode *bone_node = kvp.second; + ASSIMP_LOG_DEBUG_F("active node lookup: %s\n", 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; + } +} + +/* 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 &bone_list) { + while (bone_node) { + if (!IsBoneNode(bone_node->mName, bone_list)) { + ASSIMP_LOG_DEBUG_F("Found valid armature: %s\n", bone_node->mName.C_Str()); + return bone_node; + } + + bone_node = bone_node->mParent; + } + + ASSIMP_LOG_WARN_F("can't find armature! node: %s\n", bone_node->mName.C_Str()); + + return NULL; +} + +/* Simple IsBoneNode check if this could be a bone */ +bool ArmaturePopulate::IsBoneNode(const aiString &bone_name, + std::vector &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 &nodes) { + std::vector::iterator iter; + aiNode *found = NULL; + 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 != NULL) { + // now pop the element from the node list + nodes.erase(iter); + + return found; + } + return NULL; +} + +/* Prepare flat node list which can be used for non recursive lookups later */ +void ArmaturePopulate::BuildNodeList(const aiNode *current_node, + std::vector &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); + } +} + +/* 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 &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); + } +} + +/* 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 &bones, + std::map &bone_stack, + std::vector &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 == NULL) { + node_stack.clear(); + BuildNodeList(root_node, node_stack); + ASSIMP_LOG_DEBUG_F("Resetting bone stack: null element %s\n", bone->mName.C_Str()); + + node = GetNodeFromStack(bone->mName, node_stack); + + if (!node) { + ASSIMP_LOG_ERROR("serious import issue armature failed to be detected"); + continue; + } + } + + ASSIMP_LOG_DEBUG_F("Successfully added bone to stack and have valid armature: %s\n", bone->mName.C_Str()); + + bone_stack.insert(std::pair(bone, node)); + } +} + +} // Namespace Assimp diff --git a/code/PostProcessing/ArmaturePopulate.h b/code/PostProcessing/ArmaturePopulate.h new file mode 100644 index 0000000000..d84fbe09b6 --- /dev/null +++ b/code/PostProcessing/ArmaturePopulate.h @@ -0,0 +1,111 @@ +/* +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 SCALE_PROCESS_H_ +#define SCALE_PROCESS_H_ + +#include "Common/BaseProcess.h" +#include +#include + +struct aiNode; +struct aiBone; + +#if (!defined AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT) +# define AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT 1.0f +#endif // !! AI_DEBONE_THRESHOLD + +namespace Assimp { + +// --------------------------------------------------------------------------- +/** ScaleProcess: Class to rescale the whole model. + * Now rescales animations, bones, and blend shapes properly. + * Please note this will not write to 'scale' transform it will rewrite mesh + * and matrixes so that your scale values + * from your model package are preserved, so this is completely intentional + * bugs should be reported as soon as they are found. +*/ +class ASSIMP_API ArmaturePopulate : public BaseProcess { +public: + /// The default class constructor. + ArmaturePopulate() : BaseProcess() + {} + + /// 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 &bone_list); + + static bool IsBoneNode(const aiString &bone_name, + std::vector &bones); + + static aiNode *GetNodeFromStack(const aiString &node_name, + std::vector &nodes); + + static void BuildNodeList(const aiNode *current_node, + std::vector &nodes); + + static void BuildBoneList(aiNode *current_node, const aiNode *root_node, + const aiScene *scene, + std::vector &bones); + + static void BuildBoneStack(aiNode *current_node, const aiNode *root_node, + const aiScene *scene, + const std::vector &bones, + std::map &bone_stack, + std::vector &node_stack); +}; + +} // Namespace Assimp + + +#endif // SCALE_PROCESS_H_ \ No newline at end of file diff --git a/include/assimp/postprocess.h b/include/assimp/postprocess.h index 77d387c7e7..2af48aedd7 100644 --- a/include/assimp/postprocess.h +++ b/include/assimp/postprocess.h @@ -537,6 +537,18 @@ enum aiPostProcessSteps */ aiProcess_Debone = 0x4000000, + + // ------------------------------------------------------------------------- + /** + * 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 = 0x5000000, + // ------------------------------------------------------------------------- /**


This step will perform a global scale of the model. * From 514257f58700385c15d23b27c11a7240cd433550 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sat, 26 Oct 2019 19:48:34 +0100 Subject: [PATCH 25/74] Added unit tests for ArmaturePopulate when used (added huestos model to tests) Added clear documentation for this too to explain, you need to enable it to make it available Signed-off-by: RevoluPowered --- code/Common/PostStepRegistry.cpp | 7 ++ code/PostProcessing/ArmaturePopulate.cpp | 14 +++- code/PostProcessing/ArmaturePopulate.h | 16 ++--- include/assimp/mesh.h | 4 ++ test/CMakeLists.txt | 1 + test/models/FBX/huesitos.fbx | Bin 0 -> 111564 bytes test/unit/utArmaturePopulate.cpp | 82 +++++++++++++++++++++++ 7 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 test/models/FBX/huesitos.fbx create mode 100644 test/unit/utArmaturePopulate.cpp diff --git a/code/Common/PostStepRegistry.cpp b/code/Common/PostStepRegistry.cpp index ef58f8ddfd..8ff4af0400 100644 --- a/code/Common/PostStepRegistry.cpp +++ b/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 diff --git a/code/PostProcessing/ArmaturePopulate.cpp b/code/PostProcessing/ArmaturePopulate.cpp index 1892645a9b..11fffc3996 100644 --- a/code/PostProcessing/ArmaturePopulate.cpp +++ b/code/PostProcessing/ArmaturePopulate.cpp @@ -48,6 +48,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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; } @@ -104,9 +112,9 @@ aiNode *ArmaturePopulate::GetArmatureRoot(aiNode *bone_node, bone_node = bone_node->mParent; } - - ASSIMP_LOG_WARN_F("can't find armature! node: %s\n", bone_node->mName.C_Str()); - + + ASSIMP_LOG_WARN("GetArmatureRoot() can't find armature!"); + return NULL; } diff --git a/code/PostProcessing/ArmaturePopulate.h b/code/PostProcessing/ArmaturePopulate.h index d84fbe09b6..30bfc7509d 100644 --- a/code/PostProcessing/ArmaturePopulate.h +++ b/code/PostProcessing/ArmaturePopulate.h @@ -39,20 +39,18 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- */ -#ifndef SCALE_PROCESS_H_ -#define SCALE_PROCESS_H_ +#ifndef ARMATURE_POPULATE_H_ +#define ARMATURE_POPULATE_H_ #include "Common/BaseProcess.h" +#include #include #include + struct aiNode; struct aiBone; -#if (!defined AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT) -# define AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT 1.0f -#endif // !! AI_DEBONE_THRESHOLD - namespace Assimp { // --------------------------------------------------------------------------- @@ -66,12 +64,10 @@ namespace Assimp { class ASSIMP_API ArmaturePopulate : public BaseProcess { public: /// The default class constructor. - ArmaturePopulate() : BaseProcess() - {} + ArmaturePopulate(); /// The class destructor. - virtual ~ArmaturePopulate() - {} + virtual ~ArmaturePopulate(); /// Overwritten, @see BaseProcess virtual bool IsActive( unsigned int pFlags ) const; diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index 617835e51a..bde69ac9b4 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -271,12 +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; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 913813c3be..f59ce000ac 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -148,6 +148,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 diff --git a/test/models/FBX/huesitos.fbx b/test/models/FBX/huesitos.fbx new file mode 100644 index 0000000000000000000000000000000000000000..646271392e5fbd5c3a069498778fb70ba302ebef GIT binary patch literal 111564 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 BGFAWp literal 0 HcmV?d00001 diff --git a/test/unit/utArmaturePopulate.cpp b/test/unit/utArmaturePopulate.cpp new file mode 100644 index 0000000000..4aaf8aa7c3 --- /dev/null +++ b/test/unit/utArmaturePopulate.cpp @@ -0,0 +1,82 @@ +/* +--------------------------------------------------------------------------- +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 +#include +#include +#include +#include + +#include "PostProcessing/ArmaturePopulate.h" + +namespace Assimp { +namespace UnitTest { + +class utArmaturePopulate : public ::testing::Test { + // empty +}; + +TEST_F( utArmaturePopulate, importCheckForArmatureTest) { + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/FBX/huesitos.fbx", aiProcess_ValidateDataStructure | aiProcess_PopulateArmatureData); + 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 From a30936954ef49236397938305e9c05e57670203c Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 11:21:23 +0000 Subject: [PATCH 26/74] Best to check the number of children before checking the actual array --- code/Common/scene.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/Common/scene.cpp b/code/Common/scene.cpp index 2acb348d81..3155815047 100644 --- a/code/Common/scene.cpp +++ b/code/Common/scene.cpp @@ -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]; From 5d0c63391b729cd7c839a9d8088625d1716c5fa5 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 11:21:49 +0000 Subject: [PATCH 27/74] Explicitly set the size of the parent node if we have no children --- code/FBX/FBXConverter.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index 88abd6721b..8f916a25b8 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -313,6 +313,12 @@ namespace Assimp { std::swap_ranges(nodes.begin(), nodes.end(), parent->mChildren); } + else + { + parent->mNumChildren = 0; + parent->mChildren = nullptr; + } + } catch (std::exception&) { Util::delete_fun deleter; From 9c8d8357046e4d422b1ba20ffcf3b2da072c7774 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 11:29:56 +0000 Subject: [PATCH 28/74] Explicitly use nullptr --- code/Common/scene.cpp | 16 ++++++++-------- code/FBX/FBXConverter.cpp | 20 ++++++++++---------- code/PostProcessing/ArmaturePopulate.cpp | 12 ++++++------ include/assimp/mesh.h | 18 +++++++++--------- include/assimp/scene.h | 22 +++++++++++----------- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/code/Common/scene.cpp b/code/Common/scene.cpp index 3155815047..d15619acff 100644 --- a/code/Common/scene.cpp +++ b/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 } diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index 8f916a25b8..20344331cc 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -1134,7 +1134,7 @@ namespace Assimp { binormals = &tempBinormals; } else { - binormals = NULL; + binormals = nullptr; } } @@ -1184,7 +1184,7 @@ namespace Assimp { ConvertMaterialForMesh(out_mesh, model, mesh, mindices[0]); } - if (doc.Settings().readWeights && mesh.DeformerSkin() != NULL) { + if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr) { ConvertWeights(out_mesh, model, mesh, absolute_transform, parent, root_node, NO_MATERIAL_SEPARATION, nullptr); } @@ -1264,7 +1264,7 @@ namespace Assimp { const std::vector& vertices = mesh.GetVertices(); const std::vector& 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; @@ -1324,7 +1324,7 @@ namespace Assimp { binormals = &tempBinormals; } else { - binormals = NULL; + binormals = nullptr; } } @@ -1513,9 +1513,9 @@ namespace Assimp { 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); @@ -1586,7 +1586,7 @@ namespace Assimp { std::string deformer_name = cl->TargetNode()->Name(); aiString bone_name = aiString(FixNodeName(deformer_name)); - aiBone *bone = NULL; + aiBone *bone = nullptr; if (bone_map.count(deformer_name)) { std::cout << "retrieved bone from lookup " << bone_name.C_Str() << ". Deformer: " << deformer_name @@ -2740,7 +2740,7 @@ void FBXConverter::SetShadingPropertiesRaw(aiMaterial* out_mat, const PropertyTa // sanity check whether the input is ok static void validateAnimCurveNodes(const std::vector& curves, bool strictMode) { - const Object* target(NULL); + const Object* target(nullptr); for (const AnimationCurveNode* node : curves) { if (!target) { target = node->Target(); @@ -2771,7 +2771,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); @@ -3619,7 +3619,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. diff --git a/code/PostProcessing/ArmaturePopulate.cpp b/code/PostProcessing/ArmaturePopulate.cpp index 11fffc3996..c677b193a8 100644 --- a/code/PostProcessing/ArmaturePopulate.cpp +++ b/code/PostProcessing/ArmaturePopulate.cpp @@ -115,7 +115,7 @@ aiNode *ArmaturePopulate::GetArmatureRoot(aiNode *bone_node, ASSIMP_LOG_WARN("GetArmatureRoot() can't find armature!"); - return NULL; + return nullptr; } /* Simple IsBoneNode check if this could be a bone */ @@ -139,7 +139,7 @@ bool ArmaturePopulate::IsBoneNode(const aiString &bone_name, aiNode *ArmaturePopulate::GetNodeFromStack(const aiString &node_name, std::vector &nodes) { std::vector::iterator iter; - aiNode *found = NULL; + aiNode *found = nullptr; for (iter = nodes.begin(); iter < nodes.end(); ++iter) { aiNode *element = *iter; ai_assert(element); @@ -150,13 +150,13 @@ aiNode *ArmaturePopulate::GetNodeFromStack(const aiString &node_name, } } - if (found != NULL) { + if (found != nullptr) { // now pop the element from the node list nodes.erase(iter); return found; } - return NULL; + return nullptr; } /* Prepare flat node list which can be used for non recursive lookups later */ @@ -233,10 +233,10 @@ void ArmaturePopulate::BuildBoneStack(aiNode *current_node, for (aiBone *bone : bones) { ai_assert(bone); aiNode *node = GetNodeFromStack(bone->mName, node_stack); - if (node == NULL) { + if (node == nullptr) { node_stack.clear(); BuildNodeList(root_node, node_stack); - ASSIMP_LOG_DEBUG_F("Resetting bone stack: null element %s\n", bone->mName.C_Str()); + ASSIMP_LOG_DEBUG_F("Resetting bone stack: nullptr element %s\n", bone->mName.C_Str()); node = GetNodeFromStack(bone->mName, node_stack); diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index bde69ac9b4..fbf2a857ad 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -435,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; @@ -613,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 @@ -636,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 @@ -650,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. @@ -659,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]; @@ -688,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; diff --git a/include/assimp/scene.h b/include/assimp/scene.h index e69c81803d..b76709eb15 100644 --- a/include/assimp/scene.h +++ b/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 From 212bcfe75c15bc21a14a715fa4e06886a41d944a Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 12:27:54 +0000 Subject: [PATCH 29/74] Test disable cache --- code/PostProcessing/ArmaturePopulate.cpp | 3 ++- test/unit/utArmaturePopulate.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/code/PostProcessing/ArmaturePopulate.cpp b/code/PostProcessing/ArmaturePopulate.cpp index c677b193a8..5de2a38709 100644 --- a/code/PostProcessing/ArmaturePopulate.cpp +++ b/code/PostProcessing/ArmaturePopulate.cpp @@ -45,6 +45,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include namespace Assimp { @@ -76,7 +77,7 @@ void ArmaturePopulate::Execute(aiScene *out) { BuildBoneStack(out->mRootNode, out->mRootNode, out, bones, bone_stack, nodes); ASSIMP_LOG_DEBUG_F("Bone stack size: %ld\n", bone_stack.size()); - + std::cout << "post process for armature population has run!" << std::endl; for (std::pair kvp : bone_stack) { aiBone *bone = kvp.first; aiNode *bone_node = kvp.second; diff --git a/test/unit/utArmaturePopulate.cpp b/test/unit/utArmaturePopulate.cpp index 4aaf8aa7c3..835d3fdb99 100644 --- a/test/unit/utArmaturePopulate.cpp +++ b/test/unit/utArmaturePopulate.cpp @@ -64,7 +64,7 @@ class utArmaturePopulate : public ::testing::Test { TEST_F( utArmaturePopulate, importCheckForArmatureTest) { Assimp::Importer importer; - const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/FBX/huesitos.fbx", aiProcess_ValidateDataStructure | aiProcess_PopulateArmatureData); + const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/FBX/huesitos.fbx", aiProcess_PopulateArmatureData); EXPECT_NE( nullptr, scene ); EXPECT_EQ(scene->mNumMeshes, 1u); aiMesh* mesh = scene->mMeshes[0]; From a9a0d4d29b8875f37da55b8e8d621732be24e471 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 13:53:11 +0000 Subject: [PATCH 30/74] Tidying order of function calls and fixed debug statements --- code/PostProcessing/ArmaturePopulate.cpp | 176 ++++++++++++----------- 1 file changed, 94 insertions(+), 82 deletions(-) diff --git a/code/PostProcessing/ArmaturePopulate.cpp b/code/PostProcessing/ArmaturePopulate.cpp index 5de2a38709..75daeb6b59 100644 --- a/code/PostProcessing/ArmaturePopulate.cpp +++ b/code/PostProcessing/ArmaturePopulate.cpp @@ -76,12 +76,12 @@ void ArmaturePopulate::Execute(aiScene *out) { BuildBoneStack(out->mRootNode, out->mRootNode, out, bones, bone_stack, nodes); - ASSIMP_LOG_DEBUG_F("Bone stack size: %ld\n", bone_stack.size()); - std::cout << "post process for armature population has run!" << std::endl; + ASSIMP_LOG_DEBUG_F("Bone stack size: ", bone_stack.size()); + for (std::pair kvp : bone_stack) { aiBone *bone = kvp.first; aiNode *bone_node = kvp.second; - ASSIMP_LOG_DEBUG_F("active node lookup: %s\n", bone->mName.C_Str()); + ASSIMP_LOG_DEBUG_F("active node lookup: ", bone->mName.C_Str()); // lcl transform grab - done in generate_nodes :) // bone->mOffsetMatrix = bone_node->mTransformation; @@ -98,82 +98,6 @@ void ArmaturePopulate::Execute(aiScene *out) { } } -/* 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 &bone_list) { - while (bone_node) { - if (!IsBoneNode(bone_node->mName, bone_list)) { - ASSIMP_LOG_DEBUG_F("Found valid armature: %s\n", bone_node->mName.C_Str()); - return bone_node; - } - - bone_node = bone_node->mParent; - } - - ASSIMP_LOG_WARN("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 &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 &nodes) { - std::vector::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) { - // now pop the element from the node list - nodes.erase(iter); - - return found; - } - return nullptr; -} - -/* Prepare flat node list which can be used for non recursive lookups later */ -void ArmaturePopulate::BuildNodeList(const aiNode *current_node, - std::vector &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); - } -} /* Reprocess all nodes to calculate bone transforms properly based on the REAL * mOffsetMatrix not the local. */ @@ -217,6 +141,21 @@ void ArmaturePopulate::BuildBoneList(aiNode *current_node, } } +/* Prepare flat node list which can be used for non recursive lookups later */ +void ArmaturePopulate::BuildNodeList(const aiNode *current_node, + std::vector &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 :) @@ -237,20 +176,93 @@ void ArmaturePopulate::BuildBoneStack(aiNode *current_node, if (node == nullptr) { node_stack.clear(); BuildNodeList(root_node, node_stack); - ASSIMP_LOG_DEBUG_F("Resetting bone stack: nullptr element %s\n", bone->mName.C_Str()); + 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 armature failed to be detected"); + ASSIMP_LOG_ERROR("serious import issue node for bone was not detected"); continue; } } - ASSIMP_LOG_DEBUG_F("Successfully added bone to stack and have valid armature: %s\n", bone->mName.C_Str()); + 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(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 &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 &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 &nodes) { + std::vector::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 From 5155efe888530daaee69ec098f9627f7a14a9e68 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 13:53:43 +0000 Subject: [PATCH 31/74] Fixed bitmask issue We are approaching the limit for the number of post processes --- include/assimp/postprocess.h | 23 +++++++++++++---------- test/unit/utArmaturePopulate.cpp | 3 ++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/include/assimp/postprocess.h b/include/assimp/postprocess.h index 2af48aedd7..4b6732e80a 100644 --- a/include/assimp/postprocess.h +++ b/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, + // ------------------------------------------------------------------------- /**
This step splits meshes with more than one primitive type in * homogeneous sub-meshes. @@ -538,16 +551,6 @@ enum aiPostProcessSteps aiProcess_Debone = 0x4000000, - // ------------------------------------------------------------------------- - /** - * 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 = 0x5000000, // ------------------------------------------------------------------------- /**
This step will perform a global scale of the model. diff --git a/test/unit/utArmaturePopulate.cpp b/test/unit/utArmaturePopulate.cpp index 835d3fdb99..8eb577d614 100644 --- a/test/unit/utArmaturePopulate.cpp +++ b/test/unit/utArmaturePopulate.cpp @@ -64,7 +64,8 @@ class utArmaturePopulate : public ::testing::Test { TEST_F( utArmaturePopulate, importCheckForArmatureTest) { Assimp::Importer importer; - const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/FBX/huesitos.fbx", aiProcess_PopulateArmatureData); + 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]; From d7d79db0ac2a55f3138bcc1bc1799f00646f0b61 Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Sun, 27 Oct 2019 12:57:47 +0000 Subject: [PATCH 32/74] Tests should always debug log --- test/unit/Main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Main.cpp b/test/unit/Main.cpp index 5ba5c487de..333fd655d9 100644 --- a/test/unit/Main.cpp +++ b/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); From 0baec5f0bd726dc2cd40317abc1015bd6add6c93 Mon Sep 17 00:00:00 2001 From: bzt Date: Tue, 29 Oct 2019 14:14:00 +0100 Subject: [PATCH 33/74] Added M3D format support --- Readme.md | 1 + code/CMakeLists.txt | 12 + code/Common/Exporter.cpp | 7 + code/Common/ImporterRegistry.cpp | 6 + code/M3D/M3DExporter.cpp | 392 +++++++++++ code/M3D/M3DExporter.h | 98 +++ code/M3D/M3DImporter.cpp | 734 +++++++++++++++++++++ code/M3D/M3DImporter.h | 106 +++ code/M3D/M3DMaterials.h | 106 +++ code/M3D/m3d.h | 1 + test/models/M3D/README.md | 14 + test/models/M3D/WusonBlitz0.m3d | Bin 0 -> 29223 bytes test/models/M3D/WusonBlitz1.m3d | Bin 0 -> 35058 bytes test/models/M3D/WusonBlitz2.m3d | Bin 0 -> 42228 bytes test/models/M3D/aliveai_character.m3d | Bin 0 -> 2532 bytes test/models/M3D/cube_normals.m3d | Bin 0 -> 156 bytes test/models/M3D/cube_usemtl.m3d | Bin 0 -> 249 bytes test/models/M3D/cube_with_vertexcolors.a3d | 33 + test/models/M3D/cube_with_vertexcolors.m3d | Bin 0 -> 228 bytes test/models/M3D/mobs_dwarves_character.m3d | Bin 0 -> 11255 bytes test/models/M3D/suzanne.m3d | Bin 0 -> 11645 bytes 21 files changed, 1510 insertions(+) create mode 100644 code/M3D/M3DExporter.cpp create mode 100644 code/M3D/M3DExporter.h create mode 100644 code/M3D/M3DImporter.cpp create mode 100644 code/M3D/M3DImporter.h create mode 100644 code/M3D/M3DMaterials.h create mode 120000 code/M3D/m3d.h create mode 100644 test/models/M3D/README.md create mode 100644 test/models/M3D/WusonBlitz0.m3d create mode 100644 test/models/M3D/WusonBlitz1.m3d create mode 100644 test/models/M3D/WusonBlitz2.m3d create mode 100644 test/models/M3D/aliveai_character.m3d create mode 100644 test/models/M3D/cube_normals.m3d create mode 100644 test/models/M3D/cube_usemtl.m3d create mode 100644 test/models/M3D/cube_with_vertexcolors.a3d create mode 100644 test/models/M3D/cube_with_vertexcolors.m3d create mode 100644 test/models/M3D/mobs_dwarves_character.m3d create mode 100644 test/models/M3D/suzanne.m3d diff --git a/Readme.md b/Readme.md index f749993fd9..f02a3b617a 100644 --- a/Readme.md +++ b/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 diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index eec805b544..910a43562c 100644 --- a/code/CMakeLists.txt +++ b/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 diff --git a/code/Common/Exporter.cpp b/code/Common/Exporter.cpp index 34d49c472a..4ce1a2bd80 100644 --- a/code/Common/Exporter.cpp +++ b/code/Common/Exporter.cpp @@ -102,6 +102,8 @@ 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*); // ------------------------------------------------------------------------------------------------ @@ -179,6 +181,11 @@ Exporter::ExportFormatEntry gExporters[] = Exporter::ExportFormatEntry( "fbxa", "Autodesk FBX (ascii)", "fbx", &ExportSceneFBXA, 0 ), #endif +#ifndef ASSIMP_BUILD_NO_M3D_EXPORTER + Exporter::ExportFormatEntry( "m3d", "Model 3D (binary)", "m3d", &ExportSceneM3D, 0 ), + 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 ), #endif diff --git a/code/Common/ImporterRegistry.cpp b/code/Common/ImporterRegistry.cpp index 32ac3b4168..b9f28f0356 100644 --- a/code/Common/ImporterRegistry.cpp +++ b/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 diff --git a/code/M3D/M3DExporter.cpp b/code/M3D/M3DExporter.cpp new file mode 100644 index 0000000000..35cae078a7 --- /dev/null +++ b/code/M3D/M3DExporter.cpp @@ -0,0 +1,392 @@ +/* +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 // shared_ptr +#include +#include + +#include // aiGetVersion +#include +#include +#include +#include // StreamWriterLE +#include // DeadlyExportError +#include // aiTextureType +#include +#include +#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*)malloc(sizeof(m3d_t)); + if(!m3d) { + throw DeadlyExportError( "memory allocation error" ); + } + memset(m3d, 0, sizeof(m3d_t)); + 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) +{ + unsigned int i, j, k, l, n, mi, idx; + aiMatrix4x4 nm = m * pNode->mTransformation; + m3dv_t vertex; + m3dti_t ti; + + for(i = 0; i < pNode->mNumMeshes; i++) { + const aiMesh *mesh = mScene->mMeshes[pNode->mMeshes[i]]; + + 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(j = 0; j < mesh->mNumFaces; j++) { + 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 */ + memset(&m3d->face[n], 255, sizeof(m3df_t)); + m3d->face[n].materialid = mi; + for(k = 0; k < face->mNumIndices; k++) { + // get the vertex's index + l = face->mIndices[k]; + // multiply the position vector by the transformation matrix + aiVector3D v = mesh->mVertices[l]; + v *= nm; + memset(&vertex, 0, sizeof(m3dv_t)); + vertex.x = v.x; + vertex.y = v.y; + vertex.z = v.z; + vertex.w = 1.0; + // 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 (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 i, j, k, 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(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) { + 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 < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); + k++) { + 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) { + 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 diff --git a/code/M3D/M3DExporter.h b/code/M3D/M3DExporter.h new file mode 100644 index 0000000000..dfcff8bc93 --- /dev/null +++ b/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 +//#include +#include // StreamWriterLE +#include // DeadlyExportError + +#include // 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 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 diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp new file mode 100644 index 0000000000..76ba513642 --- /dev/null +++ b/code/M3D/M3DImporter.cpp @@ -0,0 +1,734 @@ +/* +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 +#include +#include +#include +#include +#include +#include +#include +#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" { + struct Assimp::IOSystem* 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 pStream( 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 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 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." ); + } + unsigned char data[fileSize]; + 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 *meshes = new std::vector(); + std::vector *faces = nullptr; + std::vector *vertices = nullptr; + std::vector *normals = nullptr; + std::vector *texcoords = nullptr; + std::vector *colors = nullptr; + std::vector *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(); + vertices = new std::vector(); + normals = new std::vector(); + texcoords = new std::vector(); + colors = new std::vector(); + vertexids = new std::vector(); + } + // 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, n, 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++) { + 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 *faces, std::vector *vertices, + std::vector *normals, std::vector *texcoords, std::vector *colors, + std::vector *vertexids) { + unsigned int i, j, k, s; + aiNode *pNode; + + 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(i = 0; i < m3d->numbone; i++) { + 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()) { + // first count how many vertices we have per bone + for(i = 0; i < vertexids->size(); i++) { + s = m3d->vertex[vertexids->at(i)].skinid; + if(s != -1U && s!= -2U) { + for(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++) { + s = m3d->vertex[vertexids->at(i)].skinid; + if(s != -1U && s!= -2U) { + for(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 diff --git a/code/M3D/M3DImporter.h b/code/M3D/M3DImporter.h new file mode 100644 index 0000000000..06cc757b69 --- /dev/null +++ b/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 +#include +#include + +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 *faces, std::vector *verteces, + std::vector *normals, std::vector *texcoords, std::vector *colors, + std::vector *vertexids); +}; + +} // Namespace Assimp + +#endif // ASSIMP_BUILD_NO_M3D_IMPORTER + +#endif // AI_M3DIMPORTER_H_INC diff --git a/code/M3D/M3DMaterials.h b/code/M3D/M3DMaterials.h new file mode 100644 index 0000000000..86a802021c --- /dev/null +++ b/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 { + char *pKey; + unsigned int type; + unsigned int index; +} aiMatProp; + +/* --- Scalar Properties --- !!!!! must match m3d_propertytypes !!!!! */ +static 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 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 diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h new file mode 120000 index 0000000000..bb58a6d640 --- /dev/null +++ b/code/M3D/m3d.h @@ -0,0 +1 @@ +../../../m3d.h \ No newline at end of file diff --git a/test/models/M3D/README.md b/test/models/M3D/README.md new file mode 100644 index 0000000000..144d1ec644 --- /dev/null +++ b/test/models/M3D/README.md @@ -0,0 +1,14 @@ +Model 3D Samples +================ + + aliveai_character.m3d - from Minetest aliveai mod (textures, animations, original 47k, m3d 2.5k) + cube.m3d - smallest possible example, 119 bytes only + cube_normals.m3d - cube with normal vectors, 159 bytes + cube_usemtl.m3d - converted from Assimp sample OBJ by the same name, cube with materials + cube_with_vertexcolors.m3d - converted from Assimp sample OBJ by the same name, cube with vertex colors + cube_with_vertexcolors.a3d - same, but saved in ASCII variant with Windows line endings (\r\n) + mobs_dwarves_character.m3d - from Minetest mobs_dwarves mod (with Assimp artifacts converted perfectly too...) + suzanne.m3d - exported from Blender (monkey face, with normals and texture UVs and materials) + WusonBlitz0.m3d - from Assimp sample by the same name (was 87k, triangle mesh) with int8 coordinates, minor quality degradation + WusonBlitz1.m3d - same, but uses int16 coordinates (no noticable difference to the original, but just 35k) + WusonBlitz2.m3d - same, but with 32 bit floating point numbers (same precision as the original, half the file size, 42k) diff --git a/test/models/M3D/WusonBlitz0.m3d b/test/models/M3D/WusonBlitz0.m3d new file mode 100644 index 0000000000000000000000000000000000000000..29a397b7915ba06070c728840890f2099b3d001e GIT binary patch literal 29223 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 literal 0 HcmV?d00001 diff --git a/test/models/M3D/WusonBlitz1.m3d b/test/models/M3D/WusonBlitz1.m3d new file mode 100644 index 0000000000000000000000000000000000000000..3661252576ba84b860410277330b71d6fb4fbf08 GIT binary patch literal 35058 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 literal 0 HcmV?d00001 diff --git a/test/models/M3D/WusonBlitz2.m3d b/test/models/M3D/WusonBlitz2.m3d new file mode 100644 index 0000000000000000000000000000000000000000..682876816c37b3d05a5aba27d5267661759bf692 GIT binary patch literal 42228 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 literal 0 HcmV?d00001 diff --git a/test/models/M3D/suzanne.m3d b/test/models/M3D/suzanne.m3d new file mode 100644 index 0000000000000000000000000000000000000000..9bc64d7d787a4cd7c135af96c71cc0c6d8c463f5 GIT binary patch literal 11645 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 W?QqGf)qqRRMyLE2$&$JCoc{se_CU4( literal 0 HcmV?d00001 From a622e109a0739435e3e2f05bfbedba0e8385282d Mon Sep 17 00:00:00 2001 From: bzt Date: Tue, 29 Oct 2019 18:42:10 +0100 Subject: [PATCH 34/74] Fixed PR Quality Review Issues --- code/M3D/M3DExporter.cpp | 9 +- code/M3D/M3DImporter.cpp | 11 +- code/M3D/m3d.h | 4530 +++++++++++++++++++++++++++++++++++++- 3 files changed, 4539 insertions(+), 11 deletions(-) mode change 120000 => 100644 code/M3D/m3d.h diff --git a/code/M3D/M3DExporter.cpp b/code/M3D/M3DExporter.cpp index 35cae078a7..d25b918915 100644 --- a/code/M3D/M3DExporter.cpp +++ b/code/M3D/M3DExporter.cpp @@ -174,15 +174,14 @@ void M3DExporter::doExport ( // recursive node walker void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) { - unsigned int i, j, k, l, n, mi, idx; + unsigned int i, j, k, l, n, idx; aiMatrix4x4 nm = m * pNode->mTransformation; m3dv_t vertex; m3dti_t ti; for(i = 0; i < pNode->mNumMeshes; i++) { const aiMesh *mesh = mScene->mMeshes[pNode->mMeshes[i]]; - - mi = (M3D_INDEX)-1U; + unsigned int mi = (M3D_INDEX)-1U; if(mScene->mMaterials) { // get the material for this mesh mi = addMaterial(mScene->mMaterials[mesh->mMaterialIndex]); @@ -263,7 +262,7 @@ uint32_t M3DExporter::mkColor(aiColor4D* c) // add a material to the output M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) { - unsigned int i, j, k, mi = -1U; + unsigned int i, j, mi = -1U; aiColor4D c; aiString name; ai_real f; @@ -290,7 +289,7 @@ M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) m3d->material[mi].numprop = 0; m3d->material[mi].prop = NULL; // iterate through the material property table and see what we got - for(k = 0; + for(unsigned int k = 0; k < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); k++) { if(m3d_propertytypes[k].format == m3dpf_map) diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index 76ba513642..d34cd982ff 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -486,7 +486,7 @@ void M3DImporter::importBones(unsigned int parentid, aiNode *pParent) // bone, so we have to convert between the two conceptually different representation forms void M3DImporter::importAnimations() { - unsigned int i, j, k, l, n, pos, ori; + unsigned int i, j, k, l, pos, ori; double t; m3da_t *a; @@ -511,6 +511,7 @@ void M3DImporter::importAnimations() 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 @@ -628,8 +629,7 @@ void M3DImporter::calculateOffsetMatrix(aiNode *pNode, aiMatrix4x4 *m) void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::vector *vertices, std::vector *normals, std::vector *texcoords, std::vector *colors, std::vector *vertexids) { - unsigned int i, j, k, s; - aiNode *pNode; + unsigned int i, j, k; ai_assert(pMesh != nullptr); ai_assert(faces != nullptr); @@ -670,6 +670,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v if(pMesh->mNumBones) { pMesh->mBones = new aiBone*[pMesh->mNumBones]; for(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; @@ -683,7 +684,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v if(vertexids->size()) { // first count how many vertices we have per bone for(i = 0; i < vertexids->size(); i++) { - s = m3d->vertex[vertexids->at(i)].skinid; + unsigned int s = m3d->vertex[vertexids->at(i)].skinid; if(s != -1U && s!= -2U) { for(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)); @@ -706,7 +707,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v } // fill up with data for(i = 0; i < vertexids->size(); i++) { - s = m3d->vertex[vertexids->at(i)].skinid; + unsigned int s = m3d->vertex[vertexids->at(i)].skinid; if(s != -1U && s!= -2U) { for(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)); diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h deleted file mode 120000 index bb58a6d640..0000000000 --- a/code/M3D/m3d.h +++ /dev/null @@ -1 +0,0 @@ -../../../m3d.h \ No newline at end of file diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h new file mode 100644 index 0000000000..e7eccc5b2d --- /dev/null +++ b/code/M3D/m3d.h @@ -0,0 +1,4529 @@ +/* + * 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 + +/*** 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 + +/*** 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; +} __attribute__((packed)) m3dhdr_t; + +typedef struct { + char magic[4]; + uint32_t length; +} __attribute__((packed)) 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 */ +} __attribute__((packed)) 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 properties */ +/* 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 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 */ +} __attribute__((packed)) 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) +/* 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 +#include + +#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 /* get sprintf */ +#include /* 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_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; +} +/* 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; +} +/* 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 -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 -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 -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; + w = h = 0; + model->texture[i].d = (uint32_t*)stbi__png_load(&s, (int*)&w, (int*)&h, (int*)&len, STBI_rgb_alpha, &ri); + } 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 || !w || !h) { + if(model->texture[i].d) M3D_FREE(model->texture[i].d); + model->errcode = M3D_ERR_UNKIMG; + model->numtexture--; + return -1U; + } + model->texture[i].w = w; + model->texture[i].h = h; + model->texture[i].name = fn; + return i; +} + +/* helper function to load and generate a procedural surface */ +void _m3d_getpr(m3d_t *model, __attribute__((unused)) m3dread_t readfilecb, __attribute__((unused)) m3dfree_t freecb, + __attribute__((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) > 65533 ? *((int16_t*)data) : *((uint16_t*)data); data += 2; break; + case 4: *idx = *((int32_t*)data); data += 4; break; + } + return data; +} + +/* 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 +} + +#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 */ +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_FLOAT)-1e-7 && r[ 0]<(M3D_FLOAT)1e-7) r[ 0]=(M3D_FLOAT)0.0; + r[ 1] = 2 * (q->x * q->y - q->z * q->w); if(r[ 1]>(M3D_FLOAT)-1e-7 && r[ 1]<(M3D_FLOAT)1e-7) r[ 1]=(M3D_FLOAT)0.0; + r[ 2] = 2 * (q->x * q->z + q->y * q->w); if(r[ 2]>(M3D_FLOAT)-1e-7 && r[ 2]<(M3D_FLOAT)1e-7) r[ 2]=(M3D_FLOAT)0.0; + r[ 4] = 2 * (q->x * q->y + q->z * q->w); if(r[ 4]>(M3D_FLOAT)-1e-7 && r[ 4]<(M3D_FLOAT)1e-7) r[ 4]=(M3D_FLOAT)0.0; + r[ 5] = 1 - 2 * (q->x * q->x + q->z * q->z); if(r[ 5]>(M3D_FLOAT)-1e-7 && r[ 5]<(M3D_FLOAT)1e-7) r[ 5]=(M3D_FLOAT)0.0; + r[ 6] = 2 * (q->y * q->z - q->x * q->w); if(r[ 6]>(M3D_FLOAT)-1e-7 && r[ 6]<(M3D_FLOAT)1e-7) r[ 6]=(M3D_FLOAT)0.0; + r[ 8] = 2 * (q->x * q->z - q->y * q->w); if(r[ 8]>(M3D_FLOAT)-1e-7 && r[ 8]<(M3D_FLOAT)1e-7) r[ 8]=(M3D_FLOAT)0.0; + r[ 9] = 2 * (q->y * q->z + q->x * q->w); if(r[ 9]>(M3D_FLOAT)-1e-7 && r[ 9]<(M3D_FLOAT)1e-7) r[ 9]=(M3D_FLOAT)0.0; + r[10] = 1 - 2 * (q->x * q->x + q->y * q->y); if(r[10]>(M3D_FLOAT)-1e-7 && r[10]<(M3D_FLOAT)1e-7) 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 + +/** + * 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, numnorm = 0; + char *material; + m3dv_t *norm = NULL, *v0, *v1, *v2, va, vb, vn; + m3d_t *model; + M3D_INDEX mi, *ni = NULL, *vi = NULL; + M3D_FLOAT w, r[16]; + 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 == 4 || model->ci_s == 4 || model->ti_s == 4 || + model->bi_s == 4 || model->sk_s == 4 || model->fi_s == 4)) { + 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; break; } + if(model->ci_s >= 4) { M3D_LOG("Color map chunk, shouldn't be any"); model->errcode = M3D_ERR_CMAP; break; } + 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; break; } + 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; break; } + 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+0))) / 32767; + model->vertex[i].y = (M3D_FLOAT)(*((int16_t*)(data+2))) / 32767; + model->vertex[i].z = (M3D_FLOAT)(*((int16_t*)(data+4))) / 32767; + model->vertex[i].w = (M3D_FLOAT)(*((int16_t*)(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; break; } + if(model->bi_s > 4) { M3D_LOG("Bone chunk, shouldn't be any"); model->errcode=M3D_ERR_BONE; break; } + 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 */ +postprocess: + if(model) { + 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); + } + } + if(model->numbone && model->bone && model->numskin && model->skin && model->numvertex && model->vertex) { + 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]; + } + } + } +#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) ? -1 : 1; + 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; + register 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) { 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 +} + +#include +#include +#include + +/*** 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(__attribute__((unused)) const std::string &data, __attribute__((unused)) m3dread_t ReadFileCB, + __attribute__((unused)) m3dfree_t FreeCB, __attribute__((unused)) M3D::Model mtllib) { +#ifndef M3D_NOIMPORTER + this->model = m3d_load((unsigned char *)data.data(), ReadFileCB, FreeCB, mtllib.model); +#else + Model(); +#endif + } + Model(__attribute__((unused)) const std::vector data, __attribute__((unused)) m3dread_t ReadFileCB, + __attribute__((unused)) m3dfree_t FreeCB, __attribute__((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 getColorMap() { return this->model->cmap ? std::vector(this->model->cmap, + this->model->cmap + this->model->numcmap) : std::vector(); } + std::vector getTextureMap() { return this->model->tmap ? std::vector(this->model->tmap, + this->model->tmap + this->model->numtmap) : std::vector(); } + std::vector getTextures() { return this->model->texture ? std::vector(this->model->texture, + this->model->texture + this->model->numtexture) : std::vector(); } + std::string getTextureName(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numtexture ? + std::string(this->model->texture[idx].name) : nullptr; } + std::vector getBones() { return this->model->bone ? std::vector(this->model->bone, this->model->bone + + this->model->numbone) : std::vector(); } + std::string getBoneName(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numbone ? + std::string(this->model->bone[idx].name) : nullptr; } + std::vector getMaterials() { return this->model->material ? std::vector(this->model->material, + this->model->material + this->model->nummaterial) : std::vector(); } + 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.num; + } + 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 getVertices() { return this->model->vertex ? std::vector(this->model->vertex, + this->model->vertex + this->model->numvertex) : std::vector(); } + std::vector getFace() { return this->model->face ? std::vector(this->model->face, this->model->face + + this->model->numface) : std::vector(); } + std::vector getSkin() { return this->model->skin ? std::vector(this->model->skin, this->model->skin + + this->model->numskin) : std::vector(); } + std::vector getActions() { return this->model->action ? std::vector(this->model->action, + this->model->action + this->model->numaction) : std::vector(); } + 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 getActionFrames(int aidx) { return aidx >= 0 && (unsigned int)aidx < this->model->numaction ? + std::vector(this->model->action[aidx].frame, this->model->action[aidx].frame + + this->model->action[aidx].numframe) : std::vector(); } + 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 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(this->model->action[aidx].frame[fidx].transform, + this->model->action[aidx].frame[fidx].transform + this->model->action[aidx].frame[fidx].numtransform) : + std::vector()) : std::vector(); } + std::vector getActionFrame(int aidx, int fidx, std::vector skeleton) { + m3dtr_t *pose = m3d_frame(this->model, (unsigned int)aidx, (unsigned int)fidx, + skeleton.size() ? &skeleton[0] : nullptr); + return std::vector(pose, pose + this->model->numbone); } + std::vector getActionPose(int aidx, unsigned int msec) { + m3db_t *pose = m3d_pose(this->model, (unsigned int)aidx, (unsigned int)msec); + return std::vector(pose, pose + this->model->numbone); } + std::vector getInlinedAssets() { return this->model->inlined ? std::vector(this->model->inlined, + this->model->inlined + this->model->numinlined) : std::vector(); } + std::vector> getUnknowns() { return this->model->unknown ? + std::vector>(this->model->unknown, + this->model->unknown + this->model->numunknown) : std::vector>(); } + std::vector Save(__attribute__((unused)) int quality, __attribute__((unused)) int flags) { +#ifdef M3D_EXPORTER + unsigned int size; + unsigned char *ptr = m3d_save(this->model, quality, flags, &size); + return ptr && size ? std::vector(ptr, ptr + size) : std::vector(); +#else + return std::vector(); +#endif + } + }; + +#else + class Model { + public: + m3d_t *model; + + public: + Model(const std::string &data, m3dread_t ReadFileCB, m3dfree_t FreeCB); + Model(const std::vector 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 getColorMap(); + std::vector getTextureMap(); + std::vector getTextures(); + std::string getTextureName(int idx); + std::vector getBones(); + std::string getBoneName(int idx); + std::vector 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 getVertices(); + std::vector getFace(); + std::vector getSkin(); + std::vector getActions(); + std::string getActionName(int aidx); + unsigned int getActionDuration(int aidx); + std::vector getActionFrames(int aidx); + unsigned int getActionFrameTimestamp(int aidx, int fidx); + std::vector getActionFrameTransforms(int aidx, int fidx); + std::vector getActionFrame(int aidx, int fidx, std::vector skeleton); + std::vector getActionPose(int aidx, unsigned int msec); + std::vector getInlinedAssets(); + std::vector> getUnknowns(); + std::vector Save(int quality, int flags); + }; + +#endif /* impl */ +} + +#endif /* __cplusplus */ + +#endif From 02a63f8b1070e71bc9ee67d611d18078966451aa Mon Sep 17 00:00:00 2001 From: RevoluPowered Date: Tue, 29 Oct 2019 20:21:16 +0000 Subject: [PATCH 35/74] Fixed template being used in file --- code/PostProcessing/ArmaturePopulate.h | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/code/PostProcessing/ArmaturePopulate.h b/code/PostProcessing/ArmaturePopulate.h index 30bfc7509d..aa1ad7c80c 100644 --- a/code/PostProcessing/ArmaturePopulate.h +++ b/code/PostProcessing/ArmaturePopulate.h @@ -54,12 +54,17 @@ struct aiBone; namespace Assimp { // --------------------------------------------------------------------------- -/** ScaleProcess: Class to rescale the whole model. - * Now rescales animations, bones, and blend shapes properly. - * Please note this will not write to 'scale' transform it will rewrite mesh - * and matrixes so that your scale values - * from your model package are preserved, so this is completely intentional - * bugs should be reported as soon as they are found. +/** 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 + * For more info about this */ class ASSIMP_API ArmaturePopulate : public BaseProcess { public: From 5b18baf88351f2e1a8f9ee88088fac7d2298d73b Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 00:55:54 +0100 Subject: [PATCH 36/74] Fixed issues with MSVC --- code/M3D/M3DExporter.cpp | 13 +++--- code/M3D/M3DImporter.cpp | 6 +-- code/M3D/m3d.h | 89 ++++++++++++++++++++++------------------ 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/code/M3D/M3DExporter.cpp b/code/M3D/M3DExporter.cpp index d25b918915..9e774fa7f3 100644 --- a/code/M3D/M3DExporter.cpp +++ b/code/M3D/M3DExporter.cpp @@ -142,11 +142,10 @@ void M3DExporter::doExport ( } // use malloc() here because m3d_free() will call free() - m3d = (m3d_t*)malloc(sizeof(m3d_t)); + m3d = (m3d_t*)calloc(1, sizeof(m3d_t)); if(!m3d) { throw DeadlyExportError( "memory allocation error" ); } - memset(m3d, 0, sizeof(m3d_t)); m3d->name = _m3d_safestr((char*)&mScene->mRootNode->mName.data, 2); // Create a model from assimp structures @@ -201,7 +200,9 @@ void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) throw DeadlyExportError( "memory allocation error" ); } /* set all index to -1 by default */ - memset(&m3d->face[n], 255, sizeof(m3df_t)); + 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(k = 0; k < face->mNumIndices; k++) { // get the vertex's index @@ -209,11 +210,12 @@ void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) // multiply the position vector by the transformation matrix aiVector3D v = mesh->mVertices[l]; v *= nm; - memset(&vertex, 0, sizeof(m3dv_t)); 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]); @@ -262,7 +264,7 @@ uint32_t M3DExporter::mkColor(aiColor4D* c) // add a material to the output M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) { - unsigned int i, j, mi = -1U; + unsigned int i, mi = -1U; aiColor4D c; aiString name; ai_real f; @@ -292,6 +294,7 @@ M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) for(unsigned int k = 0; k < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); k++) { + unsigned int j; if(m3d_propertytypes[k].format == m3dpf_map) continue; if(aiProps[k].pKey) { diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index d34cd982ff..d732387ce5 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -629,7 +629,7 @@ void M3DImporter::calculateOffsetMatrix(aiNode *pNode, aiMatrix4x4 *m) void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::vector *vertices, std::vector *normals, std::vector *texcoords, std::vector *colors, std::vector *vertexids) { - unsigned int i, j, k; + unsigned int i, j; ai_assert(pMesh != nullptr); ai_assert(faces != nullptr); @@ -684,7 +684,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v if(vertexids->size()) { // 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; + unsigned int s = m3d->vertex[vertexids->at(i)].skinid, k; if(s != -1U && s!= -2U) { for(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)); @@ -707,7 +707,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v } // fill up with data for(i = 0; i < vertexids->size(); i++) { - unsigned int s = m3d->vertex[vertexids->at(i)].skinid; + unsigned int s = m3d->vertex[vertexids->at(i)].skinid, k; if(s != -1U && s!= -2U) { for(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)); diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index e7eccc5b2d..82238f6d62 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -74,6 +74,15 @@ typedef uint16_t M3D_INDEX; #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 /*** File format structures ***/ @@ -104,12 +113,12 @@ typedef struct { uint32_t length; float scale; /* deliberately not M3D_FLOAT */ uint32_t types; -} __attribute__((packed)) m3dhdr_t; +} _pack m3dhdr_t; typedef struct { char magic[4]; uint32_t length; -} __attribute__((packed)) m3dchunk_t; +} _pack m3dchunk_t; /*** in-memory model structure ***/ @@ -125,7 +134,7 @@ typedef struct { uint32_t *d; /* pixels data */ uint16_t w; /* width */ uint16_t h; /* height */ -} __attribute__((packed)) m3dtx_t; +} _pack m3dtx_t; typedef struct { M3D_INDEX vertexid; @@ -179,7 +188,7 @@ typedef struct { #endif } m3dpd_t; -/* material properties */ +/* 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 */ @@ -216,6 +225,7 @@ enum { /* aliases */ m3dp_bump = m3dp_map_Km, m3dp_refl = m3dp_map_Pm }; +extern m3dpd_t m3d_propertytypes[]; /* material property */ typedef struct { @@ -243,7 +253,7 @@ typedef struct { M3D_INDEX texcoord[3]; /* UV coordinates */ } m3df_t; -/* frame transformations entry */ +/* frame transformations / working copy skeleton entry */ typedef struct { M3D_INDEX boneid; /* selects a node in bone hierarchy */ M3D_INDEX pos; /* vertex index new position */ @@ -270,7 +280,7 @@ typedef struct { char *name; /* asset name (same pointer as in texture[].name) */ uint8_t *data; /* compressed asset data */ uint32_t length; /* compressed data length */ -} __attribute__((packed)) m3di_t; +} m3di_t; /*** in-memory model structure ***/ #define M3D_FLG_FREERAW (1<<0) @@ -374,8 +384,8 @@ char *_m3d_safestr(char *in, int morelines); /*** C implementation ***/ #ifdef M3D_IMPLEMENTATION #if !defined(M3D_NOIMPORTER) || defined(M3D_EXPORTER) -/* property definitions */ -static m3dpd_t m3d_propertytypes[] = { +/* material property definitions */ +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 */ @@ -462,14 +472,14 @@ typedef struct #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) +_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) +_inline static int stbi__at_eof(stbi__context *s) { return s->img_buffer >= s->img_buffer_end; } @@ -512,7 +522,7 @@ static int stbi__errstr(const char *str) return 0; } -__inline__ static void *stbi__malloc(size_t size) +_inline static void *stbi__malloc(size_t size) { return STBI_MALLOC(size); } @@ -662,7 +672,7 @@ typedef struct stbi__uint16 value[288]; } stbi__zhuffman; -__inline__ static int stbi__bitreverse16(int n) +_inline static int stbi__bitreverse16(int n) { n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); @@ -671,7 +681,7 @@ __inline__ static int stbi__bitreverse16(int n) return n; } -__inline__ static int stbi__bit_reverse(int v, int bits) +_inline static int stbi__bit_reverse(int v, int bits) { STBI_ASSERT(bits <= 16); return stbi__bitreverse16(v) >> (16-bits); @@ -737,7 +747,7 @@ typedef struct stbi__zhuffman z_length, z_distance; } stbi__zbuf; -__inline__ static stbi_uc stbi__zget8(stbi__zbuf *z) +_inline static stbi_uc stbi__zget8(stbi__zbuf *z) { if (z->zbuffer >= z->zbuffer_end) return 0; return *z->zbuffer++; @@ -752,7 +762,7 @@ static void stbi__fill_bits(stbi__zbuf *z) } while (z->num_bits <= 24); } -__inline__ static unsigned int stbi__zreceive(stbi__zbuf *z, int n) +_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n) { unsigned int k; if (z->num_bits < n) stbi__fill_bits(z); @@ -777,7 +787,7 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) return z->value[b]; } -__inline__ static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) +_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) { int b,s; if (a->num_bits < 16) stbi__fill_bits(a); @@ -915,7 +925,7 @@ static int stbi__compute_huffman_codes(stbi__zbuf *a) return 1; } -__inline__ static int stbi__parse_uncompressed_block(stbi__zbuf *a) +_inline static int stbi__parse_uncompressed_block(stbi__zbuf *a) { stbi_uc header[4]; int len,nlen,k; @@ -1034,7 +1044,7 @@ static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) return c; } -__inline__ static int stbi__check_png_header(stbi__context *s) +_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; @@ -2008,8 +2018,11 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char 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) { @@ -2022,21 +2035,18 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char #endif } if(freecb) (*freecb)(buff); - if(!model->texture[i].d || !w || !h) { - if(model->texture[i].d) M3D_FREE(model->texture[i].d); + if(!model->texture[i].d) { + M3D_FREE(model->texture[i].d); model->errcode = M3D_ERR_UNKIMG; model->numtexture--; return -1U; } - model->texture[i].w = w; - model->texture[i].h = h; model->texture[i].name = fn; return i; } /* helper function to load and generate a procedural surface */ -void _m3d_getpr(m3d_t *model, __attribute__((unused)) m3dread_t readfilecb, __attribute__((unused)) m3dfree_t freecb, - __attribute__((unused)) char *fn) +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; @@ -2065,7 +2075,7 @@ void _m3d_getpr(m3d_t *model, __attribute__((unused)) m3dread_t readfilecb, __at } /* 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) +_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; @@ -2606,8 +2616,8 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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 == 4 || model->ci_s == 4 || model->ti_s == 4 || - model->bi_s == 4 || model->sk_s == 4 || model->fi_s == 4)) { + 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; @@ -2667,15 +2677,16 @@ memerr: M3D_LOG("Out of memory"); /* 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; break; } - if(model->ci_s >= 4) { M3D_LOG("Color map chunk, shouldn't be any"); model->errcode = M3D_ERR_CMAP; break; } + 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; break; } + 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)); @@ -2705,7 +2716,7 @@ memerr: M3D_LOG("Out of memory"); /* 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; break; } + 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; @@ -2756,8 +2767,8 @@ memerr: M3D_LOG("Out of memory"); /* 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; break; } - if(model->bi_s > 4) { M3D_LOG("Bone chunk, shouldn't be any"); model->errcode=M3D_ERR_BONE; break; } + 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; @@ -3353,7 +3364,7 @@ static uint32_t _m3d_stridx(m3dstr_t *str, uint32_t numstr, char *s) } /* compare two colors by HSV value */ -__inline__ static int _m3d_cmapcmp(const void *a, const void *b) +_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; @@ -4348,16 +4359,16 @@ namespace M3D { Model() { this->model = (m3d_t*)malloc(sizeof(m3d_t)); memset(this->model, 0, sizeof(m3d_t)); } - Model(__attribute__((unused)) const std::string &data, __attribute__((unused)) m3dread_t ReadFileCB, - __attribute__((unused)) m3dfree_t FreeCB, __attribute__((unused)) M3D::Model mtllib) { + 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(__attribute__((unused)) const std::vector data, __attribute__((unused)) m3dread_t ReadFileCB, - __attribute__((unused)) m3dfree_t FreeCB, __attribute__((unused)) M3D::Model mtllib) { + Model(_unused const std::vector 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 @@ -4459,7 +4470,7 @@ namespace M3D { std::vector> getUnknowns() { return this->model->unknown ? std::vector>(this->model->unknown, this->model->unknown + this->model->numunknown) : std::vector>(); } - std::vector Save(__attribute__((unused)) int quality, __attribute__((unused)) int flags) { + std::vector Save(_unused int quality, _unused int flags) { #ifdef M3D_EXPORTER unsigned int size; unsigned char *ptr = m3d_save(this->model, quality, flags, &size); From 1c23d2e8de13738d06ee84de5861a0cbc4300ff4 Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 01:11:34 +0100 Subject: [PATCH 37/74] More reduced scope fix by wasting more memory... --- code/M3D/M3DExporter.cpp | 10 +++++----- code/M3D/M3DImporter.cpp | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/code/M3D/M3DExporter.cpp b/code/M3D/M3DExporter.cpp index 9e774fa7f3..9ba48b125b 100644 --- a/code/M3D/M3DExporter.cpp +++ b/code/M3D/M3DExporter.cpp @@ -264,7 +264,7 @@ uint32_t M3DExporter::mkColor(aiColor4D* c) // add a material to the output M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) { - unsigned int i, mi = -1U; + unsigned int mi = -1U; aiColor4D c; aiString name; ai_real f; @@ -274,13 +274,14 @@ M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) 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(i = 0; i < m3d->nummaterial; i++) + 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)); @@ -291,9 +292,7 @@ M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) m3d->material[mi].numprop = 0; m3d->material[mi].prop = NULL; // iterate through the material property table and see what we got - for(unsigned int k = 0; - k < sizeof(m3d_propertytypes)/sizeof(m3d_propertytypes[0]); - k++) { + for(k = 0; k < 15; k++) { unsigned int j; if(m3d_propertytypes[k].format == m3dpf_map) continue; @@ -341,6 +340,7 @@ M3D_INDEX M3DExporter::addMaterial(const aiMaterial *mat) 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') && diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index d732387ce5..873c51f976 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -629,7 +629,6 @@ void M3DImporter::calculateOffsetMatrix(aiNode *pNode, aiMatrix4x4 *m) void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::vector *vertices, std::vector *normals, std::vector *texcoords, std::vector *colors, std::vector *vertexids) { - unsigned int i, j; ai_assert(pMesh != nullptr); ai_assert(faces != nullptr); @@ -644,6 +643,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v " numnormals ", normals->size(), " numtexcoord ", texcoords->size(), " numbones ", m3d->numbone); if(vertices->size() && faces->size()) { + unsigned int i; pMesh->mNumFaces = faces->size(); pMesh->mFaces = new aiFace[pMesh->mNumFaces]; std::copy(faces->begin(), faces->end(), pMesh->mFaces); @@ -682,11 +682,12 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v pMesh->mBones[i]->mOffsetMatrix = aiMatrix4x4(); } if(vertexids->size()) { + unsigned int 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, k; + unsigned int s = m3d->vertex[vertexids->at(i)].skinid; if(s != -1U && s!= -2U) { - for(k = 0; k < M3D_NUMBONE && m3d->skin[s].weight[k] > 0.0; k++) { + 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) { @@ -707,9 +708,9 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v } // fill up with data for(i = 0; i < vertexids->size(); i++) { - unsigned int s = m3d->vertex[vertexids->at(i)].skinid, k; + unsigned int s = m3d->vertex[vertexids->at(i)].skinid; if(s != -1U && s!= -2U) { - for(k = 0; k < M3D_NUMBONE && m3d->skin[s].weight[k] > 0.0; k++) { + 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) { From 7ed621b53f08fb9e4b3c0ff76c615ef44a70d58c Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 01:14:20 +0100 Subject: [PATCH 38/74] More reduced scope fix by wasting more memory... --- code/M3D/m3d.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index 82238f6d62..d8d0602f6f 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -225,7 +225,6 @@ enum { /* aliases */ m3dp_bump = m3dp_map_Km, m3dp_refl = m3dp_map_Pm }; -extern m3dpd_t m3d_propertytypes[]; /* material property */ typedef struct { @@ -385,7 +384,7 @@ char *_m3d_safestr(char *in, int morelines); #ifdef M3D_IMPLEMENTATION #if !defined(M3D_NOIMPORTER) || defined(M3D_EXPORTER) /* material property definitions */ -m3dpd_t m3d_propertytypes[] = { +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 */ From 5a7928704141e4f2c39a4a108b410b0051a2a298 Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 01:21:15 +0100 Subject: [PATCH 39/74] More reduced scope fix, what's wrong with reusing i as a loop variable? --- code/M3D/M3DImporter.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index 873c51f976..980171f736 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -643,7 +643,6 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v " numnormals ", normals->size(), " numtexcoord ", texcoords->size(), " numbones ", m3d->numbone); if(vertices->size() && faces->size()) { - unsigned int i; pMesh->mNumFaces = faces->size(); pMesh->mFaces = new aiFace[pMesh->mNumFaces]; std::copy(faces->begin(), faces->end(), pMesh->mFaces); @@ -669,7 +668,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v /* we need aiBone with mOffsetMatrix for bones without weights as well */ if(pMesh->mNumBones) { pMesh->mBones = new aiBone*[pMesh->mNumBones]; - for(i = 0; i < m3d->numbone; i++) { + 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)); @@ -682,7 +681,7 @@ void M3DImporter::populateMesh(aiMesh *pMesh, std::vector *faces, std::v pMesh->mBones[i]->mOffsetMatrix = aiMatrix4x4(); } if(vertexids->size()) { - unsigned int j; + 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; From 60e9157699fcc3c0c00a3fcd024ae455eee05f05 Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 01:37:28 +0100 Subject: [PATCH 40/74] Fixed AI_MATKEY list string constant issue. My gcc didn't comply about this --- code/M3D/M3DMaterials.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/M3D/M3DMaterials.h b/code/M3D/M3DMaterials.h index 86a802021c..b3c91ab7aa 100644 --- a/code/M3D/M3DMaterials.h +++ b/code/M3D/M3DMaterials.h @@ -54,7 +54,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * only once. D.R.Y. and K.I.S.S. */ typedef struct { - char *pKey; + const char *pKey; unsigned int type; unsigned int index; } aiMatProp; From 0ff3e4015728b3d208c6c2199b11e1425a8434d4 Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 02:35:51 +0100 Subject: [PATCH 41/74] Fixed clang's casting issue and MSVC's buffer allocation problem --- code/M3D/M3DImporter.cpp | 8 +++++--- code/M3D/m3d.h | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index 980171f736..fcff49df76 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -96,13 +96,14 @@ static const aiImporterDesc desc = { // workaround: the SDK expects a C callback, but we want to use Assimp::IOSystem to implement that extern "C" { - struct Assimp::IOSystem* m3dimporter_pIOHandler; + 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 pStream( m3dimporter_pIOHandler->Open( file, "rb")); + std::unique_ptr pStream( + (reinterpret_cast(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); @@ -179,7 +180,8 @@ void M3DImporter::InternReadFile( const std::string &file, aiScene* pScene, IOSy if( fileSize < 8 ) { throw DeadlyImportError( "M3D-file " + file + " is too small." ); } - unsigned char data[fileSize]; + std::unique_ptr _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 + "." ); } diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index d8d0602f6f..b43f0721c1 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -1987,7 +1987,7 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char i = strlen(fn); if(i < 5 || fn[i - 4] != '.') { fn2 = (char*)M3D_MALLOC(i + 5); - if(!fn2) { model->errcode = M3D_ERR_ALLOC; return -1U; } + if(!fn2) { model->errcode = M3D_ERR_ALLOC; return (M3D_INDEX)-1U; } memcpy(fn2, fn, i); memcpy(fn2+i, ".png", 5); buff = (*readfilecb)(fn2, &len); @@ -2005,12 +2005,12 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char break; } } - if(!buff) return -1U; + 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 -1U; + 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') { @@ -2038,7 +2038,7 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char M3D_FREE(model->texture[i].d); model->errcode = M3D_ERR_UNKIMG; model->numtexture--; - return -1U; + return (M3D_INDEX)-1U; } model->texture[i].name = fn; return i; @@ -3198,7 +3198,7 @@ m3db_t *m3d_pose(m3d_t *model, M3D_INDEX actionid, uint32_t msec) 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) ? -1 : 1; + 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); @@ -4419,7 +4419,7 @@ namespace M3D { !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.num; + return this->model->material[idx].prop[i].value.fnum; } return -1.0f; } From 37cc29c020c9ddad25bdee11e2fcb2734da2aa38 Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 03:04:33 +0100 Subject: [PATCH 42/74] Fixed clang's problem with register keyword --- code/M3D/m3d.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index b43f0721c1..cccfa1a0f1 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -83,6 +83,10 @@ typedef uint16_t M3D_INDEX; #define _pack #define _unused #endif +#ifndef __cplusplus +#define _register register +#define _register +#endif /*** File format structures ***/ @@ -3366,7 +3370,7 @@ static uint32_t _m3d_stridx(m3dstr_t *str, uint32_t numstr, char *s) _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; + _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]; @@ -3464,7 +3468,7 @@ static unsigned char *_m3d_addidx(unsigned char *out, char type, uint32_t idx) { /* round a vertex position */ static void _m3d_round(int quality, m3dv_t *src, m3dv_t *dst) { - register int t; + _register int t; /* copy additional attributes */ if(src != dst) memcpy(dst, src, sizeof(m3dv_t)); /* round according to quality */ @@ -3518,7 +3522,7 @@ unsigned char *m3d_save(m3d_t *model, int quality, int flags, unsigned int *size 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; - register float scale = 0.0f, min_x, max_x, min_y, max_y, min_z, max_z; + 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; From 3bf81375dac4d0dd3a66a12333e3134723515aa3 Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 03:06:24 +0100 Subject: [PATCH 43/74] Fixed clang's problem with register keyword --- code/M3D/m3d.h | 1 + 1 file changed, 1 insertion(+) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index cccfa1a0f1..eed66d3e56 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -85,6 +85,7 @@ typedef uint16_t M3D_INDEX; #endif #ifndef __cplusplus #define _register register +#else #define _register #endif From 805bc2e766fa215f858af1ac787b2766e65f6e4c Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Thu, 31 Oct 2019 20:18:39 +0100 Subject: [PATCH 44/74] Update ColladaParser.cpp Add missing brackets. --- code/Collada/ColladaParser.cpp | 69 +++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/code/Collada/ColladaParser.cpp b/code/Collada/ColladaParser.cpp index fc7ab8c2f8..d5e4382a56 100644 --- a/code/Collada/ColladaParser.cpp +++ b/code/Collada/ColladaParser.cpp @@ -3170,13 +3170,12 @@ void ColladaParser::ReadScene() // ------------------------------------------------------------------------------------------------ // Aborts the file reading with an exception -AI_WONT_RETURN void ColladaParser::ThrowException(const std::string& pError) const -{ +AI_WONT_RETURN void ColladaParser::ThrowException(const std::string& pError) const { throw DeadlyImportError(format() << "Collada: " << mFileName << " - " << pError); } -void ColladaParser::ReportWarning(const char* msg, ...) -{ - ai_assert(NULL != msg); + +void ColladaParser::ReportWarning(const char* msg, ...) { + ai_assert(nullptr != msg); va_list args; va_start(args, msg); @@ -3191,11 +3190,11 @@ void ColladaParser::ReportWarning(const char* msg, ...) // ------------------------------------------------------------------------------------------------ // Skips all data until the end node of the current element -void ColladaParser::SkipElement() -{ +void ColladaParser::SkipElement() { // nothing to skip if it's an - if (mReader->isEmptyElement()) + if (mReader->isEmptyElement()) { return; + } // reroute SkipElement(mReader->getNodeName()); @@ -3203,67 +3202,75 @@ void ColladaParser::SkipElement() // ------------------------------------------------------------------------------------------------ // Skips all data until the end node of the given element -void ColladaParser::SkipElement(const char* pElement) -{ +void ColladaParser::SkipElement(const char* pElement) { // copy the current node's name because it'a pointer to the reader's internal buffer, // which is going to change with the upcoming parsing std::string element = pElement; - while (mReader->read()) - { - if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) - if (mReader->getNodeName() == element) + while (mReader->read()) { + if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END) { + if (mReader->getNodeName() == element) { break; + } + } } } // ------------------------------------------------------------------------------------------------ // Tests for an opening element of the given name, throws an exception if not found -void ColladaParser::TestOpening(const char* pName) -{ +void ColladaParser::TestOpening(const char* pName) { // read element start - if (!mReader->read()) + if (!mReader->read()) { ThrowException(format() << "Unexpected end of file while beginning of <" << pName << "> element."); + } // whitespace in front is ok, just read again if found - if (mReader->getNodeType() == irr::io::EXN_TEXT) - if (!mReader->read()) + if (mReader->getNodeType() == irr::io::EXN_TEXT) { + if (!mReader->read()) { ThrowException(format() << "Unexpected end of file while reading beginning of <" << pName << "> element."); + } + } - if (mReader->getNodeType() != irr::io::EXN_ELEMENT || strcmp(mReader->getNodeName(), pName) != 0) + if (mReader->getNodeType() != irr::io::EXN_ELEMENT || strcmp(mReader->getNodeName(), pName) != 0) { ThrowException(format() << "Expected start of <" << pName << "> element."); + } } // ------------------------------------------------------------------------------------------------ // Tests for the closing tag of the given element, throws an exception if not found -void ColladaParser::TestClosing(const char* pName) -{ +void ColladaParser::TestClosing(const char* pName) { // check if we have an empty (self-closing) element - if (mReader->isEmptyElement()) + if (mReader->isEmptyElement()) { return; + } // check if we're already on the closing tag and return right away - if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END && strcmp(mReader->getNodeName(), pName) == 0) + if (mReader->getNodeType() == irr::io::EXN_ELEMENT_END && strcmp(mReader->getNodeName(), pName) == 0) { return; + } // if not, read some more - if (!mReader->read()) + if (!mReader->read()) { ThrowException(format() << "Unexpected end of file while reading end of <" << pName << "> element."); + } // whitespace in front is ok, just read again if found - if (mReader->getNodeType() == irr::io::EXN_TEXT) - if (!mReader->read()) + if (mReader->getNodeType() == irr::io::EXN_TEXT) { + if (!mReader->read()) { ThrowException(format() << "Unexpected end of file while reading end of <" << pName << "> element."); + } + } // but this has the be the closing tag, or we're lost - if (mReader->getNodeType() != irr::io::EXN_ELEMENT_END || strcmp(mReader->getNodeName(), pName) != 0) + if (mReader->getNodeType() != irr::io::EXN_ELEMENT_END || strcmp(mReader->getNodeName(), pName) != 0) { ThrowException(format() << "Expected end of <" << pName << "> element."); + } } // ------------------------------------------------------------------------------------------------ // Returns the index of the named attribute or -1 if not found. Does not throw, therefore useful for optional attributes -int ColladaParser::GetAttribute(const char* pAttr) const -{ +int ColladaParser::GetAttribute(const char* pAttr) const { int index = TestAttribute(pAttr); - if (index != -1) + if (index != -1) { return index; + } // attribute not found -> throw an exception ThrowException(format() << "Expected attribute \"" << pAttr << "\" for element <" << mReader->getNodeName() << ">."); From 8ebd48442eab97ea1567ff819c082d84abd22852 Mon Sep 17 00:00:00 2001 From: bzt Date: Fri, 1 Nov 2019 03:39:36 +0100 Subject: [PATCH 45/74] Made the M3D SDK C++ wrapper optional --- code/M3D/m3d.h | 93 +++++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index eed66d3e56..7218f83e7b 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -1899,26 +1899,7 @@ static char *_m3d_getfloat(char *s, M3D_FLOAT *ret) return _m3d_findarg(e); } #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; -} +#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) { @@ -1939,6 +1920,27 @@ m3dv_t *_m3d_addnorm(m3dv_t *vrtx, uint32_t *numvrtx, m3dv_t *v, uint32_t *idx) (*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) { @@ -2089,19 +2091,6 @@ _inline static unsigned char *_m3d_getidx(unsigned char *data, char type, M3D_IN return data; } -/* 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 -} - #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. */ @@ -2176,6 +2165,20 @@ void _m3d_mat(M3D_FLOAT *r, m3dv_t *p, m3dv_t *q) 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 @@ -2183,12 +2186,19 @@ void _m3d_mat(M3D_FLOAT *r, m3dv_t *p, m3dv_t *q) 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, numnorm = 0; + 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, *ni = NULL, *vi = NULL; - M3D_FLOAT w, r[16]; + M3D_INDEX mi; + M3D_FLOAT w; +#ifndef M3D_NOANIMATION + M3D_FLOAT r[16]; +#endif m3dtx_t *tx; m3dm_t *m; m3da_t *a; @@ -3000,8 +3010,11 @@ memerr: M3D_LOG("Out of memory"); } } /* 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 */ @@ -3049,7 +3062,9 @@ memerr: M3D_LOG("Out of memory"); 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]; @@ -3067,6 +3082,7 @@ memerr: M3D_LOG("Out of memory"); } } } +#endif #ifndef M3D_NOANIMATION for(i = 0; i < model->numbone; i++) { b = &model->bone[i]; @@ -4317,7 +4333,7 @@ memerr: if(face) M3D_FREE(face); /* zlib compress */ if(!(flags & M3D_EXP_NOZLIB)) { z = stbi_zlib_compress((unsigned char *)h, len, (int*)&l, 9); - if(z && l > 0) { len = l; M3D_FREE(h); h = (m3dhdr_t*)z; } + if(z && l > 0 && l < len) { len = l; M3D_FREE(h); h = (m3dhdr_t*)z; } } /* add file header at the begining */ len += 8; @@ -4346,7 +4362,7 @@ memerr: if(face) M3D_FREE(face); #ifdef __cplusplus } - +#ifdef M3D_CPPWRAPPER #include #include #include @@ -4538,6 +4554,7 @@ namespace M3D { #endif /* impl */ } +#endif #endif /* __cplusplus */ From 55c2a3edb9377bf325b2a5606b52f4ac8d094513 Mon Sep 17 00:00:00 2001 From: Mike Samsonov Date: Fri, 1 Nov 2019 12:29:54 +0000 Subject: [PATCH 46/74] FBX orphant embedded textures --- code/FBX/FBXCompileConfig.h | 8 + code/FBX/FBXConverter.cpp | 57 +- code/FBX/FBXConverter.h | 14 +- code/FBX/FBXDocument.h | 34 +- .../FBX/box_orphant_embedded_texture.fbx | 723 ++++++++++++++++++ test/unit/utFBXImporterExporter.cpp | 20 + 6 files changed, 845 insertions(+), 11 deletions(-) create mode 100644 test/models/FBX/box_orphant_embedded_texture.fbx diff --git a/code/FBX/FBXCompileConfig.h b/code/FBX/FBXCompileConfig.h index 3a3841fa5b..03536a1823 100644 --- a/code/FBX/FBXCompileConfig.h +++ b/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 +#include // #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 +# include # 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 diff --git a/code/FBX/FBXConverter.cpp b/code/FBX/FBXConverter.cpp index 20344331cc..d8a22d9f74 100644 --- a/code/FBX/FBXConverter.cpp +++ b/code/FBX/FBXConverter.cpp @@ -97,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) { @@ -1774,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; @@ -1782,7 +1790,7 @@ namespace Assimp { else { if (media->ContentLength() > 0) { index = ConvertVideo(*media); - textures_converted[media] = index; + textures_converted[*media] = index; textureReady = true; } } @@ -2306,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) @@ -3666,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(key.end() - key.begin()); + if (strncmp(obtype, "Texture", length) == 0) + { + const Texture* texture = static_cast(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) { diff --git a/code/FBX/FBXConverter.h b/code/FBX/FBXConverter.h index 619da92c17..46693bdca6 100644 --- a/code/FBX/FBXConverter.h +++ b/code/FBX/FBXConverter.h @@ -427,6 +427,10 @@ class FBXConverter { // 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; @@ -438,21 +442,21 @@ class FBXConverter { std::vector cameras; std::vector textures; - using MaterialMap = std::map; + using MaterialMap = std::fbx_unordered_map; MaterialMap materials_converted; - using VideoMap = std::map; + using VideoMap = std::fbx_unordered_map; VideoMap textures_converted; - using MeshMap = std::map >; + using MeshMap = std::fbx_unordered_map >; MeshMap meshes_converted; // fixed node name -> which trafo chain components have animations? - using NodeAnimBitMap = std::map ; + using NodeAnimBitMap = std::fbx_unordered_map ; NodeAnimBitMap node_anim_chain_bits; // number of nodes with the same name - using NodeNameCache = std::unordered_map; + using NodeNameCache = std::fbx_unordered_map; NodeNameCache mNodeNames; // Deformer name is not the same as a bone name - it does contain the bone name though :) diff --git a/code/FBX/FBXDocument.h b/code/FBX/FBXDocument.h index 18e5c38f13..51c98da49b 100644 --- a/code/FBX/FBXDocument.h +++ b/code/FBX/FBXDocument.h @@ -637,6 +637,15 @@ class Video : public Object { return ptr; } + bool operator==(const Video& other) const + { + return ( + type == other.type + && relativeFileName == other.relativeFileName + && fileName == other.fileName + ); + } + private: std::string type; std::string relativeFileName; @@ -1005,10 +1014,10 @@ class Connection { // 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 ObjectMap; +typedef std::fbx_unordered_map ObjectMap; typedef std::fbx_unordered_map > PropertyTemplateMap; -typedef std::multimap ConnectionMap; +typedef std::fbx_unordered_multimap ConnectionMap; /** DOM class for global document settings, a single instance per document can * be accessed via Document.Globals(). */ @@ -1177,4 +1186,25 @@ class Document { } // Namespace FBX } // Namespace Assimp +namespace std +{ + template <> + struct hash + { + 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()(video.Name()); + res = res * 31 + hash()(video.RelativeFilename()); + res = res * 31 + hash()(video.Type()); + + return res; + } + }; +} + #endif // INCLUDED_AI_FBX_DOCUMENT_H diff --git a/test/models/FBX/box_orphant_embedded_texture.fbx b/test/models/FBX/box_orphant_embedded_texture.fbx new file mode 100644 index 0000000000..d874751d03 --- /dev/null +++ b/test/models/FBX/box_orphant_embedded_texture.fbx @@ -0,0 +1,723 @@ +; FBX 7.5.0 project file +; ---------------------------------------------------- + +FBXHeaderExtension: { + FBXHeaderVersion: 1003 + FBXVersion: 7500 + CreationTimeStamp: { + Version: 1000 + Year: 2019 + Month: 3 + Day: 1 + Hour: 12 + Minute: 46 + Second: 3 + Millisecond: 995 + } + Creator: "FBX SDK/FBX Plugins version 2018.1.1" + SceneInfo: "SceneInfo::GlobalInfo", "UserData" { + Type: "UserData" + Version: 100 + MetaData: { + Version: 100 + Title: "" + Subject: "" + Author: "" + Keywords: "" + Revision: "" + Comment: "" + } + Properties70: { + P: "DocumentUrl", "KString", "Url", "", "U:\Some\Absolute\Path\box_embedded_texture.fbx" + P: "SrcDocumentUrl", "KString", "Url", "", "U:\Some\Absolute\Path\box_embedded_texture.fbx" + P: "Original", "Compound", "", "" + P: "Original|ApplicationVendor", "KString", "", "", "Autodesk" + P: "Original|ApplicationName", "KString", "", "", "Maya" + P: "Original|ApplicationVersion", "KString", "", "", "201800" + P: "Original|DateTime_GMT", "DateTime", "", "", "01/03/2019 12:46:03.994" + P: "Original|FileName", "KString", "", "", "U:\Some\Absolute\Path\box_embedded_texture.fbx" + P: "LastSaved", "Compound", "", "" + P: "LastSaved|ApplicationVendor", "KString", "", "", "Autodesk" + P: "LastSaved|ApplicationName", "KString", "", "", "Maya" + P: "LastSaved|ApplicationVersion", "KString", "", "", "201800" + P: "LastSaved|DateTime_GMT", "DateTime", "", "", "01/03/2019 12:46:03.994" + P: "Original|ApplicationActiveProject", "KString", "", "", "U:\Some\Absolute\Path" + } + } +} +GlobalSettings: { + Version: 1000 + Properties70: { + P: "UpAxis", "int", "Integer", "",1 + P: "UpAxisSign", "int", "Integer", "",1 + P: "FrontAxis", "int", "Integer", "",2 + P: "FrontAxisSign", "int", "Integer", "",1 + P: "CoordAxis", "int", "Integer", "",0 + P: "CoordAxisSign", "int", "Integer", "",1 + P: "OriginalUpAxis", "int", "Integer", "",1 + P: "OriginalUpAxisSign", "int", "Integer", "",1 + P: "UnitScaleFactor", "double", "Number", "",100 + P: "OriginalUnitScaleFactor", "double", "Number", "",1 + P: "AmbientColor", "ColorRGB", "Color", "",0,0,0 + P: "DefaultCamera", "KString", "", "", "Producer Perspective" + P: "TimeMode", "enum", "", "",6 + P: "TimeProtocol", "enum", "", "",2 + P: "SnapOnFrameMode", "enum", "", "",0 + P: "TimeSpanStart", "KTime", "Time", "",0 + P: "TimeSpanStop", "KTime", "Time", "",153953860000 + P: "CustomFrameRate", "double", "Number", "",-1 + P: "TimeMarker", "Compound", "", "" + P: "CurrentTimeMarker", "int", "Integer", "",-1 + } +} + +; Documents Description +;------------------------------------------------------------------ + +Documents: { + Count: 1 + Document: 2957686739424, "", "Scene" { + Properties70: { + P: "SourceObject", "object", "", "" + P: "ActiveAnimStackName", "KString", "", "", "Take 001" + } + RootNode: 0 + } +} + +; Document References +;------------------------------------------------------------------ + +References: { +} + +; Object definitions +;------------------------------------------------------------------ + +Definitions: { + Version: 100 + Count: 17 + ObjectType: "GlobalSettings" { + Count: 1 + } + ObjectType: "AnimationStack" { + Count: 1 + PropertyTemplate: "FbxAnimStack" { + Properties70: { + P: "Description", "KString", "", "", "" + P: "LocalStart", "KTime", "Time", "",0 + P: "LocalStop", "KTime", "Time", "",0 + P: "ReferenceStart", "KTime", "Time", "",0 + P: "ReferenceStop", "KTime", "Time", "",0 + } + } + } + ObjectType: "AnimationLayer" { + Count: 1 + PropertyTemplate: "FbxAnimLayer" { + Properties70: { + P: "Weight", "Number", "", "A",100 + P: "Mute", "bool", "", "",0 + P: "Solo", "bool", "", "",0 + P: "Lock", "bool", "", "",0 + P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8 + P: "BlendMode", "enum", "", "",0 + P: "RotationAccumulationMode", "enum", "", "",0 + P: "ScaleAccumulationMode", "enum", "", "",0 + P: "BlendModeBypass", "ULongLong", "", "",0 + } + } + } + ObjectType: "Geometry" { + Count: 1 + PropertyTemplate: "FbxMesh" { + Properties70: { + P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8 + P: "BBoxMin", "Vector3D", "Vector", "",0,0,0 + P: "BBoxMax", "Vector3D", "Vector", "",0,0,0 + P: "Primary Visibility", "bool", "", "",1 + P: "Casts Shadows", "bool", "", "",1 + P: "Receive Shadows", "bool", "", "",1 + } + } + } + ObjectType: "Material" { + Count: 1 + PropertyTemplate: "FbxSurfacePhong" { + Properties70: { + P: "ShadingModel", "KString", "", "", "Phong" + P: "MultiLayer", "bool", "", "",0 + P: "EmissiveColor", "Color", "", "A",0,0,0 + P: "EmissiveFactor", "Number", "", "A",1 + P: "AmbientColor", "Color", "", "A",0.2,0.2,0.2 + P: "AmbientFactor", "Number", "", "A",1 + P: "DiffuseColor", "Color", "", "A",0.8,0.8,0.8 + P: "DiffuseFactor", "Number", "", "A",1 + P: "Bump", "Vector3D", "Vector", "",0,0,0 + P: "NormalMap", "Vector3D", "Vector", "",0,0,0 + P: "BumpFactor", "double", "Number", "",1 + P: "TransparentColor", "Color", "", "A",0,0,0 + P: "TransparencyFactor", "Number", "", "A",0 + P: "DisplacementColor", "ColorRGB", "Color", "",0,0,0 + P: "DisplacementFactor", "double", "Number", "",1 + P: "VectorDisplacementColor", "ColorRGB", "Color", "",0,0,0 + P: "VectorDisplacementFactor", "double", "Number", "",1 + P: "SpecularColor", "Color", "", "A",0.2,0.2,0.2 + P: "SpecularFactor", "Number", "", "A",1 + P: "ShininessExponent", "Number", "", "A",20 + P: "ReflectionColor", "Color", "", "A",0,0,0 + P: "ReflectionFactor", "Number", "", "A",1 + } + } + } + ObjectType: "Texture" { + Count: 1 + PropertyTemplate: "FbxFileTexture" { + Properties70: { + P: "TextureTypeUse", "enum", "", "",0 + P: "Texture alpha", "Number", "", "A",1 + P: "CurrentMappingType", "enum", "", "",0 + P: "WrapModeU", "enum", "", "",0 + P: "WrapModeV", "enum", "", "",0 + P: "UVSwap", "bool", "", "",0 + P: "PremultiplyAlpha", "bool", "", "",1 + P: "Translation", "Vector", "", "A",0,0,0 + P: "Rotation", "Vector", "", "A",0,0,0 + P: "Scaling", "Vector", "", "A",1,1,1 + P: "TextureRotationPivot", "Vector3D", "Vector", "",0,0,0 + P: "TextureScalingPivot", "Vector3D", "Vector", "",0,0,0 + P: "CurrentTextureBlendMode", "enum", "", "",1 + P: "UVSet", "KString", "", "", "default" + P: "UseMaterial", "bool", "", "",0 + P: "UseMipMap", "bool", "", "",0 + } + } + } + ObjectType: "Model" { + Count: 1 + PropertyTemplate: "FbxNode" { + Properties70: { + P: "QuaternionInterpolate", "enum", "", "",0 + P: "RotationOffset", "Vector3D", "Vector", "",0,0,0 + P: "RotationPivot", "Vector3D", "Vector", "",0,0,0 + P: "ScalingOffset", "Vector3D", "Vector", "",0,0,0 + P: "ScalingPivot", "Vector3D", "Vector", "",0,0,0 + P: "TranslationActive", "bool", "", "",0 + P: "TranslationMin", "Vector3D", "Vector", "",0,0,0 + P: "TranslationMax", "Vector3D", "Vector", "",0,0,0 + P: "TranslationMinX", "bool", "", "",0 + P: "TranslationMinY", "bool", "", "",0 + P: "TranslationMinZ", "bool", "", "",0 + P: "TranslationMaxX", "bool", "", "",0 + P: "TranslationMaxY", "bool", "", "",0 + P: "TranslationMaxZ", "bool", "", "",0 + P: "RotationOrder", "enum", "", "",0 + P: "RotationSpaceForLimitOnly", "bool", "", "",0 + P: "RotationStiffnessX", "double", "Number", "",0 + P: "RotationStiffnessY", "double", "Number", "",0 + P: "RotationStiffnessZ", "double", "Number", "",0 + P: "AxisLen", "double", "Number", "",10 + P: "PreRotation", "Vector3D", "Vector", "",0,0,0 + P: "PostRotation", "Vector3D", "Vector", "",0,0,0 + P: "RotationActive", "bool", "", "",0 + P: "RotationMin", "Vector3D", "Vector", "",0,0,0 + P: "RotationMax", "Vector3D", "Vector", "",0,0,0 + P: "RotationMinX", "bool", "", "",0 + P: "RotationMinY", "bool", "", "",0 + P: "RotationMinZ", "bool", "", "",0 + P: "RotationMaxX", "bool", "", "",0 + P: "RotationMaxY", "bool", "", "",0 + P: "RotationMaxZ", "bool", "", "",0 + P: "InheritType", "enum", "", "",0 + P: "ScalingActive", "bool", "", "",0 + P: "ScalingMin", "Vector3D", "Vector", "",0,0,0 + P: "ScalingMax", "Vector3D", "Vector", "",1,1,1 + P: "ScalingMinX", "bool", "", "",0 + P: "ScalingMinY", "bool", "", "",0 + P: "ScalingMinZ", "bool", "", "",0 + P: "ScalingMaxX", "bool", "", "",0 + P: "ScalingMaxY", "bool", "", "",0 + P: "ScalingMaxZ", "bool", "", "",0 + P: "GeometricTranslation", "Vector3D", "Vector", "",0,0,0 + P: "GeometricRotation", "Vector3D", "Vector", "",0,0,0 + P: "GeometricScaling", "Vector3D", "Vector", "",1,1,1 + P: "MinDampRangeX", "double", "Number", "",0 + P: "MinDampRangeY", "double", "Number", "",0 + P: "MinDampRangeZ", "double", "Number", "",0 + P: "MaxDampRangeX", "double", "Number", "",0 + P: "MaxDampRangeY", "double", "Number", "",0 + P: "MaxDampRangeZ", "double", "Number", "",0 + P: "MinDampStrengthX", "double", "Number", "",0 + P: "MinDampStrengthY", "double", "Number", "",0 + P: "MinDampStrengthZ", "double", "Number", "",0 + P: "MaxDampStrengthX", "double", "Number", "",0 + P: "MaxDampStrengthY", "double", "Number", "",0 + P: "MaxDampStrengthZ", "double", "Number", "",0 + P: "PreferedAngleX", "double", "Number", "",0 + P: "PreferedAngleY", "double", "Number", "",0 + P: "PreferedAngleZ", "double", "Number", "",0 + P: "LookAtProperty", "object", "", "" + P: "UpVectorProperty", "object", "", "" + P: "Show", "bool", "", "",1 + P: "NegativePercentShapeSupport", "bool", "", "",1 + P: "DefaultAttributeIndex", "int", "Integer", "",-1 + P: "Freeze", "bool", "", "",0 + P: "LODBox", "bool", "", "",0 + P: "Lcl Translation", "Lcl Translation", "", "A",0,0,0 + P: "Lcl Rotation", "Lcl Rotation", "", "A",0,0,0 + P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 + P: "Visibility", "Visibility", "", "A",1 + P: "Visibility Inheritance", "Visibility Inheritance", "", "",1 + } + } + } + ObjectType: "AnimationCurveNode" { + Count: 8 + PropertyTemplate: "FbxAnimCurveNode" { + Properties70: { + P: "d", "Compound", "", "" + } + } + } + ObjectType: "CollectionExclusive" { + Count: 1 + PropertyTemplate: "FbxDisplayLayer" { + Properties70: { + P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8 + P: "Show", "bool", "", "",1 + P: "Freeze", "bool", "", "",0 + P: "LODBox", "bool", "", "",0 + } + } + } + ObjectType: "Video" { + Count: 1 + PropertyTemplate: "FbxVideo" { + Properties70: { + P: "Path", "KString", "XRefUrl", "", "" + P: "RelPath", "KString", "XRefUrl", "", "" + P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8 + P: "ClipIn", "KTime", "Time", "",0 + P: "ClipOut", "KTime", "Time", "",0 + P: "Offset", "KTime", "Time", "",0 + P: "PlaySpeed", "double", "Number", "",0 + P: "FreeRunning", "bool", "", "",0 + P: "Loop", "bool", "", "",0 + P: "Mute", "bool", "", "",0 + P: "AccessMode", "enum", "", "",0 + P: "ImageSequence", "bool", "", "",0 + P: "ImageSequenceOffset", "int", "Integer", "",0 + P: "FrameRate", "double", "Number", "",0 + P: "LastFrame", "int", "Integer", "",0 + P: "Width", "int", "Integer", "",0 + P: "Height", "int", "Integer", "",0 + P: "StartFrame", "int", "Integer", "",0 + P: "StopFrame", "int", "Integer", "",0 + P: "InterlaceMode", "enum", "", "",0 + } + } + } +} + +; Object properties +;------------------------------------------------------------------ + +Objects: { + Geometry: 2957764348544, "Geometry::", "Mesh" { + Vertices: *24 { + a: -0.5,-0.5,-0.5,0.5,-0.50000011920929,-0.5,-0.5,0.50000011920929,-0.5,0.5,0.50000011920929,-0.5,-0.5,-0.500000059604645,0.5,0.5,-0.500000059604645,0.5,-0.5,0.500000059604645,0.5,0.5,0.500000059604645,0.5 + } + PolygonVertexIndex: *24 { + a: 0,2,3,-2,4,5,7,-7,0,1,5,-5,1,3,7,-6,3,2,6,-8,2,0,4,-7 + } + Edges: *12 { + a: 0,1,2,3,4,5,6,7,9,11,13,17 + } + GeometryVersion: 124 + LayerElementNormal: 0 { + Version: 102 + Name: "" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "Direct" + Normals: *72 { + a: 0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,1,0,0,1,0,0,1,0,0,1,-5.96046447753906e-08,-1,0,-5.96046447753906e-08,-1,0,-5.96046447753906e-08,-1,0,-5.96046447753906e-08,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,1,5.96046447753906e-08,0,1,5.96046447753906e-08,0,1,5.96046447753906e-08,0,1,5.96046447753906e-08,-1,0,0,-1,0,0,-1,0,0,-1,0,0 + } + NormalsW: *24 { + a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + } + } + LayerElementBinormal: 0 { + Version: 102 + Name: "UVChannel_1" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "Direct" + Binormals: *72 { + a: 1.19209289550781e-07,1,0,5.96046447753906e-08,1,0,0,1,0,5.96046447753906e-08,1,0,0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,1,0,0,1,0,0,1,0,0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,-0,-5.96046447753906e-08,1,-0,-5.96046447753906e-08,1,-0,-5.96046447753906e-08,1,-0,-5.96046447753906e-08,1,0,0,1,0,0,1,0,0,1,0,0,1 + } + BinormalsW: *24 { + a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + } + + } + LayerElementBinormal: 1 { + Version: 102 + Name: "UVChannel_3" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "Direct" + Binormals: *72 { + a: -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-0,0,-1,-0,0,-1,0,-0,-1,-0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,5.96046447753906e-08,-1,0,5.96046447753906e-08,-1,0,5.96046447753906e-08,-1,0,5.96046447753906e-08,-1,-0,0,-1,-0,0,-1,-0,0,-1,-0,0,-1 + } + BinormalsW: *24 { + a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + } + + } + LayerElementTangent: 0 { + Version: 102 + Name: "UVChannel_1" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "Direct" + Tangents: *72 { + a: -1,1.19209289550781e-07,0,-1,5.96046447753906e-08,0,-1,-0,0,-1,5.96046447753906e-08,0,1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-5.96046447753906e-08,-0,1,-5.96046447753906e-08,0,1,-5.96046447753906e-08,0,1,-5.96046447753906e-08,0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,-1,0,-0,-1,0,-0,-1,0,-0,-1,0,-0,0,-1,-0,0,-1,0,0,-1,0,0,-1,0 + } + TangentsW: *24 { + a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + } + } + LayerElementTangent: 1 { + Version: 102 + Name: "UVChannel_3" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "Direct" + Tangents: *72 { + a: -0,-1,0,-0,-1,0,-0,-1,0,-0,-1,0,0,1,0,0,1,0,-0,1,-0,0,1,0,-1,5.96046447753906e-08,0,-1,5.96046447753906e-08,0,-1,5.96046447753906e-08,-0,-1,5.96046447753906e-08,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,-0,-0,1,-0,-0,1,-0,-0,1,-0,-0,0,1,-0,0,1,-0,0,1,-0,0,1,-0 + } + TangentsW: *24 { + a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + } + } + LayerElementUV: 0 { + Version: 101 + Name: "UVChannel_1" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "IndexToDirect" + UV: *48 { + a: 1,0,0,0,1,1,0,1,0,0,1,0,0,1,1,1,0,0,1,0,0,1,1,1,0,0,1,0,0,1,1,1,0,0,1,0,0,1,1,1,0,0,1,0,0,1,1,1 + } + UVIndex: *24 { + a: 0,2,3,1,4,5,7,6,8,9,11,10,12,13,15,14,16,17,19,18,20,21,23,22 + } + } + LayerElementUV: 1 { + Version: 101 + Name: "UVChannel_3" + MappingInformationType: "ByPolygonVertex" + ReferenceInformationType: "IndexToDirect" + UV: *48 { + a: 0.28125,0.28125,0,0.28125,0,0,0.28125,0,0.34375,1,0.34375,0.71875,0.625,0.71875,0.625,1,0.28125,0.65625,0,0.65625,0,0.375,0.28125,0.375,0.625,0.28125,0.34375,0.28125,0.34375,0,0.625,0,0.28125,1,0,1,0,0.71875,0.28125,0.71875,0.625,0.65625,0.34375,0.65625,0.34375,0.375,0.625,0.375 + } + UVIndex: *24 { + a: 0,1,2,3,4,5,6,7,20,21,22,23,8,9,10,11,16,17,18,19,12,13,14,15 + } + } + LayerElementSmoothing: 0 { + Version: 102 + Name: "" + MappingInformationType: "ByEdge" + ReferenceInformationType: "Direct" + Smoothing: *12 { + a: 0,0,0,0,0,0,0,0,0,0,0,0 + } + } + LayerElementMaterial: 0 { + Version: 101 + Name: "" + MappingInformationType: "AllSame" + ReferenceInformationType: "IndexToDirect" + Materials: *1 { + a: 0 + } + } + Layer: 0 { + Version: 100 + LayerElement: { + Type: "LayerElementNormal" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementBinormal" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementTangent" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementMaterial" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementSmoothing" + TypedIndex: 0 + } + LayerElement: { + Type: "LayerElementUV" + TypedIndex: 0 + } + } + Layer: 1 { + Version: 100 + LayerElement: { + Type: "LayerElementBinormal" + TypedIndex: 1 + } + LayerElement: { + Type: "LayerElementTangent" + TypedIndex: 1 + } + LayerElement: { + Type: "LayerElementUV" + TypedIndex: 1 + } + } + } + Model: 2957618625584, "Model::Box", "Mesh" { + Version: 232 + Properties70: { + P: "RotationActive", "bool", "", "",1 + P: "InheritType", "enum", "", "",1 + P: "ScalingMax", "Vector3D", "Vector", "",0,0,0 + P: "DefaultAttributeIndex", "int", "Integer", "",0 + P: "Lcl Translation", "Lcl Translation", "", "A",0,0.5,2.18556946492754e-06 + P: "Lcl Rotation", "Lcl Rotation", "", "A",-90,0,0 + P: "currentUVSet", "KString", "", "U", "UVChannel_1" + P: "mr displacement use global settings", "Bool", "", "A+U",1 + P: "mr displacement view dependent", "Bool", "", "A+U",1 + P: "mr displacement method", "Integer", "", "A+U",6,6,6 + P: "mr displacement smoothing on", "Bool", "", "A+U",1 + P: "mr displacement edge length", "Number", "", "A+U",2,2,2 + P: "mr displacement max displace", "Number", "", "A+U",20,20,20 + P: "mr displacement parametric subdivision level", "Integer", "", "A+U",5,5,5 + P: "MaxHandle", "Integer", "", "A+UH",1,0,0 + } + Shading: T + Culling: "CullingOff" + } + Material: 2957776713840, "Material::Default", "" { + Version: 102 + ShadingModel: "phong" + MultiLayer: 0 + Properties70: { + P: "AmbientColor", "Color", "", "A",0,0,0 + P: "DiffuseColor", "Color", "", "A",1,1,1 + P: "TransparencyFactor", "Number", "", "A",1 + P: "SpecularColor", "Color", "", "A",0,0,0 + P: "ShininessExponent", "Number", "", "A",2 + P: "Emissive", "Vector3D", "Vector", "",0,0,0 + P: "Ambient", "Vector3D", "Vector", "",0,0,0 + P: "Diffuse", "Vector3D", "Vector", "",1,1,1 + P: "Specular", "Vector3D", "Vector", "",0,0,0 + P: "Shininess", "double", "Number", "",2 + P: "Opacity", "double", "Number", "",1 + P: "Reflectivity", "double", "Number", "",0 + } + } + Video: 2957776707120, "Video::Map #2", "Clip" { + Type: "Clip" + Properties70: { + P: "Path", "KString", "XRefUrl", "", "U:/Some/Absolute/Primitives/GridGrey.tga" + P: "RelPath", "KString", "XRefUrl", "", "..\Primitives\GridGrey.tga" + } + UseMipMap: 0 + Filename: "U:/Some/Absolute/Primitives/GridGrey.tga" + RelativeFilename: "..\Primitives\GridGrey.tga" + } + Video: 2957776707121, "Video::Map #2", "Clip" { + Type: "Clip" + Properties70: { + P: "Path", "KString", "XRefUrl", "", "U:/Some/Absolute/Primitives/GridGrey.tga" + P: "RelPath", "KString", "XRefUrl", "", "..\Primitives\GridGrey.tga" + } + UseMipMap: 0 + Filename: "U:/Some/Absolute/Primitives/GridGrey.tga" + RelativeFilename: "..\Primitives\GridGrey.tga" + Content: , + "AAAKAAAAAAAAAAAAAAEAARgAh+rq6v+Pj4/wj4+Phurq6ofq6uq2dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+3dHR0hurq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6gCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+Pj/+Pj4//j4+P/4+Pj/+Pj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj48Aj4+PvXR0dIGPj4+9dHR0gY+Pj710dHSBj4+PvXR0dACPj4//j4+P/4+Pj/+Pj4//j4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+PAI+Pj710dHSBj4+PvXR0dIGPj4+9dHR0gY+Pj710dHQAj4+P/4+Pj/+Pj4//j4+P/4+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+PjwCPj4+9dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+9dHR0AI+Pj4Hq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6oHq6uq8dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+8dHR0gerq6ofq6uq2dHR0gY+Pj710dHSBj4+PvXR0dIGPj4+3dHR0hurq6ofq6ur/j4+P8I+Pj4bq6uo=" + } + Texture: 2957776714320, "Texture::Map #2", "" { + Type: "TextureVideoClip" + Version: 202 + TextureName: "Texture::Map #2" + Properties70: { + P: "CurrentTextureBlendMode", "enum", "", "",0 + P: "UVSet", "KString", "", "", "UVChannel_1" + P: "UseMaterial", "bool", "", "",1 + } + Media: "Video::Map #2" + FileName: "U:/Some/Absolute/Primitives/GridGrey.tga" + RelativeFilename: "..\Primitives\GridGrey.tga" + ModelUVTranslation: 0,0 + ModelUVScaling: 1,1 + Texture_Alpha_Source: "None" + Cropping: 0,0,0,0 + } + Texture: 2957776714321, "Texture::Map #2", "" { + Type: "TextureVideoClip" + Version: 202 + TextureName: "Texture::Map #2" + Properties70: { + P: "CurrentTextureBlendMode", "enum", "", "",0 + P: "UVSet", "KString", "", "", "UVChannel_1" + P: "UseMaterial", "bool", "", "",1 + } + Media: "Video::Map #2" + FileName: "U:/Some/Absolute/Primitives/GridGrey.tga" + RelativeFilename: "..\Primitives\GridGrey.tga" + ModelUVTranslation: 0,0 + ModelUVScaling: 1,1 + Texture_Alpha_Source: "None" + Cropping: 0,0,0,0 + } + AnimationStack: 2957627494560, "AnimStack::Take 001", "" { + Properties70: { + P: "LocalStop", "KTime", "Time", "",153953860000 + P: "ReferenceStop", "KTime", "Time", "",153953860000 + } + } + AnimationCurveNode: 2957627500176, "AnimCurveNode::mr displacement use global settings", "" { + Properties70: { + P: "d|mr displacement use global settings", "Bool", "", "A",1 + } + } + AnimationCurveNode: 2957627490192, "AnimCurveNode::mr displacement view dependent", "" { + Properties70: { + P: "d|mr displacement view dependent", "Bool", "", "A",1 + } + } + AnimationCurveNode: 2957627487904, "AnimCurveNode::mr displacement method", "" { + Properties70: { + P: "d|mr displacement method", "Integer", "", "A",6 + } + } + AnimationCurveNode: 2957627492688, "AnimCurveNode::mr displacement smoothing on", "" { + Properties70: { + P: "d|mr displacement smoothing on", "Bool", "", "A",1 + } + } + AnimationCurveNode: 2957627492064, "AnimCurveNode::mr displacement edge length", "" { + Properties70: { + P: "d|mr displacement edge length", "Number", "", "A",2 + } + } + AnimationCurveNode: 2957627492896, "AnimCurveNode::mr displacement max displace", "" { + Properties70: { + P: "d|mr displacement max displace", "Number", "", "A",20 + } + } + AnimationCurveNode: 2957627492272, "AnimCurveNode::mr displacement parametric subdivision level", "" { + Properties70: { + P: "d|mr displacement parametric subdivision level", "Integer", "", "A",5 + } + } + AnimationCurveNode: 2957627488320, "AnimCurveNode::MaxHandle", "" { + Properties70: { + P: "d|MaxHandle", "Integer", "", "A",1 + } + } + AnimationLayer: 2957566198176, "AnimLayer::BaseLayer", "" { + } + CollectionExclusive: 2959111896352, "DisplayLayer::Box", "DisplayLayer" { + Properties70: { + P: "Color", "ColorRGB", "Color", "",0.607999980449677,0,0.157000005245209 + } + } +} + +; Object connections +;------------------------------------------------------------------ + +Connections: { + + ;Model::Box, Model::RootNode + C: "OO",2957618625584,0 + + ;AnimLayer::BaseLayer, AnimStack::Take 001 + C: "OO",2957566198176,2957627494560 + + ;AnimCurveNode::mr displacement use global settings, AnimLayer::BaseLayer + C: "OO",2957627500176,2957566198176 + + ;AnimCurveNode::mr displacement view dependent, AnimLayer::BaseLayer + C: "OO",2957627490192,2957566198176 + + ;AnimCurveNode::mr displacement method, AnimLayer::BaseLayer + C: "OO",2957627487904,2957566198176 + + ;AnimCurveNode::mr displacement smoothing on, AnimLayer::BaseLayer + C: "OO",2957627492688,2957566198176 + + ;AnimCurveNode::mr displacement edge length, AnimLayer::BaseLayer + C: "OO",2957627492064,2957566198176 + + ;AnimCurveNode::mr displacement max displace, AnimLayer::BaseLayer + C: "OO",2957627492896,2957566198176 + + ;AnimCurveNode::mr displacement parametric subdivision level, AnimLayer::BaseLayer + C: "OO",2957627492272,2957566198176 + + ;AnimCurveNode::MaxHandle, AnimLayer::BaseLayer + C: "OO",2957627488320,2957566198176 + + ;Texture::Map #2, Material::Default + C: "OP",2957776714320,2957776713840, "DiffuseColor" + + ;Video::Map #2, Texture::Map #2 + C: "OO",2957776707120,2957776714320 + + ;Video::Map #2, Texture::Map #2 + C: "OO",2957776707121,2957776714321 + + ;Geometry::, Model::Box + C: "OO",2957764348544,2957618625584 + + ;Material::Default, Model::Box + C: "OO",2957776713840,2957618625584 + + ;AnimCurveNode::mr displacement use global settings, Model::Box + C: "OP",2957627500176,2957618625584, "mr displacement use global settings" + + ;AnimCurveNode::mr displacement view dependent, Model::Box + C: "OP",2957627490192,2957618625584, "mr displacement view dependent" + + ;AnimCurveNode::mr displacement method, Model::Box + C: "OP",2957627487904,2957618625584, "mr displacement method" + + ;AnimCurveNode::mr displacement smoothing on, Model::Box + C: "OP",2957627492688,2957618625584, "mr displacement smoothing on" + + ;AnimCurveNode::mr displacement edge length, Model::Box + C: "OP",2957627492064,2957618625584, "mr displacement edge length" + + ;AnimCurveNode::mr displacement max displace, Model::Box + C: "OP",2957627492896,2957618625584, "mr displacement max displace" + + ;AnimCurveNode::mr displacement parametric subdivision level, Model::Box + C: "OP",2957627492272,2957618625584, "mr displacement parametric subdivision level" + + ;AnimCurveNode::MaxHandle, Model::Box + C: "OP",2957627488320,2957618625584, "MaxHandle" + + ;Model::Box, DisplayLayer::Box + C: "OO",2957618625584,2959111896352 +} +;Takes section +;---------------------------------------------------- + +Takes: { + Current: "Take 001" + Take: "Take 001" { + FileName: "Take_001.tak" + LocalTime: 0,153953860000 + ReferenceTime: 0,153953860000 + } +} diff --git a/test/unit/utFBXImporterExporter.cpp b/test/unit/utFBXImporterExporter.cpp index e73733a0df..43dc40e88c 100644 --- a/test/unit/utFBXImporterExporter.cpp +++ b/test/unit/utFBXImporterExporter.cpp @@ -263,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."; +} From 536fea1c8d091f8763b13e807216323f1fdd632a Mon Sep 17 00:00:00 2001 From: Mike Samsonov Date: Fri, 1 Nov 2019 14:50:30 +0000 Subject: [PATCH 47/74] operator less for old compilers --- code/FBX/FBXDocument.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/FBX/FBXDocument.h b/code/FBX/FBXDocument.h index 51c98da49b..a60d7d9efa 100644 --- a/code/FBX/FBXDocument.h +++ b/code/FBX/FBXDocument.h @@ -646,6 +646,11 @@ class Video : public Object { ); } + 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; From 82f926971b5220e6662f8b3726c8736944854e99 Mon Sep 17 00:00:00 2001 From: tanolino Date: Fri, 1 Nov 2019 17:01:52 +0100 Subject: [PATCH 48/74] Update CXMLReaderImpl.h Issues with German locale number converter expecting German numbers. In Germany we have numbers like #,## instead of #.## . Thus a unit conversion in COLLADA of 0.01 (centimeter) turned out to be 0.00. --- contrib/irrXML/CXMLReaderImpl.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contrib/irrXML/CXMLReaderImpl.h b/contrib/irrXML/CXMLReaderImpl.h index 6f3bec5fa1..a125312a16 100644 --- a/contrib/irrXML/CXMLReaderImpl.h +++ b/contrib/irrXML/CXMLReaderImpl.h @@ -15,6 +15,9 @@ #include //using namespace Assimp; +// For locale independent number conversion +#include +#include #ifdef _DEBUG #define IRR_DEBUGPRINT(x) printf((x)); @@ -178,8 +181,11 @@ class CXMLReaderImpl : public IIrrXMLReader return 0; core::stringc c = attrvalue; - return static_cast(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; } From 7d9e9aadbbceb8be0aaf50c7fcd6990715f32242 Mon Sep 17 00:00:00 2001 From: bzt Date: Fri, 1 Nov 2019 17:21:24 +0100 Subject: [PATCH 49/74] Asked modifications and unit test --- code/M3D/M3DExporter.cpp | 17 +++--- code/M3D/M3DMaterials.h | 4 +- code/M3D/m3d.h | 22 ++++--- test/CMakeLists.txt | 1 + test/models/M3D/README.md | 14 ----- test/models/M3D/aliveai_character.m3d | Bin 2532 -> 0 bytes test/models/M3D/mobs_dwarves_character.m3d | Bin 11255 -> 0 bytes test/unit/utM3DImportExport.cpp | 68 +++++++++++++++++++++ 8 files changed, 93 insertions(+), 33 deletions(-) delete mode 100644 test/models/M3D/README.md delete mode 100644 test/models/M3D/aliveai_character.m3d delete mode 100644 test/models/M3D/mobs_dwarves_character.m3d create mode 100644 test/unit/utM3DImportExport.cpp diff --git a/code/M3D/M3DExporter.cpp b/code/M3D/M3DExporter.cpp index 9ba48b125b..c22943396e 100644 --- a/code/M3D/M3DExporter.cpp +++ b/code/M3D/M3DExporter.cpp @@ -173,12 +173,9 @@ void M3DExporter::doExport ( // recursive node walker void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) { - unsigned int i, j, k, l, n, idx; aiMatrix4x4 nm = m * pNode->mTransformation; - m3dv_t vertex; - m3dti_t ti; - for(i = 0; i < pNode->mNumMeshes; i++) { + 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) { @@ -186,7 +183,8 @@ void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) mi = addMaterial(mScene->mMaterials[mesh->mMaterialIndex]); } // iterate through the mesh faces - for(j = 0; j < mesh->mNumFaces; j++) { + 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) { @@ -204,9 +202,12 @@ void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) 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(k = 0; k < face->mNumIndices; k++) { + for(unsigned int k = 0; k < face->mNumIndices; k++) { // get the vertex's index - l = face->mIndices[k]; + 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; @@ -245,7 +246,7 @@ void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) } } // repeat for the children nodes - for (i = 0; i < pNode->mNumChildren; i++) { + for (unsigned int i = 0; i < pNode->mNumChildren; i++) { NodeWalk(pNode->mChildren[i], nm); } } diff --git a/code/M3D/M3DMaterials.h b/code/M3D/M3DMaterials.h index b3c91ab7aa..fa02cf42be 100644 --- a/code/M3D/M3DMaterials.h +++ b/code/M3D/M3DMaterials.h @@ -60,7 +60,7 @@ typedef struct { } aiMatProp; /* --- Scalar Properties --- !!!!! must match m3d_propertytypes !!!!! */ -static aiMatProp aiProps[] = { +static const aiMatProp aiProps[] = { { AI_MATKEY_COLOR_DIFFUSE }, /* m3dp_Kd */ { AI_MATKEY_COLOR_AMBIENT }, /* m3dp_Ka */ { AI_MATKEY_COLOR_SPECULAR }, /* m3dp_Ks */ @@ -82,7 +82,7 @@ static aiMatProp aiProps[] = { }; /* --- Texture Map Properties --- !!!!! must match m3d_propertytypes !!!!! */ -static aiMatProp aiTxProps[] = { +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 */ diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index 7218f83e7b..b28dd5d2a5 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -2144,6 +2144,10 @@ void _m3d_inv(M3D_FLOAT *m) 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 && @@ -2151,15 +2155,15 @@ void _m3d_mat(M3D_FLOAT *r, m3dv_t *p, m3dv_t *q) 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_FLOAT)-1e-7 && r[ 0]<(M3D_FLOAT)1e-7) r[ 0]=(M3D_FLOAT)0.0; - r[ 1] = 2 * (q->x * q->y - q->z * q->w); if(r[ 1]>(M3D_FLOAT)-1e-7 && r[ 1]<(M3D_FLOAT)1e-7) r[ 1]=(M3D_FLOAT)0.0; - r[ 2] = 2 * (q->x * q->z + q->y * q->w); if(r[ 2]>(M3D_FLOAT)-1e-7 && r[ 2]<(M3D_FLOAT)1e-7) r[ 2]=(M3D_FLOAT)0.0; - r[ 4] = 2 * (q->x * q->y + q->z * q->w); if(r[ 4]>(M3D_FLOAT)-1e-7 && r[ 4]<(M3D_FLOAT)1e-7) r[ 4]=(M3D_FLOAT)0.0; - r[ 5] = 1 - 2 * (q->x * q->x + q->z * q->z); if(r[ 5]>(M3D_FLOAT)-1e-7 && r[ 5]<(M3D_FLOAT)1e-7) r[ 5]=(M3D_FLOAT)0.0; - r[ 6] = 2 * (q->y * q->z - q->x * q->w); if(r[ 6]>(M3D_FLOAT)-1e-7 && r[ 6]<(M3D_FLOAT)1e-7) r[ 6]=(M3D_FLOAT)0.0; - r[ 8] = 2 * (q->x * q->z - q->y * q->w); if(r[ 8]>(M3D_FLOAT)-1e-7 && r[ 8]<(M3D_FLOAT)1e-7) r[ 8]=(M3D_FLOAT)0.0; - r[ 9] = 2 * (q->y * q->z + q->x * q->w); if(r[ 9]>(M3D_FLOAT)-1e-7 && r[ 9]<(M3D_FLOAT)1e-7) r[ 9]=(M3D_FLOAT)0.0; - r[10] = 1 - 2 * (q->x * q->x + q->y * q->y); if(r[10]>(M3D_FLOAT)-1e-7 && r[10]<(M3D_FLOAT)1e-7) r[10]=(M3D_FLOAT)0.0; + r[ 0] = 1 - 2 * (q->y * q->y + q->z * q->z); if(r[ 0]>-M3D_EPSILON && r[ 0]x * q->y - q->z * q->w); if(r[ 1]>-M3D_EPSILON && r[ 1]x * q->z + q->y * q->w); if(r[ 2]>-M3D_EPSILON && r[ 2]x * q->y + q->z * q->w); if(r[ 4]>-M3D_EPSILON && r[ 4]x * q->x + q->z * q->z); if(r[ 5]>-M3D_EPSILON && r[ 5]y * q->z - q->x * q->w); if(r[ 6]>-M3D_EPSILON && r[ 6]x * q->z - q->y * q->w); if(r[ 8]>-M3D_EPSILON && r[ 8]y * q->z + q->x * q->w); if(r[ 9]>-M3D_EPSILON && r[ 9]x * q->x + q->y * q->y); if(r[10]>-M3D_EPSILON && r[10]x; r[ 7] = p->y; r[11] = p->z; r[12] = 0; r[13] = 0; r[14] = 0; r[15] = 1; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 913813c3be..c0babe9b79 100644 --- a/test/CMakeLists.txt +++ b/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 diff --git a/test/models/M3D/README.md b/test/models/M3D/README.md deleted file mode 100644 index 144d1ec644..0000000000 --- a/test/models/M3D/README.md +++ /dev/null @@ -1,14 +0,0 @@ -Model 3D Samples -================ - - aliveai_character.m3d - from Minetest aliveai mod (textures, animations, original 47k, m3d 2.5k) - cube.m3d - smallest possible example, 119 bytes only - cube_normals.m3d - cube with normal vectors, 159 bytes - cube_usemtl.m3d - converted from Assimp sample OBJ by the same name, cube with materials - cube_with_vertexcolors.m3d - converted from Assimp sample OBJ by the same name, cube with vertex colors - cube_with_vertexcolors.a3d - same, but saved in ASCII variant with Windows line endings (\r\n) - mobs_dwarves_character.m3d - from Minetest mobs_dwarves mod (with Assimp artifacts converted perfectly too...) - suzanne.m3d - exported from Blender (monkey face, with normals and texture UVs and materials) - WusonBlitz0.m3d - from Assimp sample by the same name (was 87k, triangle mesh) with int8 coordinates, minor quality degradation - WusonBlitz1.m3d - same, but uses int16 coordinates (no noticable difference to the original, but just 35k) - WusonBlitz2.m3d - same, but with 32 bit floating point numbers (same precision as the original, half the file size, 42k) diff --git a/test/models/M3D/aliveai_character.m3d b/test/models/M3D/aliveai_character.m3d deleted file mode 100644 index f4c170300bcb40712693f3e4cb7586f9c462b1c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2532 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 diff --git a/test/unit/utM3DImportExport.cpp b/test/unit/utM3DImportExport.cpp new file mode 100644 index 0000000000..c3a0fb08c3 --- /dev/null +++ b/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 +#include + +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() ); +} From 7201ebdcccb9758f9e44d7ca896dfa99627e8e53 Mon Sep 17 00:00:00 2001 From: bzt Date: Fri, 1 Nov 2019 21:52:11 +0100 Subject: [PATCH 50/74] Make Clang happy --- code/M3D/m3d.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index b28dd5d2a5..9ace802ef5 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -2085,8 +2085,8 @@ _inline static unsigned char *_m3d_getidx(unsigned char *data, char type, M3D_IN { switch(type) { case 1: *idx = data[0] > 253 ? (int8_t)data[0] : data[0]; data++; break; - case 2: *idx = *((uint16_t*)data) > 65533 ? *((int16_t*)data) : *((uint16_t*)data); data += 2; break; - case 4: *idx = *((int32_t*)data); data += 4; 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; } @@ -2751,10 +2751,10 @@ memerr: M3D_LOG("Out of memory"); data += 4; break; case 2: - model->vertex[i].x = (M3D_FLOAT)(*((int16_t*)(data+0))) / 32767; - model->vertex[i].y = (M3D_FLOAT)(*((int16_t*)(data+2))) / 32767; - model->vertex[i].z = (M3D_FLOAT)(*((int16_t*)(data+4))) / 32767; - model->vertex[i].w = (M3D_FLOAT)(*((int16_t*)(data+6))) / 32767; + 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: From c890a4d5214c89e1d96d32c7f220f6cd1bc8e0ca Mon Sep 17 00:00:00 2001 From: thewoz Date: Tue, 5 Nov 2019 16:36:03 +0100 Subject: [PATCH 51/74] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 65a54aaebf..e975976bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build .project *.kdev4* +.DS_Store # build artefacts *.o From adec426b7cc477890f9def19da9a2df6433db135 Mon Sep 17 00:00:00 2001 From: Mike Samsonov Date: Tue, 5 Nov 2019 17:11:56 +0000 Subject: [PATCH 52/74] Fix for exporting fbx bigger than 2GB --- code/Common/DefaultIOStream.cpp | 33 +++++++++++++++++++++++++++++++-- code/FBX/FBXCommon.h | 7 ++++--- code/FBX/FBXExportNode.cpp | 14 +++++++------- code/FBX/FBXExporter.cpp | 4 ++-- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/code/Common/DefaultIOStream.cpp b/code/Common/DefaultIOStream.cpp index 1c100b6189..829b44731b 100644 --- a/code/Common/DefaultIOStream.cpp +++ b/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 select_ftell(FILE* file) + { + return ::ftell(file); + } + + template + int select_fseek(FILE* file, int64_t offset, int origin) + { + return ::fseek(file, static_cast(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(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(mFile); } // ---------------------------------------------------------------------------------- diff --git a/code/FBX/FBXCommon.h b/code/FBX/FBXCommon.h index e516449130..b28601c74d 100644 --- a/code/FBX/FBXCommon.h +++ b/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 diff --git a/code/FBX/FBXExportNode.cpp b/code/FBX/FBXExportNode.cpp index 06c89cee46..9b29995ccf 100644 --- a/code/FBX/FBXExportNode.cpp +++ b/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); } diff --git a/code/FBX/FBXExporter.cpp b/code/FBX/FBXExporter.cpp index 9316dc4f02..9767f9a0a2 100644 --- a/code/FBX/FBXExporter.cpp +++ b/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. From b63285f9adada054957b0755ff496fc9f8c7b2c5 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 9 Nov 2019 11:12:47 +0100 Subject: [PATCH 53/74] closes https://github.com/assimp/assimp/issues/2684: normalize path --- assimpTargets-release.cmake.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assimpTargets-release.cmake.in b/assimpTargets-release.cmake.in index 95253a4ebd..79b643a9a3 100644 --- a/assimpTargets-release.cmake.in +++ b/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@") @@ -73,6 +75,7 @@ else() 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 ) From 58729b1b3d781f591655194b3b1b4665d09ae8da Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 9 Nov 2019 11:28:56 +0100 Subject: [PATCH 54/74] fix debug-target. --- assimpTargets-debug.cmake.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assimpTargets-debug.cmake.in b/assimpTargets-debug.cmake.in index 4667599294..e4ccbfba91 100644 --- a/assimpTargets-debug.cmake.in +++ b/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@") From 031d3b648ea98bdc6b61b4925bdc1137ff5fcb95 Mon Sep 17 00:00:00 2001 From: Justin Carpentier Date: Sun, 10 Nov 2019 08:23:17 +0100 Subject: [PATCH 55/74] defs: use noexcept only for C++11 and more --- include/assimp/defs.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/assimp/defs.h b/include/assimp/defs.h index 6f2f8ae88b..d8fc981799 100644 --- a/include/assimp/defs.h +++ b/include/assimp/defs.h @@ -306,7 +306,11 @@ static const ai_real ai_epsilon = (ai_real) 0.00001; #define AI_MAX_ALLOC(type) ((256U * 1024 * 1024) / sizeof(type)) #ifndef _MSC_VER -# define AI_NO_EXCEPT noexcept +# if __cplusplus >= 201103L // C++11 +# define AI_NO_EXCEPT noexcept +# else +# define AI_NO_EXCEPT +# endif #else # if (_MSC_VER >= 1915 ) # define AI_NO_EXCEPT noexcept From aa25c815bddd168ec6d1ac0d353c061428d28e27 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sun, 10 Nov 2019 09:47:50 +0100 Subject: [PATCH 56/74] closes https://github.com/assimp/assimp/issues/1320: make sure build works with all exporter disabled. --- code/Common/Exporter.cpp | 127 ++++++++++++++++++------------------ include/assimp/Exporter.hpp | 2 - 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/code/Common/Exporter.cpp b/code/Common/Exporter.cpp index 4ce1a2bd80..8a95ceae52 100644 --- a/code/Common/Exporter.cpp +++ b/code/Common/Exporter.cpp @@ -106,97 +106,88 @@ void ExportSceneM3D(const char*, IOSystem*, const aiScene*, const ExportProperti 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 &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 - Exporter::ExportFormatEntry( "m3d", "Model 3D (binary)", "m3d", &ExportSceneM3D, 0 ), - Exporter::ExportFormatEntry( "a3d", "Model 3D (ascii)", "m3d", &ExportSceneA3D, 0 ), + 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: @@ -212,10 +203,7 @@ class ExporterPimpl { 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() { @@ -259,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; } // ------------------------------------------------------------------------------------------------ @@ -302,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; @@ -326,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 @@ -473,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; @@ -486,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; @@ -517,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; } @@ -529,7 +529,8 @@ aiReturn Exporter::RegisterExporter(const ExportFormatEntry& desc) { // ------------------------------------------------------------------------------------------------ void Exporter::UnregisterExporter(const char* id) { - for(std::vector::iterator it = pimpl->mExporters.begin(); + ai_assert(nullptr != pimpl); + for (std::vector::iterator it = pimpl->mExporters.begin(); it != pimpl->mExporters.end(); ++it) { if (!strcmp((*it).mDescription.id,id)) { pimpl->mExporters.erase(it); diff --git a/include/assimp/Exporter.hpp b/include/assimp/Exporter.hpp index 2612e1f9d2..20e7c6c6fe 100644 --- a/include/assimp/Exporter.hpp +++ b/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, From 04db5cd5ea8f2c2095df06e11b5899796f225da6 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Thu, 14 Nov 2019 21:11:53 +0100 Subject: [PATCH 57/74] closes https://github.com/assimp/assimp/issues/2119: initial version. --- code/glTF/glTFAsset.inl | 3 --- code/glTF/glTFCommon.h | 17 +++++++---------- code/glTF2/glTF2Asset.h | 6 +++++- code/glTF2/glTF2Asset.inl | 23 ++++++++++++++++++----- code/glTF2/glTF2Exporter.h | 1 + 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/code/glTF/glTFAsset.inl b/code/glTF/glTFAsset.inl index f31781a3fc..25cf1873cc 100644 --- a/code/glTF/glTFAsset.inl +++ b/code/glTF/glTFAsset.inl @@ -1427,9 +1427,6 @@ inline void Asset::ReadExtensionsUsed(Document& doc) } } - #define CHECK_EXT(EXT) \ - if (exts.find(#EXT) != exts.end()) extensionsUsed.EXT = true; - CHECK_EXT(KHR_binary_glTF); CHECK_EXT(KHR_materials_common); diff --git a/code/glTF/glTFCommon.h b/code/glTF/glTFCommon.h index d9edee75ec..b2e28d5805 100644 --- a/code/glTF/glTFCommon.h +++ b/code/glTF/glTFCommon.h @@ -188,7 +188,7 @@ namespace glTFCommon { size_t DecodeBase64(const char* in, size_t inLength, uint8_t*& out); inline - size_t DecodeBase64(const char* in, uint8_t*& out) { + size_t DecodeBase64(const char* in, uint8_t*& out) { return DecodeBase64(in, strlen(in), out); } @@ -221,25 +221,22 @@ namespace glTFCommon { }; inline - char EncodeCharBase64(uint8_t b) { + char EncodeCharBase64(uint8_t b) { return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="[size_t(b)]; } inline - uint8_t DecodeCharBase64(char c) { + uint8_t DecodeCharBase64(char c) { return DATA::tableDecodeBase64[size_t(c)]; // TODO faster with lookup table or ifs? - /*if (c >= 'A' && c <= 'Z') return c - 'A'; - if (c >= 'a' && c <= 'z') return c - 'a' + 26; - if (c >= '0' && c <= '9') return c - '0' + 52; - if (c == '+') return 62; - if (c == '/') return 63; - return 64; // '-' */ } size_t DecodeBase64(const char* in, size_t inLength, uint8_t*& out); void EncodeBase64(const uint8_t* in, size_t inLength, std::string& out); - } + } // namespace Util + +#define CHECK_EXT(EXT) \ + if (exts.find(#EXT) != exts.end()) extensionsUsed.EXT = true; } diff --git a/code/glTF2/glTF2Asset.h b/code/glTF2/glTF2Asset.h index 15c4c44fa0..60a393170c 100644 --- a/code/glTF2/glTF2Asset.h +++ b/code/glTF2/glTF2Asset.h @@ -685,6 +685,10 @@ namespace glTF2 Ref texture; unsigned int index; unsigned int texCoord = 0; + + float offset[2]; + float rotation; + float scale[2]; }; struct NormalTextureInfo : TextureInfo @@ -1024,7 +1028,7 @@ namespace glTF2 bool KHR_materials_pbrSpecularGlossiness; bool KHR_materials_unlit; bool KHR_lights_punctual; - + bool KHR_texture_transform; } extensionsUsed; AssetMetadata asset; diff --git a/code/glTF2/glTF2Asset.inl b/code/glTF2/glTF2Asset.inl index 6b47b16070..310fcde06f 100644 --- a/code/glTF2/glTF2Asset.inl +++ b/code/glTF2/glTF2Asset.inl @@ -800,8 +800,20 @@ inline void Texture::Read(Value& obj, Asset& r) } namespace { - inline void SetTextureProperties(Asset& r, Value* prop, TextureInfo& out) - { + inline void SetTextureProperties(Asset& r, Value* prop, TextureInfo& out) { + if (r.extensionsUsed.KHR_texture_transform) { + if (Value *extensions = FindObject(*prop, "extensions")) { + if (Value *pKHR_texture_transform = FindObject(*extensions, "KHR_texture_transform")) { + if (Value *array = FindArray(*pKHR_texture_transform, "offset")) { + out.offset[0] = (*array)[0].GetFloat(); + out.offset[1] = (*array)[1].GetFloat(); + } + ReadMember(*pKHR_texture_transform, "rotation", out.rotation); + ReadMember(*pKHR_texture_transform, "scale", *out.scale); + } + } + } + if (Value* index = FindUInt(*prop, "index")) { out.texture = r.textures.Retrieve(index->GetUint()); } @@ -877,6 +889,9 @@ inline void Material::Read(Value& material, Asset& r) } } + if (r.extensionsUsed.KHR_texture_transform) { + } + unlit = nullptr != FindObject(*extensions, "KHR_materials_unlit"); } } @@ -1463,12 +1478,10 @@ inline void Asset::ReadExtensionsUsed(Document& doc) } } - #define CHECK_EXT(EXT) \ - if (exts.find(#EXT) != exts.end()) extensionsUsed.EXT = true; - CHECK_EXT(KHR_materials_pbrSpecularGlossiness); CHECK_EXT(KHR_materials_unlit); CHECK_EXT(KHR_lights_punctual); + CHECK_EXT(KHR_texture_transform); #undef CHECK_EXT } diff --git a/code/glTF2/glTF2Exporter.h b/code/glTF2/glTF2Exporter.h index 2dc0837093..b527c4bc99 100644 --- a/code/glTF2/glTF2Exporter.h +++ b/code/glTF2/glTF2Exporter.h @@ -74,6 +74,7 @@ namespace glTF2 struct Texture; // Vec/matrix types, as raw float arrays + typedef float (vec2)[2]; typedef float (vec3)[3]; typedef float (vec4)[4]; } From 74080a083ad4764485bc633156dd37d0416281a0 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Thu, 14 Nov 2019 21:15:30 +0100 Subject: [PATCH 58/74] add texture-transfrm unittest. --- test/models/glTF2/textureTransform/Arrow.png | Bin 0 -> 867 bytes .../models/glTF2/textureTransform/Correct.png | Bin 0 -> 2457 bytes test/models/glTF2/textureTransform/Error.png | Bin 0 -> 2273 bytes .../models/glTF2/textureTransform/License.txt | 0 .../glTF2/textureTransform/NotSupported.png | Bin 0 -> 3291 bytes .../textureTransform/TextureTransformTest.bin | Bin 0 -> 136 bytes .../TextureTransformTest.gltf | 540 ++++++++++++++++++ test/models/glTF2/textureTransform/UV.png | Bin 0 -> 12345 bytes test/unit/utglTF2ImportExport.cpp | 7 + 9 files changed, 547 insertions(+) create mode 100644 test/models/glTF2/textureTransform/Arrow.png create mode 100644 test/models/glTF2/textureTransform/Correct.png create mode 100644 test/models/glTF2/textureTransform/Error.png create mode 100644 test/models/glTF2/textureTransform/License.txt create mode 100644 test/models/glTF2/textureTransform/NotSupported.png create mode 100644 test/models/glTF2/textureTransform/TextureTransformTest.bin create mode 100644 test/models/glTF2/textureTransform/TextureTransformTest.gltf create mode 100644 test/models/glTF2/textureTransform/UV.png diff --git a/test/models/glTF2/textureTransform/Arrow.png b/test/models/glTF2/textureTransform/Arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..fe3405b3f6b14ffdc920bc0f4f30560cabcbab39 GIT binary patch literal 867 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVAk?aJ56J?j$lS^=gjZ_g@TqW zaQReo#@OwU{5ebgCEMCMJT~8a;-RrdgX`zf#0#yxL)Q;a@-sk40k#~WxL&^iVd z1_nkNG8?wL7TC>aTp-zK$i^^BP{s4n_S?B442yQ9`bRI_!+Sh;ipY*&d}^#?Whhux|hU{RJu8Z@=7~*iy!F?biAEizS#1`x>l{J5ExW zf1b0J$K|`;f;^Vvk3ZfncB(TMcx&kp`}o1@Z}Mj)E^J^9*(%J_emH+WcO{R-^BU*N zXAL*$2n$ZUne*+F--itL9s8wUl2_L` zFCVYk6mXw+1zP}+KHgBp5UR*g%EPe0iAAxG@hiirO;c>ntNI3Oo({Szk&zLdlfCv$ znf1;0-;>thB?JXa{Z~BZ-(r;Vc*+0fuRQIl+8DL{$1T4wKipM-07^X zlU{#~y8SmdKI;Vst03QEK`~Uy| literal 0 HcmV?d00001 diff --git a/test/models/glTF2/textureTransform/Correct.png b/test/models/glTF2/textureTransform/Correct.png new file mode 100644 index 0000000000000000000000000000000000000000..e332824fbc00f15ed290ac891bf18dedf74415e5 GIT binary patch literal 2457 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 L1NF*>w&DK)(UL^| literal 0 HcmV?d00001 diff --git a/test/models/glTF2/textureTransform/License.txt b/test/models/glTF2/textureTransform/License.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/models/glTF2/textureTransform/NotSupported.png b/test/models/glTF2/textureTransform/NotSupported.png new file mode 100644 index 0000000000000000000000000000000000000000..721e7fd765079c7f2111254145c49e1dbec124d3 GIT binary patch literal 3291 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 literal 0 HcmV?d00001 diff --git a/test/models/glTF2/textureTransform/TextureTransformTest.bin b/test/models/glTF2/textureTransform/TextureTransformTest.bin new file mode 100644 index 0000000000000000000000000000000000000000..6765a13008a0b763d65a217da7fd651a66c2f056 GIT binary patch literal 136 zcmY+6fe8R048x)x=4*cDYc4uL3ngVM1t!qLO1{~jk~i-IjO@ub=lkEu?%A$UDPgHv Fcmbi}2l)U1 literal 0 HcmV?d00001 diff --git a/test/models/glTF2/textureTransform/TextureTransformTest.gltf b/test/models/glTF2/textureTransform/TextureTransformTest.gltf new file mode 100644 index 0000000000..6dde51d9a2 --- /dev/null +++ b/test/models/glTF2/textureTransform/TextureTransformTest.gltf @@ -0,0 +1,540 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 4, + "type": "VEC3", + "max": [ + 0.5, + 0.5, + 0.0 + ], + "min": [ + -0.5, + -0.5, + 0.0 + ], + "name": "Positions" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 4, + "type": "VEC2", + "name": "UV0" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 4, + "type": "VEC2", + "name": "UV1" + }, + { + "bufferView": 3, + "componentType": 5125, + "count": 6, + "type": "SCALAR", + "name": "Indices" + } + ], + "asset": { + "version": "2.0" + }, + "buffers": [ + { + "uri": "TextureTransformTest.bin", + "byteLength": 136 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 48, + "name": "Positions" + }, + { + "buffer": 0, + "byteOffset": 48, + "byteLength": 32, + "name": "UV0" + }, + { + "buffer": 0, + "byteOffset": 80, + "byteLength": 32, + "name": "UV1" + }, + { + "buffer": 0, + "byteOffset": 112, + "byteLength": 24, + "name": "Indices" + } + ], + "extensionsUsed": [ + "KHR_texture_transform" + ], + "images": [ + { + "uri": "UV.png" + }, + { + "uri": "Arrow.png" + }, + { + "uri": "Correct.png" + }, + { + "uri": "NotSupported.png" + }, + { + "uri": "Error.png" + } + ], + "materials": [ + { + "name": "Offset U", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": { + "offset": [ + 0.5, + 0.0 + ] + } + } + }, + "metallicFactor": 0 + } + }, + { + "name": "Offset V", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": { + "offset": [ + 0.0, + 0.5 + ] + } + } + }, + "metallicFactor": 0 + } + }, + { + "name": "Offset UV", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": { + "offset": [ + 0.5, + 0.5 + ] + } + } + }, + "metallicFactor": 0 + } + }, + { + "name": "Rotation", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "rotation": 0.39269908169872415480783042290994 + } + } + }, + "metallicFactor": 0 + } + }, + { + "name": "Scale", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "scale": [ + 1.5, + 1.5 + ] + } + } + }, + "metallicFactor": 0 + } + }, + { + "name": "All", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "offset": [ + -0.2, + -0.1 + ], + "rotation": 0.3, + "scale": [ + 1.5, + 1.5 + ] + } + } + }, + "metallicFactor": 0 + } + }, + { + "name": "Correct", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 2 + }, + "metallicFactor": 0 + } + }, + { + "name": "NotSupported", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 3 + }, + "metallicFactor": 0 + } + }, + { + "name": "Error", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 4 + }, + "metallicFactor": 0 + } + } + ], + "meshes": [ + { + "name": "Offset U", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 2 + }, + "indices": 3, + "material": 0 + } + ] + }, + { + "name": "Offset V", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 2 + }, + "indices": 3, + "material": 1 + } + ] + }, + { + "name": "Offset UV", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 2 + }, + "indices": 3, + "material": 2 + } + ] + }, + { + "name": "Rotation", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1 + }, + "indices": 3, + "material": 3 + } + ] + }, + { + "name": "Scale", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1 + }, + "indices": 3, + "material": 4 + } + ] + }, + { + "name": "All", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1 + }, + "indices": 3, + "material": 5 + } + ] + }, + { + "name": "Correct Marker", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1 + }, + "indices": 3, + "material": 6 + } + ] + }, + { + "name": "Not Supported Marker", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1 + }, + "indices": 3, + "material": 7 + } + ] + }, + { + "name": "Error Marker", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1 + }, + "indices": 3, + "material": 8 + } + ] + } + ], + "nodes": [ + { + "name": "Offset U", + "mesh": 0, + "translation": [ + -1.1, + 0.55, + 0 + ] + }, + { + "name": "Offset V", + "mesh": 1, + "translation": [ + 0, + 0.55, + 0 + ] + }, + { + "name": "Offset UV", + "mesh": 2, + "translation": [ + 1.1, + 0.55, + 0 + ] + }, + { + "name": "Rotation", + "mesh": 3, + "translation": [ + -1.1, + -0.55, + 0 + ], + "children": [ + 4, + 5, + 6 + ] + }, + { + "name": "Rotation - Correct", + "mesh": 6, + "translation": [ + -0.07904822439840125109869401756656, + -0.51626748576241543174100150833647, + 0.01 + ], + "scale": [ + 0.15, + 0.15, + 0.15 + ] + }, + { + "name": "Rotation - Not Supported", + "mesh": 7, + "translation": [ + 0.27781745930520227684092879831533, + -0.27781745930520227684092879831533, + 0.01 + ], + "scale": [ + 0.15, + 0.15, + 0.15 + ] + }, + { + "name": "Rotation - Error", + "mesh": 8, + "translation": [ + 0.51626748576241543174100150833647, + 0.07904822439840125109869401756656, + 0.01 + ], + "scale": [ + 0.15, + 0.15, + 0.15 + ] + }, + { + "name": "Scale", + "mesh": 4, + "translation": [ + 0, + -0.55, + 0 + ], + "children": [ + 8, + 9 + ] + }, + { + "name": "Scale - Correct", + "mesh": 6, + "translation": [ + 0.01854497287013485122728586554355, + -0.01854497287013485122728586554355, + 0.01 + ], + "scale": [ + 0.1, + 0.1, + 0.1 + ] + }, + { + "name": "Scale - Not Supported", + "mesh": 7, + "translation": [ + 0.27781745930520227684092879831533, + -0.27781745930520227684092879831533, + 0.01 + ], + "scale": [ + 0.15, + 0.15, + 0.15 + ] + }, + { + "name": "All", + "mesh": 5, + "translation": [ + 1.1, + -0.55, + 0 + ], + "children": [ + 11 + ] + }, + { + "name": "All - Correct", + "mesh": 6, + "translation": [ + -0.07, + -0.25, + 0.01 + ], + "scale": [ + 0.1, + 0.1, + 0.1 + ] + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0, + 1, + 2, + 3, + 7, + 10 + ] + } + ], + "textures": [ + { + "source": 0, + "sampler": 0 + }, + { + "source": 1, + "sampler": 0 + }, + { + "source": 2 + }, + { + "source": 3 + }, + { + "source": 4 + } + ], + "samplers": [ + { + "wrapS": 33071, + "wrapT": 33071, + "magFilter": 9729, + "minFilter": 9729 + } + ] +} \ No newline at end of file diff --git a/test/models/glTF2/textureTransform/UV.png b/test/models/glTF2/textureTransform/UV.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a6d4d370c48c7e42ea7337dc5acb4376e5c92f GIT binary patch literal 12345 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[7]->mNumFaces, 17u); } +TEST_F( utglTF2ImportExport, texture_transform_test ) { + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/glTF2/textureTransform/TextureTransformTest.gltf", + aiProcess_ValidateDataStructure); + EXPECT_NE(nullptr, scene); +} + #ifndef ASSIMP_BUILD_NO_EXPORT TEST_F( utglTF2ImportExport, exportglTF2FromFileTest ) { EXPECT_TRUE( exporterTest() ); From 17946e26efd2b408f0b5e9bd08615a3a992611ca Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 15 Nov 2019 18:35:33 +0100 Subject: [PATCH 59/74] add missing setup of texture transform in aiMaterial. --- code/glTF2/glTF2Asset.inl | 12 ++++++++++-- code/glTF2/glTF2Importer.cpp | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/code/glTF2/glTF2Asset.inl b/code/glTF2/glTF2Asset.inl index 310fcde06f..35f285d85b 100644 --- a/code/glTF2/glTF2Asset.inl +++ b/code/glTF2/glTF2Asset.inl @@ -802,14 +802,22 @@ inline void Texture::Read(Value& obj, Asset& r) namespace { inline void SetTextureProperties(Asset& r, Value* prop, TextureInfo& out) { if (r.extensionsUsed.KHR_texture_transform) { - if (Value *extensions = FindObject(*prop, "extensions")) { + out.scale[0] = 1; + out.scale[1] = 1; + out.offset[0] = 0; + out.offset[1] = 0; + out.rotation = 0; + if (Value *extensions = FindObject(*prop, "extensions")) { if (Value *pKHR_texture_transform = FindObject(*extensions, "KHR_texture_transform")) { if (Value *array = FindArray(*pKHR_texture_transform, "offset")) { out.offset[0] = (*array)[0].GetFloat(); out.offset[1] = (*array)[1].GetFloat(); } ReadMember(*pKHR_texture_transform, "rotation", out.rotation); - ReadMember(*pKHR_texture_transform, "scale", *out.scale); + if (Value *array = FindArray(*pKHR_texture_transform, "scale")) { + out.scale[0] = (*array)[0].GetFloat(); + out.scale[1] = (*array)[1].GetFloat(); + } } } } diff --git a/code/glTF2/glTF2Importer.cpp b/code/glTF2/glTF2Importer.cpp index b3141fd960..8d88260dbb 100644 --- a/code/glTF2/glTF2Importer.cpp +++ b/code/glTF2/glTF2Importer.cpp @@ -206,6 +206,14 @@ inline void SetMaterialTextureProperty(std::vector& embeddedTexIdxs, Asset& uri.length = 1 + ASSIMP_itoa10(uri.data + 1, MAXLEN - 1, texIdx); } + aiUVTransform transform; + transform.mTranslation.x = prop.offset[0]; + transform.mTranslation.y = prop.offset[0]; + transform.mRotation = prop.rotation; + transform.mScaling.x = prop.scale[0]; + transform.mScaling.y = prop.scale[1]; + mat->AddProperty(&transform, 1, _AI_MATKEY_UVTRANSFORM_BASE, texType, texSlot); + mat->AddProperty(&uri, AI_MATKEY_TEXTURE(texType, texSlot)); mat->AddProperty(&prop.texCoord, 1, _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, texType, texSlot); From a8182d86cb4c6af514d3d286bf96907a349c3e60 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 15 Nov 2019 19:38:37 +0100 Subject: [PATCH 60/74] fix initialization + some vs2019 compiler warnings. --- code/glTF2/glTF2Asset.inl | 23 ++++++++++++++--------- code/glTF2/glTF2Importer.cpp | 6 +++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/code/glTF2/glTF2Asset.inl b/code/glTF2/glTF2Asset.inl index 35f285d85b..5bc2413424 100644 --- a/code/glTF2/glTF2Asset.inl +++ b/code/glTF2/glTF2Asset.inl @@ -802,22 +802,27 @@ inline void Texture::Read(Value& obj, Asset& r) namespace { inline void SetTextureProperties(Asset& r, Value* prop, TextureInfo& out) { if (r.extensionsUsed.KHR_texture_transform) { - out.scale[0] = 1; - out.scale[1] = 1; - out.offset[0] = 0; - out.offset[1] = 0; - out.rotation = 0; if (Value *extensions = FindObject(*prop, "extensions")) { if (Value *pKHR_texture_transform = FindObject(*extensions, "KHR_texture_transform")) { if (Value *array = FindArray(*pKHR_texture_transform, "offset")) { out.offset[0] = (*array)[0].GetFloat(); out.offset[1] = (*array)[1].GetFloat(); - } - ReadMember(*pKHR_texture_transform, "rotation", out.rotation); - if (Value *array = FindArray(*pKHR_texture_transform, "scale")) { + } else { + out.offset[0] = 0; + out.offset[1] = 0; + } + + if (!ReadMember(*pKHR_texture_transform, "rotation", out.rotation)) { + out.rotation = 0; + } + + if (Value *array = FindArray(*pKHR_texture_transform, "scale")) { out.scale[0] = (*array)[0].GetFloat(); out.scale[1] = (*array)[1].GetFloat(); - } + } else { + out.scale[0] = 1; + out.scale[1] = 1; + } } } } diff --git a/code/glTF2/glTF2Importer.cpp b/code/glTF2/glTF2Importer.cpp index 8d88260dbb..91b758c482 100644 --- a/code/glTF2/glTF2Importer.cpp +++ b/code/glTF2/glTF2Importer.cpp @@ -622,7 +622,7 @@ void glTF2Importer::ImportMeshes(glTF2::Asset& r) nFaces = count / 2; if (nFaces * 2 != count) { ASSIMP_LOG_WARN("The number of vertices was not compatible with the LINES mode. Some vertices were dropped."); - count = nFaces * 2; + count = (unsigned int) nFaces * 2; } faces = new aiFace[nFaces]; for (unsigned int i = 0; i < count; i += 2) { @@ -649,7 +649,7 @@ void glTF2Importer::ImportMeshes(glTF2::Asset& r) nFaces = count / 3; if (nFaces * 3 != count) { ASSIMP_LOG_WARN("The number of vertices was not compatible with the TRIANGLES mode. Some vertices were dropped."); - count = nFaces * 3; + count = (unsigned int) nFaces * 3; } faces = new aiFace[nFaces]; for (unsigned int i = 0; i < count; i += 3) { @@ -1134,7 +1134,7 @@ aiMeshMorphAnim* CreateMeshMorphAnim(glTF2::Asset& r, Node& node, AnimationSampl samplers.weight->output->ExtractData(values); anim->mNumKeys = static_cast(samplers.weight->input->count); - const unsigned int numMorphs = samplers.weight->output->count / anim->mNumKeys; + const unsigned int numMorphs = (unsigned int)samplers.weight->output->count / anim->mNumKeys; anim->mKeys = new aiMeshMorphKey[anim->mNumKeys]; unsigned int k = 0u; From 3a8cb442f39efee20425dfe6d5ae65bfc5ba6e48 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 15 Nov 2019 20:52:13 +0100 Subject: [PATCH 61/74] closes https://github.com/assimp/assimp/issues/2681: add dx11-sample to cmake. --- CMakeLists.txt | 2 +- .../SimpleTexturedDirectx11/CMakeLists.txt | 48 ++++++ .../SimpleTexturedDirectx11.sln | 28 ---- .../SimpleTexturedDirectx11/ModelLoader.cpp | 2 + .../SimpleTexturedDirectx11.vcxproj | 146 ------------------ .../SimpleTexturedDirectx11.vcxproj.filters | 50 ------ 6 files changed, 51 insertions(+), 225 deletions(-) create mode 100644 samples/SimpleTexturedDirectx11/CMakeLists.txt delete mode 100644 samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11.sln delete mode 100644 samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj delete mode 100644 samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj.filters diff --git a/CMakeLists.txt b/CMakeLists.txt index 693d6f16af..7a94ddf68d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ OPTION( ASSIMP_BUILD_ASSIMP_TOOLS ) OPTION ( ASSIMP_BUILD_SAMPLES "If the official samples are built as well (needs Glut)." - OFF + ON ) OPTION ( ASSIMP_BUILD_TESTS "If the test suite for Assimp is built in addition to the library." diff --git a/samples/SimpleTexturedDirectx11/CMakeLists.txt b/samples/SimpleTexturedDirectx11/CMakeLists.txt new file mode 100644 index 0000000000..a463ee282d --- /dev/null +++ b/samples/SimpleTexturedDirectx11/CMakeLists.txt @@ -0,0 +1,48 @@ +FIND_PACKAGE(DirectX) + +IF ( MSVC ) + SET(M_LIB) +ELSE ( MSVC ) + find_library(M_LIB m) +ENDIF ( MSVC ) + +if ( MSVC ) + ADD_DEFINITIONS( -D_SCL_SECURE_NO_WARNINGS ) + ADD_DEFINITIONS( -D_CRT_SECURE_NO_WARNINGS ) + REMOVE_DEFINITIONS( -DUNICODE -D_UNICODE ) +endif ( MSVC ) + +INCLUDE_DIRECTORIES( + ${Assimp_SOURCE_DIR}/include + ${Assimp_SOURCE_DIR}/code + ${OPENGL_INCLUDE_DIR} + ${GLUT_INCLUDE_DIR} + ${Assimp_SOURCE_DIR}/samples/freeglut/include +) + +LINK_DIRECTORIES( + ${Assimp_BINARY_DIR} + ${Assimp_BINARY_DIR}/lib +) + +ADD_EXECUTABLE( assimp_simpletextureddirectx11 WIN32 + SimpleTexturedDirectx11/Mesh.h + SimpleTexturedDirectx11/ModelLoader.cpp + SimpleTexturedDirectx11/ModelLoader.h + #SimpleTexturedDirectx11/PixelShader.hlsl + SimpleTexturedDirectx11/TextureLoader.cpp + SimpleTexturedDirectx11/TextureLoader.h + #SimpleTexturedDirectx11/VertexShader.hlsl + SimpleTexturedDirectx11/main.cpp +) + +SET_PROPERTY(TARGET assimp_simpletextureddirectx11 PROPERTY DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) + +TARGET_LINK_LIBRARIES( assimp_simpletextureddirectx11 assimp ${DirectX_LIBRARY} comctl32.lib winmm.lib ) +SET_TARGET_PROPERTIES( assimp_simpletextureddirectx11 PROPERTIES + OUTPUT_NAME assimp_simpletextureddirectx11 +) + +INSTALL( TARGETS assimp_simpletextureddirectx11 + DESTINATION "${ASSIMP_BIN_INSTALL_DIR}" COMPONENT assimp-dev +) diff --git a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11.sln b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11.sln deleted file mode 100644 index 381ac8f946..0000000000 --- a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SimpleTexturedDirectx11", "SimpleTexturedDirectx11\SimpleTexturedDirectx11.vcxproj", "{E3B160B5-E71F-4F3F-9310-B8F156F736D8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Debug|x64.ActiveCfg = Debug|x64 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Debug|x64.Build.0 = Debug|x64 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Debug|x86.ActiveCfg = Debug|Win32 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Debug|x86.Build.0 = Debug|Win32 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Release|x64.ActiveCfg = Release|x64 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Release|x64.Build.0 = Release|x64 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Release|x86.ActiveCfg = Release|Win32 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/ModelLoader.cpp b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/ModelLoader.cpp index a2d3faeb33..10ba07a98c 100644 --- a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/ModelLoader.cpp +++ b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/ModelLoader.cpp @@ -180,6 +180,8 @@ string ModelLoader::determineTextureType(const aiScene * scene, aiMaterial * mat { return "textures are on disk"; } + + return "."; } int ModelLoader::getTextureIndex(aiString * str) diff --git a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj deleted file mode 100644 index 6584b7d7ca..0000000000 --- a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj +++ /dev/null @@ -1,146 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 15.0 - {E3B160B5-E71F-4F3F-9310-B8F156F736D8} - SimpleTexturedDirectx11 - 10.0.14393.0 - - - - Application - true - v141 - MultiByte - - - Application - false - v141 - true - MultiByte - - - Application - true - v141 - MultiByte - - - Application - false - v141 - true - MultiByte - - - - - - - - - - - - - - - - - - - - - $(IncludePath);E:\OpenGL VS Files\include - $(LibraryPath);E:\OpenGL VS Files\lib - - - - Level3 - Disabled - true - - - assimp-vc140-mt.lib;%(AdditionalDependencies) - - - - - Level3 - Disabled - true - - - - - Level3 - MaxSpeed - true - true - true - - - true - true - - - - - Level3 - MaxSpeed - true - true - true - - - true - true - - - - - - - - - - Pixel - Pixel - Pixel - Pixel - - - Vertex - Vertex - Vertex - Vertex - - - - - - - - - - - \ No newline at end of file diff --git a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj.filters b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj.filters deleted file mode 100644 index 3568b73c50..0000000000 --- a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/SimpleTexturedDirectx11.vcxproj.filters +++ /dev/null @@ -1,50 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - {b6a86d3e-70a5-4d1e-ba05-c20902300206} - - - - - Source Files - - - Source Files - - - Source Files - - - - - Shaders - - - Shaders - - - - - Header Files - - - Header Files - - - Header Files - - - \ No newline at end of file From 4ad2368116f984e2ba36f32083f649dbba175fcc Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 15 Nov 2019 21:46:09 +0100 Subject: [PATCH 62/74] disable samples per default. --- CMakeLists.txt | 2 +- samples/SimpleTexturedDirectx11/CMakeLists.txt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a94ddf68d..693d6f16af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ OPTION( ASSIMP_BUILD_ASSIMP_TOOLS ) OPTION ( ASSIMP_BUILD_SAMPLES "If the official samples are built as well (needs Glut)." - ON + OFF ) OPTION ( ASSIMP_BUILD_TESTS "If the test suite for Assimp is built in addition to the library." diff --git a/samples/SimpleTexturedDirectx11/CMakeLists.txt b/samples/SimpleTexturedDirectx11/CMakeLists.txt index a463ee282d..373b5a9dbd 100644 --- a/samples/SimpleTexturedDirectx11/CMakeLists.txt +++ b/samples/SimpleTexturedDirectx11/CMakeLists.txt @@ -2,8 +2,6 @@ FIND_PACKAGE(DirectX) IF ( MSVC ) SET(M_LIB) -ELSE ( MSVC ) - find_library(M_LIB m) ENDIF ( MSVC ) if ( MSVC ) From 2eed8b18208d93000ae5b23339f7ecb4672fda8e Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 16 Nov 2019 08:08:57 +0100 Subject: [PATCH 63/74] TextureTransform: set material transform only when the extension is provided. --- code/glTF2/glTF2Asset.h | 11 +- code/glTF2/glTF2Asset.inl | 23 +- code/glTF2/glTF2Importer.cpp | 2254 +++++++++++++++++----------------- 3 files changed, 1125 insertions(+), 1163 deletions(-) diff --git a/code/glTF2/glTF2Asset.h b/code/glTF2/glTF2Asset.h index 60a393170c..831a763cd5 100644 --- a/code/glTF2/glTF2Asset.h +++ b/code/glTF2/glTF2Asset.h @@ -685,10 +685,13 @@ namespace glTF2 Ref texture; unsigned int index; unsigned int texCoord = 0; - - float offset[2]; - float rotation; - float scale[2]; + + bool textureTransformSupported = false; + struct TextureTransformExt { + float offset[2]; + float rotation; + float scale[2]; + } TextureTransformExt_t; }; struct NormalTextureInfo : TextureInfo diff --git a/code/glTF2/glTF2Asset.inl b/code/glTF2/glTF2Asset.inl index 5bc2413424..4259022e9b 100644 --- a/code/glTF2/glTF2Asset.inl +++ b/code/glTF2/glTF2Asset.inl @@ -803,25 +803,26 @@ namespace { inline void SetTextureProperties(Asset& r, Value* prop, TextureInfo& out) { if (r.extensionsUsed.KHR_texture_transform) { if (Value *extensions = FindObject(*prop, "extensions")) { - if (Value *pKHR_texture_transform = FindObject(*extensions, "KHR_texture_transform")) { + out.textureTransformSupported = true; + if (Value *pKHR_texture_transform = FindObject(*extensions, "KHR_texture_transform")) { if (Value *array = FindArray(*pKHR_texture_transform, "offset")) { - out.offset[0] = (*array)[0].GetFloat(); - out.offset[1] = (*array)[1].GetFloat(); + out.TextureTransformExt_t.offset[0] = (*array)[0].GetFloat(); + out.TextureTransformExt_t.offset[1] = (*array)[1].GetFloat(); } else { - out.offset[0] = 0; - out.offset[1] = 0; + out.TextureTransformExt_t.offset[0] = 0; + out.TextureTransformExt_t.offset[1] = 0; } - if (!ReadMember(*pKHR_texture_transform, "rotation", out.rotation)) { - out.rotation = 0; + if (!ReadMember(*pKHR_texture_transform, "rotation", out.TextureTransformExt_t.rotation)) { + out.TextureTransformExt_t.rotation = 0; } if (Value *array = FindArray(*pKHR_texture_transform, "scale")) { - out.scale[0] = (*array)[0].GetFloat(); - out.scale[1] = (*array)[1].GetFloat(); + out.TextureTransformExt_t.scale[0] = (*array)[0].GetFloat(); + out.TextureTransformExt_t.scale[1] = (*array)[1].GetFloat(); } else { - out.scale[0] = 1; - out.scale[1] = 1; + out.TextureTransformExt_t.scale[0] = 1; + out.TextureTransformExt_t.scale[1] = 1; } } } diff --git a/code/glTF2/glTF2Importer.cpp b/code/glTF2/glTF2Importer.cpp index 91b758c482..3dffa4b278 100644 --- a/code/glTF2/glTF2Importer.cpp +++ b/code/glTF2/glTF2Importer.cpp @@ -43,18 +43,18 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef ASSIMP_BUILD_NO_GLTF_IMPORTER #include "glTF2/glTF2Importer.h" +#include "PostProcessing/MakeVerboseFormat.h" #include "glTF2/glTF2Asset.h" #include "glTF2/glTF2AssetWriter.h" -#include "PostProcessing/MakeVerboseFormat.h" +#include #include #include -#include -#include #include -#include #include -#include +#include +#include +#include #include #include @@ -67,11 +67,11 @@ using namespace glTF2; using namespace glTFCommon; namespace { - // generate bi-tangents from normals and tangents according to spec - struct Tangent { - aiVector3D xyz; - ai_real w; - }; +// generate bi-tangents from normals and tangents according to spec +struct Tangent { + aiVector3D xyz; + ai_real w; +}; } // namespace // @@ -79,66 +79,63 @@ namespace { // static const aiImporterDesc desc = { - "glTF2 Importer", - "", - "", - "", - aiImporterFlags_SupportTextFlavour | aiImporterFlags_SupportBinaryFlavour | aiImporterFlags_LimitedSupport | aiImporterFlags_Experimental, - 0, - 0, - 0, - 0, - "gltf glb" + "glTF2 Importer", + "", + "", + "", + aiImporterFlags_SupportTextFlavour | aiImporterFlags_SupportBinaryFlavour | aiImporterFlags_LimitedSupport | aiImporterFlags_Experimental, + 0, + 0, + 0, + 0, + "gltf glb" }; -glTF2Importer::glTF2Importer() -: BaseImporter() -, meshOffsets() -, embeddedTexIdxs() -, mScene( NULL ) { - // empty +glTF2Importer::glTF2Importer() : + BaseImporter(), + meshOffsets(), + embeddedTexIdxs(), + mScene(NULL) { + // empty } glTF2Importer::~glTF2Importer() { - // empty + // empty } -const aiImporterDesc* glTF2Importer::GetInfo() const -{ - return &desc; +const aiImporterDesc *glTF2Importer::GetInfo() const { + return &desc; } -bool glTF2Importer::CanRead(const std::string& pFile, IOSystem* pIOHandler, bool /* checkSig */) const -{ - const std::string &extension = GetExtension(pFile); +bool glTF2Importer::CanRead(const std::string &pFile, IOSystem *pIOHandler, bool /* checkSig */) const { + const std::string &extension = GetExtension(pFile); - if (extension != "gltf" && extension != "glb") - return false; + if (extension != "gltf" && extension != "glb") + return false; - if (pIOHandler) { - glTF2::Asset asset(pIOHandler); - asset.Load(pFile, extension == "glb"); - std::string version = asset.asset.version; - return !version.empty() && version[0] == '2'; - } + if (pIOHandler) { + glTF2::Asset asset(pIOHandler); + asset.Load(pFile, extension == "glb"); + std::string version = asset.asset.version; + return !version.empty() && version[0] == '2'; + } - return false; + return false; } -static aiTextureMapMode ConvertWrappingMode(SamplerWrap gltfWrapMode) -{ - switch (gltfWrapMode) { - case SamplerWrap::Mirrored_Repeat: - return aiTextureMapMode_Mirror; - - case SamplerWrap::Clamp_To_Edge: - return aiTextureMapMode_Clamp; - - case SamplerWrap::UNSET: - case SamplerWrap::Repeat: - default: - return aiTextureMapMode_Wrap; - } +static aiTextureMapMode ConvertWrappingMode(SamplerWrap gltfWrapMode) { + switch (gltfWrapMode) { + case SamplerWrap::Mirrored_Repeat: + return aiTextureMapMode_Mirror; + + case SamplerWrap::Clamp_To_Edge: + return aiTextureMapMode_Clamp; + + case SamplerWrap::UNSET: + case SamplerWrap::Repeat: + default: + return aiTextureMapMode_Wrap; + } } /*static void CopyValue(const glTF2::vec3& v, aiColor3D& out) @@ -180,1190 +177,1151 @@ static void CopyValue(const glTF2::vec4& v, aiQuaternion& out) o.a4 = v[12]; o.b4 = v[13]; o.c4 = v[14]; o.d4 = v[15]; }*/ -inline void SetMaterialColorProperty(Asset& /*r*/, vec4& prop, aiMaterial* mat, const char* pKey, unsigned int type, unsigned int idx) -{ - aiColor4D col; - CopyValue(prop, col); - mat->AddProperty(&col, 1, pKey, type, idx); +inline void SetMaterialColorProperty(Asset & /*r*/, vec4 &prop, aiMaterial *mat, const char *pKey, unsigned int type, unsigned int idx) { + aiColor4D col; + CopyValue(prop, col); + mat->AddProperty(&col, 1, pKey, type, idx); } -inline void SetMaterialColorProperty(Asset& /*r*/, vec3& prop, aiMaterial* mat, const char* pKey, unsigned int type, unsigned int idx) -{ - aiColor4D col; - glTFCommon::CopyValue(prop, col); - mat->AddProperty(&col, 1, pKey, type, idx); +inline void SetMaterialColorProperty(Asset & /*r*/, vec3 &prop, aiMaterial *mat, const char *pKey, unsigned int type, unsigned int idx) { + aiColor4D col; + glTFCommon::CopyValue(prop, col); + mat->AddProperty(&col, 1, pKey, type, idx); } -inline void SetMaterialTextureProperty(std::vector& embeddedTexIdxs, Asset& /*r*/, glTF2::TextureInfo prop, aiMaterial* mat, aiTextureType texType, unsigned int texSlot = 0) -{ - if (prop.texture && prop.texture->source) { - aiString uri(prop.texture->source->uri); - - int texIdx = embeddedTexIdxs[prop.texture->source.GetIndex()]; - if (texIdx != -1) { // embedded - // setup texture reference string (copied from ColladaLoader::FindFilenameForEffectTexture) - uri.data[0] = '*'; - uri.length = 1 + ASSIMP_itoa10(uri.data + 1, MAXLEN - 1, texIdx); - } - - aiUVTransform transform; - transform.mTranslation.x = prop.offset[0]; - transform.mTranslation.y = prop.offset[0]; - transform.mRotation = prop.rotation; - transform.mScaling.x = prop.scale[0]; - transform.mScaling.y = prop.scale[1]; - mat->AddProperty(&transform, 1, _AI_MATKEY_UVTRANSFORM_BASE, texType, texSlot); - - mat->AddProperty(&uri, AI_MATKEY_TEXTURE(texType, texSlot)); - mat->AddProperty(&prop.texCoord, 1, _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, texType, texSlot); - - if (prop.texture->sampler) { - Ref sampler = prop.texture->sampler; - - aiString name(sampler->name); - aiString id(sampler->id); - - mat->AddProperty(&name, AI_MATKEY_GLTF_MAPPINGNAME(texType, texSlot)); - mat->AddProperty(&id, AI_MATKEY_GLTF_MAPPINGID(texType, texSlot)); - - aiTextureMapMode wrapS = ConvertWrappingMode(sampler->wrapS); - aiTextureMapMode wrapT = ConvertWrappingMode(sampler->wrapT); - mat->AddProperty(&wrapS, 1, AI_MATKEY_MAPPINGMODE_U(texType, texSlot)); - mat->AddProperty(&wrapT, 1, AI_MATKEY_MAPPINGMODE_V(texType, texSlot)); - - if (sampler->magFilter != SamplerMagFilter::UNSET) { - mat->AddProperty(&sampler->magFilter, 1, AI_MATKEY_GLTF_MAPPINGFILTER_MAG(texType, texSlot)); - } - - if (sampler->minFilter != SamplerMinFilter::UNSET) { - mat->AddProperty(&sampler->minFilter, 1, AI_MATKEY_GLTF_MAPPINGFILTER_MIN(texType, texSlot)); - } - } - } +inline void SetMaterialTextureProperty(std::vector &embeddedTexIdxs, Asset & /*r*/, glTF2::TextureInfo prop, aiMaterial *mat, aiTextureType texType, unsigned int texSlot = 0) { + if (prop.texture && prop.texture->source) { + aiString uri(prop.texture->source->uri); + + int texIdx = embeddedTexIdxs[prop.texture->source.GetIndex()]; + if (texIdx != -1) { // embedded + // setup texture reference string (copied from ColladaLoader::FindFilenameForEffectTexture) + uri.data[0] = '*'; + uri.length = 1 + ASSIMP_itoa10(uri.data + 1, MAXLEN - 1, texIdx); + } + + if (prop.textureTransformSupported) { + aiUVTransform transform; + transform.mTranslation.x = prop.TextureTransformExt_t.offset[0]; + transform.mTranslation.y = prop.TextureTransformExt_t.offset[0]; + transform.mRotation = prop.TextureTransformExt_t.rotation; + transform.mScaling.x = prop.TextureTransformExt_t.scale[0]; + transform.mScaling.y = prop.TextureTransformExt_t.scale[1]; + mat->AddProperty(&transform, 1, _AI_MATKEY_UVTRANSFORM_BASE, texType, texSlot); + } + + mat->AddProperty(&uri, AI_MATKEY_TEXTURE(texType, texSlot)); + mat->AddProperty(&prop.texCoord, 1, _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, texType, texSlot); + + if (prop.texture->sampler) { + Ref sampler = prop.texture->sampler; + + aiString name(sampler->name); + aiString id(sampler->id); + + mat->AddProperty(&name, AI_MATKEY_GLTF_MAPPINGNAME(texType, texSlot)); + mat->AddProperty(&id, AI_MATKEY_GLTF_MAPPINGID(texType, texSlot)); + + aiTextureMapMode wrapS = ConvertWrappingMode(sampler->wrapS); + aiTextureMapMode wrapT = ConvertWrappingMode(sampler->wrapT); + mat->AddProperty(&wrapS, 1, AI_MATKEY_MAPPINGMODE_U(texType, texSlot)); + mat->AddProperty(&wrapT, 1, AI_MATKEY_MAPPINGMODE_V(texType, texSlot)); + + if (sampler->magFilter != SamplerMagFilter::UNSET) { + mat->AddProperty(&sampler->magFilter, 1, AI_MATKEY_GLTF_MAPPINGFILTER_MAG(texType, texSlot)); + } + + if (sampler->minFilter != SamplerMinFilter::UNSET) { + mat->AddProperty(&sampler->minFilter, 1, AI_MATKEY_GLTF_MAPPINGFILTER_MIN(texType, texSlot)); + } + } + } } -inline void SetMaterialTextureProperty(std::vector& embeddedTexIdxs, Asset& r, glTF2::NormalTextureInfo& prop, aiMaterial* mat, aiTextureType texType, unsigned int texSlot = 0) -{ - SetMaterialTextureProperty( embeddedTexIdxs, r, (glTF2::TextureInfo) prop, mat, texType, texSlot ); +inline void SetMaterialTextureProperty(std::vector &embeddedTexIdxs, Asset &r, glTF2::NormalTextureInfo &prop, aiMaterial *mat, aiTextureType texType, unsigned int texSlot = 0) { + SetMaterialTextureProperty(embeddedTexIdxs, r, (glTF2::TextureInfo)prop, mat, texType, texSlot); - if (prop.texture && prop.texture->source) { - mat->AddProperty(&prop.scale, 1, AI_MATKEY_GLTF_TEXTURE_SCALE(texType, texSlot)); - } + if (prop.texture && prop.texture->source) { + mat->AddProperty(&prop.scale, 1, AI_MATKEY_GLTF_TEXTURE_SCALE(texType, texSlot)); + } } -inline void SetMaterialTextureProperty(std::vector& embeddedTexIdxs, Asset& r, glTF2::OcclusionTextureInfo& prop, aiMaterial* mat, aiTextureType texType, unsigned int texSlot = 0) -{ - SetMaterialTextureProperty( embeddedTexIdxs, r, (glTF2::TextureInfo) prop, mat, texType, texSlot ); +inline void SetMaterialTextureProperty(std::vector &embeddedTexIdxs, Asset &r, glTF2::OcclusionTextureInfo &prop, aiMaterial *mat, aiTextureType texType, unsigned int texSlot = 0) { + SetMaterialTextureProperty(embeddedTexIdxs, r, (glTF2::TextureInfo)prop, mat, texType, texSlot); - if (prop.texture && prop.texture->source) { - mat->AddProperty(&prop.strength, 1, AI_MATKEY_GLTF_TEXTURE_STRENGTH(texType, texSlot)); - } + if (prop.texture && prop.texture->source) { + mat->AddProperty(&prop.strength, 1, AI_MATKEY_GLTF_TEXTURE_STRENGTH(texType, texSlot)); + } } -static aiMaterial* ImportMaterial(std::vector& embeddedTexIdxs, Asset& r, Material& mat) -{ - aiMaterial* aimat = new aiMaterial(); +static aiMaterial *ImportMaterial(std::vector &embeddedTexIdxs, Asset &r, Material &mat) { + aiMaterial *aimat = new aiMaterial(); - if (!mat.name.empty()) { - aiString str(mat.name); + if (!mat.name.empty()) { + aiString str(mat.name); - aimat->AddProperty(&str, AI_MATKEY_NAME); - } + aimat->AddProperty(&str, AI_MATKEY_NAME); + } - SetMaterialColorProperty(r, mat.pbrMetallicRoughness.baseColorFactor, aimat, AI_MATKEY_COLOR_DIFFUSE); - SetMaterialColorProperty(r, mat.pbrMetallicRoughness.baseColorFactor, aimat, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_FACTOR); + SetMaterialColorProperty(r, mat.pbrMetallicRoughness.baseColorFactor, aimat, AI_MATKEY_COLOR_DIFFUSE); + SetMaterialColorProperty(r, mat.pbrMetallicRoughness.baseColorFactor, aimat, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_FACTOR); - SetMaterialTextureProperty(embeddedTexIdxs, r, mat.pbrMetallicRoughness.baseColorTexture, aimat, aiTextureType_DIFFUSE); - SetMaterialTextureProperty(embeddedTexIdxs, r, mat.pbrMetallicRoughness.baseColorTexture, aimat, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_TEXTURE); + SetMaterialTextureProperty(embeddedTexIdxs, r, mat.pbrMetallicRoughness.baseColorTexture, aimat, aiTextureType_DIFFUSE); + SetMaterialTextureProperty(embeddedTexIdxs, r, mat.pbrMetallicRoughness.baseColorTexture, aimat, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_TEXTURE); - SetMaterialTextureProperty(embeddedTexIdxs, r, mat.pbrMetallicRoughness.metallicRoughnessTexture, aimat, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE); + SetMaterialTextureProperty(embeddedTexIdxs, r, mat.pbrMetallicRoughness.metallicRoughnessTexture, aimat, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE); - aimat->AddProperty(&mat.pbrMetallicRoughness.metallicFactor, 1, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLIC_FACTOR); - aimat->AddProperty(&mat.pbrMetallicRoughness.roughnessFactor, 1, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR); + aimat->AddProperty(&mat.pbrMetallicRoughness.metallicFactor, 1, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLIC_FACTOR); + aimat->AddProperty(&mat.pbrMetallicRoughness.roughnessFactor, 1, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR); - float roughnessAsShininess = 1 - mat.pbrMetallicRoughness.roughnessFactor; - roughnessAsShininess *= roughnessAsShininess * 1000; - aimat->AddProperty(&roughnessAsShininess, 1, AI_MATKEY_SHININESS); + float roughnessAsShininess = 1 - mat.pbrMetallicRoughness.roughnessFactor; + roughnessAsShininess *= roughnessAsShininess * 1000; + aimat->AddProperty(&roughnessAsShininess, 1, AI_MATKEY_SHININESS); - SetMaterialTextureProperty(embeddedTexIdxs, r, mat.normalTexture, aimat, aiTextureType_NORMALS); - SetMaterialTextureProperty(embeddedTexIdxs, r, mat.occlusionTexture, aimat, aiTextureType_LIGHTMAP); - SetMaterialTextureProperty(embeddedTexIdxs, r, mat.emissiveTexture, aimat, aiTextureType_EMISSIVE); - SetMaterialColorProperty(r, mat.emissiveFactor, aimat, AI_MATKEY_COLOR_EMISSIVE); + SetMaterialTextureProperty(embeddedTexIdxs, r, mat.normalTexture, aimat, aiTextureType_NORMALS); + SetMaterialTextureProperty(embeddedTexIdxs, r, mat.occlusionTexture, aimat, aiTextureType_LIGHTMAP); + SetMaterialTextureProperty(embeddedTexIdxs, r, mat.emissiveTexture, aimat, aiTextureType_EMISSIVE); + SetMaterialColorProperty(r, mat.emissiveFactor, aimat, AI_MATKEY_COLOR_EMISSIVE); - aimat->AddProperty(&mat.doubleSided, 1, AI_MATKEY_TWOSIDED); + aimat->AddProperty(&mat.doubleSided, 1, AI_MATKEY_TWOSIDED); - aiString alphaMode(mat.alphaMode); - aimat->AddProperty(&alphaMode, AI_MATKEY_GLTF_ALPHAMODE); - aimat->AddProperty(&mat.alphaCutoff, 1, AI_MATKEY_GLTF_ALPHACUTOFF); + aiString alphaMode(mat.alphaMode); + aimat->AddProperty(&alphaMode, AI_MATKEY_GLTF_ALPHAMODE); + aimat->AddProperty(&mat.alphaCutoff, 1, AI_MATKEY_GLTF_ALPHACUTOFF); - //pbrSpecularGlossiness - if (mat.pbrSpecularGlossiness.isPresent) { - PbrSpecularGlossiness &pbrSG = mat.pbrSpecularGlossiness.value; + //pbrSpecularGlossiness + if (mat.pbrSpecularGlossiness.isPresent) { + PbrSpecularGlossiness &pbrSG = mat.pbrSpecularGlossiness.value; - aimat->AddProperty(&mat.pbrSpecularGlossiness.isPresent, 1, AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS); - SetMaterialColorProperty(r, pbrSG.diffuseFactor, aimat, AI_MATKEY_COLOR_DIFFUSE); - SetMaterialColorProperty(r, pbrSG.specularFactor, aimat, AI_MATKEY_COLOR_SPECULAR); + aimat->AddProperty(&mat.pbrSpecularGlossiness.isPresent, 1, AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS); + SetMaterialColorProperty(r, pbrSG.diffuseFactor, aimat, AI_MATKEY_COLOR_DIFFUSE); + SetMaterialColorProperty(r, pbrSG.specularFactor, aimat, AI_MATKEY_COLOR_SPECULAR); - float glossinessAsShininess = pbrSG.glossinessFactor * 1000.0f; - aimat->AddProperty(&glossinessAsShininess, 1, AI_MATKEY_SHININESS); - aimat->AddProperty(&pbrSG.glossinessFactor, 1, AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS_GLOSSINESS_FACTOR); + float glossinessAsShininess = pbrSG.glossinessFactor * 1000.0f; + aimat->AddProperty(&glossinessAsShininess, 1, AI_MATKEY_SHININESS); + aimat->AddProperty(&pbrSG.glossinessFactor, 1, AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS_GLOSSINESS_FACTOR); - SetMaterialTextureProperty(embeddedTexIdxs, r, pbrSG.diffuseTexture, aimat, aiTextureType_DIFFUSE); + SetMaterialTextureProperty(embeddedTexIdxs, r, pbrSG.diffuseTexture, aimat, aiTextureType_DIFFUSE); - SetMaterialTextureProperty(embeddedTexIdxs, r, pbrSG.specularGlossinessTexture, aimat, aiTextureType_SPECULAR); - } - if (mat.unlit) { - aimat->AddProperty(&mat.unlit, 1, AI_MATKEY_GLTF_UNLIT); - } + SetMaterialTextureProperty(embeddedTexIdxs, r, pbrSG.specularGlossinessTexture, aimat, aiTextureType_SPECULAR); + } + if (mat.unlit) { + aimat->AddProperty(&mat.unlit, 1, AI_MATKEY_GLTF_UNLIT); + } - return aimat; + return aimat; } -void glTF2Importer::ImportMaterials(glTF2::Asset& r) -{ - const unsigned int numImportedMaterials = unsigned(r.materials.Size()); - Material defaultMaterial; +void glTF2Importer::ImportMaterials(glTF2::Asset &r) { + const unsigned int numImportedMaterials = unsigned(r.materials.Size()); + Material defaultMaterial; - mScene->mNumMaterials = numImportedMaterials + 1; - mScene->mMaterials = new aiMaterial*[mScene->mNumMaterials]; - mScene->mMaterials[numImportedMaterials] = ImportMaterial(embeddedTexIdxs, r, defaultMaterial); + mScene->mNumMaterials = numImportedMaterials + 1; + mScene->mMaterials = new aiMaterial *[mScene->mNumMaterials]; + mScene->mMaterials[numImportedMaterials] = ImportMaterial(embeddedTexIdxs, r, defaultMaterial); - for (unsigned int i = 0; i < numImportedMaterials; ++i) { - mScene->mMaterials[i] = ImportMaterial(embeddedTexIdxs, r, r.materials[i]); - } + for (unsigned int i = 0; i < numImportedMaterials; ++i) { + mScene->mMaterials[i] = ImportMaterial(embeddedTexIdxs, r, r.materials[i]); + } } - -static inline void SetFace(aiFace& face, int a) -{ - face.mNumIndices = 1; - face.mIndices = new unsigned int[1]; - face.mIndices[0] = a; +static inline void SetFace(aiFace &face, int a) { + face.mNumIndices = 1; + face.mIndices = new unsigned int[1]; + face.mIndices[0] = a; } -static inline void SetFace(aiFace& face, int a, int b) -{ - face.mNumIndices = 2; - face.mIndices = new unsigned int[2]; - face.mIndices[0] = a; - face.mIndices[1] = b; +static inline void SetFace(aiFace &face, int a, int b) { + face.mNumIndices = 2; + face.mIndices = new unsigned int[2]; + face.mIndices[0] = a; + face.mIndices[1] = b; } -static inline void SetFace(aiFace& face, int a, int b, int c) -{ - face.mNumIndices = 3; - face.mIndices = new unsigned int[3]; - face.mIndices[0] = a; - face.mIndices[1] = b; - face.mIndices[2] = c; +static inline void SetFace(aiFace &face, int a, int b, int c) { + face.mNumIndices = 3; + face.mIndices = new unsigned int[3]; + face.mIndices[0] = a; + face.mIndices[1] = b; + face.mIndices[2] = c; } #ifdef ASSIMP_BUILD_DEBUG -static inline bool CheckValidFacesIndices(aiFace* faces, unsigned nFaces, unsigned nVerts) -{ - for (unsigned i = 0; i < nFaces; ++i) { - for (unsigned j = 0; j < faces[i].mNumIndices; ++j) { - unsigned idx = faces[i].mIndices[j]; - if (idx >= nVerts) - return false; - } - } - return true; +static inline bool CheckValidFacesIndices(aiFace *faces, unsigned nFaces, unsigned nVerts) { + for (unsigned i = 0; i < nFaces; ++i) { + for (unsigned j = 0; j < faces[i].mNumIndices; ++j) { + unsigned idx = faces[i].mIndices[j]; + if (idx >= nVerts) + return false; + } + } + return true; } #endif // ASSIMP_BUILD_DEBUG -void glTF2Importer::ImportMeshes(glTF2::Asset& r) -{ - std::vector meshes; - - unsigned int k = 0; - - for (unsigned int m = 0; m < r.meshes.Size(); ++m) { - Mesh& mesh = r.meshes[m]; - - meshOffsets.push_back(k); - k += unsigned(mesh.primitives.size()); - - for (unsigned int p = 0; p < mesh.primitives.size(); ++p) { - Mesh::Primitive& prim = mesh.primitives[p]; - - aiMesh* aim = new aiMesh(); - meshes.push_back(aim); - - aim->mName = mesh.name.empty() ? mesh.id : mesh.name; - - if (mesh.primitives.size() > 1) { - ai_uint32& len = aim->mName.length; - aim->mName.data[len] = '-'; - len += 1 + ASSIMP_itoa10(aim->mName.data + len + 1, unsigned(MAXLEN - len - 1), p); - } - - switch (prim.mode) { - case PrimitiveMode_POINTS: - aim->mPrimitiveTypes |= aiPrimitiveType_POINT; - break; - - case PrimitiveMode_LINES: - case PrimitiveMode_LINE_LOOP: - case PrimitiveMode_LINE_STRIP: - aim->mPrimitiveTypes |= aiPrimitiveType_LINE; - break; - - case PrimitiveMode_TRIANGLES: - case PrimitiveMode_TRIANGLE_STRIP: - case PrimitiveMode_TRIANGLE_FAN: - aim->mPrimitiveTypes |= aiPrimitiveType_TRIANGLE; - break; - - } - - Mesh::Primitive::Attributes& attr = prim.attributes; - - if (attr.position.size() > 0 && attr.position[0]) { - aim->mNumVertices = static_cast(attr.position[0]->count); - attr.position[0]->ExtractData(aim->mVertices); - } - - if (attr.normal.size() > 0 && attr.normal[0]) { - attr.normal[0]->ExtractData(aim->mNormals); - - // only extract tangents if normals are present - if (attr.tangent.size() > 0 && attr.tangent[0]) { - // generate bitangents from normals and tangents according to spec - Tangent *tangents = nullptr; - - attr.tangent[0]->ExtractData(tangents); - - aim->mTangents = new aiVector3D[aim->mNumVertices]; - aim->mBitangents = new aiVector3D[aim->mNumVertices]; - - for (unsigned int i = 0; i < aim->mNumVertices; ++i) { - aim->mTangents[i] = tangents[i].xyz; - aim->mBitangents[i] = (aim->mNormals[i] ^ tangents[i].xyz) * tangents[i].w; - } - - delete [] tangents; - } - } - - for (size_t c = 0; c < attr.color.size() && c < AI_MAX_NUMBER_OF_COLOR_SETS; ++c) { - if (attr.color[c]->count != aim->mNumVertices) { - DefaultLogger::get()->warn("Color stream size in mesh \"" + mesh.name + - "\" does not match the vertex count"); - continue; - } - attr.color[c]->ExtractData(aim->mColors[c]); - } - for (size_t tc = 0; tc < attr.texcoord.size() && tc < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++tc) { - if (attr.texcoord[tc]->count != aim->mNumVertices) { - DefaultLogger::get()->warn("Texcoord stream size in mesh \"" + mesh.name + - "\" does not match the vertex count"); - continue; - } - - attr.texcoord[tc]->ExtractData(aim->mTextureCoords[tc]); - aim->mNumUVComponents[tc] = attr.texcoord[tc]->GetNumComponents(); - - aiVector3D* values = aim->mTextureCoords[tc]; - for (unsigned int i = 0; i < aim->mNumVertices; ++i) { - values[i].y = 1 - values[i].y; // Flip Y coords - } - } - - std::vector& targets = prim.targets; - if (targets.size() > 0) { - aim->mNumAnimMeshes = (unsigned int)targets.size(); - aim->mAnimMeshes = new aiAnimMesh*[aim->mNumAnimMeshes]; - for (size_t i = 0; i < targets.size(); i++) { - aim->mAnimMeshes[i] = aiCreateAnimMesh(aim); - aiAnimMesh& aiAnimMesh = *(aim->mAnimMeshes[i]); - Mesh::Primitive::Target& target = targets[i]; - - if (target.position.size() > 0) { - aiVector3D *positionDiff = nullptr; - target.position[0]->ExtractData(positionDiff); - for(unsigned int vertexId = 0; vertexId < aim->mNumVertices; vertexId++) { - aiAnimMesh.mVertices[vertexId] += positionDiff[vertexId]; - } - delete [] positionDiff; - } - if (target.normal.size() > 0) { - aiVector3D *normalDiff = nullptr; - target.normal[0]->ExtractData(normalDiff); - for(unsigned int vertexId = 0; vertexId < aim->mNumVertices; vertexId++) { - aiAnimMesh.mNormals[vertexId] += normalDiff[vertexId]; - } - delete [] normalDiff; - } - if (target.tangent.size() > 0) { - Tangent *tangent = nullptr; - attr.tangent[0]->ExtractData(tangent); - - aiVector3D *tangentDiff = nullptr; - target.tangent[0]->ExtractData(tangentDiff); - - for (unsigned int vertexId = 0; vertexId < aim->mNumVertices; ++vertexId) { - tangent[vertexId].xyz += tangentDiff[vertexId]; - aiAnimMesh.mTangents[vertexId] = tangent[vertexId].xyz; - aiAnimMesh.mBitangents[vertexId] = (aiAnimMesh.mNormals[vertexId] ^ tangent[vertexId].xyz) * tangent[vertexId].w; - } - delete [] tangent; - delete [] tangentDiff; - } - if (mesh.weights.size() > i) { - aiAnimMesh.mWeight = mesh.weights[i]; - } - } - } - - - aiFace* faces = 0; - size_t nFaces = 0; - - if (prim.indices) { - size_t count = prim.indices->count; - - Accessor::Indexer data = prim.indices->GetIndexer(); - ai_assert(data.IsValid()); - - switch (prim.mode) { - case PrimitiveMode_POINTS: { - nFaces = count; - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < count; ++i) { - SetFace(faces[i], data.GetUInt(i)); - } - break; - } - - case PrimitiveMode_LINES: { - nFaces = count / 2; - if (nFaces * 2 != count) { - ASSIMP_LOG_WARN("The number of vertices was not compatible with the LINES mode. Some vertices were dropped."); - count = nFaces * 2; - } - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < count; i += 2) { - SetFace(faces[i / 2], data.GetUInt(i), data.GetUInt(i + 1)); - } - break; - } - - case PrimitiveMode_LINE_LOOP: - case PrimitiveMode_LINE_STRIP: { - nFaces = count - ((prim.mode == PrimitiveMode_LINE_STRIP) ? 1 : 0); - faces = new aiFace[nFaces]; - SetFace(faces[0], data.GetUInt(0), data.GetUInt(1)); - for (unsigned int i = 2; i < count; ++i) { - SetFace(faces[i - 1], faces[i - 2].mIndices[1], data.GetUInt(i)); - } - if (prim.mode == PrimitiveMode_LINE_LOOP) { // close the loop - SetFace(faces[count - 1], faces[count - 2].mIndices[1], faces[0].mIndices[0]); - } - break; - } - - case PrimitiveMode_TRIANGLES: { - nFaces = count / 3; - if (nFaces * 3 != count) { - ASSIMP_LOG_WARN("The number of vertices was not compatible with the TRIANGLES mode. Some vertices were dropped."); - count = nFaces * 3; - } - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < count; i += 3) { - SetFace(faces[i / 3], data.GetUInt(i), data.GetUInt(i + 1), data.GetUInt(i + 2)); - } - break; - } - case PrimitiveMode_TRIANGLE_STRIP: { - nFaces = count - 2; - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < nFaces; ++i) { - //The ordering is to ensure that the triangles are all drawn with the same orientation - if ((i + 1) % 2 == 0) - { - //For even n, vertices n + 1, n, and n + 2 define triangle n - SetFace(faces[i], data.GetUInt(i + 1), data.GetUInt(i), data.GetUInt(i + 2)); - } - else - { - //For odd n, vertices n, n+1, and n+2 define triangle n - SetFace(faces[i], data.GetUInt(i), data.GetUInt(i + 1), data.GetUInt(i + 2)); - } - } - break; - } - case PrimitiveMode_TRIANGLE_FAN: - nFaces = count - 2; - faces = new aiFace[nFaces]; - SetFace(faces[0], data.GetUInt(0), data.GetUInt(1), data.GetUInt(2)); - for (unsigned int i = 1; i < nFaces; ++i) { - SetFace(faces[i], faces[0].mIndices[0], faces[i - 1].mIndices[2], data.GetUInt(i + 2)); - } - break; - } - } - else { // no indices provided so directly generate from counts - - // use the already determined count as it includes checks - unsigned int count = aim->mNumVertices; - - switch (prim.mode) { - case PrimitiveMode_POINTS: { - nFaces = count; - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < count; ++i) { - SetFace(faces[i], i); - } - break; - } - - case PrimitiveMode_LINES: { - nFaces = count / 2; - if (nFaces * 2 != count) { - ASSIMP_LOG_WARN("The number of vertices was not compatible with the LINES mode. Some vertices were dropped."); - count = (unsigned int) nFaces * 2; - } - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < count; i += 2) { - SetFace(faces[i / 2], i, i + 1); - } - break; - } - - case PrimitiveMode_LINE_LOOP: - case PrimitiveMode_LINE_STRIP: { - nFaces = count - ((prim.mode == PrimitiveMode_LINE_STRIP) ? 1 : 0); - faces = new aiFace[nFaces]; - SetFace(faces[0], 0, 1); - for (unsigned int i = 2; i < count; ++i) { - SetFace(faces[i - 1], faces[i - 2].mIndices[1], i); - } - if (prim.mode == PrimitiveMode_LINE_LOOP) { // close the loop - SetFace(faces[count - 1], faces[count - 2].mIndices[1], faces[0].mIndices[0]); - } - break; - } - - case PrimitiveMode_TRIANGLES: { - nFaces = count / 3; - if (nFaces * 3 != count) { - ASSIMP_LOG_WARN("The number of vertices was not compatible with the TRIANGLES mode. Some vertices were dropped."); - count = (unsigned int) nFaces * 3; - } - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < count; i += 3) { - SetFace(faces[i / 3], i, i + 1, i + 2); - } - break; - } - case PrimitiveMode_TRIANGLE_STRIP: { - nFaces = count - 2; - faces = new aiFace[nFaces]; - for (unsigned int i = 0; i < nFaces; ++i) { - //The ordering is to ensure that the triangles are all drawn with the same orientation - if ((i+1) % 2 == 0) - { - //For even n, vertices n + 1, n, and n + 2 define triangle n - SetFace(faces[i], i+1, i, i+2); - } - else - { - //For odd n, vertices n, n+1, and n+2 define triangle n - SetFace(faces[i], i, i+1, i+2); - } - } - break; - } - case PrimitiveMode_TRIANGLE_FAN: - nFaces = count - 2; - faces = new aiFace[nFaces]; - SetFace(faces[0], 0, 1, 2); - for (unsigned int i = 1; i < nFaces; ++i) { - SetFace(faces[i], faces[0].mIndices[0], faces[i - 1].mIndices[2], i + 2); - } - break; - } - } - - if (faces) { - aim->mFaces = faces; - aim->mNumFaces = static_cast(nFaces); - ai_assert(CheckValidFacesIndices(faces, static_cast(nFaces), aim->mNumVertices)); - } - - if (prim.material) { - aim->mMaterialIndex = prim.material.GetIndex(); - } - else { - aim->mMaterialIndex = mScene->mNumMaterials - 1; - } - - } - } - - meshOffsets.push_back(k); - - CopyVector(meshes, mScene->mMeshes, mScene->mNumMeshes); +void glTF2Importer::ImportMeshes(glTF2::Asset &r) { + std::vector meshes; + + unsigned int k = 0; + + for (unsigned int m = 0; m < r.meshes.Size(); ++m) { + Mesh &mesh = r.meshes[m]; + + meshOffsets.push_back(k); + k += unsigned(mesh.primitives.size()); + + for (unsigned int p = 0; p < mesh.primitives.size(); ++p) { + Mesh::Primitive &prim = mesh.primitives[p]; + + aiMesh *aim = new aiMesh(); + meshes.push_back(aim); + + aim->mName = mesh.name.empty() ? mesh.id : mesh.name; + + if (mesh.primitives.size() > 1) { + ai_uint32 &len = aim->mName.length; + aim->mName.data[len] = '-'; + len += 1 + ASSIMP_itoa10(aim->mName.data + len + 1, unsigned(MAXLEN - len - 1), p); + } + + switch (prim.mode) { + case PrimitiveMode_POINTS: + aim->mPrimitiveTypes |= aiPrimitiveType_POINT; + break; + + case PrimitiveMode_LINES: + case PrimitiveMode_LINE_LOOP: + case PrimitiveMode_LINE_STRIP: + aim->mPrimitiveTypes |= aiPrimitiveType_LINE; + break; + + case PrimitiveMode_TRIANGLES: + case PrimitiveMode_TRIANGLE_STRIP: + case PrimitiveMode_TRIANGLE_FAN: + aim->mPrimitiveTypes |= aiPrimitiveType_TRIANGLE; + break; + } + + Mesh::Primitive::Attributes &attr = prim.attributes; + + if (attr.position.size() > 0 && attr.position[0]) { + aim->mNumVertices = static_cast(attr.position[0]->count); + attr.position[0]->ExtractData(aim->mVertices); + } + + if (attr.normal.size() > 0 && attr.normal[0]) { + attr.normal[0]->ExtractData(aim->mNormals); + + // only extract tangents if normals are present + if (attr.tangent.size() > 0 && attr.tangent[0]) { + // generate bitangents from normals and tangents according to spec + Tangent *tangents = nullptr; + + attr.tangent[0]->ExtractData(tangents); + + aim->mTangents = new aiVector3D[aim->mNumVertices]; + aim->mBitangents = new aiVector3D[aim->mNumVertices]; + + for (unsigned int i = 0; i < aim->mNumVertices; ++i) { + aim->mTangents[i] = tangents[i].xyz; + aim->mBitangents[i] = (aim->mNormals[i] ^ tangents[i].xyz) * tangents[i].w; + } + + delete[] tangents; + } + } + + for (size_t c = 0; c < attr.color.size() && c < AI_MAX_NUMBER_OF_COLOR_SETS; ++c) { + if (attr.color[c]->count != aim->mNumVertices) { + DefaultLogger::get()->warn("Color stream size in mesh \"" + mesh.name + + "\" does not match the vertex count"); + continue; + } + attr.color[c]->ExtractData(aim->mColors[c]); + } + for (size_t tc = 0; tc < attr.texcoord.size() && tc < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++tc) { + if (attr.texcoord[tc]->count != aim->mNumVertices) { + DefaultLogger::get()->warn("Texcoord stream size in mesh \"" + mesh.name + + "\" does not match the vertex count"); + continue; + } + + attr.texcoord[tc]->ExtractData(aim->mTextureCoords[tc]); + aim->mNumUVComponents[tc] = attr.texcoord[tc]->GetNumComponents(); + + aiVector3D *values = aim->mTextureCoords[tc]; + for (unsigned int i = 0; i < aim->mNumVertices; ++i) { + values[i].y = 1 - values[i].y; // Flip Y coords + } + } + + std::vector &targets = prim.targets; + if (targets.size() > 0) { + aim->mNumAnimMeshes = (unsigned int)targets.size(); + aim->mAnimMeshes = new aiAnimMesh *[aim->mNumAnimMeshes]; + for (size_t i = 0; i < targets.size(); i++) { + aim->mAnimMeshes[i] = aiCreateAnimMesh(aim); + aiAnimMesh &aiAnimMesh = *(aim->mAnimMeshes[i]); + Mesh::Primitive::Target &target = targets[i]; + + if (target.position.size() > 0) { + aiVector3D *positionDiff = nullptr; + target.position[0]->ExtractData(positionDiff); + for (unsigned int vertexId = 0; vertexId < aim->mNumVertices; vertexId++) { + aiAnimMesh.mVertices[vertexId] += positionDiff[vertexId]; + } + delete[] positionDiff; + } + if (target.normal.size() > 0) { + aiVector3D *normalDiff = nullptr; + target.normal[0]->ExtractData(normalDiff); + for (unsigned int vertexId = 0; vertexId < aim->mNumVertices; vertexId++) { + aiAnimMesh.mNormals[vertexId] += normalDiff[vertexId]; + } + delete[] normalDiff; + } + if (target.tangent.size() > 0) { + Tangent *tangent = nullptr; + attr.tangent[0]->ExtractData(tangent); + + aiVector3D *tangentDiff = nullptr; + target.tangent[0]->ExtractData(tangentDiff); + + for (unsigned int vertexId = 0; vertexId < aim->mNumVertices; ++vertexId) { + tangent[vertexId].xyz += tangentDiff[vertexId]; + aiAnimMesh.mTangents[vertexId] = tangent[vertexId].xyz; + aiAnimMesh.mBitangents[vertexId] = (aiAnimMesh.mNormals[vertexId] ^ tangent[vertexId].xyz) * tangent[vertexId].w; + } + delete[] tangent; + delete[] tangentDiff; + } + if (mesh.weights.size() > i) { + aiAnimMesh.mWeight = mesh.weights[i]; + } + } + } + + aiFace *faces = 0; + size_t nFaces = 0; + + if (prim.indices) { + size_t count = prim.indices->count; + + Accessor::Indexer data = prim.indices->GetIndexer(); + ai_assert(data.IsValid()); + + switch (prim.mode) { + case PrimitiveMode_POINTS: { + nFaces = count; + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < count; ++i) { + SetFace(faces[i], data.GetUInt(i)); + } + break; + } + + case PrimitiveMode_LINES: { + nFaces = count / 2; + if (nFaces * 2 != count) { + ASSIMP_LOG_WARN("The number of vertices was not compatible with the LINES mode. Some vertices were dropped."); + count = nFaces * 2; + } + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < count; i += 2) { + SetFace(faces[i / 2], data.GetUInt(i), data.GetUInt(i + 1)); + } + break; + } + + case PrimitiveMode_LINE_LOOP: + case PrimitiveMode_LINE_STRIP: { + nFaces = count - ((prim.mode == PrimitiveMode_LINE_STRIP) ? 1 : 0); + faces = new aiFace[nFaces]; + SetFace(faces[0], data.GetUInt(0), data.GetUInt(1)); + for (unsigned int i = 2; i < count; ++i) { + SetFace(faces[i - 1], faces[i - 2].mIndices[1], data.GetUInt(i)); + } + if (prim.mode == PrimitiveMode_LINE_LOOP) { // close the loop + SetFace(faces[count - 1], faces[count - 2].mIndices[1], faces[0].mIndices[0]); + } + break; + } + + case PrimitiveMode_TRIANGLES: { + nFaces = count / 3; + if (nFaces * 3 != count) { + ASSIMP_LOG_WARN("The number of vertices was not compatible with the TRIANGLES mode. Some vertices were dropped."); + count = nFaces * 3; + } + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < count; i += 3) { + SetFace(faces[i / 3], data.GetUInt(i), data.GetUInt(i + 1), data.GetUInt(i + 2)); + } + break; + } + case PrimitiveMode_TRIANGLE_STRIP: { + nFaces = count - 2; + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < nFaces; ++i) { + //The ordering is to ensure that the triangles are all drawn with the same orientation + if ((i + 1) % 2 == 0) { + //For even n, vertices n + 1, n, and n + 2 define triangle n + SetFace(faces[i], data.GetUInt(i + 1), data.GetUInt(i), data.GetUInt(i + 2)); + } else { + //For odd n, vertices n, n+1, and n+2 define triangle n + SetFace(faces[i], data.GetUInt(i), data.GetUInt(i + 1), data.GetUInt(i + 2)); + } + } + break; + } + case PrimitiveMode_TRIANGLE_FAN: + nFaces = count - 2; + faces = new aiFace[nFaces]; + SetFace(faces[0], data.GetUInt(0), data.GetUInt(1), data.GetUInt(2)); + for (unsigned int i = 1; i < nFaces; ++i) { + SetFace(faces[i], faces[0].mIndices[0], faces[i - 1].mIndices[2], data.GetUInt(i + 2)); + } + break; + } + } else { // no indices provided so directly generate from counts + + // use the already determined count as it includes checks + unsigned int count = aim->mNumVertices; + + switch (prim.mode) { + case PrimitiveMode_POINTS: { + nFaces = count; + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < count; ++i) { + SetFace(faces[i], i); + } + break; + } + + case PrimitiveMode_LINES: { + nFaces = count / 2; + if (nFaces * 2 != count) { + ASSIMP_LOG_WARN("The number of vertices was not compatible with the LINES mode. Some vertices were dropped."); + count = (unsigned int)nFaces * 2; + } + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < count; i += 2) { + SetFace(faces[i / 2], i, i + 1); + } + break; + } + + case PrimitiveMode_LINE_LOOP: + case PrimitiveMode_LINE_STRIP: { + nFaces = count - ((prim.mode == PrimitiveMode_LINE_STRIP) ? 1 : 0); + faces = new aiFace[nFaces]; + SetFace(faces[0], 0, 1); + for (unsigned int i = 2; i < count; ++i) { + SetFace(faces[i - 1], faces[i - 2].mIndices[1], i); + } + if (prim.mode == PrimitiveMode_LINE_LOOP) { // close the loop + SetFace(faces[count - 1], faces[count - 2].mIndices[1], faces[0].mIndices[0]); + } + break; + } + + case PrimitiveMode_TRIANGLES: { + nFaces = count / 3; + if (nFaces * 3 != count) { + ASSIMP_LOG_WARN("The number of vertices was not compatible with the TRIANGLES mode. Some vertices were dropped."); + count = (unsigned int)nFaces * 3; + } + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < count; i += 3) { + SetFace(faces[i / 3], i, i + 1, i + 2); + } + break; + } + case PrimitiveMode_TRIANGLE_STRIP: { + nFaces = count - 2; + faces = new aiFace[nFaces]; + for (unsigned int i = 0; i < nFaces; ++i) { + //The ordering is to ensure that the triangles are all drawn with the same orientation + if ((i + 1) % 2 == 0) { + //For even n, vertices n + 1, n, and n + 2 define triangle n + SetFace(faces[i], i + 1, i, i + 2); + } else { + //For odd n, vertices n, n+1, and n+2 define triangle n + SetFace(faces[i], i, i + 1, i + 2); + } + } + break; + } + case PrimitiveMode_TRIANGLE_FAN: + nFaces = count - 2; + faces = new aiFace[nFaces]; + SetFace(faces[0], 0, 1, 2); + for (unsigned int i = 1; i < nFaces; ++i) { + SetFace(faces[i], faces[0].mIndices[0], faces[i - 1].mIndices[2], i + 2); + } + break; + } + } + + if (faces) { + aim->mFaces = faces; + aim->mNumFaces = static_cast(nFaces); + ai_assert(CheckValidFacesIndices(faces, static_cast(nFaces), aim->mNumVertices)); + } + + if (prim.material) { + aim->mMaterialIndex = prim.material.GetIndex(); + } else { + aim->mMaterialIndex = mScene->mNumMaterials - 1; + } + } + } + + meshOffsets.push_back(k); + + CopyVector(meshes, mScene->mMeshes, mScene->mNumMeshes); } -void glTF2Importer::ImportCameras(glTF2::Asset& r) -{ - if (!r.cameras.Size()) return; - - mScene->mNumCameras = r.cameras.Size(); - mScene->mCameras = new aiCamera*[r.cameras.Size()]; - - for (size_t i = 0; i < r.cameras.Size(); ++i) { - Camera& cam = r.cameras[i]; - - aiCamera* aicam = mScene->mCameras[i] = new aiCamera(); - - // cameras point in -Z by default, rest is specified in node transform - aicam->mLookAt = aiVector3D(0.f,0.f,-1.f); - - if (cam.type == Camera::Perspective) { - - aicam->mAspect = cam.cameraProperties.perspective.aspectRatio; - aicam->mHorizontalFOV = cam.cameraProperties.perspective.yfov * ((aicam->mAspect == 0.f) ? 1.f : aicam->mAspect); - aicam->mClipPlaneFar = cam.cameraProperties.perspective.zfar; - aicam->mClipPlaneNear = cam.cameraProperties.perspective.znear; - } else { - aicam->mClipPlaneFar = cam.cameraProperties.ortographic.zfar; - aicam->mClipPlaneNear = cam.cameraProperties.ortographic.znear; - aicam->mHorizontalFOV = 0.0; - aicam->mAspect = 1.0f; - if (0.f != cam.cameraProperties.ortographic.ymag ) { - aicam->mAspect = cam.cameraProperties.ortographic.xmag / cam.cameraProperties.ortographic.ymag; - } - } - } +void glTF2Importer::ImportCameras(glTF2::Asset &r) { + if (!r.cameras.Size()) return; + + mScene->mNumCameras = r.cameras.Size(); + mScene->mCameras = new aiCamera *[r.cameras.Size()]; + + for (size_t i = 0; i < r.cameras.Size(); ++i) { + Camera &cam = r.cameras[i]; + + aiCamera *aicam = mScene->mCameras[i] = new aiCamera(); + + // cameras point in -Z by default, rest is specified in node transform + aicam->mLookAt = aiVector3D(0.f, 0.f, -1.f); + + if (cam.type == Camera::Perspective) { + + aicam->mAspect = cam.cameraProperties.perspective.aspectRatio; + aicam->mHorizontalFOV = cam.cameraProperties.perspective.yfov * ((aicam->mAspect == 0.f) ? 1.f : aicam->mAspect); + aicam->mClipPlaneFar = cam.cameraProperties.perspective.zfar; + aicam->mClipPlaneNear = cam.cameraProperties.perspective.znear; + } else { + aicam->mClipPlaneFar = cam.cameraProperties.ortographic.zfar; + aicam->mClipPlaneNear = cam.cameraProperties.ortographic.znear; + aicam->mHorizontalFOV = 0.0; + aicam->mAspect = 1.0f; + if (0.f != cam.cameraProperties.ortographic.ymag) { + aicam->mAspect = cam.cameraProperties.ortographic.xmag / cam.cameraProperties.ortographic.ymag; + } + } + } } -void glTF2Importer::ImportLights(glTF2::Asset& r) -{ - if (!r.lights.Size()) - return; - - mScene->mNumLights = r.lights.Size(); - mScene->mLights = new aiLight*[r.lights.Size()]; - - for (size_t i = 0; i < r.lights.Size(); ++i) { - Light& light = r.lights[i]; - - aiLight* ail = mScene->mLights[i] = new aiLight(); - - switch (light.type) - { - case Light::Directional: - ail->mType = aiLightSource_DIRECTIONAL; break; - case Light::Point: - ail->mType = aiLightSource_POINT; break; - case Light::Spot: - ail->mType = aiLightSource_SPOT; break; - } - - if (ail->mType != aiLightSource_POINT) - { - ail->mDirection = aiVector3D(0.0f, 0.0f, -1.0f); - ail->mUp = aiVector3D(0.0f, 1.0f, 0.0f); - } - - vec3 colorWithIntensity = { light.color[0] * light.intensity, light.color[1] * light.intensity, light.color[2] * light.intensity }; - CopyValue(colorWithIntensity, ail->mColorAmbient); - CopyValue(colorWithIntensity, ail->mColorDiffuse); - CopyValue(colorWithIntensity, ail->mColorSpecular); - - if (ail->mType == aiLightSource_DIRECTIONAL) - { - ail->mAttenuationConstant = 1.0; - ail->mAttenuationLinear = 0.0; - ail->mAttenuationQuadratic = 0.0; - } - else - { - //in PBR attenuation is calculated using inverse square law which can be expressed - //using assimps equation: 1/(att0 + att1 * d + att2 * d*d) with the following parameters - //this is correct equation for the case when range (see - //https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual) - //is not present. When range is not present it is assumed that it is infinite and so numerator is 1. - //When range is present then numerator might be any value in range [0,1] and then assimps equation - //will not suffice. In this case range is added into metadata in ImportNode function - //and its up to implementation to read it when it wants to - ail->mAttenuationConstant = 0.0; - ail->mAttenuationLinear = 0.0; - ail->mAttenuationQuadratic = 1.0; - } - - if (ail->mType == aiLightSource_SPOT) - { - ail->mAngleInnerCone = light.innerConeAngle; - ail->mAngleOuterCone = light.outerConeAngle; - } - } +void glTF2Importer::ImportLights(glTF2::Asset &r) { + if (!r.lights.Size()) + return; + + mScene->mNumLights = r.lights.Size(); + mScene->mLights = new aiLight *[r.lights.Size()]; + + for (size_t i = 0; i < r.lights.Size(); ++i) { + Light &light = r.lights[i]; + + aiLight *ail = mScene->mLights[i] = new aiLight(); + + switch (light.type) { + case Light::Directional: + ail->mType = aiLightSource_DIRECTIONAL; + break; + case Light::Point: + ail->mType = aiLightSource_POINT; + break; + case Light::Spot: + ail->mType = aiLightSource_SPOT; + break; + } + + if (ail->mType != aiLightSource_POINT) { + ail->mDirection = aiVector3D(0.0f, 0.0f, -1.0f); + ail->mUp = aiVector3D(0.0f, 1.0f, 0.0f); + } + + vec3 colorWithIntensity = { light.color[0] * light.intensity, light.color[1] * light.intensity, light.color[2] * light.intensity }; + CopyValue(colorWithIntensity, ail->mColorAmbient); + CopyValue(colorWithIntensity, ail->mColorDiffuse); + CopyValue(colorWithIntensity, ail->mColorSpecular); + + if (ail->mType == aiLightSource_DIRECTIONAL) { + ail->mAttenuationConstant = 1.0; + ail->mAttenuationLinear = 0.0; + ail->mAttenuationQuadratic = 0.0; + } else { + //in PBR attenuation is calculated using inverse square law which can be expressed + //using assimps equation: 1/(att0 + att1 * d + att2 * d*d) with the following parameters + //this is correct equation for the case when range (see + //https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual) + //is not present. When range is not present it is assumed that it is infinite and so numerator is 1. + //When range is present then numerator might be any value in range [0,1] and then assimps equation + //will not suffice. In this case range is added into metadata in ImportNode function + //and its up to implementation to read it when it wants to + ail->mAttenuationConstant = 0.0; + ail->mAttenuationLinear = 0.0; + ail->mAttenuationQuadratic = 1.0; + } + + if (ail->mType == aiLightSource_SPOT) { + ail->mAngleInnerCone = light.innerConeAngle; + ail->mAngleOuterCone = light.outerConeAngle; + } + } } -static void GetNodeTransform(aiMatrix4x4& matrix, const glTF2::Node& node) { - if (node.matrix.isPresent) { - CopyValue(node.matrix.value, matrix); - } - else { - if (node.translation.isPresent) { - aiVector3D trans; - CopyValue(node.translation.value, trans); - aiMatrix4x4 t; - aiMatrix4x4::Translation(trans, t); - matrix = matrix * t; - } - - if (node.rotation.isPresent) { - aiQuaternion rot; - CopyValue(node.rotation.value, rot); - matrix = matrix * aiMatrix4x4(rot.GetMatrix()); - } - - if (node.scale.isPresent) { - aiVector3D scal(1.f); - CopyValue(node.scale.value, scal); - aiMatrix4x4 s; - aiMatrix4x4::Scaling(scal, s); - matrix = matrix * s; - } - } +static void GetNodeTransform(aiMatrix4x4 &matrix, const glTF2::Node &node) { + if (node.matrix.isPresent) { + CopyValue(node.matrix.value, matrix); + } else { + if (node.translation.isPresent) { + aiVector3D trans; + CopyValue(node.translation.value, trans); + aiMatrix4x4 t; + aiMatrix4x4::Translation(trans, t); + matrix = matrix * t; + } + + if (node.rotation.isPresent) { + aiQuaternion rot; + CopyValue(node.rotation.value, rot); + matrix = matrix * aiMatrix4x4(rot.GetMatrix()); + } + + if (node.scale.isPresent) { + aiVector3D scal(1.f); + CopyValue(node.scale.value, scal); + aiMatrix4x4 s; + aiMatrix4x4::Scaling(scal, s); + matrix = matrix * s; + } + } } -static void BuildVertexWeightMapping(Mesh::Primitive& primitive, std::vector>& map) -{ - Mesh::Primitive::Attributes& attr = primitive.attributes; - if (attr.weight.empty() || attr.joint.empty()) { - return; - } - if (attr.weight[0]->count != attr.joint[0]->count) { - return; - } - - size_t num_vertices = attr.weight[0]->count; - - struct Weights { float values[4]; }; - Weights* weights = nullptr; - attr.weight[0]->ExtractData(weights); - - struct Indices8 { uint8_t values[4]; }; - struct Indices16 { uint16_t values[4]; }; - Indices8* indices8 = nullptr; - Indices16* indices16 = nullptr; - if (attr.joint[0]->GetElementSize() == 4) { - attr.joint[0]->ExtractData(indices8); - }else { - attr.joint[0]->ExtractData(indices16); - } - // - if (nullptr == indices8 && nullptr == indices16) { - // Something went completely wrong! - ai_assert(false); - return; - } - - for (size_t i = 0; i < num_vertices; ++i) { - for (int j = 0; j < 4; ++j) { - const unsigned int bone = (indices8!=nullptr) ? indices8[i].values[j] : indices16[i].values[j]; - const float weight = weights[i].values[j]; - if (weight > 0 && bone < map.size()) { - map[bone].reserve(8); - map[bone].emplace_back(static_cast(i), weight); - } - } - } - - delete[] weights; - delete[] indices8; - delete[] indices16; +static void BuildVertexWeightMapping(Mesh::Primitive &primitive, std::vector> &map) { + Mesh::Primitive::Attributes &attr = primitive.attributes; + if (attr.weight.empty() || attr.joint.empty()) { + return; + } + if (attr.weight[0]->count != attr.joint[0]->count) { + return; + } + + size_t num_vertices = attr.weight[0]->count; + + struct Weights { + float values[4]; + }; + Weights *weights = nullptr; + attr.weight[0]->ExtractData(weights); + + struct Indices8 { + uint8_t values[4]; + }; + struct Indices16 { + uint16_t values[4]; + }; + Indices8 *indices8 = nullptr; + Indices16 *indices16 = nullptr; + if (attr.joint[0]->GetElementSize() == 4) { + attr.joint[0]->ExtractData(indices8); + } else { + attr.joint[0]->ExtractData(indices16); + } + // + if (nullptr == indices8 && nullptr == indices16) { + // Something went completely wrong! + ai_assert(false); + return; + } + + for (size_t i = 0; i < num_vertices; ++i) { + for (int j = 0; j < 4; ++j) { + const unsigned int bone = (indices8 != nullptr) ? indices8[i].values[j] : indices16[i].values[j]; + const float weight = weights[i].values[j]; + if (weight > 0 && bone < map.size()) { + map[bone].reserve(8); + map[bone].emplace_back(static_cast(i), weight); + } + } + } + + delete[] weights; + delete[] indices8; + delete[] indices16; } -static std::string GetNodeName(const Node& node) -{ - return node.name.empty() ? node.id : node.name; +static std::string GetNodeName(const Node &node) { + return node.name.empty() ? node.id : node.name; } -aiNode* ImportNode(aiScene* pScene, glTF2::Asset& r, std::vector& meshOffsets, glTF2::Ref& ptr) -{ - Node& node = *ptr; - - aiNode* ainode = new aiNode(GetNodeName(node)); - - if (!node.children.empty()) { - ainode->mNumChildren = unsigned(node.children.size()); - ainode->mChildren = new aiNode*[ainode->mNumChildren]; - - for (unsigned int i = 0; i < ainode->mNumChildren; ++i) { - aiNode* child = ImportNode(pScene, r, meshOffsets, node.children[i]); - child->mParent = ainode; - ainode->mChildren[i] = child; - } - } - - GetNodeTransform(ainode->mTransformation, node); - - if (!node.meshes.empty()) { - // GLTF files contain at most 1 mesh per node. - assert(node.meshes.size() == 1); - int mesh_idx = node.meshes[0].GetIndex(); - int count = meshOffsets[mesh_idx + 1] - meshOffsets[mesh_idx]; - - ainode->mNumMeshes = count; - ainode->mMeshes = new unsigned int[count]; - - if (node.skin) { - for (int primitiveNo = 0; primitiveNo < count; ++primitiveNo) { - aiMesh* mesh = pScene->mMeshes[meshOffsets[mesh_idx]+primitiveNo]; - mesh->mNumBones = static_cast(node.skin->jointNames.size()); - mesh->mBones = new aiBone*[mesh->mNumBones]; - - // GLTF and Assimp choose to store bone weights differently. - // GLTF has each vertex specify which bones influence the vertex. - // Assimp has each bone specify which vertices it has influence over. - // To convert this data, we first read over the vertex data and pull - // out the bone-to-vertex mapping. Then, when creating the aiBones, - // we copy the bone-to-vertex mapping into the bone. This is unfortunate - // both because it's somewhat slow and because, for many applications, - // we then need to reconvert the data back into the vertex-to-bone - // mapping which makes things doubly-slow. - std::vector> weighting(mesh->mNumBones); - BuildVertexWeightMapping(node.meshes[0]->primitives[primitiveNo], weighting); - - mat4* pbindMatrices = nullptr; - node.skin->inverseBindMatrices->ExtractData(pbindMatrices); - - for (uint32_t i = 0; i < mesh->mNumBones; ++i) { - aiBone* bone = new aiBone(); - - Ref joint = node.skin->jointNames[i]; - if (!joint->name.empty()) { - bone->mName = joint->name; - } else { - // Assimp expects each bone to have a unique name. - static const std::string kDefaultName = "bone_"; - char postfix[10] = {0}; - ASSIMP_itoa10(postfix, i); - bone->mName = (kDefaultName + postfix); - } - GetNodeTransform(bone->mOffsetMatrix, *joint); - - CopyValue(pbindMatrices[i], bone->mOffsetMatrix); - - std::vector& weights = weighting[i]; - - bone->mNumWeights = static_cast(weights.size()); - if (bone->mNumWeights > 0) { - bone->mWeights = new aiVertexWeight[bone->mNumWeights]; - memcpy(bone->mWeights, weights.data(), bone->mNumWeights * sizeof(aiVertexWeight)); - } else { - // Assimp expects all bones to have at least 1 weight. - bone->mWeights = new aiVertexWeight[1]; - bone->mNumWeights = 1; - bone->mWeights->mVertexId = 0; - bone->mWeights->mWeight = 0.f; - } - mesh->mBones[i] = bone; - } - - if (pbindMatrices) { - delete[] pbindMatrices; - } - } - } - - int k = 0; - for (unsigned int j = meshOffsets[mesh_idx]; j < meshOffsets[mesh_idx + 1]; ++j, ++k) { - ainode->mMeshes[k] = j; - } - } - - if (node.camera) { - pScene->mCameras[node.camera.GetIndex()]->mName = ainode->mName; - } - - if (node.light) { - pScene->mLights[node.light.GetIndex()]->mName = ainode->mName; - - //range is optional - see https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual - //it is added to meta data of parent node, because there is no other place to put it - if (node.light->range.isPresent) - { - ainode->mMetaData = aiMetadata::Alloc(1); - ainode->mMetaData->Set(0, "PBR_LightRange", node.light->range.value); - } - } - - return ainode; +aiNode *ImportNode(aiScene *pScene, glTF2::Asset &r, std::vector &meshOffsets, glTF2::Ref &ptr) { + Node &node = *ptr; + + aiNode *ainode = new aiNode(GetNodeName(node)); + + if (!node.children.empty()) { + ainode->mNumChildren = unsigned(node.children.size()); + ainode->mChildren = new aiNode *[ainode->mNumChildren]; + + for (unsigned int i = 0; i < ainode->mNumChildren; ++i) { + aiNode *child = ImportNode(pScene, r, meshOffsets, node.children[i]); + child->mParent = ainode; + ainode->mChildren[i] = child; + } + } + + GetNodeTransform(ainode->mTransformation, node); + + if (!node.meshes.empty()) { + // GLTF files contain at most 1 mesh per node. + assert(node.meshes.size() == 1); + int mesh_idx = node.meshes[0].GetIndex(); + int count = meshOffsets[mesh_idx + 1] - meshOffsets[mesh_idx]; + + ainode->mNumMeshes = count; + ainode->mMeshes = new unsigned int[count]; + + if (node.skin) { + for (int primitiveNo = 0; primitiveNo < count; ++primitiveNo) { + aiMesh *mesh = pScene->mMeshes[meshOffsets[mesh_idx] + primitiveNo]; + mesh->mNumBones = static_cast(node.skin->jointNames.size()); + mesh->mBones = new aiBone *[mesh->mNumBones]; + + // GLTF and Assimp choose to store bone weights differently. + // GLTF has each vertex specify which bones influence the vertex. + // Assimp has each bone specify which vertices it has influence over. + // To convert this data, we first read over the vertex data and pull + // out the bone-to-vertex mapping. Then, when creating the aiBones, + // we copy the bone-to-vertex mapping into the bone. This is unfortunate + // both because it's somewhat slow and because, for many applications, + // we then need to reconvert the data back into the vertex-to-bone + // mapping which makes things doubly-slow. + std::vector> weighting(mesh->mNumBones); + BuildVertexWeightMapping(node.meshes[0]->primitives[primitiveNo], weighting); + + mat4 *pbindMatrices = nullptr; + node.skin->inverseBindMatrices->ExtractData(pbindMatrices); + + for (uint32_t i = 0; i < mesh->mNumBones; ++i) { + aiBone *bone = new aiBone(); + + Ref joint = node.skin->jointNames[i]; + if (!joint->name.empty()) { + bone->mName = joint->name; + } else { + // Assimp expects each bone to have a unique name. + static const std::string kDefaultName = "bone_"; + char postfix[10] = { 0 }; + ASSIMP_itoa10(postfix, i); + bone->mName = (kDefaultName + postfix); + } + GetNodeTransform(bone->mOffsetMatrix, *joint); + + CopyValue(pbindMatrices[i], bone->mOffsetMatrix); + + std::vector &weights = weighting[i]; + + bone->mNumWeights = static_cast(weights.size()); + if (bone->mNumWeights > 0) { + bone->mWeights = new aiVertexWeight[bone->mNumWeights]; + memcpy(bone->mWeights, weights.data(), bone->mNumWeights * sizeof(aiVertexWeight)); + } else { + // Assimp expects all bones to have at least 1 weight. + bone->mWeights = new aiVertexWeight[1]; + bone->mNumWeights = 1; + bone->mWeights->mVertexId = 0; + bone->mWeights->mWeight = 0.f; + } + mesh->mBones[i] = bone; + } + + if (pbindMatrices) { + delete[] pbindMatrices; + } + } + } + + int k = 0; + for (unsigned int j = meshOffsets[mesh_idx]; j < meshOffsets[mesh_idx + 1]; ++j, ++k) { + ainode->mMeshes[k] = j; + } + } + + if (node.camera) { + pScene->mCameras[node.camera.GetIndex()]->mName = ainode->mName; + } + + if (node.light) { + pScene->mLights[node.light.GetIndex()]->mName = ainode->mName; + + //range is optional - see https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual + //it is added to meta data of parent node, because there is no other place to put it + if (node.light->range.isPresent) { + ainode->mMetaData = aiMetadata::Alloc(1); + ainode->mMetaData->Set(0, "PBR_LightRange", node.light->range.value); + } + } + + return ainode; } -void glTF2Importer::ImportNodes(glTF2::Asset& r) -{ - if (!r.scene) return; - - std::vector< Ref > rootNodes = r.scene->nodes; - - // The root nodes - unsigned int numRootNodes = unsigned(rootNodes.size()); - if (numRootNodes == 1) { // a single root node: use it - mScene->mRootNode = ImportNode(mScene, r, meshOffsets, rootNodes[0]); - } - else if (numRootNodes > 1) { // more than one root node: create a fake root - aiNode* root = new aiNode("ROOT"); - root->mChildren = new aiNode*[numRootNodes]; - for (unsigned int i = 0; i < numRootNodes; ++i) { - aiNode* node = ImportNode(mScene, r, meshOffsets, rootNodes[i]); - node->mParent = root; - root->mChildren[root->mNumChildren++] = node; - } - mScene->mRootNode = root; - } - - //if (!mScene->mRootNode) { - // mScene->mRootNode = new aiNode("EMPTY"); - //} +void glTF2Importer::ImportNodes(glTF2::Asset &r) { + if (!r.scene) return; + + std::vector> rootNodes = r.scene->nodes; + + // The root nodes + unsigned int numRootNodes = unsigned(rootNodes.size()); + if (numRootNodes == 1) { // a single root node: use it + mScene->mRootNode = ImportNode(mScene, r, meshOffsets, rootNodes[0]); + } else if (numRootNodes > 1) { // more than one root node: create a fake root + aiNode *root = new aiNode("ROOT"); + root->mChildren = new aiNode *[numRootNodes]; + for (unsigned int i = 0; i < numRootNodes; ++i) { + aiNode *node = ImportNode(mScene, r, meshOffsets, rootNodes[i]); + node->mParent = root; + root->mChildren[root->mNumChildren++] = node; + } + mScene->mRootNode = root; + } } struct AnimationSamplers { - AnimationSamplers() - : translation(nullptr) - , rotation(nullptr) - , scale(nullptr) - , weight(nullptr) { - // empty - } - - Animation::Sampler* translation; - Animation::Sampler* rotation; - Animation::Sampler* scale; - Animation::Sampler* weight; + AnimationSamplers() : + translation(nullptr), + rotation(nullptr), + scale(nullptr), + weight(nullptr) { + // empty + } + + Animation::Sampler *translation; + Animation::Sampler *rotation; + Animation::Sampler *scale; + Animation::Sampler *weight; }; -aiNodeAnim* CreateNodeAnim(glTF2::Asset& r, Node& node, AnimationSamplers& samplers) -{ - aiNodeAnim* anim = new aiNodeAnim(); - anim->mNodeName = GetNodeName(node); - - static const float kMillisecondsFromSeconds = 1000.f; - - if (samplers.translation) { - float* times = nullptr; - samplers.translation->input->ExtractData(times); - aiVector3D* values = nullptr; - samplers.translation->output->ExtractData(values); - anim->mNumPositionKeys = static_cast(samplers.translation->input->count); - anim->mPositionKeys = new aiVectorKey[anim->mNumPositionKeys]; - for (unsigned int i = 0; i < anim->mNumPositionKeys; ++i) { - anim->mPositionKeys[i].mTime = times[i] * kMillisecondsFromSeconds; - anim->mPositionKeys[i].mValue = values[i]; - } - delete[] times; - delete[] values; - } else if (node.translation.isPresent) { - anim->mNumPositionKeys = 1; - anim->mPositionKeys = new aiVectorKey[anim->mNumPositionKeys]; - anim->mPositionKeys->mTime = 0.f; - anim->mPositionKeys->mValue.x = node.translation.value[0]; - anim->mPositionKeys->mValue.y = node.translation.value[1]; - anim->mPositionKeys->mValue.z = node.translation.value[2]; - } - - if (samplers.rotation) { - float* times = nullptr; - samplers.rotation->input->ExtractData(times); - aiQuaternion* values = nullptr; - samplers.rotation->output->ExtractData(values); - anim->mNumRotationKeys = static_cast(samplers.rotation->input->count); - anim->mRotationKeys = new aiQuatKey[anim->mNumRotationKeys]; - for (unsigned int i = 0; i < anim->mNumRotationKeys; ++i) { - anim->mRotationKeys[i].mTime = times[i] * kMillisecondsFromSeconds; - anim->mRotationKeys[i].mValue.x = values[i].w; - anim->mRotationKeys[i].mValue.y = values[i].x; - anim->mRotationKeys[i].mValue.z = values[i].y; - anim->mRotationKeys[i].mValue.w = values[i].z; - } - delete[] times; - delete[] values; - } else if (node.rotation.isPresent) { - anim->mNumRotationKeys = 1; - anim->mRotationKeys = new aiQuatKey[anim->mNumRotationKeys]; - anim->mRotationKeys->mTime = 0.f; - anim->mRotationKeys->mValue.x = node.rotation.value[0]; - anim->mRotationKeys->mValue.y = node.rotation.value[1]; - anim->mRotationKeys->mValue.z = node.rotation.value[2]; - anim->mRotationKeys->mValue.w = node.rotation.value[3]; - } - - if (samplers.scale) { - float* times = nullptr; - samplers.scale->input->ExtractData(times); - aiVector3D* values = nullptr; - samplers.scale->output->ExtractData(values); - anim->mNumScalingKeys = static_cast(samplers.scale->input->count); - anim->mScalingKeys = new aiVectorKey[anim->mNumScalingKeys]; - for (unsigned int i = 0; i < anim->mNumScalingKeys; ++i) { - anim->mScalingKeys[i].mTime = times[i] * kMillisecondsFromSeconds; - anim->mScalingKeys[i].mValue = values[i]; - } - delete[] times; - delete[] values; - } else if (node.scale.isPresent) { - anim->mNumScalingKeys = 1; - anim->mScalingKeys = new aiVectorKey[anim->mNumScalingKeys]; - anim->mScalingKeys->mTime = 0.f; - anim->mScalingKeys->mValue.x = node.scale.value[0]; - anim->mScalingKeys->mValue.y = node.scale.value[1]; - anim->mScalingKeys->mValue.z = node.scale.value[2]; - } - - return anim; +aiNodeAnim *CreateNodeAnim(glTF2::Asset &r, Node &node, AnimationSamplers &samplers) { + aiNodeAnim *anim = new aiNodeAnim(); + anim->mNodeName = GetNodeName(node); + + static const float kMillisecondsFromSeconds = 1000.f; + + if (samplers.translation) { + float *times = nullptr; + samplers.translation->input->ExtractData(times); + aiVector3D *values = nullptr; + samplers.translation->output->ExtractData(values); + anim->mNumPositionKeys = static_cast(samplers.translation->input->count); + anim->mPositionKeys = new aiVectorKey[anim->mNumPositionKeys]; + for (unsigned int i = 0; i < anim->mNumPositionKeys; ++i) { + anim->mPositionKeys[i].mTime = times[i] * kMillisecondsFromSeconds; + anim->mPositionKeys[i].mValue = values[i]; + } + delete[] times; + delete[] values; + } else if (node.translation.isPresent) { + anim->mNumPositionKeys = 1; + anim->mPositionKeys = new aiVectorKey[anim->mNumPositionKeys]; + anim->mPositionKeys->mTime = 0.f; + anim->mPositionKeys->mValue.x = node.translation.value[0]; + anim->mPositionKeys->mValue.y = node.translation.value[1]; + anim->mPositionKeys->mValue.z = node.translation.value[2]; + } + + if (samplers.rotation) { + float *times = nullptr; + samplers.rotation->input->ExtractData(times); + aiQuaternion *values = nullptr; + samplers.rotation->output->ExtractData(values); + anim->mNumRotationKeys = static_cast(samplers.rotation->input->count); + anim->mRotationKeys = new aiQuatKey[anim->mNumRotationKeys]; + for (unsigned int i = 0; i < anim->mNumRotationKeys; ++i) { + anim->mRotationKeys[i].mTime = times[i] * kMillisecondsFromSeconds; + anim->mRotationKeys[i].mValue.x = values[i].w; + anim->mRotationKeys[i].mValue.y = values[i].x; + anim->mRotationKeys[i].mValue.z = values[i].y; + anim->mRotationKeys[i].mValue.w = values[i].z; + } + delete[] times; + delete[] values; + } else if (node.rotation.isPresent) { + anim->mNumRotationKeys = 1; + anim->mRotationKeys = new aiQuatKey[anim->mNumRotationKeys]; + anim->mRotationKeys->mTime = 0.f; + anim->mRotationKeys->mValue.x = node.rotation.value[0]; + anim->mRotationKeys->mValue.y = node.rotation.value[1]; + anim->mRotationKeys->mValue.z = node.rotation.value[2]; + anim->mRotationKeys->mValue.w = node.rotation.value[3]; + } + + if (samplers.scale) { + float *times = nullptr; + samplers.scale->input->ExtractData(times); + aiVector3D *values = nullptr; + samplers.scale->output->ExtractData(values); + anim->mNumScalingKeys = static_cast(samplers.scale->input->count); + anim->mScalingKeys = new aiVectorKey[anim->mNumScalingKeys]; + for (unsigned int i = 0; i < anim->mNumScalingKeys; ++i) { + anim->mScalingKeys[i].mTime = times[i] * kMillisecondsFromSeconds; + anim->mScalingKeys[i].mValue = values[i]; + } + delete[] times; + delete[] values; + } else if (node.scale.isPresent) { + anim->mNumScalingKeys = 1; + anim->mScalingKeys = new aiVectorKey[anim->mNumScalingKeys]; + anim->mScalingKeys->mTime = 0.f; + anim->mScalingKeys->mValue.x = node.scale.value[0]; + anim->mScalingKeys->mValue.y = node.scale.value[1]; + anim->mScalingKeys->mValue.z = node.scale.value[2]; + } + + return anim; } -aiMeshMorphAnim* CreateMeshMorphAnim(glTF2::Asset& r, Node& node, AnimationSamplers& samplers) -{ - aiMeshMorphAnim* anim = new aiMeshMorphAnim(); - anim->mName = GetNodeName(node); - - static const float kMillisecondsFromSeconds = 1000.f; - - if (nullptr != samplers.weight) { - float* times = nullptr; - samplers.weight->input->ExtractData(times); - float* values = nullptr; - samplers.weight->output->ExtractData(values); - anim->mNumKeys = static_cast(samplers.weight->input->count); - - const unsigned int numMorphs = (unsigned int)samplers.weight->output->count / anim->mNumKeys; - - anim->mKeys = new aiMeshMorphKey[anim->mNumKeys]; - unsigned int k = 0u; - for (unsigned int i = 0u; i < anim->mNumKeys; ++i) { - anim->mKeys[i].mTime = times[i] * kMillisecondsFromSeconds; - anim->mKeys[i].mNumValuesAndWeights = numMorphs; - anim->mKeys[i].mValues = new unsigned int[numMorphs]; - anim->mKeys[i].mWeights = new double[numMorphs]; - - for (unsigned int j = 0u; j < numMorphs; ++j, ++k) { - anim->mKeys[i].mValues[j] = j; - anim->mKeys[i].mWeights[j] = ( 0.f > values[k] ) ? 0.f : values[k]; - } - } - - delete[] times; - delete[] values; - } - - return anim; +aiMeshMorphAnim *CreateMeshMorphAnim(glTF2::Asset &r, Node &node, AnimationSamplers &samplers) { + aiMeshMorphAnim *anim = new aiMeshMorphAnim(); + anim->mName = GetNodeName(node); + + static const float kMillisecondsFromSeconds = 1000.f; + + if (nullptr != samplers.weight) { + float *times = nullptr; + samplers.weight->input->ExtractData(times); + float *values = nullptr; + samplers.weight->output->ExtractData(values); + anim->mNumKeys = static_cast(samplers.weight->input->count); + + const unsigned int numMorphs = (unsigned int)samplers.weight->output->count / anim->mNumKeys; + + anim->mKeys = new aiMeshMorphKey[anim->mNumKeys]; + unsigned int k = 0u; + for (unsigned int i = 0u; i < anim->mNumKeys; ++i) { + anim->mKeys[i].mTime = times[i] * kMillisecondsFromSeconds; + anim->mKeys[i].mNumValuesAndWeights = numMorphs; + anim->mKeys[i].mValues = new unsigned int[numMorphs]; + anim->mKeys[i].mWeights = new double[numMorphs]; + + for (unsigned int j = 0u; j < numMorphs; ++j, ++k) { + anim->mKeys[i].mValues[j] = j; + anim->mKeys[i].mWeights[j] = (0.f > values[k]) ? 0.f : values[k]; + } + } + + delete[] times; + delete[] values; + } + + return anim; } -std::unordered_map GatherSamplers(Animation& anim) -{ - std::unordered_map samplers; - for (unsigned int c = 0; c < anim.channels.size(); ++c) { - Animation::Channel& channel = anim.channels[c]; - if (channel.sampler >= static_cast(anim.samplers.size())) { - continue; - } - - const unsigned int node_index = channel.target.node.GetIndex(); - - AnimationSamplers& sampler = samplers[node_index]; - if (channel.target.path == AnimationPath_TRANSLATION) { - sampler.translation = &anim.samplers[channel.sampler]; - } else if (channel.target.path == AnimationPath_ROTATION) { - sampler.rotation = &anim.samplers[channel.sampler]; - } else if (channel.target.path == AnimationPath_SCALE) { - sampler.scale = &anim.samplers[channel.sampler]; - } else if (channel.target.path == AnimationPath_WEIGHTS) { - sampler.weight = &anim.samplers[channel.sampler]; - } - } - - return samplers; +std::unordered_map GatherSamplers(Animation &anim) { + std::unordered_map samplers; + for (unsigned int c = 0; c < anim.channels.size(); ++c) { + Animation::Channel &channel = anim.channels[c]; + if (channel.sampler >= static_cast(anim.samplers.size())) { + continue; + } + + const unsigned int node_index = channel.target.node.GetIndex(); + + AnimationSamplers &sampler = samplers[node_index]; + if (channel.target.path == AnimationPath_TRANSLATION) { + sampler.translation = &anim.samplers[channel.sampler]; + } else if (channel.target.path == AnimationPath_ROTATION) { + sampler.rotation = &anim.samplers[channel.sampler]; + } else if (channel.target.path == AnimationPath_SCALE) { + sampler.scale = &anim.samplers[channel.sampler]; + } else if (channel.target.path == AnimationPath_WEIGHTS) { + sampler.weight = &anim.samplers[channel.sampler]; + } + } + + return samplers; } -void glTF2Importer::ImportAnimations(glTF2::Asset& r) -{ - if (!r.scene) return; - - mScene->mNumAnimations = r.animations.Size(); - if (mScene->mNumAnimations == 0) { - return; - } - - mScene->mAnimations = new aiAnimation*[mScene->mNumAnimations]; - for (unsigned int i = 0; i < r.animations.Size(); ++i) { - Animation& anim = r.animations[i]; - - aiAnimation* ai_anim = new aiAnimation(); - ai_anim->mName = anim.name; - ai_anim->mDuration = 0; - ai_anim->mTicksPerSecond = 0; - - std::unordered_map samplers = GatherSamplers(anim); - - uint32_t numChannels = 0u; - uint32_t numMorphMeshChannels = 0u; - - for (auto& iter : samplers) { - if ((nullptr != iter.second.rotation) || (nullptr != iter.second.scale) || (nullptr != iter.second.translation)) { - ++numChannels; - } - if (nullptr != iter.second.weight) { - ++numMorphMeshChannels; - } - } - - ai_anim->mNumChannels = numChannels; - if (ai_anim->mNumChannels > 0) { - ai_anim->mChannels = new aiNodeAnim*[ai_anim->mNumChannels]; - int j = 0; - for (auto& iter : samplers) { - if ((nullptr != iter.second.rotation) || (nullptr != iter.second.scale) || (nullptr != iter.second.translation)) { - ai_anim->mChannels[j] = CreateNodeAnim(r, r.nodes[iter.first], iter.second); - ++j; - } - } - } - - ai_anim->mNumMorphMeshChannels = numMorphMeshChannels; - if (ai_anim->mNumMorphMeshChannels > 0) { - ai_anim->mMorphMeshChannels = new aiMeshMorphAnim*[ai_anim->mNumMorphMeshChannels]; - int j = 0; - for (auto& iter : samplers) { - if (nullptr != iter.second.weight) { - ai_anim->mMorphMeshChannels[j] = CreateMeshMorphAnim(r, r.nodes[iter.first], iter.second); - ++j; - } - } - } - - // Use the latest keyframe for the duration of the animation - double maxDuration = 0; - unsigned int maxNumberOfKeys = 0; - for (unsigned int j = 0; j < ai_anim->mNumChannels; ++j) { - auto chan = ai_anim->mChannels[j]; - if (chan->mNumPositionKeys) { - auto lastPosKey = chan->mPositionKeys[chan->mNumPositionKeys - 1]; - if (lastPosKey.mTime > maxDuration) { - maxDuration = lastPosKey.mTime; - } - maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumPositionKeys); - } - if (chan->mNumRotationKeys) { - auto lastRotKey = chan->mRotationKeys[chan->mNumRotationKeys - 1]; - if (lastRotKey.mTime > maxDuration) { - maxDuration = lastRotKey.mTime; - } - maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumRotationKeys); - } - if (chan->mNumScalingKeys) { - auto lastScaleKey = chan->mScalingKeys[chan->mNumScalingKeys - 1]; - if (lastScaleKey.mTime > maxDuration) { - maxDuration = lastScaleKey.mTime; - } - maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumScalingKeys); - } - } - - for (unsigned int j = 0; j < ai_anim->mNumMorphMeshChannels; ++j) { - const auto* const chan = ai_anim->mMorphMeshChannels[j]; - - if (0u != chan->mNumKeys) { - const auto& lastKey = chan->mKeys[chan->mNumKeys - 1u]; - if (lastKey.mTime > maxDuration) { - maxDuration = lastKey.mTime; - } - maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumKeys); - } - } - - ai_anim->mDuration = maxDuration; - ai_anim->mTicksPerSecond = 1000.0; - - mScene->mAnimations[i] = ai_anim; - } +void glTF2Importer::ImportAnimations(glTF2::Asset &r) { + if (!r.scene) return; + + mScene->mNumAnimations = r.animations.Size(); + if (mScene->mNumAnimations == 0) { + return; + } + + mScene->mAnimations = new aiAnimation *[mScene->mNumAnimations]; + for (unsigned int i = 0; i < r.animations.Size(); ++i) { + Animation &anim = r.animations[i]; + + aiAnimation *ai_anim = new aiAnimation(); + ai_anim->mName = anim.name; + ai_anim->mDuration = 0; + ai_anim->mTicksPerSecond = 0; + + std::unordered_map samplers = GatherSamplers(anim); + + uint32_t numChannels = 0u; + uint32_t numMorphMeshChannels = 0u; + + for (auto &iter : samplers) { + if ((nullptr != iter.second.rotation) || (nullptr != iter.second.scale) || (nullptr != iter.second.translation)) { + ++numChannels; + } + if (nullptr != iter.second.weight) { + ++numMorphMeshChannels; + } + } + + ai_anim->mNumChannels = numChannels; + if (ai_anim->mNumChannels > 0) { + ai_anim->mChannels = new aiNodeAnim *[ai_anim->mNumChannels]; + int j = 0; + for (auto &iter : samplers) { + if ((nullptr != iter.second.rotation) || (nullptr != iter.second.scale) || (nullptr != iter.second.translation)) { + ai_anim->mChannels[j] = CreateNodeAnim(r, r.nodes[iter.first], iter.second); + ++j; + } + } + } + + ai_anim->mNumMorphMeshChannels = numMorphMeshChannels; + if (ai_anim->mNumMorphMeshChannels > 0) { + ai_anim->mMorphMeshChannels = new aiMeshMorphAnim *[ai_anim->mNumMorphMeshChannels]; + int j = 0; + for (auto &iter : samplers) { + if (nullptr != iter.second.weight) { + ai_anim->mMorphMeshChannels[j] = CreateMeshMorphAnim(r, r.nodes[iter.first], iter.second); + ++j; + } + } + } + + // Use the latest keyframe for the duration of the animation + double maxDuration = 0; + unsigned int maxNumberOfKeys = 0; + for (unsigned int j = 0; j < ai_anim->mNumChannels; ++j) { + auto chan = ai_anim->mChannels[j]; + if (chan->mNumPositionKeys) { + auto lastPosKey = chan->mPositionKeys[chan->mNumPositionKeys - 1]; + if (lastPosKey.mTime > maxDuration) { + maxDuration = lastPosKey.mTime; + } + maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumPositionKeys); + } + if (chan->mNumRotationKeys) { + auto lastRotKey = chan->mRotationKeys[chan->mNumRotationKeys - 1]; + if (lastRotKey.mTime > maxDuration) { + maxDuration = lastRotKey.mTime; + } + maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumRotationKeys); + } + if (chan->mNumScalingKeys) { + auto lastScaleKey = chan->mScalingKeys[chan->mNumScalingKeys - 1]; + if (lastScaleKey.mTime > maxDuration) { + maxDuration = lastScaleKey.mTime; + } + maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumScalingKeys); + } + } + + for (unsigned int j = 0; j < ai_anim->mNumMorphMeshChannels; ++j) { + const auto *const chan = ai_anim->mMorphMeshChannels[j]; + + if (0u != chan->mNumKeys) { + const auto &lastKey = chan->mKeys[chan->mNumKeys - 1u]; + if (lastKey.mTime > maxDuration) { + maxDuration = lastKey.mTime; + } + maxNumberOfKeys = std::max(maxNumberOfKeys, chan->mNumKeys); + } + } + + ai_anim->mDuration = maxDuration; + ai_anim->mTicksPerSecond = 1000.0; + + mScene->mAnimations[i] = ai_anim; + } } -void glTF2Importer::ImportEmbeddedTextures(glTF2::Asset& r) -{ - embeddedTexIdxs.resize(r.images.Size(), -1); +void glTF2Importer::ImportEmbeddedTextures(glTF2::Asset &r) { + embeddedTexIdxs.resize(r.images.Size(), -1); - int numEmbeddedTexs = 0; - for (size_t i = 0; i < r.images.Size(); ++i) { - if (r.images[i].HasData()) - numEmbeddedTexs += 1; - } + int numEmbeddedTexs = 0; + for (size_t i = 0; i < r.images.Size(); ++i) { + if (r.images[i].HasData()) + numEmbeddedTexs += 1; + } - if (numEmbeddedTexs == 0) - return; + if (numEmbeddedTexs == 0) + return; - mScene->mTextures = new aiTexture*[numEmbeddedTexs]; + mScene->mTextures = new aiTexture *[numEmbeddedTexs]; - // Add the embedded textures - for (size_t i = 0; i < r.images.Size(); ++i) { - Image &img = r.images[i]; - if (!img.HasData()) continue; + // Add the embedded textures + for (size_t i = 0; i < r.images.Size(); ++i) { + Image &img = r.images[i]; + if (!img.HasData()) continue; - int idx = mScene->mNumTextures++; - embeddedTexIdxs[i] = idx; + int idx = mScene->mNumTextures++; + embeddedTexIdxs[i] = idx; - aiTexture* tex = mScene->mTextures[idx] = new aiTexture(); + aiTexture *tex = mScene->mTextures[idx] = new aiTexture(); - size_t length = img.GetDataLength(); - void* data = img.StealData(); + size_t length = img.GetDataLength(); + void *data = img.StealData(); - tex->mWidth = static_cast(length); - tex->mHeight = 0; - tex->pcData = reinterpret_cast(data); + tex->mWidth = static_cast(length); + tex->mHeight = 0; + tex->pcData = reinterpret_cast(data); - if (!img.mimeType.empty()) { - const char* ext = strchr(img.mimeType.c_str(), '/') + 1; - if (ext) { - if (strcmp(ext, "jpeg") == 0) ext = "jpg"; + if (!img.mimeType.empty()) { + const char *ext = strchr(img.mimeType.c_str(), '/') + 1; + if (ext) { + if (strcmp(ext, "jpeg") == 0) ext = "jpg"; - size_t len = strlen(ext); - if (len <= 3) { - strcpy(tex->achFormatHint, ext); - } - } - } - } + size_t len = strlen(ext); + if (len <= 3) { + strcpy(tex->achFormatHint, ext); + } + } + } + } } -void glTF2Importer::InternReadFile(const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler) -{ - // clean all member arrays - meshOffsets.clear(); - embeddedTexIdxs.clear(); +void glTF2Importer::InternReadFile(const std::string &pFile, aiScene *pScene, IOSystem *pIOHandler) { + // clean all member arrays + meshOffsets.clear(); + embeddedTexIdxs.clear(); - this->mScene = pScene; + this->mScene = pScene; - // read the asset file - glTF2::Asset asset(pIOHandler); - asset.Load(pFile, GetExtension(pFile) == "glb"); + // read the asset file + glTF2::Asset asset(pIOHandler); + asset.Load(pFile, GetExtension(pFile) == "glb"); - // - // Copy the data out - // + // + // Copy the data out + // - ImportEmbeddedTextures(asset); - ImportMaterials(asset); + ImportEmbeddedTextures(asset); + ImportMaterials(asset); - ImportMeshes(asset); + ImportMeshes(asset); - ImportCameras(asset); - ImportLights(asset); + ImportCameras(asset); + ImportLights(asset); - ImportNodes(asset); + ImportNodes(asset); - ImportAnimations(asset); + ImportAnimations(asset); - if (pScene->mNumMeshes == 0) { - pScene->mFlags |= AI_SCENE_FLAGS_INCOMPLETE; - } + if (pScene->mNumMeshes == 0) { + pScene->mFlags |= AI_SCENE_FLAGS_INCOMPLETE; + } } #endif // ASSIMP_BUILD_NO_GLTF_IMPORTER - From 17257cd2aeed5de4b86b95fc258f79436a860ff2 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 16 Nov 2019 15:51:26 +0100 Subject: [PATCH 64/74] just a try, i dunno have a clue ... --- code/Common/DefaultLogger.cpp | 4 ++-- code/Material/MaterialSystem.cpp | 6 +++--- code/PostProcessing/ValidateDataStructure.cpp | 11 +++++++---- code/glTF2/glTF2Importer.cpp | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/code/Common/DefaultLogger.cpp b/code/Common/DefaultLogger.cpp index de3528d2b4..eee53bd7cc 100644 --- a/code/Common/DefaultLogger.cpp +++ b/code/Common/DefaultLogger.cpp @@ -107,7 +107,7 @@ LogStream* LogStream::createDefaultStream(aiDefaultLogStream streams, return nullptr; #endif - // Platform-independent default streams + // Platform-independent default streams case aiDefaultLogStream_STDERR: return new StdOStreamLogStream(std::cerr); case aiDefaultLogStream_STDOUT: @@ -121,7 +121,7 @@ LogStream* LogStream::createDefaultStream(aiDefaultLogStream streams, }; // For compilers without dead code path detection - return NULL; + return nullptr; } // ---------------------------------------------------------------------------------- diff --git a/code/Material/MaterialSystem.cpp b/code/Material/MaterialSystem.cpp index 0be6e9f7bb..fabd9415a0 100644 --- a/code/Material/MaterialSystem.cpp +++ b/code/Material/MaterialSystem.cpp @@ -471,12 +471,12 @@ aiReturn aiMaterial::AddBinaryProperty (const void* pInput, aiPropertyTypeInfo pType ) { - ai_assert( pInput != NULL ); - ai_assert( pKey != NULL ); + ai_assert( pInput != nullptr ); + ai_assert(pKey != nullptr ); ai_assert( 0 != pSizeInBytes ); if ( 0 == pSizeInBytes ) { - + return AI_FAILURE; } // first search the list whether there is already an entry with this key diff --git a/code/PostProcessing/ValidateDataStructure.cpp b/code/PostProcessing/ValidateDataStructure.cpp index 75d1b6ef78..b7f56a5829 100644 --- a/code/PostProcessing/ValidateDataStructure.cpp +++ b/code/PostProcessing/ValidateDataStructure.cpp @@ -603,15 +603,18 @@ void ValidateDSProcess::SearchForInvalidTextures(const aiMaterial* pMaterial, ReportError("%s #%i is set, but there are only %i %s textures", szType,iIndex,iNumIndices,szType); } - if (!iNumIndices)return; + if (!iNumIndices) { + return; + } std::vector mappings(iNumIndices); // Now check whether all UV indices are valid ... bool bNoSpecified = true; - for (unsigned int i = 0; i < pMaterial->mNumProperties;++i) - { + for (unsigned int i = 0; i < pMaterial->mNumProperties;++i) { aiMaterialProperty* prop = pMaterial->mProperties[i]; - if (prop->mSemantic != type)continue; + if (prop->mSemantic != type) { + continue; + } if ((int)prop->mIndex >= iNumIndices) { diff --git a/code/glTF2/glTF2Importer.cpp b/code/glTF2/glTF2Importer.cpp index 3dffa4b278..584a2717f5 100644 --- a/code/glTF2/glTF2Importer.cpp +++ b/code/glTF2/glTF2Importer.cpp @@ -200,6 +200,9 @@ inline void SetMaterialTextureProperty(std::vector &embeddedTexIdxs, Asset uri.length = 1 + ASSIMP_itoa10(uri.data + 1, MAXLEN - 1, texIdx); } + mat->AddProperty(&uri, AI_MATKEY_TEXTURE(texType, texSlot)); + mat->AddProperty(&prop.texCoord, 1, _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, texType, texSlot); + if (prop.textureTransformSupported) { aiUVTransform transform; transform.mTranslation.x = prop.TextureTransformExt_t.offset[0]; @@ -210,9 +213,6 @@ inline void SetMaterialTextureProperty(std::vector &embeddedTexIdxs, Asset mat->AddProperty(&transform, 1, _AI_MATKEY_UVTRANSFORM_BASE, texType, texSlot); } - mat->AddProperty(&uri, AI_MATKEY_TEXTURE(texType, texSlot)); - mat->AddProperty(&prop.texCoord, 1, _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, texType, texSlot); - if (prop.texture->sampler) { Ref sampler = prop.texture->sampler; From 75204e20bdc9a2a71cc1c183d14c549576e3d1f4 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 16 Nov 2019 19:22:37 +0100 Subject: [PATCH 65/74] fix invalid setup for texture enum. --- code/glTF2/glTF2Importer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/code/glTF2/glTF2Importer.cpp b/code/glTF2/glTF2Importer.cpp index 584a2717f5..43eabdab73 100644 --- a/code/glTF2/glTF2Importer.cpp +++ b/code/glTF2/glTF2Importer.cpp @@ -201,7 +201,6 @@ inline void SetMaterialTextureProperty(std::vector &embeddedTexIdxs, Asset } mat->AddProperty(&uri, AI_MATKEY_TEXTURE(texType, texSlot)); - mat->AddProperty(&prop.texCoord, 1, _AI_MATKEY_GLTF_TEXTURE_TEXCOORD_BASE, texType, texSlot); if (prop.textureTransformSupported) { aiUVTransform transform; From 41ae01a6b2545699a2d5ec1b93e20f957e5fe3cc Mon Sep 17 00:00:00 2001 From: bzt Date: Mon, 18 Nov 2019 03:04:52 +0100 Subject: [PATCH 66/74] Upgraded to newest SDK and improved texture import --- code/M3D/M3DExporter.cpp | 37 +- code/M3D/M3DExporter.h | 2 + code/M3D/M3DImporter.cpp | 64 +- code/M3D/M3DMaterials.h | 4 +- code/M3D/m3d.h | 2279 ++++++++++++++++++++++--------- test/unit/utM3DImportExport.cpp | 2 +- 6 files changed, 1741 insertions(+), 647 deletions(-) diff --git a/code/M3D/M3DExporter.cpp b/code/M3D/M3DExporter.cpp index c22943396e..b1c7ebdbab 100644 --- a/code/M3D/M3DExporter.cpp +++ b/code/M3D/M3DExporter.cpp @@ -169,6 +169,33 @@ void M3DExporter::doExport ( outfile.reset(); } + +// ------------------------------------------------------------------------------------------------ +// helper to add a vertex (private to NodeWalk) +m3dv_t *M3DExporter::AddVrtx(m3dv_t *vrtx, uint32_t *numvrtx, m3dv_t *v, uint32_t *idx) +{ + 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; + vrtx = (m3dv_t*)M3D_REALLOC(vrtx, ((*numvrtx) + 1) * sizeof(m3dv_t)); + memcpy(&vrtx[*numvrtx], v, sizeof(m3dv_t)); + *idx = *numvrtx; + (*numvrtx)++; + return vrtx; +} + +// ------------------------------------------------------------------------------------------------ +// helper to add a tmap (private to NodeWalk) +m3dti_t *M3DExporter::AddTmap(m3dti_t *tmap, uint32_t *numtmap, m3dti_t *ti, uint32_t *idx) +{ + tmap = (m3dti_t*)M3D_REALLOC(tmap, ((*numtmap) + 1) * sizeof(m3dti_t)); + memcpy(&tmap[*numtmap], ti, sizeof(m3dti_t)); + *idx = *numtmap; + (*numtmap)++; + return tmap; +} + // ------------------------------------------------------------------------------------------------ // recursive node walker void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) @@ -221,25 +248,23 @@ void M3DExporter::NodeWalk(const aiNode* pNode, aiMatrix4x4 m) 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, + m3d->vertex = 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->tmap = 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); + vertex.color = 0; + m3d->vertex = AddVrtx(m3d->vertex, &m3d->numvertex, &vertex, &idx); m3d->face[n].normal[k] = (M3D_INDEX)idx; } } diff --git a/code/M3D/M3DExporter.h b/code/M3D/M3DExporter.h index dfcff8bc93..58d8d597e5 100644 --- a/code/M3D/M3DExporter.h +++ b/code/M3D/M3DExporter.h @@ -87,6 +87,8 @@ namespace Assimp // helper to do the recursive walking void NodeWalk(const aiNode* pNode, aiMatrix4x4 m); + m3dv_t *AddVrtx(m3dv_t *vrtx, uint32_t *numvrtx, m3dv_t *v, uint32_t *idx); + m3dti_t *AddTmap(m3dti_t *tmap, uint32_t *numtmap, m3dti_t *ti, uint32_t *idx); uint32_t mkColor(aiColor4D* c); M3D_INDEX addMaterial(const aiMaterial *mat); void addProp(m3dm_t *m, uint8_t type, uint32_t value); diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index fcff49df76..0156950c31 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -44,6 +44,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define M3D_IMPLEMENTATION #define M3D_ASCII +#define M3D_NONORMALS /* leave the post-processing to Assimp */ +#define M3D_NOWEIGHTS +#define M3D_NOANIMATION #include #include @@ -104,16 +107,21 @@ extern "C" { std::string file(fn); std::unique_ptr pStream( (reinterpret_cast(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)) { + size_t fileSize = 0; + unsigned char *data = NULL; + // sometimes pStream is nullptr for some reason (should be an empty object returning nothing I guess) + if(pStream) { + fileSize = pStream->FileSize(); + // should be allocated with malloc(), because the library will call free() to deallocate + 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 = 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; } @@ -307,7 +315,7 @@ void M3DImporter::importMaterials() 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); + mat->AddProperty(&name, aiTxProps[k].pKey, aiTxProps[k].type, aiTxProps[k].index); n = 0; mat->AddProperty(&n, 1, _AI_MATKEY_UVWSRC_BASE, aiProps[k].type, aiProps[k].index); } @@ -321,6 +329,7 @@ void M3DImporter::importMaterials() void M3DImporter::importTextures() { unsigned int i; + const char *formatHint[] = { "rgba0800", "rgba0808", "rgba8880", "rgba8888" }; m3dtx_t *t; ai_assert(mScene != nullptr); @@ -334,14 +343,29 @@ void M3DImporter::importTextures() mScene->mTextures = new aiTexture*[m3d->numtexture]; for(i = 0; i < m3d->numtexture; i++) { + unsigned int j, k; t = &m3d->texture[i]; + if(!t->w || !t->h || !t->f || !t->d) continue; aiTexture *tx = new aiTexture; - strcpy(tx->achFormatHint, "rgba8888"); + strcpy(tx->achFormatHint, formatHint[t->f - 1]); 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); + for(j = k = 0; j < tx->mWidth*tx->mHeight; j++) { + switch(t->f) { + case 1: tx->pcData[j].g = t->d[k++]; break; + case 2: tx->pcData[j].g = t->d[k++]; tx->pcData[j].a = t->d[k++]; break; + case 3: + tx->pcData[j].r = t->d[k++]; tx->pcData[j].g = t->d[k++]; + tx->pcData[j].b = t->d[k++]; tx->pcData[j].a = 255; + break; + case 4: + tx->pcData[j].r = t->d[k++]; tx->pcData[j].g = t->d[k++]; + tx->pcData[j].b = t->d[k++]; tx->pcData[j].a = t->d[k++]; + break; + } + } mScene->mTextures[i] = tx; } } @@ -574,15 +598,15 @@ void M3DImporter::convertPose(aiMatrix4x4 *m, unsigned int posid, unsigned int o 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; + m->a1 = 1 - 2 * (q->y * q->y + q->z * q->z); if(m->a1 > -M3D_EPSILON && m->a1 < M3D_EPSILON) m->a1 = 0.0; + m->a2 = 2 * (q->x * q->y - q->z * q->w); if(m->a2 > -M3D_EPSILON && m->a2 < M3D_EPSILON) m->a2 = 0.0; + m->a3 = 2 * (q->x * q->z + q->y * q->w); if(m->a3 > -M3D_EPSILON && m->a3 < M3D_EPSILON) m->a3 = 0.0; + m->b1 = 2 * (q->x * q->y + q->z * q->w); if(m->b1 > -M3D_EPSILON && m->b1 < M3D_EPSILON) m->b1 = 0.0; + m->b2 = 1 - 2 * (q->x * q->x + q->z * q->z); if(m->b2 > -M3D_EPSILON && m->b2 < M3D_EPSILON) m->b2 = 0.0; + m->b3 = 2 * (q->y * q->z - q->x * q->w); if(m->b3 > -M3D_EPSILON && m->b3 < M3D_EPSILON) m->b3 = 0.0; + m->c1 = 2 * (q->x * q->z - q->y * q->w); if(m->c1 > -M3D_EPSILON && m->c1 < M3D_EPSILON) m->c1 = 0.0; + m->c2 = 2 * (q->y * q->z + q->x * q->w); if(m->c2 > -M3D_EPSILON && m->c2 < M3D_EPSILON) m->c2 = 0.0; + m->c3 = 1 - 2 * (q->x * q->x + q->y * q->y); if(m->c3 > -M3D_EPSILON && m->c3 < M3D_EPSILON) m->c3 = 0.0; } /* set translation */ diff --git a/code/M3D/M3DMaterials.h b/code/M3D/M3DMaterials.h index fa02cf42be..3d6fe246d1 100644 --- a/code/M3D/M3DMaterials.h +++ b/code/M3D/M3DMaterials.h @@ -75,7 +75,7 @@ static const aiMatProp aiProps[] = { { AI_MATKEY_REFLECTIVITY }, /* m3dp_Pm */ { NULL, 0, 0 }, /* m3dp_Ps */ { AI_MATKEY_REFRACTI }, /* m3dp_Ni */ - { NULL, 0, 0 }, + { NULL, 0, 0 }, /* m3dp_Nt */ { NULL, 0, 0 }, { NULL, 0, 0 }, { NULL, 0, 0 } @@ -97,7 +97,7 @@ static const aiMatProp aiTxProps[] = { { 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 }, /* m3dp_map_Nt */ { NULL, 0, 0 }, { NULL, 0, 0 }, { NULL, 0, 0 } diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index 9ace802ef5..767e15d5af 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -58,8 +58,15 @@ extern "C" { #define M3D_APIVERSION 0x0100 #ifndef M3D_DOUBLE typedef float M3D_FLOAT; +#ifndef M3D_EPSILON +/* carefully choosen for IEEE 754 don't change */ +#define M3D_EPSILON ((M3D_FLOAT)1e-7) +#endif #else typedef double M3D_FLOAT; +#ifndef M3D_EPSILON +#define M3D_EPSILON ((M3D_FLOAT)1e-14) +#endif #endif #if !defined(M3D_SMALLINDEX) typedef uint32_t M3D_INDEX; @@ -96,23 +103,35 @@ typedef uint16_t M3D_INDEX; * 3DMO m3dchunk_t file header chunk, may followed by compressed data * HEAD m3dhdr_t model header chunk * n x m3dchunk_t more chunks follow + * PRVW preview chunk (optional) * 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 + * n x m3ds_t skin group records * 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) + * MESH* triangle mesh (vertex index list) or + * SHPE* mathematical shapes like parameterized surfaces + * LBLS* annotation label chunks, can be more (optional) * 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 + * + * Typical chunks for a game engine: 3DMO, HEAD, CMAP, TMAP, VRTS, BONE, MTRL, MESH, ACTN, OMD3 + * Typical chunks for CAD software: 3DMO, HEAD, PRVW, CMAP, TMAP, VRTS, MTRL, SHPE, LBLS, OMD3 */ +#ifdef _MSC_VER +#pragma pack(push) +#pragma pack(1) +#endif + typedef struct { char magic[4]; uint32_t length; @@ -125,6 +144,10 @@ typedef struct { uint32_t length; } _pack m3dchunk_t; +#ifdef _MSC_VER +#pragma pack(pop) +#endif + /*** in-memory model structure ***/ /* textmap entry */ @@ -132,19 +155,23 @@ typedef struct { M3D_FLOAT u; M3D_FLOAT v; } m3dti_t; +#define m3d_textureindex_t m3dti_t /* texture */ typedef struct { char *name; /* texture name */ - uint32_t *d; /* pixels data */ + uint8_t *d; /* pixels data */ uint16_t w; /* width */ uint16_t h; /* height */ -} _pack m3dtx_t; + uint8_t f; /* format, 1 = grayscale, 2 = grayscale+alpha, 3 = rgb, 4 = rgba */ +} m3dtx_t; +#define m3d_texturedata_t m3dtx_t typedef struct { M3D_INDEX vertexid; M3D_FLOAT weight; } m3dw_t; +#define m3d_weight_t m3dw_t /* bone entry */ typedef struct { @@ -156,12 +183,14 @@ typedef struct { m3dw_t *weight; /* weights for those vertices */ M3D_FLOAT mat4[16]; /* transformation matrix */ } m3db_t; +#define m3d_bone_t m3db_t /* skin: bone per vertex entry */ typedef struct { M3D_INDEX boneid[M3D_NUMBONE]; M3D_FLOAT weight[M3D_NUMBONE]; } m3ds_t; +#define m3d_skin_t m3ds_t /* vertex entry */ typedef struct { @@ -171,7 +200,11 @@ typedef struct { M3D_FLOAT w; uint32_t color; /* default vertex color */ M3D_INDEX skinid; /* skin index */ +#ifdef M3D_VERTEXTYPE + uint8_t type; +#endif } m3dv_t; +#define m3d_vertex_t m3dv_t /* material property formats */ enum { @@ -210,6 +243,7 @@ enum { m3dp_Pm, m3dp_Ps, m3dp_Ni, + m3dp_Nt, m3dp_map_Kd = 128, /* textured display map properties */ m3dp_map_Ka, @@ -224,7 +258,8 @@ enum { m3dp_map_Pr = 192, /* textured physical map properties */ m3dp_map_Pm, m3dp_map_Ps, - m3dp_map_Ni + m3dp_map_Ni, + m3dp_map_Nt }; enum { /* aliases */ m3dp_bump = m3dp_map_Km, @@ -241,6 +276,7 @@ typedef struct { M3D_INDEX textureid; /* if value is a texture, m3dpf_map */ } value; } m3dp_t; +#define m3d_property_t m3dp_t /* material entry */ typedef struct { @@ -248,6 +284,7 @@ typedef struct { uint8_t numprop; /* number of properties */ m3dp_t *prop; /* properties array */ } m3dm_t; +#define m3d_material_t m3dm_t /* face entry */ typedef struct { @@ -256,6 +293,107 @@ typedef struct { M3D_INDEX normal[3]; /* normal vectors */ M3D_INDEX texcoord[3]; /* UV coordinates */ } m3df_t; +#define m3d_face_t m3df_t + +/* shape command types. must match the row in m3d_commandtypes */ +enum { + /* special commands */ + m3dc_use = 0, /* use material */ + m3dc_inc, /* include another shape */ + m3dc_mesh, /* include part of polygon mesh */ + /* approximations */ + m3dc_div, /* subdivision by constant resolution for both u, v */ + m3dc_sub, /* subdivision by constant, different for u and v */ + m3dc_len, /* spacial subdivision by maxlength */ + m3dc_dist, /* subdivision by maxdistance and maxangle */ + /* modifiers */ + m3dc_degu, /* degree for both u, v */ + m3dc_deg, /* separate degree for u and v */ + m3dc_rangeu, /* range for u */ + m3dc_range, /* range for u and v */ + m3dc_paru, /* u parameters (knots) */ + m3dc_parv, /* v parameters */ + m3dc_trim, /* outer trimming curve */ + m3dc_hole, /* inner trimming curve */ + m3dc_scrv, /* spacial curve */ + m3dc_sp, /* special points */ + /* helper curves */ + m3dc_bez1, /* Bezier 1D */ + m3dc_bsp1, /* B-spline 1D */ + m3dc_bez2, /* bezier 2D */ + m3dc_bsp2, /* B-spline 2D */ + /* surfaces */ + m3dc_bezun, /* Bezier 3D with control, UV, normal */ + m3dc_bezu, /* with control and UV */ + m3dc_bezn, /* with control and normal */ + m3dc_bez, /* control points only */ + m3dc_nurbsun, /* B-spline 3D */ + m3dc_nurbsu, + m3dc_nurbsn, + m3dc_nurbs, + m3dc_conn, /* connect surfaces */ + /* geometrical */ + m3dc_line, + m3dc_polygon, + m3dc_circle, + m3dc_cylinder, + m3dc_shpere, + m3dc_torus, + m3dc_cube +}; + +/* shape command argument types */ +enum { + m3dcp_mi_t = 1, /* material index */ + m3dcp_hi_t, /* shape index */ + m3dcp_fi_t, /* face index */ + m3dcp_ti_t, /* texture map index */ + m3dcp_vi_t, /* vertex index */ + m3dcp_qi_t, /* vertex index for quaternions */ + m3dcp_vc_t, /* coordinate or radius, float scalar */ + m3dcp_i1_t, /* int8 scalar */ + m3dcp_i2_t, /* int16 scalar */ + m3dcp_i4_t, /* int32 scalar */ + m3dcp_va_t /* variadic arguments */ +}; + +#define M3D_CMDMAXARG 8 /* if you increase this, add more arguments to the macro below */ +typedef struct { +#ifdef M3D_ASCII +#define M3D_CMDDEF(t,n,p,a,b,c,d,e,f,g,h) { (char*)(n), (p), { (a), (b), (c), (d), (e), (f), (g), (h) } } + char *key; +#else +#define M3D_CMDDEF(t,n,p,a,b,c,d,e,f,g,h) { (p), { (a), (b), (c), (d), (e), (f), (g), (h) } } +#endif + uint8_t p; + uint8_t a[M3D_CMDMAXARG]; +} m3dcd_t; + +/* shape command */ +typedef struct { + uint16_t type; /* shape type */ + uint32_t *arg; /* arguments array */ +} m3dc_t; +#define m3d_shapecommand_t m3dc_t + +/* shape entry */ +typedef struct { + char *name; /* name of the mathematical shape */ + M3D_INDEX group; /* group this shape belongs to or -1 */ + uint32_t numcmd; /* number of commands */ + m3dc_t *cmd; /* commands array */ +} m3dh_t; +#define m3d_shape_t m3dh_t + +/* label entry */ +typedef struct { + char *name; /* name of the annotation group or NULL */ + char *lang; /* language code or NULL */ + char *text; /* the label text */ + uint32_t color; /* color */ + M3D_INDEX vertexid; /* the vertex the label refers to */ +} m3dl_t; +#define m3d_label_t m3dl_t /* frame transformations / working copy skeleton entry */ typedef struct { @@ -263,6 +401,7 @@ typedef struct { M3D_INDEX pos; /* vertex index new position */ M3D_INDEX ori; /* vertex index new orientation (quaternion) */ } m3dtr_t; +#define m3d_transform_t m3dtr_t /* animation frame entry */ typedef struct { @@ -270,6 +409,7 @@ typedef struct { M3D_INDEX numtransform; /* number of transformations in this frame */ m3dtr_t *transform; /* transformations */ } m3dfr_t; +#define m3d_frame_t m3dfr_t /* model action entry */ typedef struct { @@ -278,6 +418,7 @@ typedef struct { M3D_INDEX numframe; /* number of frames in this animation */ m3dfr_t *frame; /* frames array */ } m3da_t; +#define m3d_action_t m3da_t /* inlined asset */ typedef struct { @@ -285,17 +426,19 @@ typedef struct { uint8_t *data; /* compressed asset data */ uint32_t length; /* compressed data length */ } m3di_t; +#define m3d_inlinedasset_t m3di_t /*** in-memory model structure ***/ #define M3D_FLG_FREERAW (1<<0) #define M3D_FLG_FREESTR (1<<1) #define M3D_FLG_MTLLIB (1<<2) +#define M3D_FLG_GENNORM (1<<3) 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 vc_s, vi_s, si_s, ci_s, ti_s, bi_s, nb_s, sk_s, fc_s, hi_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. */ @@ -316,13 +459,18 @@ typedef struct { M3D_INDEX nummaterial; m3dm_t *material; /* material list */ M3D_INDEX numface; - m3df_t *face; /* model face, triangle mesh */ + m3df_t *face; /* model face, polygon (triangle) mesh */ + M3D_INDEX numshape; + m3dh_t *shape; /* model face, shape commands */ + M3D_INDEX numlabel; + m3dl_t *label; /* annotation labels */ 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_INDEX numextra; + m3dchunk_t **extra; /* unknown chunks, application / engine specific data probably */ + m3di_t preview; /* preview chunk */ } m3d_t; /*** export parameters ***/ @@ -355,12 +503,14 @@ typedef struct { #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_UNKCMD -70 +#define M3D_ERR_TRUNC -71 +#define M3D_ERR_CMAP -72 +#define M3D_ERR_TMAP -73 +#define M3D_ERR_VRTS -74 +#define M3D_ERR_BONE -75 +#define M3D_ERR_MTRL -76 +#define M3D_ERR_SHPE -77 #define M3D_ERR_ISFATAL(x) ((x) < 0 && (x) > -65) @@ -381,8 +531,6 @@ m3dtr_t *m3d_frame(m3d_t *model, M3D_INDEX actionid, M3D_INDEX frameid, m3dtr_t 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 ***/ @@ -404,11 +552,58 @@ static m3dpd_t m3d_propertytypes[] = { 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) */ + M3D_PROPERTYDEF(m3dpf_float, m3dp_Nt, "Nt"), /* thickness of face in millimeter, for printing */ /* aliases, note that "map_*" aliases are handled automatically */ M3D_PROPERTYDEF(m3dpf_map, m3dp_map_Km, "bump"), M3D_PROPERTYDEF(m3dpf_map, m3dp_map_Pm, "refl") }; +/* shape command definitions. if more commands start with the same string, the longer must come first */ +static m3dcd_t m3d_commandtypes[] = { + /* technical */ + M3D_CMDDEF(m3dc_use, "use", 1, m3dcp_mi_t, 0, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_inc, "inc", 3, m3dcp_hi_t, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vi_t, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_mesh, "mesh", 1, m3dcp_fi_t, m3dcp_fi_t, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vi_t, 0, 0, 0), + /* approximations */ + M3D_CMDDEF(m3dc_div, "div", 1, m3dcp_vc_t, 0, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_sub, "sub", 2, m3dcp_vc_t, m3dcp_vc_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_len, "len", 1, m3dcp_vc_t, 0, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_dist, "dist", 2, m3dcp_vc_t, m3dcp_vc_t, 0, 0, 0, 0, 0, 0), + /* modifiers */ + M3D_CMDDEF(m3dc_degu, "degu", 1, m3dcp_i1_t, 0, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_deg, "deg", 2, m3dcp_i1_t, m3dcp_i1_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_rangeu, "rangeu", 1, m3dcp_ti_t, 0, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_range, "range", 2, m3dcp_ti_t, m3dcp_ti_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_paru, "paru", 2, m3dcp_va_t, m3dcp_vc_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_parv, "parv", 2, m3dcp_va_t, m3dcp_vc_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_trim, "trim", 3, m3dcp_va_t, m3dcp_ti_t, m3dcp_i2_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_hole, "hole", 3, m3dcp_va_t, m3dcp_ti_t, m3dcp_i2_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_scrv, "scrv", 3, m3dcp_va_t, m3dcp_ti_t, m3dcp_i2_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_sp, "sp", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + /* helper curves */ + M3D_CMDDEF(m3dc_bez1, "bez1", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_bsp1, "bsp1", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_bez2, "bez2", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_bsp2, "bsp2", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + /* surfaces */ + M3D_CMDDEF(m3dc_bezun, "bezun", 4, m3dcp_va_t, m3dcp_vi_t, m3dcp_ti_t, m3dcp_vi_t, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_bezu, "bezu", 3, m3dcp_va_t, m3dcp_vi_t, m3dcp_ti_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_bezn, "bezn", 3, m3dcp_va_t, m3dcp_vi_t, m3dcp_vi_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_bez, "bez", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_nurbsun, "nurbsun", 4, m3dcp_va_t, m3dcp_vi_t, m3dcp_ti_t, m3dcp_vi_t, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_nurbsu, "nurbsu", 3, m3dcp_va_t, m3dcp_vi_t, m3dcp_ti_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_nurbsn, "nurbsn", 3, m3dcp_va_t, m3dcp_vi_t, m3dcp_vi_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_nurbs, "nurbs", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_conn, "conn", 6, m3dcp_i2_t, m3dcp_ti_t, m3dcp_i2_t, m3dcp_i2_t, m3dcp_ti_t, m3dcp_i2_t, 0, 0), + /* geometrical */ + M3D_CMDDEF(m3dc_line, "line", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_polygon, "polygon", 2, m3dcp_va_t, m3dcp_vi_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_circle, "circle", 3, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_cylinder,"cylinder",6, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, 0, 0), + M3D_CMDDEF(m3dc_shpere, "shpere", 2, m3dcp_vi_t, m3dcp_vc_t, 0, 0, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_torus, "torus", 4, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, m3dcp_vc_t, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_cube, "cube", 3, m3dcp_vi_t, m3dcp_vi_t, m3dcp_vi_t, 0, 0, 0, 0, 0) +}; #endif #include @@ -1885,7 +2080,7 @@ static char *_m3d_gethex(char *s, uint32_t *ret) static char *_m3d_getint(char *s, uint32_t *ret) { char *e = s; - if(!s || !*s) return s; + if(!s || !*s || *s == '\r' || *s == '\n') return s; for(; *e >= '0' && *e <= '9'; e++); *ret = atoi(s); return e; @@ -1893,54 +2088,13 @@ static char *_m3d_getint(char *s, uint32_t *ret) static char *_m3d_getfloat(char *s, M3D_FLOAT *ret) { char *e = s; - if(!s || !*s) return s; + if(!s || !*s || *s == '\r' || *s == '\n') 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) { @@ -1985,12 +2139,26 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char unsigned int i, len = 0, w, h; unsigned char *buff = NULL; char *fn2; +#ifdef STBI__PNG_TYPE stbi__context s; stbi__result_info ri; +#endif + /* do we have loaded this texture already? */ for(i = 0; i < model->numtexture; i++) if(!strcmp(fn, model->texture[i].name)) return i; - if(readfilecb) { + /* see if it's inlined in the model */ + if(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; + } + } + /* try to load from external source */ + if(!buff && readfilecb) { i = strlen(fn); if(i < 5 || fn[i - 4] != '.') { fn2 = (char*)M3D_MALLOC(i + 5); @@ -2003,32 +2171,30 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char 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; + /* add to textures array */ 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->errcode = M3D_ERR_ALLOC; + return (M3D_INDEX)-1U; } + model->texture[i].name = fn; 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') { +#ifdef STBI__PNG_TYPE 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); + w = h = len = 0; + ri.bits_per_channel = 8; + model->texture[i].d = (uint8_t*)stbi__png_load(&s, (int*)&w, (int*)&h, (int*)&len, 0, &ri); model->texture[i].w = w; model->texture[i].h = h; + model->texture[i].f = (uint8_t)len; +#endif } else { #ifdef M3D_TX_INTERP if((model->errcode = M3D_TX_INTERP(fn, buff, len, &model->texture[i])) != M3D_SUCCESS) { @@ -2041,13 +2207,8 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char #endif } if(freecb) (*freecb)(buff); - if(!model->texture[i].d) { - M3D_FREE(model->texture[i].d); + if(!model->texture[i].d) model->errcode = M3D_ERR_UNKIMG; - model->numtexture--; - return (M3D_INDEX)-1U; - } - model->texture[i].name = fn; return i; } @@ -2085,8 +2246,8 @@ _inline static unsigned char *_m3d_getidx(unsigned char *data, char type, M3D_IN { 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; + case 2: *idx = *((uint16_t*)data) > 65533 ? *((int16_t*)data) : *((uint16_t*)data); data += 2; break; + case 4: *idx = *((int32_t*)data); data += 4; break; } return data; } @@ -2144,10 +2305,6 @@ void _m3d_inv(M3D_FLOAT *m) 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 && @@ -2190,30 +2347,35 @@ static M3D_FLOAT _m3d_rsq(M3D_FLOAT x) 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 + unsigned int i, j, k, l, n, am, len = 0, reclen, offs; + char *name, *lang; + float f; m3d_t *model; M3D_INDEX mi; M3D_FLOAT w; -#ifndef M3D_NOANIMATION - M3D_FLOAT r[16]; -#endif + m3dcd_t *cd; m3dtx_t *tx; + m3dh_t *h; m3dm_t *m; m3da_t *a; - m3db_t *b; m3di_t *t; +#ifndef M3D_NONORMALS + m3dv_t *norm = NULL, *v0, *v1, *v2, va, vb; +#endif +#ifndef M3D_NOANIMATION + M3D_FLOAT r[16]; +#endif +#if !defined(M3D_NOWEIGHTS) || !defined(M3D_NOANIMATION) + m3db_t *b; +#endif +#ifndef M3D_NOWEIGHTS m3ds_t *sk; +#endif #ifdef M3D_ASCII m3ds_t s; M3D_INDEX bi[M3D_BONEMAXLEVEL+1], level; const char *ol; - char *ptr, *pe; + char *ptr, *pe, *fn; #endif if(!data || (!M3D_CHUNKMAGIC(data, '3','D','M','O') @@ -2250,13 +2412,14 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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); + model->name = _m3d_safestr(ptr, 2); 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); + if(*ptr != '\r' && *ptr != '\n') + model->desc = _m3d_safestr(ptr, 3); while(*ptr) { while(*ptr && *ptr!='\n') ptr++; ptr++; if(*ptr=='\r') ptr++; @@ -2270,6 +2433,17 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d /* make sure there's at least one data row */ pe = ptr; ptr = _m3d_findnl(ptr); if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend; + /* Preview chunk */ + if(!memcmp(pe, "Preview", 7)) { + if(readfilecb) { + pe = _m3d_safestr(ptr, 0); + if(!pe || !*pe) goto asciiend; + model->preview.data = (*readfilecb)(pe, &model->preview.length); + M3D_FREE(pe); + } + while(*ptr && *ptr != '\r' && *ptr != '\n') + ptr = _m3d_findnl(ptr); + } else /* texture map chunk */ if(!memcmp(pe, "Textmap", 7)) { if(model->tmap) { M3D_LOG("More texture map chunks, should be unique"); goto asciiend; } @@ -2279,8 +2453,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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; + _m3d_getfloat(ptr, &model->tmap[i].v); ptr = _m3d_findnl(ptr); } } else @@ -2291,8 +2464,10 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d i = model->numvertex++; model->vertex = (m3dv_t*)M3D_REALLOC(model->vertex, model->numvertex * sizeof(m3dv_t)); if(!model->vertex) goto memerr; + memset(&model->vertex[i], 0, sizeof(m3dv_t)); model->vertex[i].skinid = (M3D_INDEX)-1U; model->vertex[i].color = 0; + model->vertex[i].w = (M3D_FLOAT)1.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); @@ -2300,7 +2475,6 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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); @@ -2308,7 +2482,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d } /* parse skin */ memset(&s, 0, sizeof(m3ds_t)); - for(j = 0; j < M3D_NUMBONE && *ptr && *ptr != '\r' && *ptr != '\n'; j++) { + for(j = 0, w = (M3D_FLOAT)0.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); @@ -2316,12 +2490,25 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d if(*ptr == ':') { ptr++; ptr = _m3d_getfloat(ptr, &s.weight[j]); + w += 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); + if(w != (M3D_FLOAT)1.0 && w != (M3D_FLOAT)0.0) + for(j = 0; j < M3D_NUMBONE && s.weight[j] > (M3D_FLOAT)0.0; j++) + s.weight[j] /= w; + k = -1U; + if(model->skin) { + for(j = 0; j < model->numskin; j++) + if(!memcmp(&model->skin[j], &s, sizeof(m3ds_t))) { k = j; break; } + } + if(k == -1U) { + k = model->numskin++; + model->skin = (m3ds_t*)M3D_REALLOC(model->skin, model->numskin * sizeof(m3ds_t)); + memcpy(&model->skin[k], &s, sizeof(m3ds_t)); + } model->vertex[i].skinid = (M3D_INDEX)k; } ptr = _m3d_findnl(ptr); @@ -2349,6 +2536,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d ptr = _m3d_findarg(ptr); if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend; model->bone[i].ori = (M3D_INDEX)k; + model->vertex[k].skinid = (M3D_INDEX)-2U; pe = _m3d_safestr(ptr, 0); if(!pe || !*pe) goto asciiend; model->bone[i].name = pe; @@ -2410,7 +2598,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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; + m->prop[j].type = n + (k == m3dpf_map && n < 128 ? 128 : 0); switch(k) { case m3dpf_color: ptr = _m3d_gethex(ptr, &m->prop[j].value.color); break; case m3dpf_uint8: @@ -2439,7 +2627,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d } if(!m->numprop) model->nummaterial--; } else - /* procedural, not implemented yet, skip chunk */ + /* procedural */ if(!memcmp(pe, "Procedural", 10)) { pe = _m3d_safestr(ptr, 0); _m3d_getpr(model, readfilecb, freecb, pe); @@ -2458,11 +2646,16 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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); + if(!strcmp(pe, model->material[j].name)) { mi = (M3D_INDEX)j; break; } + if(mi == (M3D_INDEX)-1U && !(model->flags & M3D_FLG_MTLLIB)) { + mi = model->nummaterial++; + model->material = (m3dm_t*)M3D_REALLOC(model->material, model->nummaterial * sizeof(m3dm_t)); + if(!model->material) goto memerr; + model->material[mi].name = pe; + model->material[mi].numprop = 1; + model->material[mi].prop = NULL; + } else + M3D_FREE(pe); } } else { i = model->numface++; @@ -2498,6 +2691,137 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d ptr = _m3d_findnl(ptr); } } else + /* mathematical shape */ + if(!memcmp(pe, "Shape", 5)) { + pe = _m3d_findarg(pe); + if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend; + pe = _m3d_safestr(pe, 0); + if(!pe || !*pe) goto asciiend; + i = model->numshape++; + model->shape = (m3dh_t*)M3D_REALLOC(model->shape, model->numshape * sizeof(m3ds_t)); + if(!model->shape) goto memerr; + h = &model->shape[i]; + h->name = pe; + h->group = (M3D_INDEX)-1U; + h->numcmd = 0; + h->cmd = NULL; + while(*ptr && *ptr != '\r' && *ptr != '\n') { + if(!memcmp(ptr, "group", 5)) { + ptr = _m3d_findarg(ptr); + ptr = _m3d_getint(ptr, &h->group); + ptr = _m3d_findnl(ptr); + if(h->group != (M3D_INDEX)-1U && h->group >= model->numbone) { + M3D_LOG("Unknown bone id as shape group in shape"); + M3D_LOG(pe); + h->group = (M3D_INDEX)-1U; + model->errcode = M3D_ERR_SHPE; + } + continue; + } + for(cd = NULL, k = 0; k < (unsigned int)(sizeof(m3d_commandtypes)/sizeof(m3d_commandtypes[0])); k++) { + j = strlen(m3d_commandtypes[k].key); + if(!memcmp(ptr, m3d_commandtypes[k].key, j) && (ptr[j] == ' ' || ptr[j] == '\r' || ptr[j] == '\n')) + { cd = &m3d_commandtypes[k]; break; } + } + if(cd) { + j = h->numcmd++; + h->cmd = (m3dc_t*)M3D_REALLOC(h->cmd, h->numcmd * sizeof(m3dc_t)); + if(!h->cmd) goto memerr; + h->cmd[j].type = k; + h->cmd[j].arg = (uint32_t*)M3D_MALLOC(cd->p * sizeof(uint32_t)); + if(!h->cmd[j].arg) goto memerr; + memset(h->cmd[j].arg, 0, cd->p * sizeof(uint32_t)); + for(k = n = 0, l = cd->p; k < l; k++) { + ptr = _m3d_findarg(ptr); + if(!*ptr) goto asciiend; + if(*ptr == '[') { + ptr = _m3d_findarg(ptr + 1); + if(!*ptr) goto asciiend; + } + if(*ptr == ']' || *ptr == '\r' || *ptr == '\n') break; + switch(cd->a[((k - n) % (cd->p - n)) + n]) { + case m3dcp_mi_t: + mi = (M3D_INDEX)-1U; + if(*ptr != '\r' && *ptr != '\n') { + pe = _m3d_safestr(ptr, 0); + if(!pe || !*pe) goto asciiend; + for(n = 0; n < model->nummaterial; n++) + if(!strcmp(pe, model->material[n].name)) { mi = (M3D_INDEX)n; break; } + if(mi == (M3D_INDEX)-1U && !(model->flags & M3D_FLG_MTLLIB)) { + mi = model->nummaterial++; + model->material = (m3dm_t*)M3D_REALLOC(model->material, + model->nummaterial * sizeof(m3dm_t)); + if(!model->material) goto memerr; + model->material[mi].name = pe; + model->material[mi].numprop = 1; + model->material[mi].prop = NULL; + } else + M3D_FREE(pe); + } + h->cmd[j].arg[k] = mi; + break; + case m3dcp_vc_t: + _m3d_getfloat(ptr, &w); + h->cmd[j].arg[k] = *((uint32_t*)&w); + break; + case m3dcp_va_t: + ptr = _m3d_getint(ptr, &h->cmd[j].arg[k]); + n = k + 1; l += (h->cmd[j].arg[k] - 1) * (cd->p - k - 1); + h->cmd[j].arg = (uint32_t*)M3D_REALLOC(h->cmd[j].arg, l * sizeof(uint32_t)); + if(!h->cmd[j].arg) goto memerr; + memset(&h->cmd[j].arg[k + 1], 0, (l - k - 1) * sizeof(uint32_t)); + break; + case m3dcp_qi_t: + ptr = _m3d_getint(ptr, &h->cmd[j].arg[k]); + model->vertex[h->cmd[i].arg[k]].skinid = (M3D_INDEX)-2U; + break; + default: + ptr = _m3d_getint(ptr, &h->cmd[j].arg[k]); + break; + } + } + } else { + M3D_LOG("Unknown shape command in"); + M3D_LOG(h->name); + model->errcode = M3D_ERR_UNKCMD; + } + ptr = _m3d_findnl(ptr); + } + if(!h->numcmd) model->numshape--; + } else + /* annotation labels */ + if(!memcmp(pe, "Labels", 6)) { + pe = _m3d_findarg(pe); + if(!*pe) goto asciiend; + if(*pe == '\r' || *pe == '\n') pe = NULL; + else pe = _m3d_safestr(pe, 0); + k = 0; fn = NULL; + while(*ptr && *ptr != '\r' && *ptr != '\n') { + if(*ptr == 'c') { + ptr = _m3d_findarg(ptr); + if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend; + ptr = _m3d_gethex(ptr, &k); + } else + if(*ptr == 'l') { + ptr = _m3d_findarg(ptr); + if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend; + fn = _m3d_safestr(ptr, 2); + } else { + i = model->numlabel++; + model->label = (m3dl_t*)M3D_REALLOC(model->label, model->numlabel * sizeof(m3dl_t)); + if(!model->label) goto memerr; + model->label[i].name = pe; + model->label[i].lang = fn; + model->label[i].color = k; + ptr = _m3d_getint(ptr, &j); + model->label[i].vertexid = (M3D_INDEX)j; + ptr = _m3d_findarg(ptr); + if(!*pe || *pe == '\r' || *pe == '\n') goto asciiend; + model->label[i].text = _m3d_safestr(ptr, 2); + } + ptr = _m3d_findnl(ptr); + } + } else /* action */ if(!memcmp(pe, "Action", 6)) { pe = _m3d_findarg(pe); @@ -2548,6 +2872,33 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d ptr = _m3d_getint(ptr, &k); if(!*ptr || *ptr == '\r' || *ptr == '\n') goto asciiend; a->frame[i].transform[j].ori = (M3D_INDEX)k; + model->vertex[k].skinid = (M3D_INDEX)-2U; + } + ptr = _m3d_findnl(ptr); + } + } else + /* inlined assets chunk */ + if(!memcmp(pe, "Assets", 6)) { + while(*ptr && *ptr != '\r' && *ptr != '\n') { + if(readfilecb) { + pe = _m3d_safestr(ptr, 2); + if(!pe || !*pe) goto asciiend; + i = model->numinlined++; + model->inlined = (m3di_t*)M3D_REALLOC(model->inlined, model->numinlined * sizeof(m3di_t)); + if(!model->inlined) goto memerr; + t = &model->inlined[i]; + model->inlined[i].data = (*readfilecb)(pe, &model->inlined[i].length); + if(model->inlined[i].data) { + fn = strrchr(pe, '.'); + if(fn && (fn[1] == 'p' || fn[1] == 'P') && (fn[2] == 'n' || fn[2] == 'N') && + (fn[3] == 'g' || fn[3] == 'G')) *fn = 0; + fn = strrchr(pe, '/'); + if(!fn) fn = strrchr(pe, '\\'); + if(!fn) fn = pe; else fn++; + model->inlined[i].name = _m3d_safestr(fn, 0); + } else + model->numinlined--; + M3D_FREE(pe); } ptr = _m3d_findnl(ptr); } @@ -2558,18 +2909,18 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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); + i = model->numextra++; + model->extra = (m3dchunk_t**)M3D_REALLOC(model->extra, model->numextra * sizeof(m3dchunk_t*)); + if(!model->extra) goto memerr; + model->extra[i] = (m3dchunk_t*)M3D_MALLOC(k + sizeof(m3dchunk_t)); + if(!model->extra[i]) goto memerr; + memcpy(&model->extra[i]->magic, pe, 4); + model->extra[i]->length = sizeof(m3dchunk_t); + pe = (char*)model->extra[i] + sizeof(m3dchunk_t); while(*ptr && *ptr != '\r' && *ptr != '\n') { ptr = _m3d_gethex(ptr, &k); *pe++ = (uint8_t)k; - model->unknown[i]->length++; + model->extra[i]->length++; } } else goto asciiend; @@ -2622,11 +2973,15 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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 */ + model->fc_s = 1 << ((model->raw->types >>16) & 3); /* frame counter size */ + model->hi_s = 1 << ((model->raw->types >>18) & 3); /* shape index size */ + model->fi_s = 1 << ((model->raw->types >>20) & 3); /* face index 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->fc_s == 8) model->fc_s = 0; + if(model->hi_s == 8) model->hi_s = 0; if(model->fi_s == 8) model->fi_s = 0; /* variable limit checks */ @@ -2635,7 +2990,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d 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)) { + model->bi_s > 2 || model->sk_s > 2 || model->fc_s > 2 || model->hi_s > 2 || model->fi_s > 2)) { M3D_LOG("32 bit indices not supported, unable to load model"); M3D_FREE(model); return NULL; @@ -2646,7 +3001,7 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d return NULL; } if(model->nb_s > M3D_NUMBONE) { - M3D_LOG("Model has more bones per vertex than importer supports"); + M3D_LOG("Model has more bones per vertex than what importer configured to support"); model->errcode = M3D_ERR_TRUNC; } @@ -2692,6 +3047,11 @@ memerr: M3D_LOG("Out of memory"); chunk += len; len -= sizeof(m3dchunk_t); + /* preview chunk */ + if(M3D_CHUNKMAGIC(data, 'P','R','V','W') && len > 0) { + model->preview.length = len; + model->preview.data = data + sizeof(m3dchunk_t); + } else /* color map */ if(M3D_CHUNKMAGIC(data, 'C','M','A','P')) { M3D_LOG("Color map"); @@ -2751,10 +3111,10 @@ memerr: M3D_LOG("Out of memory"); 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; + model->vertex[i].x = (M3D_FLOAT)(*((int16_t*)(data+0))) / 32767; + model->vertex[i].y = (M3D_FLOAT)(*((int16_t*)(data+2))) / 32767; + model->vertex[i].z = (M3D_FLOAT)(*((int16_t*)(data+4))) / 32767; + model->vertex[i].w = (M3D_FLOAT)(*((int16_t*)(data+6))) / 32767; data += 8; break; case 4: @@ -2797,15 +3157,6 @@ memerr: M3D_LOG("Out of memory"); } 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); @@ -2816,41 +3167,55 @@ memerr: M3D_LOG("Out of memory"); 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]); + if(model->numskin) { + model->skin = (m3ds_t*)M3D_MALLOC(model->numskin * sizeof(m3ds_t)); + if(!model->skin) goto memerr; + for(i = 0; data < chunk && 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; + } + 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, w = (M3D_FLOAT)0.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; + w += model->skin[i].weight[j]; + data = _m3d_getidx(data, model->bi_s, &model->skin[i].boneid[j]); + } } } + /* this can occur if model has more bones than what the importer is configured to handle */ + if(w != (M3D_FLOAT)1.0 && w != (M3D_FLOAT)0.0) { + for(j = 0; j < M3D_NUMBONE; j++) + model->skin[i].weight[j] /= w; + } } } } else /* material */ if(M3D_CHUNKMAGIC(data, 'M','T','R','L')) { data += sizeof(m3dchunk_t); - M3D_GETSTR(material); + M3D_GETSTR(name); M3D_LOG("Material"); - M3D_LOG(material); + M3D_LOG(name); 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)) { + if(!strcmp(name, model->material[i].name)) { model->errcode = M3D_ERR_MTRL; M3D_LOG("Multiple definitions for material"); - M3D_LOG(material); - material = NULL; + M3D_LOG(name); + name = NULL; break; } - if(material) { + if(name) { i = model->nummaterial++; if(model->flags & M3D_FLG_MTLLIB) { m = model->material; @@ -2870,9 +3235,8 @@ memerr: M3D_LOG("Out of memory"); } 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)); + m->name = name; + m->prop = (m3dp_t*)M3D_MALLOC((len / 2) * sizeof(m3dp_t)); if(!m->prop) goto memerr; while(data < chunk) { i = m->numprop++; @@ -2899,12 +3263,12 @@ memerr: M3D_LOG("Out of memory"); 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); + M3D_GETSTR(name); + m->prop[i].value.textureid = _m3d_gettx(model, readfilecb, freecb, name); 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); + M3D_LOG(m->name); m->numprop--; } break; @@ -2924,10 +3288,10 @@ memerr: M3D_LOG("Out of memory"); /* face */ if(M3D_CHUNKMAGIC(data, 'P','R','O','C')) { /* procedural surface */ - M3D_GETSTR(material); + M3D_GETSTR(name); M3D_LOG("Procedural surface"); - M3D_LOG(material); - _m3d_getpr(model, readfilecb, freecb, material); + M3D_LOG(name); + _m3d_getpr(model, readfilecb, freecb, name); } else if(M3D_CHUNKMAGIC(data, 'M','E','S','H')) { M3D_LOG("Mesh data"); @@ -2942,10 +3306,10 @@ memerr: M3D_LOG("Out of memory"); if(!n) { /* use material */ mi = (M3D_INDEX)-1U; - M3D_GETSTR(material); - if(material) { + M3D_GETSTR(name); + if(name) { for(j = 0; j < model->nummaterial; j++) - if(!strcmp(material, model->material[j].name)) { + if(!strcmp(name, model->material[j].name)) { mi = (M3D_INDEX)j; break; } @@ -2975,6 +3339,117 @@ memerr: M3D_LOG("Out of memory"); } model->face = (m3df_t*)M3D_REALLOC(model->face, model->numface * sizeof(m3df_t)); } else + if(M3D_CHUNKMAGIC(data, 'S','H','P','E')) { + /* mathematical shape */ + data += sizeof(m3dchunk_t); + M3D_GETSTR(name); + M3D_LOG("Mathematical Shape"); + M3D_LOG(name); + i = model->numshape++; + model->shape = (m3dh_t*)M3D_REALLOC(model->shape, model->numshape * sizeof(m3dh_t)); + if(!model->shape) goto memerr; + h = &model->shape[i]; + h->numcmd = 0; + h->cmd = NULL; + h->name = name; + h->group = (M3D_INDEX)-1U; + data = _m3d_getidx(data, model->bi_s, &h->group); + if(h->group != (M3D_INDEX)-1U && h->group >= model->numbone) { + M3D_LOG("Unknown bone id as shape group in shape"); + M3D_LOG(name); + h->group = (M3D_INDEX)-1U; + model->errcode = M3D_ERR_SHPE; + } + while(data < chunk) { + i = h->numcmd++; + h->cmd = (m3dc_t*)M3D_REALLOC(h->cmd, h->numcmd * sizeof(m3dc_t)); + if(!h->cmd) goto memerr; + h->cmd[i].type = *data++; + if(h->cmd[i].type & 0x80) { + h->cmd[i].type &= 0x7F; + h->cmd[i].type |= (*data++ << 7); + } + if(h->cmd[i].type >= (unsigned int)(sizeof(m3d_commandtypes)/sizeof(m3d_commandtypes[0]))) { + M3D_LOG("Unknown shape command in"); + M3D_LOG(h->name); + model->errcode = M3D_ERR_UNKCMD; + break; + } + cd = &m3d_commandtypes[h->cmd[i].type]; + h->cmd[i].arg = (uint32_t*)M3D_MALLOC(cd->p * sizeof(uint32_t)); + if(!h->cmd[i].arg) goto memerr; + memset(h->cmd[i].arg, 0, cd->p * sizeof(uint32_t)); + for(k = n = 0, l = cd->p; k < l; k++) + switch(cd->a[((k - n) % (cd->p - n)) + n]) { + case m3dcp_mi_t: + h->cmd[i].arg[k] = -1U; + M3D_GETSTR(name); + if(name) { + for(n = 0; n < model->nummaterial; n++) + if(!strcmp(name, model->material[n].name)) { + h->cmd[i].arg[k] = n; + break; + } + if(h->cmd[i].arg[k] == -1U) model->errcode = M3D_ERR_MTRL; + } + break; + case m3dcp_vc_t: + f = 0.0f; + switch(model->vc_s) { + case 1: f = (float)((int8_t)data[0]) / 127; break; + case 2: f = (float)(*((int16_t*)(data+0))) / 32767; break; + case 4: f = (float)(*((float*)(data+0))); break; + case 8: f = (float)(*((double*)(data+0))); break; + } + h->cmd[i].arg[k] = *((uint32_t*)&f); + data += model->vc_s; + break; + case m3dcp_hi_t: data = _m3d_getidx(data, model->hi_s, &h->cmd[i].arg[k]); break; + case m3dcp_fi_t: data = _m3d_getidx(data, model->fi_s, &h->cmd[i].arg[k]); break; + case m3dcp_ti_t: data = _m3d_getidx(data, model->ti_s, &h->cmd[i].arg[k]); break; + case m3dcp_qi_t: + case m3dcp_vi_t: data = _m3d_getidx(data, model->vi_s, &h->cmd[i].arg[k]); break; + case m3dcp_i1_t: data = _m3d_getidx(data, 1, &h->cmd[i].arg[k]); break; + case m3dcp_i2_t: data = _m3d_getidx(data, 2, &h->cmd[i].arg[k]); break; + case m3dcp_i4_t: data = _m3d_getidx(data, 4, &h->cmd[i].arg[k]); break; + case m3dcp_va_t: data = _m3d_getidx(data, 4, &h->cmd[i].arg[k]); + n = k + 1; l += (h->cmd[i].arg[k] - 1) * (cd->p - k - 1); + h->cmd[i].arg = (uint32_t*)M3D_REALLOC(h->cmd[i].arg, l * sizeof(uint32_t)); + if(!h->cmd[i].arg) goto memerr; + memset(&h->cmd[i].arg[k + 1], 0, (l - k - 1) * sizeof(uint32_t)); + break; + } + } + } else + /* annotation label list */ + if(M3D_CHUNKMAGIC(data, 'L','B','L','S')) { + data += sizeof(m3dchunk_t); + M3D_GETSTR(name); + M3D_GETSTR(lang); + M3D_LOG("Label list"); + if(name) { M3D_LOG(name); } + if(lang) { M3D_LOG(lang); } + if(model->ci_s && model->ci_s < 4 && !model->cmap) model->errcode = M3D_ERR_CMAP; + k = 0; + switch(model->ci_s) { + case 1: k = model->cmap ? model->cmap[data[0]] : 0; data++; break; + case 2: k = model->cmap ? model->cmap[*((uint16_t*)data)] : 0; data += 2; break; + case 4: k = *((uint32_t*)data); data += 4; break; + /* case 8: break; */ + } + reclen = model->vi_s + model->si_s; + i = model->numlabel; model->numlabel += len / reclen; + model->label = (m3dl_t*)M3D_REALLOC(model->label, model->numlabel * sizeof(m3dl_t)); + if(!model->label) goto memerr; + memset(&model->label[i], 0, (model->numlabel - i) * sizeof(m3dl_t)); + for(; data < chunk && i < model->numlabel; i++) { + model->label[i].name = name; + model->label[i].lang = lang; + model->label[i].color = k; + data = _m3d_getidx(data, model->vi_s, &model->label[i].vertexid); + M3D_GETSTR(model->label[i].text); + } + } else /* action */ if(M3D_CHUNKMAGIC(data, 'A','C','T','N')) { M3D_LOG("Action"); @@ -2995,7 +3470,7 @@ memerr: M3D_LOG("Out of memory"); 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); + data = _m3d_getidx(data, model->fc_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++) { @@ -3007,10 +3482,10 @@ memerr: M3D_LOG("Out of memory"); } } } 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; + i = model->numextra++; + model->extra = (m3dchunk_t**)M3D_REALLOC(model->extra, model->numextra * sizeof(m3dchunk_t*)); + if(!model->extra) goto memerr; + model->extra[i] = (m3dchunk_t*)data; } } /* calculate normals, normalize skin weights, create bone/vertex cross-references and calculate transform matrices */ @@ -3018,59 +3493,61 @@ memerr: M3D_LOG("Out of memory"); postprocess: #endif if(model) { + M3D_LOG("Post-process"); #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++) + for(i = 0, n = model->numvertex; 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]]; + 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; + if(!norm) { + norm = (m3dv_t*)M3D_MALLOC(model->numface * sizeof(m3dv_t)); + if(!norm) goto memerr; } - ni[i] = j; + v0 = &norm[i]; + v0->x = (va.y * vb.z) - (va.z * vb.y); + v0->y = (va.z * vb.x) - (va.x * vb.z); + v0->z = (va.x * vb.y) - (va.y * vb.x); + w = _m3d_rsq((v0->x * v0->x) + (v0->y * v0->y) + (v0->z * v0->z)); + v0->x *= w; v0->y *= w; v0->z *= w; + model->face[i].normal[0] = model->face[i].vertex[0] + n; + model->face[i].normal[1] = model->face[i].vertex[1] + n; + model->face[i].normal[2] = model->face[i].vertex[2] + n; } - if(ni && norm) { - vi = (M3D_INDEX*)M3D_MALLOC(model->numvertex * sizeof(M3D_INDEX)); - if(!vi) goto memerr; + /* this is the fast way, we don't care if a normal is repeated in model->vertex */ + if(norm) { + M3D_LOG("Generating normals"); + model->flags |= M3D_FLG_GENNORM; + model->numvertex <<= 1; + model->vertex = (m3dv_t*)M3D_REALLOC(model->vertex, model->numvertex * sizeof(m3dv_t)); + if(!model->vertex) goto memerr; + memset(&model->vertex[n], 0, n * sizeof(m3dv_t)); + for(i = 0; i < model->numface; i++) + for(j = 0; j < 3; j++) { + v0 = &model->vertex[model->face[i].vertex[j] + n]; + v0->x += norm[i].x; + v0->y += norm[i].y; + v0->z += norm[i].z; + } /* 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(i = 0, v0 = &model->vertex[n]; i < n; i++, v0++) { + w = _m3d_rsq((v0->x * v0->x) + (v0->y * v0->y) + (v0->z * v0->z)); + v0->x *= w; v0->y *= w; v0->z *= w; + v0->skinid = -1U; } - 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 + M3D_LOG("Generating weight cross-reference"); for(i = 0; i < model->numvertex; i++) { - if(model->vertex[i].skinid < M3D_INDEXMAX) { + if(model->vertex[i].skinid < model->numskin) { 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++) @@ -3088,6 +3565,7 @@ memerr: M3D_LOG("Out of memory"); } #endif #ifndef M3D_NOANIMATION + M3D_LOG("Calculating bone transformation matrices"); for(i = 0; i < model->numbone; i++) { b = &model->bone[i]; if(model->bone[i].parent == (M3D_INDEX)-1U) { @@ -3155,7 +3633,7 @@ gen: s = 0; m3db_t *m3d_pose(m3d_t *model, M3D_INDEX actionid, uint32_t msec) { unsigned int i, j, l; - M3D_FLOAT r[16], t, d; + M3D_FLOAT r[16], t, c, d, s; m3db_t *ret; m3dv_t *v, *p, *f; m3dtr_t *tmp; @@ -3208,7 +3686,7 @@ m3db_t *m3d_pose(m3d_t *model, M3D_INDEX actionid, uint32_t msec) tmp[fr->transform[i].boneid].ori = fr->transform[i].ori; } for(i = 0, j = model->numvertex; i < model->numbone; i++) { - /* LERP interpolation of position */ + /* interpolation of position */ if(ret[i].pos != tmp[i].pos) { p = &model->vertex[ret[i].pos]; f = &model->vertex[tmp[i].pos]; @@ -3218,18 +3696,33 @@ m3db_t *m3d_pose(m3d_t *model, M3D_INDEX actionid, uint32_t msec) 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) */ + /* interpolation of orientation */ 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 = p->w * f->w + p->x * f->x + p->y * f->y + p->z * f->z; + if(d < 0) { d = -d; s = (M3D_FLOAT)-1.0; } else s = (M3D_FLOAT)1.0; +#if 0 + /* don't use SLERP, requires two more variables, libm linkage and it is slow (but nice) */ + a = (M3D_FLOAT)1.0 - t; b = t; + if(d < (M3D_FLOAT)0.999999) { c = acosf(d); b = 1 / sinf(c); a = sinf(a * c) * b; b *= sinf(t * c) * s; } + v->x = p->x * a + f->x * b; + v->y = p->y * a + f->y * b; + v->z = p->z * a + f->z * b; + v->w = p->w * a + f->w * b; +#else + /* approximated NLERP, original approximation by Arseny Kapoulkine, heavily optimized by me */ + c = t - (M3D_FLOAT)0.5; t += t * c * (t - (M3D_FLOAT)1.0) * (((M3D_FLOAT)1.0904 + d * ((M3D_FLOAT)-3.2452 + + d * ((M3D_FLOAT)3.55645 - d * (M3D_FLOAT)1.43519))) * c * c + ((M3D_FLOAT)0.848013 + d * + ((M3D_FLOAT)-1.06021 + d * (M3D_FLOAT)0.215638))); + v->x = p->x + t * (s * f->x - p->x); + v->y = p->y + t * (s * f->y - p->y); + v->z = p->z + t * (s * f->z - p->z); + v->w = p->w + t * (s * 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; +#endif ret[i].ori = j++; } } @@ -3271,6 +3764,10 @@ void m3d_free(m3d_t *model) for(i = 0; i < model->numbone; i++) if(model->bone[i].name) M3D_FREE(model->bone[i].name); + if(model->shape) + for(i = 0; i < model->numshape; i++) + if(model->shape[i].name) + M3D_FREE(model->shape[i].name); if(model->material) for(i = 0; i < model->nummaterial; i++) if(model->material[i].name) @@ -3283,10 +3780,36 @@ void m3d_free(m3d_t *model) 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]); + if(model->inlined) + for(i = 0; i < model->numinlined; i++) { + if(model->inlined[i].name) + M3D_FREE(model->inlined[i].name); + if(model->inlined[i].data) + M3D_FREE(model->inlined[i].data); + } + if(model->extra) + for(i = 0; i < model->numextra; i++) + if(model->extra[i]) + M3D_FREE(model->extra[i]); + if(model->label) + for(i = 0; i < model->numlabel; i++) { + if(model->label[i].name) { + for(j = i + 1; j < model->numlabel; j++) + if(model->label[j].name == model->label[i].name) + model->label[j].name = NULL; + M3D_FREE(model->label[i].name); + } + if(model->label[i].lang) { + for(j = i + 1; j < model->numlabel; j++) + if(model->label[j].lang == model->label[i].lang) + model->label[j].lang = NULL; + M3D_FREE(model->label[i].lang); + } + if(model->label[i].text) + M3D_FREE(model->label[i].text); + } + if(model->preview.data) + M3D_FREE(model->preview.data); } #endif if(model->flags & M3D_FLG_FREERAW) M3D_FREE(model->raw); @@ -3301,6 +3824,16 @@ void m3d_free(m3d_t *model) if(model->skin) M3D_FREE(model->skin); if(model->vertex) M3D_FREE(model->vertex); if(model->face) M3D_FREE(model->face); + if(model->shape) { + for(i = 0; i < model->numshape; i++) { + if(model->shape[i].cmd) { + for(j = 0; j < model->shape[i].numcmd; j++) + if(model->shape[i].cmd[j].arg) M3D_FREE(model->shape[i].cmd[j].arg); + M3D_FREE(model->shape[i].cmd); + } + } + M3D_FREE(model->shape); + } 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); @@ -3321,8 +3854,9 @@ void m3d_free(m3d_t *model) } M3D_FREE(model->action); } + if(model->label) M3D_FREE(model->label); if(model->inlined) M3D_FREE(model->inlined); - if(model->unknown) M3D_FREE(model->unknown); + if(model->extra) M3D_FREE(model->extra); free(model); } #endif @@ -3333,6 +3867,31 @@ typedef struct { uint32_t offs; } m3dstr_t; +typedef struct { + m3dti_t data; + M3D_INDEX oldidx; + M3D_INDEX newidx; +} m3dtisave_t; + +typedef struct { + m3dv_t data; + M3D_INDEX oldidx; + M3D_INDEX newidx; + unsigned char norm; +} m3dvsave_t; + +typedef struct { + m3ds_t data; + M3D_INDEX oldidx; + M3D_INDEX newidx; +} m3dssave_t; + +typedef struct { + m3df_t data; + int group; + uint8_t opacity; +} m3dfsave_t; + /* create unique list of strings */ static m3dstr_t *_m3d_addstr(m3dstr_t *str, uint32_t *numstr, char *s) { @@ -3387,6 +3946,41 @@ static uint32_t _m3d_stridx(m3dstr_t *str, uint32_t numstr, char *s) return 0; } +/* compare to faces by their material */ +static int _m3d_facecmp(const void *a, const void *b) { + const m3dfsave_t *A = (const m3dfsave_t*)a, *B = (const m3dfsave_t*)b; + return A->group != B->group ? A->group - B->group : (A->opacity != B->opacity ? (int)B->opacity - (int)A->opacity : + (int)A->data.materialid - (int)B->data.materialid); +} +/* compare face groups */ +static int _m3d_grpcmp(const void *a, const void *b) { return *((uint32_t*)a) - *((uint32_t*)b); } +/* compare UVs */ +static int _m3d_ticmp(const void *a, const void *b) { return memcmp(a, b, sizeof(m3dti_t)); } +/* compare skin groups */ +static int _m3d_skincmp(const void *a, const void *b) { return memcmp(a, b, sizeof(m3ds_t)); } +/* compare vertices */ +static int _m3d_vrtxcmp(const void *a, const void *b) { + int c = memcmp(a, b, 3 * sizeof(M3D_FLOAT)); + if(c) return c; + c = ((m3dvsave_t*)a)->norm - ((m3dvsave_t*)b)->norm; + if(c) return c; + return memcmp(a, b, sizeof(m3dv_t)); +} +/* compare labels */ +_inline int _m3d_strcmp(char *a, char *b) +{ + if(a == NULL && b != NULL) return -1; + if(a != NULL && b == NULL) return 1; + if(a == NULL && b == NULL) return 0; + return strcmp(a, b); +} +static int _m3d_lblcmp(const void *a, const void *b) { + const m3dl_t *A = (const m3dl_t*)a, *B = (const m3dl_t*)b; + int c = _m3d_strcmp(A->lang, B->lang); + if(!c) c = _m3d_strcmp(A->name, B->name); + if(!c) c = _m3d_strcmp(A->text, B->text); + return c; +} /* compare two colors by HSV value */ _inline static int _m3d_cmapcmp(const void *a, const void *b) { @@ -3421,60 +4015,13 @@ static uint32_t *_m3d_addcmap(uint32_t *cmap, uint32_t *numcmap, uint32_t color) static uint32_t _m3d_cmapidx(uint32_t *cmap, uint32_t numcmap, uint32_t color) { uint32_t i; + if(numcmap >= 65536) + return color; 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) { @@ -3495,23 +4042,27 @@ static void _m3d_round(int quality, m3dv_t *src, m3dv_t *dst) /* 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; + t = src->x * 127 + (src->x >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->x = (M3D_FLOAT)t / 127; + t = src->y * 127 + (src->y >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->y = (M3D_FLOAT)t / 127; + t = src->z * 127 + (src->z >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->z = (M3D_FLOAT)t / 127; + t = src->w * 127 + (src->w >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); 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; + t = src->x * 32767 + (src->x >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->x = (M3D_FLOAT)t / 32767; + t = src->y * 32767 + (src->y >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->y = (M3D_FLOAT)t / 32767; + t = src->z * 32767 + (src->z >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->z = (M3D_FLOAT)t / 32767; + t = src->w * 32767 + (src->w >= 0 ? (M3D_FLOAT)0.5 : (M3D_FLOAT)-0.5); dst->w = (M3D_FLOAT)t / 32767; break; } + if(dst->x == (M3D_FLOAT)-0.0) dst->x = (M3D_FLOAT)0.0; + if(dst->y == (M3D_FLOAT)-0.0) dst->y = (M3D_FLOAT)0.0; + if(dst->z == (M3D_FLOAT)-0.0) dst->z = (M3D_FLOAT)0.0; + if(dst->w == (M3D_FLOAT)-0.0) dst->w = (M3D_FLOAT)0.0; } #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) +static char *_m3d_prtbone(char *ptr, m3db_t *bone, M3D_INDEX numbone, M3D_INDEX parent, uint32_t level, M3D_INDEX *vrtxidx) { uint32_t i, j; char *sn; @@ -3521,9 +4072,9 @@ static char *_m3d_prtbone(char *ptr, m3db_t *bone, M3D_INDEX numbone, M3D_INDEX 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); + ptr += sprintf(ptr, "%d %d %s\r\n", vrtxidx[bone[i].pos], vrtxidx[bone[i].ori], sn); M3D_FREE(sn); - ptr = _m3d_prtbone(ptr, bone, numbone, i, level + 1); + ptr = _m3d_prtbone(ptr, bone, numbone, i, level + 1, vrtxidx); } } return ptr; @@ -3539,23 +4090,25 @@ unsigned char *m3d_save(m3d_t *model, int quality, int flags, unsigned int *size 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 vc_s, vi_s, si_s, ci_s, ti_s, bi_s, nb_s, sk_s, fc_s, hi_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; + unsigned char *out = NULL, *z = NULL, weights[M3D_NUMBONE], *norm = NULL; + unsigned int i, j, k, l, n, len, chunklen, *length; + M3D_FLOAT scale = (M3D_FLOAT)0.0, min_x, max_x, min_y, max_y, min_z, max_z; + M3D_INDEX last, *vrtxidx = NULL, *mtrlidx = NULL, *tmapidx = NULL, *skinidx = NULL; + uint32_t idx, numcmap = 0, *cmap = NULL, numvrtx = 0, maxvrtx = 0, numtmap = 0, maxtmap = 0, numproc = 0; + uint32_t numskin = 0, maxskin = 0, numstr = 0, maxt = 0, maxbone = 0, numgrp = 0, maxgrp = 0, *grpidx = NULL; + uint8_t *opa; + m3dcd_t *cd; + m3dc_t *cmd; 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; + m3dvsave_t *vrtx = NULL, vertex; + m3dtisave_t *tmap = NULL, tcoord; + m3dssave_t *skin = NULL, sk; + m3dfsave_t *face = NULL; m3dhdr_t *h = NULL; - m3dm_t *m, **mtrl = NULL; + m3dm_t *m; m3da_t *a; - M3D_INDEX last; if(!model) { if(size) *size = 0; @@ -3565,235 +4118,417 @@ unsigned char *m3d_save(m3d_t *model, int quality, int flags, unsigned int *size #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); + vrtxidx = (M3D_INDEX*)M3D_MALLOC(model->numvertex * sizeof(M3D_INDEX)); + if(!vrtxidx) goto memerr; + memset(vrtxidx, 255, model->numvertex * sizeof(M3D_INDEX)); + if(model->numvertex && !(flags & M3D_EXP_NONORMAL)){ + norm = (unsigned char*)M3D_MALLOC(model->numvertex * sizeof(unsigned char)); + if(!norm) goto memerr; + memset(norm, 0, model->numvertex * sizeof(unsigned char)); + } + if(model->nummaterial && !(flags & M3D_EXP_NOMATERIAL)) { + mtrlidx = (M3D_INDEX*)M3D_MALLOC(model->nummaterial * sizeof(M3D_INDEX)); + if(!mtrlidx) goto memerr; + memset(mtrlidx, 255, model->nummaterial * sizeof(M3D_INDEX)); + opa = (uint8_t*)M3D_MALLOC(model->nummaterial * 2 * sizeof(M3D_INDEX)); + if(!opa) goto memerr; + memset(opa, 255, model->nummaterial * 2 * sizeof(M3D_INDEX)); + } + if(model->numtmap && !(flags & M3D_EXP_NOTXTCRD)) { + tmapidx = (M3D_INDEX*)M3D_MALLOC(model->numtmap * sizeof(M3D_INDEX)); + if(!tmapidx) goto memerr; + memset(tmapidx, 255, model->numtmap * sizeof(M3D_INDEX)); + } + /** collect array elements that are actually referenced **/ + if(!(flags & M3D_EXP_NOFACE)) { + /* face */ + if(model->numface && model->face) { + M3D_LOG("Processing mesh face"); + face = (m3dfsave_t*)M3D_MALLOC(model->numface * sizeof(m3dfsave_t)); + if(!face) goto memerr; + for(i = 0; i < model->numface; i++) { + memcpy(&face[i].data, &model->face[i], sizeof(m3df_t)); + face[i].group = 0; + face[i].opacity = 255; + if(!(flags & M3D_EXP_NOMATERIAL) && model->face[i].materialid < model->nummaterial) { + if(model->material[model->face[i].materialid].numprop) { + mtrlidx[model->face[i].materialid] = 0; + if(opa[model->face[i].materialid * 2]) { + m = &model->material[model->face[i].materialid]; + for(j = 0; j < m->numprop; j++) + if(m->prop[j].type == m3dp_Kd) { + opa[model->face[i].materialid * 2 + 1] = ((uint8_t*)&m->prop[j].value.color)[3]; + break; + } + for(j = 0; j < m->numprop; j++) + if(m->prop[j].type == m3dp_d) { + opa[model->face[i].materialid * 2 + 1] = (uint8_t)(m->prop[j].value.fnum * 255); + break; + } + opa[model->face[i].materialid * 2] = 0; + } + face[i].opacity = opa[model->face[i].materialid * 2 + 1]; } else - vrtx = _m3d_addnorm(vrtx, &numvrtx, &model->vertex[k], &idx); - if(!vrtx) goto memerr; - face[i].normal[j] = (M3D_INDEX)idx; + face[i].data.materialid = (M3D_INDEX)-1U; } - 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; + for(j = 0; j < 3; j++) { + k = model->face[i].vertex[j]; + if(k < model->numvertex) + vrtxidx[k] = 0; + if(!(flags & M3D_EXP_NOCMAP)) { + cmap = _m3d_addcmap(cmap, &numcmap, model->vertex[k].color); + if(!cmap) goto memerr; } - 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; + k = model->face[i].normal[j]; + if(k < model->numvertex && !(flags & M3D_EXP_NONORMAL)) { + vrtxidx[k] = 0; + norm[k] = 1; + } + k = model->face[i].texcoord[j]; + if(k < model->numtmap && !(flags & M3D_EXP_NOTXTCRD)) + tmapidx[k] = 0; + } + /* convert from CW to CCW */ + if(flags & M3D_EXP_IDOSUCK) { + j = face[i].data.vertex[1]; + face[i].data.vertex[1] = face[i].data.vertex[2]; + face[i].data.vertex[2] = face[i].data.vertex[1]; + j = face[i].data.normal[1]; + face[i].data.normal[1] = face[i].data.normal[2]; + face[i].data.normal[2] = face[i].data.normal[1]; + j = face[i].data.texcoord[1]; + face[i].data.texcoord[1] = face[i].data.texcoord[2]; + face[i].data.texcoord[2] = face[i].data.texcoord[1]; } - } - /* 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(model->numshape && model->shape) { + M3D_LOG("Processing shape face"); + for(i = 0; i < model->numshape; i++) { + if(!model->shape[i].numcmd) continue; + str = _m3d_addstr(str, &numstr, model->shape[i].name); + if(!str) goto memerr; + for(j = 0; j < model->shape[i].numcmd; j++) { + cmd = &model->shape[i].cmd[j]; + if(cmd->type >= (unsigned int)(sizeof(m3d_commandtypes)/sizeof(m3d_commandtypes[0])) || !cmd->arg) + continue; + if(cmd->type == m3dc_mesh) { + if(numgrp + 2 < maxgrp) { + maxgrp += 1024; + grpidx = (uint32_t*)realloc(grpidx, maxgrp * sizeof(uint32_t)); + if(!grpidx) goto memerr; + if(!numgrp) { + grpidx[0] = 0; + grpidx[1] = model->numface; + numgrp += 2; + } + } + grpidx[numgrp + 0] = cmd->arg[0]; + grpidx[numgrp + 1] = cmd->arg[0] + cmd->arg[1]; + numgrp += 2; } + cd = &m3d_commandtypes[cmd->type]; + for(k = n = 0, l = cd->p; k < l; k++) + switch(cd->a[((k - n) % (cd->p - n)) + n]) { + case m3dcp_mi_t: + if(!(flags & M3D_EXP_NOMATERIAL) && cmd->arg[k] < model->nummaterial) + mtrlidx[cmd->arg[k]] = 0; + break; + case m3dcp_ti_t: + if(!(flags & M3D_EXP_NOTXTCRD) && cmd->arg[k] < model->numtmap) + tmapidx[cmd->arg[k]] = 0; + break; + case m3dcp_qi_t: + case m3dcp_vi_t: + if(cmd->arg[k] < model->numvertex) + vrtxidx[cmd->arg[k]] = 0; + break; + case m3dcp_va_t: + n = k + 1; l += (cmd->arg[k] - 1) * (cd->p - k - 1); + 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; + } + if(model->numface && face) { + if(numgrp && grpidx) { + qsort(grpidx, numgrp, sizeof(uint32_t), _m3d_grpcmp); + for(i = j = 0; i < model->numface && j < numgrp; i++) { + while(j < numgrp && grpidx[j] < i) j++; + face[i].group = j; + } } + qsort(face, model->numface, sizeof(m3dfsave_t), _m3d_facecmp); } + if(grpidx) { M3D_FREE(grpidx); grpidx = NULL; } + if(model->numlabel && model->label) { + M3D_LOG("Processing annotation labels"); + for(i = 0; i < model->numlabel; i++) { + str = _m3d_addstr(str, &numstr, model->label[i].name); + str = _m3d_addstr(str, &numstr, model->label[i].lang); + str = _m3d_addstr(str, &numstr, model->label[i].text); + if(!(flags & M3D_EXP_NOCMAP)) { + cmap = _m3d_addcmap(cmap, &numcmap, model->label[i].color); + if(!cmap) goto memerr; + } + if(model->label[i].vertexid < model->numvertex) + vrtxidx[model->label[i].vertexid] = 0; + } + qsort(model->label, model->numlabel, sizeof(m3dl_t), _m3d_lblcmp); + } + } else if(!(flags & M3D_EXP_NOMATERIAL)) { + /* without a face, simply add all materials, because it can be an mtllib */ + for(i = 0; i < model->nummaterial; i++) + mtrlidx[i] = i; } - /* get bind-pose skeleton and skin */ + /* bind-pose skeleton */ 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)); + M3D_LOG("Processing bones"); 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); + str = _m3d_addstr(str, &numstr, model->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; + k = model->bone[i].pos; + if(k < model->numvertex) + vrtxidx[k] = 0; + k = model->bone[i].ori; + if(k < model->numvertex) + vrtxidx[k] = 0; } } /* actions, animated skeleton poses */ if(model->numaction && model->action && !(flags & M3D_EXP_NOACTION)) { + M3D_LOG("Processing action list"); 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++]); + for(l = 0; l < a->frame[i].numtransform; l++) { + k = a->frame[i].transform[l].pos; + if(k < model->numvertex) + vrtxidx[k] = 0; + k = a->frame[i].transform[l].ori; + if(k < model->numvertex) + vrtxidx[k] = 0; + } + if(l > maxt) maxt = l; + } + } + } + /* add colors to color map and texture names to string table */ + if(!(flags & M3D_EXP_NOMATERIAL)) { + M3D_LOG("Processing materials"); + for(i = k = 0; i < model->nummaterial; i++) { + if(mtrlidx[i] == (M3D_INDEX)-1U || !model->material[i].numprop) continue; + mtrlidx[i] = k++; + m = &model->material[i]; + str = _m3d_addstr(str, &numstr, m->name); + if(!str) goto memerr; + if(m->prop) + for(j = 0; j < m->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) { + ((uint8_t*)&m->prop[j].value.color)[3] = opa[i * 2 + 1]; + 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; } - if(!vrtx) goto memerr; } + } + } + /* if there's only one black color, don't store it */ + if(numcmap == 1 && cmap && !cmap[0]) numcmap = 0; + + /** compress lists **/ + if(model->numtmap && !(flags & M3D_EXP_NOTXTCRD)) { + M3D_LOG("Compressing tmap"); + tmap = (m3dtisave_t*)M3D_MALLOC(model->numtmap * sizeof(m3dtisave_t)); + if(!tmap) goto memerr; + for(i = 0; i < model->numtmap; i++) { + if(tmapidx[i] == (M3D_INDEX)-1U) continue; + switch(quality) { + case M3D_EXP_INT8: + l = model->tmap[i].u * 255; tcoord.data.u = (M3D_FLOAT)l / 255; + l = model->tmap[i].v * 255; tcoord.data.v = (M3D_FLOAT)l / 255; + break; + case M3D_EXP_INT16: + l = model->tmap[i].u * 65535; tcoord.data.u = (M3D_FLOAT)l / 65535; + l = model->tmap[i].v * 65535; tcoord.data.v = (M3D_FLOAT)l / 65535; + break; + default: + tcoord.data.u = model->tmap[i].u; + tcoord.data.v = model->tmap[i].v; + break; + } + if(flags & M3D_EXP_FLIPTXTCRD) + tcoord.data.v = (M3D_FLOAT)1.0 - tcoord.data.v; + tcoord.oldidx = i; + memcpy(&tmap[numtmap++], &tcoord, sizeof(m3dtisave_t)); + } + if(numtmap) { + qsort(tmap, numtmap, sizeof(m3dtisave_t), _m3d_ticmp); + memcpy(&tcoord.data, &tmap[0], sizeof(m3dti_t)); + for(i = 0; i < numtmap; i++) { + if(memcmp(&tcoord.data, &tmap[i].data, sizeof(m3dti_t))) { + memcpy(&tcoord.data, &tmap[i].data, sizeof(m3dti_t)); + maxtmap++; + } + tmap[i].newidx = maxtmap; + tmapidx[tmap[i].oldidx] = maxtmap; + } + maxtmap++; + } + } + if(model->numskin && model->skin && !(flags & M3D_EXP_NOBONE)) { + M3D_LOG("Compressing skin"); + skinidx = (M3D_INDEX*)M3D_MALLOC(model->numskin * sizeof(M3D_INDEX)); + if(!skinidx) goto memerr; + skin = (m3dssave_t*)M3D_MALLOC(model->numskin * sizeof(m3dssave_t)); + if(!skin) goto memerr; + memset(skinidx, 255, model->numskin * sizeof(M3D_INDEX)); + for(i = 0; i < model->numvertex; i++) { + if(vrtxidx[i] != (M3D_INDEX)-1U && model->vertex[i].skinid < model->numskin) + skinidx[model->vertex[i].skinid] = 0; + } + for(i = 0; i < model->numskin; i++) { + if(skinidx[i] == (M3D_INDEX)-1U) continue; + memset(&sk, 0, sizeof(m3dssave_t)); + for(j = 0, min_x = (M3D_FLOAT)0.0; j < M3D_NUMBONE && model->skin[i].boneid[j] != (M3D_INDEX)-1U && + model->skin[i].weight[j] > (M3D_FLOAT)0.0; j++) { + sk.data.boneid[j] = model->skin[i].boneid[j]; + sk.data.weight[j] = model->skin[i].weight[j]; + min_x += sk.data.weight[j]; + } + if(j > maxbone) maxbone = j; + if(min_x != (M3D_FLOAT)1.0 && min_x != (M3D_FLOAT)0.0) + for(j = 0; j < M3D_NUMBONE && sk.data.weight[j] > (M3D_FLOAT)0.0; j++) + sk.data.weight[j] /= min_x; + sk.oldidx = i; + memcpy(&skin[numskin++], &sk, sizeof(m3dssave_t)); + } + if(numskin) { + qsort(skin, numskin, sizeof(m3dssave_t), _m3d_skincmp); + memcpy(&sk.data, &skin[0].data, sizeof(m3ds_t)); + for(i = 0; i < numskin; i++) { + if(memcmp(&sk.data, &skin[i].data, sizeof(m3ds_t))) { + memcpy(&sk.data, &skin[i].data, sizeof(m3ds_t)); + maxskin++; + } + skin[i].newidx = maxskin; + skinidx[skin[i].oldidx] = maxskin; } + maxskin++; } } - /* 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; + + M3D_LOG("Compressing vertex list"); + min_x = min_y = min_z = (M3D_FLOAT)1e10; + max_x = max_y = max_z = (M3D_FLOAT)-1e10; + if(vrtxidx) { + vrtx = (m3dvsave_t*)M3D_MALLOC(model->numvertex * sizeof(m3dvsave_t)); + if(!vrtx) goto memerr; + for(i = numvrtx = 0; i < model->numvertex; i++) { + if(vrtxidx[i] == (M3D_INDEX)-1U) continue; + _m3d_round(quality, &model->vertex[i], &vertex.data); + vertex.norm = norm ? norm[i] : 0; + if(vertex.data.skinid != (M3D_INDEX)-2U && !vertex.norm) { + vertex.data.skinid = vertex.data.skinid != (M3D_INDEX)-1U && skinidx ? skinidx[vertex.data.skinid] : (M3D_INDEX)-1U; + if(vertex.data.x > max_x) max_x = vertex.data.x; + if(vertex.data.x < min_x) min_x = vertex.data.x; + if(vertex.data.y > max_y) max_y = vertex.data.y; + if(vertex.data.y < min_y) min_y = vertex.data.y; + if(vertex.data.z > max_z) max_z = vertex.data.z; + if(vertex.data.z < min_z) min_z = vertex.data.z; + } +#ifdef M3D_VERTEXTYPE + vertex.data.type = 0; +#endif + vertex.oldidx = i; + memcpy(&vrtx[numvrtx++], &vertex, sizeof(m3dvsave_t)); + } + if(numvrtx) { + qsort(vrtx, numvrtx, sizeof(m3dvsave_t), _m3d_vrtxcmp); + memcpy(&vertex.data, &vrtx[0].data, sizeof(m3dv_t)); + for(i = 0; i < numvrtx; i++) { + if(memcmp(&vertex.data, &vrtx[i].data, vrtx[i].norm ? 3 * sizeof(M3D_FLOAT) : sizeof(m3dv_t))) { + memcpy(&vertex.data, &vrtx[i].data, sizeof(m3dv_t)); + maxvrtx++; + } + vrtx[i].newidx = maxvrtx; + vrtxidx[vrtx[i].oldidx] = maxvrtx; } - 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; + maxvrtx++; } - 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; + } + if(skinidx) { M3D_FREE(skinidx); skinidx = NULL; } + if(norm) { M3D_FREE(norm); norm = NULL; } + + /* normalize to bounding cube */ + if(numvrtx && !(flags & M3D_EXP_NORECALC)) { + M3D_LOG("Normalizing coordinates"); + if(min_x < (M3D_FLOAT)0.0) min_x = -min_x; + if(max_x < (M3D_FLOAT)0.0) max_x = -max_x; + if(min_y < (M3D_FLOAT)0.0) min_y = -min_y; + if(max_y < (M3D_FLOAT)0.0) max_y = -max_y; + if(min_z < (M3D_FLOAT)0.0) min_z = -min_z; + if(max_z < (M3D_FLOAT)0.0) 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)) { + if(scale == (M3D_FLOAT)0.0) scale = (M3D_FLOAT)1.0; + if(scale != (M3D_FLOAT)1.0) { 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(vrtx[i].data.skinid == (M3D_INDEX)-2U) continue; + vrtx[i].data.x /= scale; + vrtx[i].data.y /= scale; + vrtx[i].data.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; + if(model->scale > (M3D_FLOAT)0.0) scale = model->scale; + if(scale <= (M3D_FLOAT)0.0) scale = (M3D_FLOAT)1.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); +memerr: if(vrtxidx) M3D_FREE(vrtxidx); + if(mtrlidx) M3D_FREE(mtrlidx); + if(tmapidx) M3D_FREE(tmapidx); + if(skinidx) M3D_FREE(skinidx); + if(grpidx) M3D_FREE(grpidx); + if(norm) M3D_FREE(norm); + 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(vrtx) M3D_FREE(vrtx); 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; + + M3D_LOG("Serializing model"); #ifdef M3D_ASCII if(flags & M3D_EXP_ASCII) { /* use CRLF to make model creators on Win happy... */ @@ -3808,58 +4543,77 @@ memerr: if(face) M3D_FREE(face); 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; + M3D_FREE(sl); M3D_FREE(sa); M3D_FREE(sd); + sl = sa = sd = NULL; + /* preview chunk */ + if(model->preview.data && model->preview.length) { + sl = _m3d_safestr(sn, 0); + if(sl) { + ptr -= (uint64_t)out; len = (uint64_t)ptr + 20; + out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out; + if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr += sprintf(ptr, "Preview\r\n%s.png\r\n\r\n", sl); + M3D_FREE(sl); sl = NULL; + } + } + M3D_FREE(sn); sn = 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; + ptr -= (uint64_t)out; len = (uint64_t)ptr + maxtmap * 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); + last = (M3D_INDEX)-1U; + for(i = 0; i < numtmap; i++) { + if(tmap[i].newidx == last) continue; + last = tmap[i].newidx; + ptr += sprintf(ptr, "%g %g\r\n", tmap[i].data.u, tmap[i].data.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; + ptr -= (uint64_t)out; len = (uint64_t)ptr + maxvrtx * 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"); + last = (M3D_INDEX)-1U; 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]); + if(vrtx[i].newidx == last) continue; + last = vrtx[i].newidx; + ptr += sprintf(ptr, "%g %g %g %g", vrtx[i].data.x, vrtx[i].data.y, vrtx[i].data.z, vrtx[i].data.w); + if(!(flags & M3D_EXP_NOCMAP) && vrtx[i].data.color) + ptr += sprintf(ptr, " #%08x", vrtx[i].data.color); + if(!(flags & M3D_EXP_NOBONE) && model->numbone && maxskin && vrtx[i].data.skinid < M3D_INDEXMAX) { + if(skin[vrtx[i].data.skinid].data.weight[0] == (M3D_FLOAT)1.0) + ptr += sprintf(ptr, " %d", skin[vrtx[i].data.skinid].data.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]); + for(j = 0; j < M3D_NUMBONE && skin[vrtx[i].data.skinid].data.boneid[j] != (M3D_INDEX)-1U && + skin[vrtx[i].data.skinid].data.weight[j] > (M3D_FLOAT)0.0; j++) + ptr += sprintf(ptr, " %d:%g", skin[vrtx[i].data.skinid].data.boneid[j], + skin[vrtx[i].data.skinid].data.weight[j]); } ptr += sprintf(ptr, "\r\n"); } ptr += sprintf(ptr, "\r\n"); } /* bones chunk */ - if(numbone && bone && !(flags & M3D_EXP_NOBONE)) { + if(model->numbone && model->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; + for(i = 0; i < model->numbone; i++) { + len += strlen(model->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 = _m3d_prtbone(ptr, model->bone, model->numbone, (M3D_INDEX)-1U, 0, vrtxidx); 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]; + if(model->nummaterial && !(flags & M3D_EXP_NOMATERIAL)) { + for(j = 0; j < model->nummaterial; j++) { + if(mtrlidx[j] == (M3D_INDEX)-1U || !model->material[j].numprop || !model->material[j].prop) continue; + m = &model->material[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; @@ -3918,16 +4672,37 @@ memerr: if(face) M3D_FREE(face); ptr += sprintf(ptr, "\r\n"); } } + /* procedural face */ + if(model->numinlined && model->inlined && !(flags & M3D_EXP_NOFACE)) { + /* all inlined assets which are not textures should be procedural surfaces */ + for(j = 0; j < model->numinlined; j++) { + if(!model->inlined[j].name || !*model->inlined[j].name || !model->inlined[j].length || !model->inlined[j].data || + (model->inlined[j].data[1] == 'P' && model->inlined[j].data[2] == 'N' && model->inlined[j].data[3] == 'G')) + continue; + for(i = k = 0; i < model->numtexture; i++) { + if(!strcmp(model->inlined[j].name, model->texture[i].name)) { k = 1; break; } + } + if(k) continue; + sn = _m3d_safestr(model->inlined[j].name, 0); + if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr -= (uint64_t)out; len = (uint64_t)ptr + strlen(sn) + 18; + out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out; + if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr += sprintf(ptr, "Procedural\r\n%s\r\n\r\n", sn); + M3D_FREE(sn); sn = NULL; + } + } /* 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); + j = face[i].data.materialid < model->nummaterial ? face[i].data.materialid : (M3D_INDEX)-1U; + if(j != last) { + last = j; + if(last < model->nummaterial) + len += strlen(model->material[last].name); len += 6; } } @@ -3936,10 +4711,11 @@ memerr: if(face) M3D_FREE(face); 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); + j = face[i].data.materialid < model->nummaterial ? face[i].data.materialid : (M3D_INDEX)-1U; + if(!(flags & M3D_EXP_NOMATERIAL) && j != last) { + last = j; + if(last < model->nummaterial) { + sn = _m3d_safestr(model->material[last].name, 0); if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; } ptr += sprintf(ptr, "use %s\r\n", sn); M3D_FREE(sn); sn = NULL; @@ -3948,21 +4724,108 @@ memerr: if(face) M3D_FREE(face); } /* 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, "%s%d", j?" ":"", vrtxidx[face[i].data.vertex[j]]); + k = -1U; + if(!(flags & M3D_EXP_NOTXTCRD) && (face[i].data.texcoord[j] != (M3D_INDEX)-1U) && + (tmapidx[face[i].data.texcoord[j]] != (M3D_INDEX)-1U)) { + k = tmapidx[face[i].data.texcoord[j]]; + ptr += sprintf(ptr, "/%d", k); + } + if(!(flags & M3D_EXP_NONORMAL) && (face[i].data.normal[j] != (M3D_INDEX)-1U)) + ptr += sprintf(ptr, "%s/%d", k == -1U? "/" : "", vrtxidx[face[i].data.normal[j]]); } ptr += sprintf(ptr, "\r\n"); } ptr += sprintf(ptr, "\r\n"); } + /* mathematical shapes face */ + if(model->numshape && model->numshape && !(flags & M3D_EXP_NOFACE)) { + for(j = 0; j < model->numshape; j++) { + sn = _m3d_safestr(model->shape[j].name, 0); + if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr -= (uint64_t)out; len = (uint64_t)ptr + strlen(sn) + 33; + out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out; + if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr += sprintf(ptr, "Shape %s\r\n", sn); + M3D_FREE(sn); sn = NULL; + if(model->shape[j].group != (M3D_INDEX)-1U && !(flags & M3D_EXP_NOBONE)) + ptr += sprintf(ptr, "group %d\r\n", model->shape[j].group); + for(i = 0; i < model->shape[j].numcmd; i++) { + cmd = &model->shape[j].cmd[i]; + if(cmd->type >= (unsigned int)(sizeof(m3d_commandtypes)/sizeof(m3d_commandtypes[0])) || !cmd->arg) + continue; + cd = &m3d_commandtypes[cmd->type]; + ptr -= (uint64_t)out; len = (uint64_t)ptr + strlen(cd->key) + 3; + for(k = 0; k < cd->p; k++) + switch(cd->a[k]) { + case m3dcp_mi_t: if(cmd->arg[k] != -1U) { len += strlen(model->material[cmd->arg[k]].name) + 1; } break; + case m3dcp_va_t: len += cmd->arg[k] * (cd->p - k - 1) * 16; k = cd->p; break; + default: len += 16; break; + } + out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out; + if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr += sprintf(ptr, "%s", cd->key); + for(k = n = 0, l = cd->p; k < l; k++) { + switch(cd->a[((k - n) % (cd->p - n)) + n]) { + case m3dcp_mi_t: + if(cmd->arg[k] != -1U) { + sn = _m3d_safestr(model->material[cmd->arg[k]].name, 0); + if(!sn) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr += sprintf(ptr, " %s", sn); + M3D_FREE(sn); sn = NULL; + } + break; + case m3dcp_vc_t: ptr += sprintf(ptr, " %g", *((float*)&cmd->arg[k])); break; + case m3dcp_va_t: ptr += sprintf(ptr, " %d[", cmd->arg[k]); + n = k + 1; l += (cmd->arg[k] - 1) * (cd->p - k - 1); + break; + default: ptr += sprintf(ptr, " %d", cmd->arg[k]); break; + } + } + ptr += sprintf(ptr, "%s\r\n", l > cd->p ? " ]" : ""); + } + ptr += sprintf(ptr, "\r\n"); + } + } + /* annotation labels */ + if(model->numlabel && model->label && !(flags & M3D_EXP_NOFACE)) { + for(i = 0, j = 3, length = NULL; i < model->numlabel; i++) { + if(model->label[i].name) j += strlen(model->label[i].name); + if(model->label[i].lang) j += strlen(model->label[i].lang); + if(model->label[i].text) j += strlen(model->label[i].text); + j += 40; + } + ptr -= (uint64_t)out; len = (uint64_t)ptr + j; + out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out; + if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } + for(i = 0; i < model->numlabel; i++) { + if(!i || _m3d_strcmp(sl, model->label[i].lang) || _m3d_strcmp(sn, model->label[i].name)) { + sl = model->label[i].lang; + sn = model->label[i].name; + sd = _m3d_safestr(sn, 0); + if(!sd) { setlocale(LC_NUMERIC, ol); sn = sl = NULL; goto memerr; } + if(i) ptr += sprintf(ptr, "\r\n"); + ptr += sprintf(ptr, "Labels %s\r\n", sd); + M3D_FREE(sd); sd = NULL; + if(model->label[i].color) + ptr += sprintf(ptr, "color #0x%08x\r\n", model->label[i].color); + if(sl && *sl) { + sd = _m3d_safestr(sl, 0); + if(!sd) { setlocale(LC_NUMERIC, ol); sn = sl = NULL; goto memerr; } + ptr += sprintf(ptr, "lang %s\r\n", sd); + M3D_FREE(sd); sd = NULL; + } + } + sd = _m3d_safestr(model->label[i].text, 2); + if(!sd) { setlocale(LC_NUMERIC, ol); sn = sl = NULL; goto memerr; } + ptr += sprintf(ptr, "%d %s\r\n", model->label[i].vertexid, sd); + M3D_FREE(sd); sd = NULL; + } + ptr += sprintf(ptr, "\r\n"); + sn = sl = NULL; + } /* actions */ - if(model->numaction && model->action && numactn && actn && !(flags & M3D_EXP_NOACTION)) { - l = 0; + if(model->numaction && model->action && !(flags & M3D_EXP_NOACTION)) { for(j = 0; j < model->numaction; j++) { a = &model->action[j]; sn = _m3d_safestr(a->name, 0); @@ -3974,31 +4837,46 @@ memerr: if(face) M3D_FREE(face); 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, "%d %d %d\r\n", a->frame[i].transform[k].boneid, + vrtxidx[a->frame[i].transform[k].pos], vrtxidx[a->frame[i].transform[k].ori]); } } ptr += sprintf(ptr, "\r\n"); } } + /* inlined assets */ + if(model->numinlined && model->inlined) { + for(i = j = 0; i < model->numinlined; i++) + if(model->inlined[i].name) + j += strlen(model->inlined[i].name) + 6; + if(j > 0) { + ptr -= (uint64_t)out; len = (uint64_t)ptr + j + 16; + out = (unsigned char*)M3D_REALLOC(out, len); ptr += (uint64_t)out; + if(!out) { setlocale(LC_NUMERIC, ol); goto memerr; } + ptr += sprintf(ptr, "Assets\r\n"); + for(i = 0; i < model->numinlined; i++) + if(model->inlined[i].name) + ptr += sprintf(ptr, "%s%s\r\n", model->inlined[i].name, strrchr(model->inlined[i].name, '.') ? "" : ".png"); + 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; + if(model->numextra && (flags & M3D_EXP_EXTRA)) { + for(i = 0; i < model->numextra; i++) { + if(model->extra[i]->length < 9) continue; + ptr -= (uint64_t)out; len = (uint64_t)ptr + 17 + model->extra[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)); + model->extra[i]->magic[0] > ' ' ? model->extra[i]->magic[0] : '_', + model->extra[i]->magic[1] > ' ' ? model->extra[i]->magic[1] : '_', + model->extra[i]->magic[2] > ' ' ? model->extra[i]->magic[2] : '_', + model->extra[i]->magic[3] > ' ' ? model->extra[i]->magic[3] : '_'); + for(j = 0; j < model->extra[i]->length; j++) + ptr += sprintf(ptr, "%02x ", *((unsigned char *)model->extra + sizeof(m3dchunk_t) + j)); ptr--; ptr += sprintf(ptr, "\r\n\r\n"); } @@ -4025,16 +4903,6 @@ memerr: if(face) M3D_FREE(face); 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) { @@ -4048,14 +4916,19 @@ memerr: if(face) M3D_FREE(face); 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); + vi_s = maxvrtx < 254 ? 1 : (maxvrtx < 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); + ti_s = !maxtmap || !tmap ? 0 : (maxtmap < 254 ? 1 : (maxtmap < 65534 ? 2 : 4)); + bi_s = !model->numbone || !model->bone || (flags & M3D_EXP_NOBONE)? 0 : (model->numbone < 254 ? 1 : + (model->numbone < 65534 ? 2 : 4)); + nb_s = maxbone < 2 ? 1 : (maxbone == 2 ? 2 : (maxbone <= 4 ? 4 : 8)); + sk_s = !bi_s || !maxskin || !skin ? 0 : (maxskin < 254 ? 1 : (maxskin < 65534 ? 2 : 4)); + fc_s = maxt < 254 ? 1 : (maxt < 65534 ? 2 : 4); + hi_s = !model->numshape || !model->shape || (flags & M3D_EXP_NOFACE)? 0 : (model->numshape < 254 ? 1 : + (model->numshape < 65534 ? 2 : 4)); + fi_s = !model->numface || !model->face || (flags & M3D_EXP_NOFACE)? 0 : (model->numface < 254 ? 1 : + (model->numface < 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))) | @@ -4064,8 +4937,20 @@ memerr: if(face) M3D_FREE(face); (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))) ; + (fc_s == 2 ? (1<<16): (fc_s == 1 ? (0<<16): (2<<16))) | + (hi_s == 2 ? (1<<18): (hi_s == 1 ? (0<<18): (hi_s == 4 ? (2<<18) : (3<<18)))) | + (fi_s == 2 ? (1<<20): (fi_s == 1 ? (0<<20): (fi_s == 4 ? (2<<20) : (3<<20)))); len = h->length; + /* preview image chunk, must be the first if exists */ + if(model->preview.data && model->preview.length) { + chunklen = 8 + model->preview.length; + h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); + if(!h) goto memerr; + memcpy((uint8_t*)h + len, "PRVW", 4); + *((uint32_t*)((uint8_t*)h + len + 4)) = chunklen; + memcpy((uint8_t*)h + len + 8, model->preview.data, model->preview.length); + len += chunklen; + } /* color map */ if(numcmap && cmap && ci_s < 4 && !(flags & M3D_EXP_NOCMAP)) { chunklen = 8 + numcmap * sizeof(uint32_t); @@ -4078,104 +4963,114 @@ memerr: if(face) M3D_FREE(face); } else numcmap = 0; /* texture map */ if(numtmap && tmap && !(flags & M3D_EXP_NOTXTCRD) && !(flags & M3D_EXP_NOFACE)) { - chunklen = 8 + numtmap * vc_s * 2; + chunklen = 8 + maxtmap * 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; + length = (uint32_t*)((uint8_t*)h + len + 4); out = (uint8_t*)h + len + 8; + last = (M3D_INDEX)-1U; for(i = 0; i < numtmap; i++) { + if(tmap[i].newidx == last) continue; + last = tmap[i].newidx; switch(vc_s) { - case 1: *out++ = (uint8_t)(tmap[i].u * 255); *out++ = (uint8_t)(tmap[i].v * 255); break; + case 1: *out++ = (uint8_t)(tmap[i].data.u * 255); *out++ = (uint8_t)(tmap[i].data.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; + *((uint16_t*)out) = (uint16_t)(tmap[i].data.u * 65535); out += 2; + *((uint16_t*)out) = (uint16_t)(tmap[i].data.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; + case 4: *((float*)out) = tmap[i].data.u; out += 4; *((float*)out) = tmap[i].data.v; out += 4; break; + case 8: *((double*)out) = tmap[i].data.u; out += 8; *((double*)out) = tmap[i].data.v; out += 8; break; } } + *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); out = NULL; - len += chunklen; + len += *length; } /* vertex */ if(numvrtx && vrtx) { - chunklen = 8 + numvrtx * (ci_s + sk_s + 4 * vc_s); + chunklen = 8 + maxvrtx * (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; + length = (uint32_t*)((uint8_t*)h + len + 4); out = (uint8_t*)h + len + 8; + last = (M3D_INDEX)-1U; for(i = 0; i < numvrtx; i++) { + if(vrtx[i].newidx == last) continue; + last = vrtx[i].newidx; 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); + *out++ = (int8_t)(vrtx[i].data.x * 127); + *out++ = (int8_t)(vrtx[i].data.y * 127); + *out++ = (int8_t)(vrtx[i].data.z * 127); + *out++ = (int8_t)(vrtx[i].data.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; + *((int16_t*)out) = (int16_t)(vrtx[i].data.x * 32767); out += 2; + *((int16_t*)out) = (int16_t)(vrtx[i].data.y * 32767); out += 2; + *((int16_t*)out) = (int16_t)(vrtx[i].data.z * 32767); out += 2; + *((int16_t*)out) = (int16_t)(vrtx[i].data.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; + *((float*)out) = vrtx[i].data.x; out += 4; + *((float*)out) = vrtx[i].data.y; out += 4; + *((float*)out) = vrtx[i].data.z; out += 4; + *((float*)out) = vrtx[i].data.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; + *((double*)out) = vrtx[i].data.x; out += 8; + *((double*)out) = vrtx[i].data.y; out += 8; + *((double*)out) = vrtx[i].data.z; out += 8; + *((double*)out) = vrtx[i].data.w; out += 8; break; } - idx = _m3d_cmapidx(cmap, numcmap, vrtx[i].color); + idx = _m3d_cmapidx(cmap, numcmap, vrtx[i].data.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; + case 4: *((uint32_t*)out) = vrtx[i].data.color; out += 4; break; } - out = _m3d_addidx(out, sk_s, numbone && numskin ? vrtx[i].skinid : -1U); + out = _m3d_addidx(out, sk_s, vrtx[i].data.skinid); } + *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); out = NULL; - len += chunklen; + len += *length; } /* bones chunk */ - if(numbone && bone && !(flags & M3D_EXP_NOBONE)) { - i = 8 + bi_s + sk_s + numbone * (bi_s + si_s + 2*vi_s); + if(model->numbone && model->bone && !(flags & M3D_EXP_NOBONE)) { + i = 8 + bi_s + sk_s + model->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); + out = _m3d_addidx(out, bi_s, model->numbone); + out = _m3d_addidx(out, sk_s, maxskin); + for(i = 0; i < model->numbone; i++) { + out = _m3d_addidx(out, bi_s, model->bone[i].parent); + out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, model->bone[i].name)); + out = _m3d_addidx(out, vi_s, vrtxidx[model->bone[i].pos]); + out = _m3d_addidx(out, vi_s, vrtxidx[model->bone[i].ori]); } if(numskin && skin && sk_s) { + last = (M3D_INDEX)-1U; for(i = 0; i < numskin; i++) { + if(skin[i].newidx == last) continue; + last = skin[i].newidx; 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); + for(j = 0; j < (uint32_t)nb_s && skin[i].data.boneid[j] != (M3D_INDEX)-1U && + skin[i].data.weight[j] > (M3D_FLOAT)0.0; j++) + weights[j] = (uint8_t)(skin[i].data.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]); + for(j = 0; j < (uint32_t)nb_s && skin[i].data.boneid[j] != (M3D_INDEX)-1U && weights[j]; j++) { + out = _m3d_addidx(out, bi_s, skin[i].data.boneid[j]); *length += bi_s; } } @@ -4185,9 +5080,10 @@ memerr: if(face) M3D_FREE(face); len += *length; } /* materials */ - if(nummtrl && !(flags & M3D_EXP_NOMATERIAL)) { - for(j = 0; j < nummtrl; j++) { - m = !mtrl ? &model->material[j] : mtrl[j]; + if(model->nummaterial && !(flags & M3D_EXP_NOMATERIAL)) { + for(j = 0; j < model->nummaterial; j++) { + if(mtrlidx[j] == (M3D_INDEX)-1U || !model->material[j].numprop || !model->material[j].prop) continue; + m = &model->material[j]; chunklen = 12 + si_s + m->numprop * 5; h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); if(!h) goto memerr; @@ -4233,6 +5129,30 @@ memerr: if(face) M3D_FREE(face); out = NULL; } } + /* procedural face */ + if(model->numinlined && model->inlined && !(flags & M3D_EXP_NOFACE)) { + /* all inlined assets which are not textures should be procedural surfaces */ + for(j = 0; j < model->numinlined; j++) { + if(!model->inlined[j].name || !model->inlined[j].name[0] || model->inlined[j].length < 4 || + !model->inlined[j].data || (model->inlined[j].data[1] == 'P' && model->inlined[j].data[2] == 'N' && + model->inlined[j].data[3] == 'G')) + continue; + for(i = k = 0; i < model->numtexture; i++) { + if(!strcmp(model->inlined[j].name, model->texture[i].name)) { k = 1; break; } + } + if(k) continue; + numproc++; + chunklen = 8 + si_s; + h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); + if(!h) goto memerr; + memcpy((uint8_t*)h + len, "PROC", 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)); + out = NULL; + len += chunklen; + } + } /* 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); @@ -4243,41 +5163,123 @@ memerr: if(face) M3D_FREE(face); 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); - } - } + if(!(flags & M3D_EXP_NOMATERIAL) && face[i].data.materialid != last) { + last = face[i].data.materialid; + idx = last < model->nummaterial ? _m3d_stridx(str, numstr, model->material[last].name) : 0; + *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); + (((flags & M3D_EXP_NOTXTCRD) || !ti_s || face[i].data.texcoord[0] == (M3D_INDEX)-1U || + face[i].data.texcoord[1] == (M3D_INDEX)-1U || face[i].data.texcoord[2] == (M3D_INDEX)-1U) ? 0 : 1) | + (((flags & M3D_EXP_NONORMAL) || face[i].data.normal[0] == (M3D_INDEX)-1U || + face[i].data.normal[1] == (M3D_INDEX)-1U || face[i].data.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]); + out = _m3d_addidx(out, vi_s, vrtxidx[face[i].data.vertex[j]]); if(k & 1) - out = _m3d_addidx(out, ti_s, face[i].texcoord[j]); + out = _m3d_addidx(out, ti_s, tmapidx[face[i].data.texcoord[j]]); if(k & 2) - out = _m3d_addidx(out, vi_s, face[i].normal[j]); + out = _m3d_addidx(out, vi_s, vrtxidx[face[i].data.normal[j]]); } } *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); len += *length; out = NULL; } + /* mathematical shapes face */ + if(model->numshape && model->shape && !(flags & M3D_EXP_NOFACE)) { + for(j = 0; j < model->numshape; j++) { + chunklen = 12 + si_s + model->shape[j].numcmd * (M3D_CMDMAXARG + 1) * 4; + h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); + if(!h) goto memerr; + memcpy((uint8_t*)h + len, "SHPE", 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, model->shape[j].name)); + out = _m3d_addidx(out, bi_s, model->shape[j].group); + for(i = 0; i < model->shape[j].numcmd; i++) { + cmd = &model->shape[j].cmd[i]; + if(cmd->type >= (unsigned int)(sizeof(m3d_commandtypes)/sizeof(m3d_commandtypes[0])) || !cmd->arg) + continue; + cd = &m3d_commandtypes[cmd->type]; + *out++ = (cmd->type & 0x7F) | (cmd->type > 127 ? 0x80 : 0); + if(cmd->type > 127) *out++ = (cmd->type >> 7) & 0xff; + for(k = n = 0, l = cd->p; k < l; k++) { + switch(cd->a[((k - n) % (cd->p - n)) + n]) { + case m3dcp_mi_t: + out = _m3d_addidx(out, si_s, cmd->arg[k] < model->nummaterial ? + _m3d_stridx(str, numstr, model->material[cmd->arg[k]].name) : 0); + break; + case m3dcp_vc_t: + min_x = *((float*)&cmd->arg[k]); + switch(vc_s) { + case 1: *out++ = (int8_t)(min_x * 127); break; + case 2: *((int16_t*)out) = (int16_t)(min_x * 32767); out += 2; break; + case 4: *((float*)out) = min_x; out += 4; break; + case 8: *((double*)out) = min_x; out += 8; break; + } + break; + case m3dcp_hi_t: out = _m3d_addidx(out, hi_s, cmd->arg[k]); break; + case m3dcp_fi_t: out = _m3d_addidx(out, fi_s, cmd->arg[k]); break; + case m3dcp_ti_t: out = _m3d_addidx(out, ti_s, cmd->arg[k]); break; + case m3dcp_qi_t: + case m3dcp_vi_t: out = _m3d_addidx(out, vi_s, cmd->arg[k]); break; + case m3dcp_i1_t: out = _m3d_addidx(out, 1, cmd->arg[k]); break; + case m3dcp_i2_t: out = _m3d_addidx(out, 2, cmd->arg[k]); break; + case m3dcp_i4_t: out = _m3d_addidx(out, 4, cmd->arg[k]); break; + case m3dcp_va_t: out = _m3d_addidx(out, 4, cmd->arg[k]); + n = k + 1; l += (cmd->arg[k] - 1) * (cd->p - k - 1); + break; + } + } + } + *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); + len += *length; + out = NULL; + } + } + /* annotation labels */ + if(model->numlabel && model->label) { + for(i = 0, length = NULL; i < model->numlabel; i++) { + if(!i || _m3d_strcmp(sl, model->label[i].lang) || _m3d_strcmp(sn, model->label[i].name)) { + sl = model->label[i].lang; + sn = model->label[i].name; + if(length) { + *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); + len += *length; + } + chunklen = 8 + 2 * si_s + ci_s + model->numlabel * (vi_s + si_s); + h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); + if(!h) { sn = NULL; sl = NULL; goto memerr; } + memcpy((uint8_t*)h + len, "LBLS", 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, model->label[l].name)); + out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, model->label[l].lang)); + idx = _m3d_cmapidx(cmap, numcmap, model->label[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) = model->label[i].color; out += 4; break; + } + } + out = _m3d_addidx(out, vi_s, vrtxidx[model->label[i].vertexid]); + out = _m3d_addidx(out, si_s, _m3d_stridx(str, numstr, model->label[l].text)); + } + if(length) { + *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); + len += *length; + } + out = NULL; + sn = sl = NULL; + } /* actions */ - if(model->numaction && model->action && numactn && actn && numbone && bone && !(flags & M3D_EXP_NOACTION)) { - l = 0; + if(model->numaction && model->action && model->numbone && model->bone && !(flags & M3D_EXP_NOACTION)) { 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)); + chunklen = 14 + si_s + a->numframe * (4 + fc_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); @@ -4288,11 +5290,11 @@ memerr: if(face) M3D_FREE(face); *((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); + out = _m3d_addidx(out, fc_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++]); + out = _m3d_addidx(out, vi_s, vrtxidx[a->frame[i].transform[k].pos]); + out = _m3d_addidx(out, vi_s, vrtxidx[a->frame[i].transform[k].ori]); } } *length = (uint64_t)out - (uint64_t)((uint8_t*)h + len); @@ -4301,10 +5303,18 @@ memerr: if(face) M3D_FREE(face); } } /* inlined assets */ - if(model->numinlined && model->inlined && (flags & M3D_EXP_INLINE)) { + if(model->numinlined && model->inlined && (numproc || (flags & M3D_EXP_INLINE))) { for(j = 0; j < model->numinlined; j++) { - if(!model->inlined[j].name || !*model->inlined[j].name || !model->inlined[j].length) + if(!model->inlined[j].name || !model->inlined[j].name[0] || model->inlined[j].length<4 || !model->inlined[j].data) continue; + if(!(flags & M3D_EXP_INLINE)) { + if(model->inlined[j].data[1] == 'P' && model->inlined[j].data[2] == 'N' && model->inlined[j].data[3] == 'G') + continue; + for(i = k = 0; i < model->numtexture; i++) { + if(!strcmp(model->inlined[j].name, model->texture[i].name)) { k = 1; break; } + } + if(k) continue; + } chunklen = 8 + si_s + model->inlined[j].length; h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); if(!h) goto memerr; @@ -4318,14 +5328,14 @@ memerr: if(face) M3D_FREE(face); } } /* 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) + if(model->numextra && model->extra && (flags & M3D_EXP_EXTRA)) { + for(j = 0; j < model->numextra; j++) { + if(!model->extra[j] || model->extra[j]->length < 8) continue; - chunklen = model->unknown[j]->length; + chunklen = model->extra[j]->length; h = (m3dhdr_t*)M3D_REALLOC(h, len + chunklen); if(!h) goto memerr; - memcpy((uint8_t*)h + len, model->unknown[j], chunklen); + memcpy((uint8_t*)h + len, model->extra[j], chunklen); len += chunklen; } } @@ -4336,6 +5346,7 @@ memerr: if(face) M3D_FREE(face); len += 4; /* zlib compress */ if(!(flags & M3D_EXP_NOZLIB)) { + M3D_LOG("Deflating chunks"); 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; } } @@ -4348,15 +5359,17 @@ memerr: if(face) M3D_FREE(face); memcpy(out + 8, h, len - 8); } if(size) *size = out ? len : 0; + if(vrtxidx) M3D_FREE(vrtxidx); + if(mtrlidx) M3D_FREE(mtrlidx); + if(tmapidx) M3D_FREE(tmapidx); + if(skinidx) M3D_FREE(skinidx); + if(norm) M3D_FREE(norm); 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(vrtx) M3D_FREE(vrtx); if(h) M3D_FREE(h); return out; } @@ -4397,6 +5410,14 @@ namespace M3D { this->model = m3d_load((unsigned char *)&data[0], ReadFileCB, FreeCB, mtllib.model); #else Model(); +#endif + } + Model(_unused const 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, ReadFileCB, FreeCB, mtllib.model); +#else + Model(); #endif } ~Model() { m3d_free(this->model); } @@ -4413,6 +5434,9 @@ namespace M3D { 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 getPreview() { return this->model->preview.data ? + std::vector(this->model->preview.data, this->model->preview.data + this->model->preview.length) : + std::vector(); } std::vector getColorMap() { return this->model->cmap ? std::vector(this->model->cmap, this->model->cmap + this->model->numcmap) : std::vector(); } std::vector getTextureMap() { return this->model->tmap ? std::vector(this->model->tmap, @@ -4462,6 +5486,18 @@ namespace M3D { this->model->vertex + this->model->numvertex) : std::vector(); } std::vector getFace() { return this->model->face ? std::vector(this->model->face, this->model->face + this->model->numface) : std::vector(); } + std::vector getShape() { return this->model->shape ? std::vector(this->model->shape, + this->model->shape + this->model->numshape) : std::vector(); } + std::string getShapeName(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numshape && + this->model->shape[idx].name && this->model->shape[idx].name[0] ? + std::string(this->model->shape[idx].name) : nullptr; } + unsigned int getShapeGroup(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numshape ? + this->model->shape[idx].group : 0xFFFFFFFF; } + std::vector getShapeCommands(int idx) { return idx >= 0 && (unsigned int)idx < this->model->numshape && + this->model->shape[idx].cmd ? std::vector(this->model->shape[idx].cmd, this->model->shape[idx].cmd + + this->model->shape[idx].numcmd) : std::vector(); } + std::vector getAnnotationLabels() { return this->model->label ? std::vector(this->model->label, + this->model->label + this->model->numlabel) : std::vector(); } std::vector getSkin() { return this->model->skin ? std::vector(this->model->skin, this->model->skin + this->model->numskin) : std::vector(); } std::vector getActions() { return this->model->action ? std::vector(this->model->action, @@ -4491,9 +5527,9 @@ namespace M3D { return std::vector(pose, pose + this->model->numbone); } std::vector getInlinedAssets() { return this->model->inlined ? std::vector(this->model->inlined, this->model->inlined + this->model->numinlined) : std::vector(); } - std::vector> getUnknowns() { return this->model->unknown ? - std::vector>(this->model->unknown, - this->model->unknown + this->model->numunknown) : std::vector>(); } + std::vector> getExtras() { return this->model->extra ? + std::vector>(this->model->extra, + this->model->extra + this->model->numextra) : std::vector>(); } std::vector Save(_unused int quality, _unused int flags) { #ifdef M3D_EXPORTER unsigned int size; @@ -4513,6 +5549,7 @@ namespace M3D { public: Model(const std::string &data, m3dread_t ReadFileCB, m3dfree_t FreeCB); Model(const std::vector data, m3dread_t ReadFileCB, m3dfree_t FreeCB); + Model(const unsigned char *data, m3dread_t ReadFileCB, m3dfree_t FreeCB); Model(); ~Model(); @@ -4528,6 +5565,7 @@ namespace M3D { void setDescription(std::string desc); float getScale(); void setScale(float scale); + std::vector getPreview(); std::vector getColorMap(); std::vector getTextureMap(); std::vector getTextures(); @@ -4542,6 +5580,11 @@ namespace M3D { m3dtx_t* getMaterialPropertyMap(int idx, int type); std::vector getVertices(); std::vector getFace(); + std::vector getShape(); + std::string getShapeName(int idx); + unsigned int getShapeGroup(int idx); + std::vector getShapeCommands(int idx); + std::vector getAnnotationLabels(); std::vector getSkin(); std::vector getActions(); std::string getActionName(int aidx); @@ -4552,7 +5595,7 @@ namespace M3D { std::vector getActionFrame(int aidx, int fidx, std::vector skeleton); std::vector getActionPose(int aidx, unsigned int msec); std::vector getInlinedAssets(); - std::vector> getUnknowns(); + std::vector> getExtras(); std::vector Save(int quality, int flags); }; diff --git a/test/unit/utM3DImportExport.cpp b/test/unit/utM3DImportExport.cpp index c3a0fb08c3..048d3770d8 100644 --- a/test/unit/utM3DImportExport.cpp +++ b/test/unit/utM3DImportExport.cpp @@ -54,7 +54,7 @@ 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 ); + const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/M3D/cube_usemtl.m3d", aiProcess_ValidateDataStructure ); #ifndef ASSIMP_BUILD_NO_M3D_IMPORTER return nullptr != scene; #else From e9df0259bba4330774a8e4522f7ac493f9825e96 Mon Sep 17 00:00:00 2001 From: bzt Date: Mon, 18 Nov 2019 03:07:57 +0100 Subject: [PATCH 67/74] Link to the new homepage --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index f02a3b617a..272bd4a3f0 100644 --- a/Readme.md +++ b/Readme.md @@ -67,7 +67,7 @@ __Importers__: - [LWO](https://en.wikipedia.org/wiki/LightWave_3D) - LWS - LXO -- [M3D](https://gitlab.com/bztsrc/model3d) +- [M3D](https://bztsrc.gitlab.io/model3d) - MD2 - MD3 - MD5 From 2b252bb9a54fb14f6417c56f248abb808f24c457 Mon Sep 17 00:00:00 2001 From: bzt Date: Mon, 18 Nov 2019 06:23:41 +0100 Subject: [PATCH 68/74] My mistake, fixed --- code/M3D/M3DImporter.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/code/M3D/M3DImporter.cpp b/code/M3D/M3DImporter.cpp index 0156950c31..9371e22289 100644 --- a/code/M3D/M3DImporter.cpp +++ b/code/M3D/M3DImporter.cpp @@ -396,9 +396,14 @@ void M3DImporter::importMeshes() // we must switch mesh if material changes if(lastMat != m3d->face[i].materialid) { lastMat = m3d->face[i].materialid; - if(pMesh && vertices->size() && faces->size()) { + if(pMesh && vertices && vertices->size() && faces && faces->size()) { populateMesh(pMesh, faces, vertices, normals, texcoords, colors, vertexids); meshes->push_back(pMesh); + delete faces; + delete vertices; + delete normals; + delete texcoords; + delete colors; delete vertexids; // this is not stored in pMesh, just to collect bone vertices } pMesh = new aiMesh; From 301748be6ab91e3d72198a190b7f9b7aa5dc95ce Mon Sep 17 00:00:00 2001 From: bzt Date: Mon, 18 Nov 2019 15:22:15 +0100 Subject: [PATCH 69/74] Make Clang happy --- code/M3D/m3d.h | 2 ++ test/unit/utM3DImportExport.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index 767e15d5af..1b88c9559e 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -339,6 +339,7 @@ enum { m3dc_cylinder, m3dc_shpere, m3dc_torus, + m3dc_cone, m3dc_cube }; @@ -602,6 +603,7 @@ static m3dcd_t m3d_commandtypes[] = { M3D_CMDDEF(m3dc_cylinder,"cylinder",6, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, 0, 0), M3D_CMDDEF(m3dc_shpere, "shpere", 2, m3dcp_vi_t, m3dcp_vc_t, 0, 0, 0, 0, 0, 0), M3D_CMDDEF(m3dc_torus, "torus", 4, m3dcp_vi_t, m3dcp_qi_t, m3dcp_vc_t, m3dcp_vc_t, 0, 0, 0, 0), + M3D_CMDDEF(m3dc_cone, "cone", 3, m3dcp_vi_t, m3dcp_vi_t, m3dcp_vi_t, 0, 0, 0, 0, 0), M3D_CMDDEF(m3dc_cube, "cube", 3, m3dcp_vi_t, m3dcp_vi_t, m3dcp_vi_t, 0, 0, 0, 0, 0) }; #endif diff --git a/test/unit/utM3DImportExport.cpp b/test/unit/utM3DImportExport.cpp index 048d3770d8..31028235db 100644 --- a/test/unit/utM3DImportExport.cpp +++ b/test/unit/utM3DImportExport.cpp @@ -54,7 +54,7 @@ class utM3DImportExport : public AbstractImportExportBase { public: virtual bool importerTest() { Assimp::Importer importer; - const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/M3D/cube_usemtl.m3d", aiProcess_ValidateDataStructure ); + const aiScene *scene = importer.ReadFile( ASSIMP_TEST_MODELS_DIR "/M3D/cube_normals.m3d", aiProcess_ValidateDataStructure ); #ifndef ASSIMP_BUILD_NO_M3D_IMPORTER return nullptr != scene; #else From 83e9c551f5d1476a44833d82db8f960282479803 Mon Sep 17 00:00:00 2001 From: bzt Date: Mon, 18 Nov 2019 18:31:53 +0100 Subject: [PATCH 70/74] stbi functions prefixed for those who do not know how to use inlcude --- code/M3D/m3d.h | 822 +++++++++++++++++++++++-------------------------- 1 file changed, 390 insertions(+), 432 deletions(-) diff --git a/code/M3D/m3d.h b/code/M3D/m3d.h index 1b88c9559e..a73e7a02c4 100644 --- a/code/M3D/m3d.h +++ b/code/M3D/m3d.h @@ -616,7 +616,7 @@ static m3dcd_t m3d_commandtypes[] = { stb_image - v2.23 - public domain image loader - http://nothings.org/stb_image.h */ -static const char *stbi__g_failure_reason; +static const char *_m3dstbi__g_failure_reason; enum { @@ -635,57 +635,56 @@ enum STBI__SCAN_header }; -typedef unsigned char stbi_uc; -typedef unsigned short stbi_us; +typedef unsigned short _m3dstbi_us; -typedef uint16_t stbi__uint16; -typedef int16_t stbi__int16; -typedef uint32_t stbi__uint32; -typedef int32_t stbi__int32; +typedef uint16_t _m3dstbi__uint16; +typedef int16_t _m3dstbi__int16; +typedef uint32_t _m3dstbi__uint32; +typedef int32_t _m3dstbi__int32; typedef struct { - stbi__uint32 img_x, img_y; + _m3dstbi__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]; + unsigned char buffer_start[128]; - stbi_uc *img_buffer, *img_buffer_end; - stbi_uc *img_buffer_original, *img_buffer_original_end; -} stbi__context; + unsigned char *img_buffer, *img_buffer_end; + unsigned char *img_buffer_original, *img_buffer_original_end; +} _m3dstbi__context; typedef struct { int bits_per_channel; int num_channels; int channel_order; -} stbi__result_info; +} _m3dstbi__result_info; #define STBI_ASSERT(v) #define STBI_NOTUSED(v) (void)sizeof(v) -#define STBI__BYTECAST(x) ((stbi_uc) ((x) & 255)) +#define STBI__BYTECAST(x) ((unsigned char) ((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) +_inline static unsigned char _m3dstbi__get8(_m3dstbi__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) +_inline static int _m3dstbi__at_eof(_m3dstbi__context *s) { return s->img_buffer >= s->img_buffer_end; } -static void stbi__skip(stbi__context *s, int n) +static void _m3dstbi__skip(_m3dstbi__context *s, int n) { if (n < 0) { s->img_buffer = s->img_buffer_end; @@ -694,7 +693,7 @@ static void stbi__skip(stbi__context *s, int n) s->img_buffer += n; } -static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) +static int _m3dstbi__getn(_m3dstbi__context *s, unsigned char *buffer, int n) { if (s->img_buffer+n <= s->img_buffer_end) { memcpy(buffer, s->img_buffer, n); @@ -704,72 +703,72 @@ static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) return 0; } -static int stbi__get16be(stbi__context *s) +static int _m3dstbi__get16be(_m3dstbi__context *s) { - int z = stbi__get8(s); - return (z << 8) + stbi__get8(s); + int z = _m3dstbi__get8(s); + return (z << 8) + _m3dstbi__get8(s); } -static stbi__uint32 stbi__get32be(stbi__context *s) +static _m3dstbi__uint32 _m3dstbi__get32be(_m3dstbi__context *s) { - stbi__uint32 z = stbi__get16be(s); - return (z << 16) + stbi__get16be(s); + _m3dstbi__uint32 z = _m3dstbi__get16be(s); + return (z << 16) + _m3dstbi__get16be(s); } -#define stbi__err(x,y) stbi__errstr(y) -static int stbi__errstr(const char *str) +#define _m3dstbi__err(x,y) _m3dstbi__errstr(y) +static int _m3dstbi__errstr(const char *str) { - stbi__g_failure_reason = str; + _m3dstbi__g_failure_reason = str; return 0; } -_inline static void *stbi__malloc(size_t size) +_inline static void *_m3dstbi__malloc(size_t size) { return STBI_MALLOC(size); } -static int stbi__addsizes_valid(int a, int b) +static int _m3dstbi__addsizes_valid(int a, int b) { if (b < 0) return 0; return a <= 2147483647 - b; } -static int stbi__mul2sizes_valid(int a, int b) +static int _m3dstbi__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) +static int _m3dstbi__mad2sizes_valid(int a, int b, int add) { - return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add); + return _m3dstbi__mul2sizes_valid(a, b) && _m3dstbi__addsizes_valid(a*b, add); } -static int stbi__mad3sizes_valid(int a, int b, int c, int add) +static int _m3dstbi__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); + return _m3dstbi__mul2sizes_valid(a, b) && _m3dstbi__mul2sizes_valid(a*b, c) && + _m3dstbi__addsizes_valid(a*b*c, add); } -static void *stbi__malloc_mad2(int a, int b, int add) +static void *_m3dstbi__malloc_mad2(int a, int b, int add) { - if (!stbi__mad2sizes_valid(a, b, add)) return NULL; - return stbi__malloc(a*b + add); + if (!_m3dstbi__mad2sizes_valid(a, b, add)) return NULL; + return _m3dstbi__malloc(a*b + add); } -static void *stbi__malloc_mad3(int a, int b, int c, int add) +static void *_m3dstbi__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); + if (!_m3dstbi__mad3sizes_valid(a, b, c, add)) return NULL; + return _m3dstbi__malloc(a*b*c + add); } -static stbi_uc stbi__compute_y(int r, int g, int b) +static unsigned char _m3dstbi__compute_y(int r, int g, int b) { - return (stbi_uc) (((r*77) + (g*150) + (29*b)) >> 8); + return (unsigned char) (((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) +static unsigned char *_m3dstbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y) { int i,j; unsigned char *good; @@ -777,10 +776,10 @@ static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int r 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); + good = (unsigned char *) _m3dstbi__malloc_mad3(req_comp, x, y, 0); if (good == NULL) { STBI_FREE(data); - stbi__err("outofmem", "Out of memory"); + _m3dstbi__err("outofmem", "Out of memory"); return NULL; } @@ -798,10 +797,10 @@ static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int r 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(3,1) { dest[0]=_m3dstbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=_m3dstbi__compute_y(src[0],src[1],src[2]), dest[1] = 255; } break; + STBI__CASE(4,1) { dest[0]=_m3dstbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=_m3dstbi__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); } @@ -812,29 +811,29 @@ static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int r return good; } -static stbi__uint16 stbi__compute_y_16(int r, int g, int b) +static _m3dstbi__uint16 _m3dstbi__compute_y_16(int r, int g, int b) { - return (stbi__uint16) (((r*77) + (g*150) + (29*b)) >> 8); + return (_m3dstbi__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) +static _m3dstbi__uint16 *_m3dstbi__convert_format16(_m3dstbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y) { int i,j; - stbi__uint16 *good; + _m3dstbi__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); + good = (_m3dstbi__uint16 *) _m3dstbi__malloc(req_comp * x * y * 2); if (good == NULL) { STBI_FREE(data); - stbi__err("outofmem", "Out of memory"); + _m3dstbi__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; + _m3dstbi__uint16 *src = data + j * x * img_n ; + _m3dstbi__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) @@ -846,10 +845,10 @@ static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int r 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(3,1) { dest[0]=_m3dstbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=_m3dstbi__compute_y_16(src[0],src[1],src[2]), dest[1] = 0xffff; } break; + STBI__CASE(4,1) { dest[0]=_m3dstbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=_m3dstbi__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); } @@ -865,15 +864,15 @@ static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int r typedef struct { - stbi__uint16 fast[1 << STBI__ZFAST_BITS]; - stbi__uint16 firstcode[16]; + _m3dstbi__uint16 fast[1 << STBI__ZFAST_BITS]; + _m3dstbi__uint16 firstcode[16]; int maxcode[17]; - stbi__uint16 firstsymbol[16]; - stbi_uc size[288]; - stbi__uint16 value[288]; -} stbi__zhuffman; + _m3dstbi__uint16 firstsymbol[16]; + unsigned char size[288]; + _m3dstbi__uint16 value[288]; +} _m3dstbi__zhuffman; -_inline static int stbi__bitreverse16(int n) +_inline static int _m3dstbi__bitreverse16(int n) { n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); @@ -882,13 +881,13 @@ _inline static int stbi__bitreverse16(int n) return n; } -_inline static int stbi__bit_reverse(int v, int bits) +_inline static int _m3dstbi__bit_reverse(int v, int bits) { STBI_ASSERT(bits <= 16); - return stbi__bitreverse16(v) >> (16-bits); + return _m3dstbi__bitreverse16(v) >> (16-bits); } -static int stbi__zbuild_huffman(stbi__zhuffman *z, stbi_uc *sizelist, int num) +static int _m3dstbi__zbuild_huffman(_m3dstbi__zhuffman *z, unsigned char *sizelist, int num) { int i,k=0; int code, next_code[16], sizes[17]; @@ -900,15 +899,15 @@ static int stbi__zbuild_huffman(stbi__zhuffman *z, stbi_uc *sizelist, int num) sizes[0] = 0; for (i=1; i < 16; ++i) if (sizes[i] > (1 << i)) - return stbi__err("bad sizes", "Corrupt PNG"); + return _m3dstbi__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; + z->firstcode[i] = (_m3dstbi__uint16) code; + z->firstsymbol[i] = (_m3dstbi__uint16) k; code = (code + sizes[i]); if (sizes[i]) - if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG"); + if (code-1 >= (1 << i)) return _m3dstbi__err("bad codelengths","Corrupt PNG"); z->maxcode[i] = code << (16-i); code <<= 1; k += sizes[i]; @@ -918,11 +917,11 @@ static int stbi__zbuild_huffman(stbi__zhuffman *z, stbi_uc *sizelist, int num) 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; + _m3dstbi__uint16 fastv = (_m3dstbi__uint16) ((s << 9) | i); + z->size [c] = (unsigned char ) s; + z->value[c] = (_m3dstbi__uint16) i; if (s <= STBI__ZFAST_BITS) { - int j = stbi__bit_reverse(next_code[s],s); + int j = _m3dstbi__bit_reverse(next_code[s],s); while (j < (1 << STBI__ZFAST_BITS)) { z->fast[j] = fastv; j += (1 << s); @@ -936,47 +935,47 @@ static int stbi__zbuild_huffman(stbi__zhuffman *z, stbi_uc *sizelist, int num) typedef struct { - stbi_uc *zbuffer, *zbuffer_end; + unsigned char *zbuffer, *zbuffer_end; int num_bits; - stbi__uint32 code_buffer; + _m3dstbi__uint32 code_buffer; char *zout; char *zout_start; char *zout_end; int z_expandable; - stbi__zhuffman z_length, z_distance; -} stbi__zbuf; + _m3dstbi__zhuffman z_length, z_distance; +} _m3dstbi__zbuf; -_inline static stbi_uc stbi__zget8(stbi__zbuf *z) +_inline static unsigned char _m3dstbi__zget8(_m3dstbi__zbuf *z) { if (z->zbuffer >= z->zbuffer_end) return 0; return *z->zbuffer++; } -static void stbi__fill_bits(stbi__zbuf *z) +static void _m3dstbi__fill_bits(_m3dstbi__zbuf *z) { do { STBI_ASSERT(z->code_buffer < (1U << z->num_bits)); - z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; + z->code_buffer |= (unsigned int) _m3dstbi__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) +_inline static unsigned int _m3dstbi__zreceive(_m3dstbi__zbuf *z, int n) { unsigned int k; - if (z->num_bits < n) stbi__fill_bits(z); + if (z->num_bits < n) _m3dstbi__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) +static int _m3dstbi__zhuffman_decode_slowpath(_m3dstbi__zbuf *a, _m3dstbi__zhuffman *z) { int b,s,k; - k = stbi__bit_reverse(a->code_buffer, 16); + k = _m3dstbi__bit_reverse(a->code_buffer, 16); for (s=STBI__ZFAST_BITS+1; ; ++s) if (k < z->maxcode[s]) break; @@ -988,10 +987,10 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) return z->value[b]; } -_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) +_inline static int _m3dstbi__zhuffman_decode(_m3dstbi__zbuf *a, _m3dstbi__zhuffman *z) { int b,s; - if (a->num_bits < 16) stbi__fill_bits(a); + if (a->num_bits < 16) _m3dstbi__fill_bits(a); b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; if (b) { s = b >> 9; @@ -999,76 +998,76 @@ _inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) a->num_bits -= s; return b & 511; } - return stbi__zhuffman_decode_slowpath(a, z); + return _m3dstbi__zhuffman_decode_slowpath(a, z); } -static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) +static int _m3dstbi__zexpand(_m3dstbi__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"); + if (!z->z_expandable) return _m3dstbi__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"); + if (q == NULL) return _m3dstbi__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] = { +static int _m3dstbi__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]= +static int _m3dstbi__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, +static int _m3dstbi__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] = +static int _m3dstbi__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) +static int _m3dstbi__parse_huffman_block(_m3dstbi__zbuf *a) { char *zout = a->zout; for(;;) { - int z = stbi__zhuffman_decode(a, &a->z_length); + int z = _m3dstbi__zhuffman_decode(a, &a->z_length); if (z < 256) { - if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); + if (z < 0) return _m3dstbi__err("bad huffman code","Corrupt PNG"); if (zout >= a->zout_end) { - if (!stbi__zexpand(a, zout, 1)) return 0; + if (!_m3dstbi__zexpand(a, zout, 1)) return 0; zout = a->zout; } *zout++ = (char) z; } else { - stbi_uc *p; + unsigned char *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"); + len = _m3dstbi__zlength_base[z]; + if (_m3dstbi__zlength_extra[z]) len += _m3dstbi__zreceive(a, _m3dstbi__zlength_extra[z]); + z = _m3dstbi__zhuffman_decode(a, &a->z_distance); + if (z < 0) return _m3dstbi__err("bad huffman code","Corrupt PNG"); + dist = _m3dstbi__zdist_base[z]; + if (_m3dstbi__zdist_extra[z]) dist += _m3dstbi__zreceive(a, _m3dstbi__zdist_extra[z]); + if (zout - a->zout_start < dist) return _m3dstbi__err("bad dist","Corrupt PNG"); if (zout + len > a->zout_end) { - if (!stbi__zexpand(a, zout, len)) return 0; + if (!_m3dstbi__zexpand(a, zout, len)) return 0; zout = a->zout; } - p = (stbi_uc *) (zout - dist); + p = (unsigned char *) (zout - dist); if (dist == 1) { - stbi_uc v = *p; + unsigned char v = *p; if (len) { do *zout++ = v; while (--len); } } else { if (len) { do *zout++ = *p++; while (--len); } @@ -1077,152 +1076,152 @@ static int stbi__parse_huffman_block(stbi__zbuf *a) } } -static int stbi__compute_huffman_codes(stbi__zbuf *a) +static int _m3dstbi__compute_huffman_codes(_m3dstbi__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]; + static unsigned char length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + _m3dstbi__zhuffman z_codelength; + unsigned char lencodes[286+32+137]; + unsigned char 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 hlit = _m3dstbi__zreceive(a,5) + 257; + int hdist = _m3dstbi__zreceive(a,5) + 1; + int hclen = _m3dstbi__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; + int s = _m3dstbi__zreceive(a,3); + codelength_sizes[length_dezigzag[i]] = (unsigned char) s; } - if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; + if (!_m3dstbi__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"); + int c = _m3dstbi__zhuffman_decode(a, &z_codelength); + if (c < 0 || c >= 19) return _m3dstbi__err("bad codelengths", "Corrupt PNG"); if (c < 16) - lencodes[n++] = (stbi_uc) c; + lencodes[n++] = (unsigned char) c; else { - stbi_uc fill = 0; + unsigned char fill = 0; if (c == 16) { - c = stbi__zreceive(a,2)+3; - if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); + c = _m3dstbi__zreceive(a,2)+3; + if (n == 0) return _m3dstbi__err("bad codelengths", "Corrupt PNG"); fill = lencodes[n-1]; } else if (c == 17) - c = stbi__zreceive(a,3)+3; + c = _m3dstbi__zreceive(a,3)+3; else { STBI_ASSERT(c == 18); - c = stbi__zreceive(a,7)+11; + c = _m3dstbi__zreceive(a,7)+11; } - if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); + if (ntot - n < c) return _m3dstbi__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; + if (n != ntot) return _m3dstbi__err("bad codelengths","Corrupt PNG"); + if (!_m3dstbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; + if (!_m3dstbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; return 1; } -_inline static int stbi__parse_uncompressed_block(stbi__zbuf *a) +_inline static int _m3dstbi__parse_uncompressed_block(_m3dstbi__zbuf *a) { - stbi_uc header[4]; + unsigned char header[4]; int len,nlen,k; if (a->num_bits & 7) - stbi__zreceive(a, a->num_bits & 7); + _m3dstbi__zreceive(a, a->num_bits & 7); k = 0; while (a->num_bits > 0) { - header[k++] = (stbi_uc) (a->code_buffer & 255); + header[k++] = (unsigned char) (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); + header[k++] = _m3dstbi__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 (nlen != (len ^ 0xffff)) return _m3dstbi__err("zlib corrupt","Corrupt PNG"); + if (a->zbuffer + len > a->zbuffer_end) return _m3dstbi__err("read past buffer","Corrupt PNG"); if (a->zout + len > a->zout_end) - if (!stbi__zexpand(a, a->zout, len)) return 0; + if (!_m3dstbi__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) +static int _m3dstbi__parse_zlib_header(_m3dstbi__zbuf *a) { - int cmf = stbi__zget8(a); + int cmf = _m3dstbi__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"); + int flg = _m3dstbi__zget8(a); + if ((cmf*256+flg) % 31 != 0) return _m3dstbi__err("bad zlib header","Corrupt PNG"); + if (flg & 32) return _m3dstbi__err("no preset dict","Corrupt PNG"); + if (cm != 8) return _m3dstbi__err("bad compression","Corrupt PNG"); return 1; } -static stbi_uc stbi__zdefault_length[288], stbi__zdefault_distance[32]; -static void stbi__init_zdefaults(void) +static unsigned char _m3dstbi__zdefault_length[288], _m3dstbi__zdefault_distance[32]; +static void _m3dstbi__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 <= 143; ++i) _m3dstbi__zdefault_length[i] = 8; + for ( ; i <= 255; ++i) _m3dstbi__zdefault_length[i] = 9; + for ( ; i <= 279; ++i) _m3dstbi__zdefault_length[i] = 7; + for ( ; i <= 287; ++i) _m3dstbi__zdefault_length[i] = 8; - for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; + for (i=0; i <= 31; ++i) _m3dstbi__zdefault_distance[i] = 5; } -static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) +static int _m3dstbi__parse_zlib(_m3dstbi__zbuf *a, int parse_header) { int final, type; if (parse_header) - if (!stbi__parse_zlib_header(a)) return 0; + if (!_m3dstbi__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); + final = _m3dstbi__zreceive(a,1); + type = _m3dstbi__zreceive(a,2); if (type == 0) { - if (!stbi__parse_uncompressed_block(a)) return 0; + if (!_m3dstbi__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; + _m3dstbi__init_zdefaults(); + if (!_m3dstbi__zbuild_huffman(&a->z_length , _m3dstbi__zdefault_length , 288)) return 0; + if (!_m3dstbi__zbuild_huffman(&a->z_distance, _m3dstbi__zdefault_distance, 32)) return 0; } else { - if (!stbi__compute_huffman_codes(a)) return 0; + if (!_m3dstbi__compute_huffman_codes(a)) return 0; } - if (!stbi__parse_huffman_block(a)) return 0; + if (!_m3dstbi__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) +static int _m3dstbi__do_zlib(_m3dstbi__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); + return _m3dstbi__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) +char *_m3dstbi_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); + _m3dstbi__zbuf a; + char *p = (char *) _m3dstbi__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)) { + a.zbuffer = (unsigned char *) buffer; + a.zbuffer_end = (unsigned char *) buffer + len; + if (_m3dstbi__do_zlib(&a, p, initial_size, 1, parse_header)) { if (outlen) *outlen = (int) (a.zout - a.zout_start); return a.zout_start; } else { @@ -1233,33 +1232,33 @@ char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, typedef struct { - stbi__uint32 length; - stbi__uint32 type; -} stbi__pngchunk; + _m3dstbi__uint32 length; + _m3dstbi__uint32 type; +} _m3dstbi__pngchunk; -static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) +static _m3dstbi__pngchunk _m3dstbi__get_chunk_header(_m3dstbi__context *s) { - stbi__pngchunk c; - c.length = stbi__get32be(s); - c.type = stbi__get32be(s); + _m3dstbi__pngchunk c; + c.length = _m3dstbi__get32be(s); + c.type = _m3dstbi__get32be(s); return c; } -_inline static int stbi__check_png_header(stbi__context *s) +_inline static int _m3dstbi__check_png_header(_m3dstbi__context *s) { - static stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 }; + static unsigned char 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"); + if (_m3dstbi__get8(s) != png_sig[i]) return _m3dstbi__err("bad png sig","Not a PNG"); return 1; } typedef struct { - stbi__context *s; - stbi_uc *idata, *expanded, *out; + _m3dstbi__context *s; + unsigned char *idata, *expanded, *out; int depth; -} stbi__png; +} _m3dstbi__png; enum { @@ -1272,7 +1271,7 @@ enum { STBI__F_paeth_first }; -static stbi_uc first_row_filter[5] = +static unsigned char first_row_filter[5] = { STBI__F_none, STBI__F_sub, @@ -1281,7 +1280,7 @@ static stbi_uc first_row_filter[5] = STBI__F_paeth_first }; -static int stbi__paeth(int a, int b, int c) +static int _m3dstbi__paeth(int a, int b, int c) { int p = a + b - c; int pa = abs(p-a); @@ -1292,14 +1291,14 @@ static int stbi__paeth(int a, int b, int c) return c; } -static stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; +static unsigned char _m3dstbi__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) +static int _m3dstbi__create_png_image_raw(_m3dstbi__png *a, unsigned char *raw, _m3dstbi__uint32 raw_len, int out_n, _m3dstbi__uint32 x, _m3dstbi__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; + _m3dstbi__context *s = a->s; + _m3dstbi__uint32 i,j,stride = x*out_n*bytes; + _m3dstbi__uint32 img_len, img_width_bytes; int k; int img_n = s->img_n; @@ -1308,24 +1307,25 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r 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"); + a->out = (unsigned char *) _m3dstbi__malloc_mad3(x, y, output_bytes, 0); + if (!a->out) return _m3dstbi__err("outofmem", "Out of memory"); + if (!_m3dstbi__mad3sizes_valid(img_n, x, depth, 7)) return _m3dstbi__err("too large", "Corrupt PNG"); 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"); + if (raw_len != img_len) return _m3dstbi__err("not enough pixels","Corrupt PNG"); } else { - if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); + if (raw_len < img_len) return _m3dstbi__err("not enough pixels","Corrupt PNG"); } for (j=0; j < y; ++j) { - stbi_uc *cur = a->out + stride*j; - stbi_uc *prior = cur - stride; + unsigned char *cur = a->out + stride*j; + unsigned char *prior = cur - stride; int filter = *raw++; if (filter > 4) - return stbi__err("invalid filter","Corrupt PNG"); + return _m3dstbi__err("invalid filter","Corrupt PNG"); if (depth < 8) { STBI_ASSERT(img_width_bytes <= x); @@ -1333,6 +1333,7 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r filter_bytes = 1; width = img_width_bytes; } + prior = cur - stride; if (j == 0) filter = first_row_filter[filter]; @@ -1342,7 +1343,7 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r 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_paeth : cur[k] = STBI__BYTECAST(raw[k] + _m3dstbi__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; } @@ -1378,9 +1379,9 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r 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_paeth) { cur[k] = STBI__BYTECAST(raw[k] + _m3dstbi__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; + STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + _m3dstbi__paeth(cur[k-filter_bytes],0,0)); } break; } #undef STBI__CASE raw += nk; @@ -1395,9 +1396,9 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r 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_paeth) { cur[k] = STBI__BYTECAST(raw[k] + _m3dstbi__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; + STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + _m3dstbi__paeth(cur[k- output_bytes],0,0)); } break; } #undef STBI__CASE @@ -1412,9 +1413,9 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r 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; + unsigned char *cur = a->out + stride*j; + unsigned char *in = a->out + stride*j + x*out_n - img_width_bytes; + unsigned char scale = (color == 0) ? _m3dstbi__depth_scale_table[depth] : 1; if (depth == 4) { for (k=x*img_n; k >= 2; k-=2, ++in) { @@ -1471,8 +1472,8 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r } } } else if (depth == 16) { - stbi_uc *cur = a->out; - stbi__uint16 *cur16 = (stbi__uint16*)cur; + unsigned char *cur = a->out; + _m3dstbi__uint16 *cur16 = (_m3dstbi__uint16*)cur; for(i=0; i < x*y*out_n; ++i,cur16++,cur+=2) { *cur16 = (cur[0] << 8) | cur[1]; @@ -1482,16 +1483,16 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r 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) +static int _m3dstbi__create_png_image(_m3dstbi__png *a, unsigned char *image_data, _m3dstbi__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; + unsigned char *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); + return _m3dstbi__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); + final = (unsigned char *) _m3dstbi__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 }; @@ -1501,8 +1502,8 @@ static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint3 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)) { + _m3dstbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; + if (!_m3dstbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) { STBI_FREE(final); return 0; } @@ -1524,11 +1525,11 @@ static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint3 return 1; } -static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) +static int _m3dstbi__compute_transparency(_m3dstbi__png *z, unsigned char 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; + _m3dstbi__context *s = z->s; + _m3dstbi__uint32 i, pixel_count = s->img_x * s->img_y; + unsigned char *p = z->out; STBI_ASSERT(out_n == 2 || out_n == 4); @@ -1547,11 +1548,11 @@ static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) return 1; } -static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n) +static int _m3dstbi__compute_transparency16(_m3dstbi__png *z, _m3dstbi__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; + _m3dstbi__context *s = z->s; + _m3dstbi__uint32 i, pixel_count = s->img_x * s->img_y; + _m3dstbi__uint16 *p = (_m3dstbi__uint16*) z->out; STBI_ASSERT(out_n == 2 || out_n == 4); @@ -1570,13 +1571,13 @@ static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int ou return 1; } -static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n) +static int _m3dstbi__expand_png_palette(_m3dstbi__png *a, unsigned char *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; + _m3dstbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; + unsigned char *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"); + p = (unsigned char *) _m3dstbi__malloc_mad2(pixel_count, pal_img_n, 0); + if (p == NULL) return _m3dstbi__err("outofmem", "Out of memory"); temp_out = p; @@ -1606,228 +1607,177 @@ static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int return 1; } -static int stbi__unpremultiply_on_load = 0; -static int stbi__de_iphone_flag = 0; +#define STBI__PNG_TYPE(a,b,c,d) (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d)) -void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) +static int _m3dstbi__parse_png_file(_m3dstbi__png *z, int scan, int req_comp) { - 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; + unsigned char palette[1024], pal_img_n=0; + unsigned char has_trans=0, tc[3]; + _m3dstbi__uint16 tc16[3]; + _m3dstbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; + int first=1,k,interlace=0, color=0; + _m3dstbi__context *s = z->s; z->expanded = NULL; z->idata = NULL; z->out = NULL; - if (!stbi__check_png_header(s)) return 0; + if (!_m3dstbi__check_png_header(s)) return 0; if (scan == STBI__SCAN_type) return 1; for (;;) { - stbi__pngchunk c = stbi__get_chunk_header(s); + _m3dstbi__pngchunk c = _m3dstbi__get_chunk_header(s); switch (c.type) { case STBI__PNG_TYPE('C','g','B','I'): - is_iphone = 1; - stbi__skip(s, c.length); + _m3dstbi__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"); + if (!first) return _m3dstbi__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 (c.length != 13) return _m3dstbi__err("bad IHDR len","Corrupt PNG"); + s->img_x = _m3dstbi__get32be(s); if (s->img_x > (1 << 24)) return _m3dstbi__err("too large","Very large image (corrupt?)"); + s->img_y = _m3dstbi__get32be(s); if (s->img_y > (1 << 24)) return _m3dstbi__err("too large","Very large image (corrupt?)"); + z->depth = _m3dstbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return _m3dstbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); + color = _m3dstbi__get8(s); if (color > 6) return _m3dstbi__err("bad ctype","Corrupt PNG"); + if (color == 3 && z->depth == 16) return _m3dstbi__err("bad ctype","Corrupt PNG"); + if (color == 3) pal_img_n = 3; else if (color & 1) return _m3dstbi__err("bad ctype","Corrupt PNG"); + comp = _m3dstbi__get8(s); if (comp) return _m3dstbi__err("bad comp method","Corrupt PNG"); + filter= _m3dstbi__get8(s); if (filter) return _m3dstbi__err("bad filter method","Corrupt PNG"); + interlace = _m3dstbi__get8(s); if (interlace>1) return _m3dstbi__err("bad interlace method","Corrupt PNG"); + if (!s->img_x || !s->img_y) return _m3dstbi__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 ((1 << 30) / s->img_x / s->img_n < s->img_y) return _m3dstbi__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"); + if ((1 << 30) / s->img_x / 4 < s->img_y) return _m3dstbi__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"); + if (first) return _m3dstbi__err("first not IHDR", "Corrupt PNG"); + if (c.length > 256*3) return _m3dstbi__err("invalid PLTE","Corrupt PNG"); pal_len = c.length / 3; - if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG"); + if (pal_len * 3 != c.length) return _m3dstbi__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+0] = _m3dstbi__get8(s); + palette[i*4+1] = _m3dstbi__get8(s); + palette[i*4+2] = _m3dstbi__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 (first) return _m3dstbi__err("first not IHDR", "Corrupt PNG"); + if (z->idata) return _m3dstbi__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"); + if (pal_len == 0) return _m3dstbi__err("tRNS before PLTE","Corrupt PNG"); + if (c.length > pal_len) return _m3dstbi__err("bad tRNS len","Corrupt PNG"); pal_img_n = 4; for (i=0; i < c.length; ++i) - palette[i*4+3] = stbi__get8(s); + palette[i*4+3] = _m3dstbi__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"); + if (!(s->img_n & 1)) return _m3dstbi__err("tRNS with alpha","Corrupt PNG"); + if (c.length != (_m3dstbi__uint32) s->img_n*2) return _m3dstbi__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); + for (k = 0; k < s->img_n; ++k) tc16[k] = (_m3dstbi__uint16)_m3dstbi__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]; + for (k = 0; k < s->img_n; ++k) tc[k] = (unsigned char)(_m3dstbi__get16be(s) & 255) * _m3dstbi__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 (first) return _m3dstbi__err("first not IHDR", "Corrupt PNG"); + if (pal_img_n && !pal_len) return _m3dstbi__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; + _m3dstbi__uint32 idata_limit_old = idata_limit; + unsigned char *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"); + p = (unsigned char *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return _m3dstbi__err("outofmem", "Out of memory"); z->idata = p; } - if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG"); + if (!_m3dstbi__getn(s, z->idata+ioff,c.length)) return _m3dstbi__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"); + _m3dstbi__uint32 raw_len, bpl; + if (first) return _m3dstbi__err("first not IHDR", "Corrupt PNG"); if (scan != STBI__SCAN_load) return 1; - if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG"); + if (z->idata == NULL) return _m3dstbi__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); + z->expanded = (unsigned char *) _m3dstbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, 1); 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 (!_m3dstbi__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; + if (!_m3dstbi__compute_transparency16(z, tc16, s->img_out_n)) return 0; } else { - if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0; + if (!_m3dstbi__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)) + if (!_m3dstbi__expand_png_palette(z, palette, pal_len, s->img_out_n)) return 0; + } else if (has_trans) { + ++s->img_n; } STBI_FREE(z->expanded); z->expanded = NULL; return 1; } default: - if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (first) return _m3dstbi__err("first not IHDR", "Corrupt PNG"); if ((c.type & (1 << 29)) == 0) { - return stbi__err("invalid_chunk", "PNG not supported: unknown PNG chunk type"); + return _m3dstbi__err("invalid_chunk", "PNG not supported: unknown PNG chunk type"); } - stbi__skip(s, c.length); + _m3dstbi__skip(s, c.length); break; } - stbi__get32be(s); + _m3dstbi__get32be(s); } } -static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri) +static void *_m3dstbi__do_png(_m3dstbi__png *p, int *x, int *y, int *n, int req_comp, _m3dstbi__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; + if (req_comp < 0 || req_comp > 4) { _m3dstbi__err("bad req_comp", "Internal error"); return NULL; } + if (_m3dstbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { + if (p->depth < 8) + ri->bits_per_channel = 8; + else + 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); + result = _m3dstbi__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); + result = _m3dstbi__convert_format16((_m3dstbi__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; } @@ -1842,12 +1792,16 @@ static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, st return result; } -static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +static void *_m3dstbi__png_load(_m3dstbi__context *s, int *x, int *y, int *comp, int req_comp, _m3dstbi__result_info *ri) { - stbi__png p; + _m3dstbi__png p; p.s = s; - return stbi__do_png(&p, x,y,comp,req_comp, ri); + return _m3dstbi__do_png(&p, x,y,comp,req_comp, ri); } +#define stbi__context _m3dstbi__context +#define stbi__result_info _m3dstbi__result_info +#define stbi__png_load _m3dstbi__png_load +#define stbi_zlib_decode_malloc_guesssize_headerflag _m3dstbi_zlib_decode_malloc_guesssize_headerflag #endif #if defined(M3D_EXPORTER) && !defined(INCLUDE_STB_IMAGE_WRITE_H) @@ -1855,13 +1809,13 @@ static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req 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 unsigned char _m3dstbiw__uc; +typedef unsigned short _m3dstbiw__us; -typedef uint16_t stbiw_uint16; -typedef int16_t stbiw_int16; -typedef uint32_t stbiw_uint32; -typedef int32_t stbiw_int32; +typedef uint16_t _m3dstbiw__uint16; +typedef int16_t _m3dstbiw__int16; +typedef uint32_t _m3dstbiw__uint32; +typedef int32_t _m3dstbiw__int32; #define STBIW_MALLOC(s) M3D_MALLOC(s) #define STBIW_REALLOC(p,ns) M3D_REALLOC(p,ns) @@ -1870,42 +1824,42 @@ typedef int32_t stbiw_int32; #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 _m3dstbiw___sbraw(a) ((int *) (a) - 2) +#define _m3dstbiw___sbm(a) _m3dstbiw___sbraw(a)[0] +#define _m3dstbiw___sbn(a) _m3dstbiw___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 _m3dstbiw___sbneedgrow(a,n) ((a)==0 || _m3dstbiw___sbn(a)+n >= _m3dstbiw___sbm(a)) +#define _m3dstbiw___sbmaybegrow(a,n) (_m3dstbiw___sbneedgrow(a,(n)) ? _m3dstbiw___sbgrow(a,n) : 0) +#define _m3dstbiw___sbgrow(a,n) _m3dstbiw___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) +#define _m3dstbiw___sbpush(a, v) (_m3dstbiw___sbmaybegrow(a,1), (a)[_m3dstbiw___sbn(a)++] = (v)) +#define _m3dstbiw___sbcount(a) ((a) ? _m3dstbiw___sbn(a) : 0) +#define _m3dstbiw___sbfree(a) ((a) ? STBIW_FREE(_m3dstbiw___sbraw(a)),0 : 0) -static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +static void *_m3dstbiw___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); + int m = *arr ? 2*_m3dstbiw___sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? _m3dstbiw___sbraw(*arr) : 0, *arr ? (_m3dstbiw___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; + _m3dstbiw___sbm(*arr) = m; } return *arr; } -static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +static unsigned char *_m3dstbiw___zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) { while (*bitcount >= 8) { - stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + _m3dstbiw___sbpush(data, STBIW_UCHAR(*bitbuffer)); *bitbuffer >>= 8; *bitcount -= 8; } return data; } -static int stbiw__zlib_bitrev(int code, int codebits) +static int _m3dstbiw___zlib_bitrev(int code, int codebits) { int res=0; while (codebits--) { @@ -1915,7 +1869,7 @@ static int stbiw__zlib_bitrev(int code, int codebits) return res; } -static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +static unsigned int _m3dstbiw___zlib_countm(unsigned char *a, unsigned char *b, int limit) { int i; for (i=0; i < limit && i < 258; ++i) @@ -1923,9 +1877,9 @@ static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int l return i; } -static unsigned int stbiw__zhash(unsigned char *data) +static unsigned int _m3dstbiw___zhash(unsigned char *data) { - stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + _m3dstbiw__uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); hash ^= hash << 3; hash += hash >> 5; hash ^= hash << 4; @@ -1935,20 +1889,20 @@ static unsigned int stbiw__zhash(unsigned char *data) 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 _m3dstbiw___zlib_flush() (out = _m3dstbiw___zlib_flushf(out, &bitbuf, &bitcount)) +#define _m3dstbiw___zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), _m3dstbiw___zlib_flush()) +#define _m3dstbiw___zlib_huffa(b,c) _m3dstbiw___zlib_add(_m3dstbiw___zlib_bitrev(b,c),c) +#define _m3dstbiw___zlib_huff1(n) _m3dstbiw___zlib_huffa(0x30 + (n), 8) +#define _m3dstbiw___zlib_huff2(n) _m3dstbiw___zlib_huffa(0x190 + (n)-144, 9) +#define _m3dstbiw___zlib_huff3(n) _m3dstbiw___zlib_huffa(0 + (n)-256,7) +#define _m3dstbiw___zlib_huff4(n) _m3dstbiw___zlib_huffa(0xc0 + (n)-280,8) +#define _m3dstbiw___zlib_huff(n) ((n) <= 143 ? _m3dstbiw___zlib_huff1(n) : (n) <= 255 ? _m3dstbiw___zlib_huff2(n) : (n) <= 279 ? _m3dstbiw___zlib_huff3(n) : _m3dstbiw___zlib_huff4(n)) +#define _m3dstbiw___zlib_huffb(n) ((n) <= 143 ? _m3dstbiw___zlib_huff1(n) : _m3dstbiw___zlib_huff2(n)) -#define stbiw__ZHASH 16384 +#define _m3dstbiw___ZHASH 16384 -unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +unsigned char * _m3dstbi_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 }; @@ -1957,42 +1911,44 @@ unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_l 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**)); + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(_m3dstbiw___ZHASH * sizeof(char**)); + if (hash_table == NULL) + return NULL; if (quality < 5) quality = 5; - stbiw__sbpush(out, 0x78); - stbiw__sbpush(out, 0x5e); - stbiw__zlib_add(1,1); - stbiw__zlib_add(1,2); + _m3dstbiw___sbpush(out, 0x78); + _m3dstbiw___sbpush(out, 0x5e); + _m3dstbiw___zlib_add(1,1); + _m3dstbiw___zlib_add(1,2); - for (i=0; i < stbiw__ZHASH; ++i) + for (i=0; i < _m3dstbiw___ZHASH; ++i) hash_table[i] = NULL; i=0; while (i < data_len-3) { - int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + int h = _m3dstbiw___zhash(data+i)&(_m3dstbiw___ZHASH-1), best=3; unsigned char *bestloc = 0; unsigned char **hlist = hash_table[h]; - int n = stbiw__sbcount(hlist); + int n = _m3dstbiw___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); + int d = _m3dstbiw___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) { + if (hash_table[h] && _m3dstbiw___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; + _m3dstbiw___sbn(hash_table[h]) = quality; } - stbiw__sbpush(hash_table[h],data+i); + _m3dstbiw___sbpush(hash_table[h],data+i); if (bestloc) { - h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + h = _m3dstbiw___zhash(data+i+1)&(_m3dstbiw___ZHASH-1); hlist = hash_table[h]; - n = stbiw__sbcount(hlist); + n = _m3dstbiw___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); + int e = _m3dstbiw___zlib_countm(hlist[j], data+i+1, data_len-i-1); if (e > best) { bestloc = NULL; break; @@ -2005,25 +1961,25 @@ unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_l 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]); + _m3dstbiw___zlib_huff(j+257); + if (lengtheb[j]) _m3dstbiw___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]); + _m3dstbiw___zlib_add(_m3dstbiw___zlib_bitrev(j,5),5); + if (disteb[j]) _m3dstbiw___zlib_add(d - distc[j], disteb[j]); i += best; } else { - stbiw__zlib_huffb(data[i]); + _m3dstbiw___zlib_huffb(data[i]); ++i; } } for (;i < data_len; ++i) - stbiw__zlib_huffb(data[i]); - stbiw__zlib_huff(256); + _m3dstbiw___zlib_huffb(data[i]); + _m3dstbiw___zlib_huff(256); while (bitcount) - stbiw__zlib_add(0,1); + _m3dstbiw___zlib_add(0,1); - for (i=0; i < stbiw__ZHASH; ++i) - (void) stbiw__sbfree(hash_table[i]); + for (i=0; i < _m3dstbiw___ZHASH; ++i) + (void) _m3dstbiw___sbfree(hash_table[i]); STBIW_FREE(hash_table); { @@ -2036,15 +1992,18 @@ unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_l 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)); + _m3dstbiw___sbpush(out, STBIW_UCHAR(s2 >> 8)); + _m3dstbiw___sbpush(out, STBIW_UCHAR(s2)); + _m3dstbiw___sbpush(out, STBIW_UCHAR(s1 >> 8)); + _m3dstbiw___sbpush(out, STBIW_UCHAR(s1)); } - *out_len = stbiw__sbn(out); - STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); - return (unsigned char *) stbiw__sbraw(out); + *out_len = _m3dstbiw___sbn(out); + STBIW_MEMMOVE(_m3dstbiw___sbraw(out), out, *out_len); + return (unsigned char *) _m3dstbiw___sbraw(out); } +#define stbi_zlib_compress _m3dstbi_zlib_compress +#else +unsigned char * _m3dstbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality); #endif #define M3D_CHUNKMAGIC(m, a,b,c,d) ((m)[0]==(a) && (m)[1]==(b) && (m)[2]==(c) && (m)[3]==(d)) @@ -2138,10 +2097,11 @@ char *_m3d_safestr(char *in, int morelines) /* 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 int i, len = 0; unsigned char *buff = NULL; char *fn2; #ifdef STBI__PNG_TYPE + unsigned int w, h; stbi__context s; stbi__result_info ri; #endif @@ -2187,8 +2147,8 @@ M3D_INDEX _m3d_gettx(m3d_t *model, m3dread_t readfilecb, m3dfree_t freecb, char if(buff[0] == 0x89 && buff[1] == 'P' && buff[2] == 'N' && buff[3] == 'G') { #ifdef STBI__PNG_TYPE 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; + s.img_buffer = s.img_buffer_original = (unsigned char *) buff; + s.img_buffer_end = s.img_buffer_original_end = (unsigned char *) buff+len; /* don't use model->texture[i].w directly, it's a uint16_t */ w = h = len = 0; ri.bits_per_channel = 8; @@ -2935,11 +2895,9 @@ m3d_t *m3d_load(unsigned char *data, m3dread_t readfilecb, m3dfree_t freecb, m3d /* 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; @@ -3969,7 +3927,7 @@ static int _m3d_vrtxcmp(const void *a, const void *b) { return memcmp(a, b, sizeof(m3dv_t)); } /* compare labels */ -_inline int _m3d_strcmp(char *a, char *b) +static _inline int _m3d_strcmp(char *a, char *b) { if(a == NULL && b != NULL) return -1; if(a != NULL && b == NULL) return 1; From 7e222f0730253b93f2348cd6874d62478668f54f Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Mon, 18 Nov 2019 18:43:28 +0100 Subject: [PATCH 71/74] fix invalid cast. --- code/PostProcessing/ValidateDataStructure.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/PostProcessing/ValidateDataStructure.cpp b/code/PostProcessing/ValidateDataStructure.cpp index b7f56a5829..1dc2176635 100644 --- a/code/PostProcessing/ValidateDataStructure.cpp +++ b/code/PostProcessing/ValidateDataStructure.cpp @@ -637,7 +637,7 @@ void ValidateDSProcess::SearchForInvalidTextures(const aiMaterial* pMaterial, ReportError("Material property %s%i is expected to be 5 floats large (size is %i)", prop->mKey.data,prop->mIndex, prop->mDataLength); } - mappings[prop->mIndex] = *((aiTextureMapping*)prop->mData); + //mappings[prop->mIndex] = ((aiUVTransform*)prop->mData); } else if (!::strcmp(prop->mKey.data,"$tex.uvwsrc")) { if (aiPTI_Integer != prop->mType || sizeof(int) > prop->mDataLength) From f78446b14aff46db2ef27d062a275b6a01fd68b1 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Tue, 19 Nov 2019 20:30:40 +0100 Subject: [PATCH 72/74] closes https://github.com/assimp/assimp/issues/2733: update of zlip to fix gcc build for v9.2.0 32 bit --- contrib/zip/.gitignore | 2 + contrib/zip/CMakeLists.txt | 83 +++++- contrib/zip/README.md | 12 +- contrib/zip/appveyor.yml | 2 +- contrib/zip/src/miniz.h | 457 ++++++++++++++++++++++++++++---- contrib/zip/src/zip.c | 62 +++-- contrib/zip/src/zip.h | 457 ++++++++++++++++---------------- contrib/zip/test/CMakeLists.txt | 27 +- contrib/zip/test/test.c | 38 ++- contrib/zip/test/test_miniz.c | 25 +- 10 files changed, 821 insertions(+), 344 deletions(-) diff --git a/contrib/zip/.gitignore b/contrib/zip/.gitignore index a7904a1efc..49b2cb2fda 100644 --- a/contrib/zip/.gitignore +++ b/contrib/zip/.gitignore @@ -1,6 +1,7 @@ /build/ /test/build/ /xcodeproj/ +.vscode/ # Object files *.o @@ -54,3 +55,4 @@ zip.dir/ test/test.exe.vcxproj.filters test/test.exe.vcxproj test/test.exe.dir/ + diff --git a/contrib/zip/CMakeLists.txt b/contrib/zip/CMakeLists.txt index b46dbb1db0..77916d2e14 100644 --- a/contrib/zip/CMakeLists.txt +++ b/contrib/zip/CMakeLists.txt @@ -1,10 +1,14 @@ -cmake_minimum_required(VERSION 2.8) -project(zip) -enable_language(C) +cmake_minimum_required(VERSION 3.0) + +project(zip + LANGUAGES C + VERSION "0.1.15") set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) +option(CMAKE_DISABLE_TESTING "Disable test creation" OFF) + if (MSVC) - # Use secure functions by defaualt and suppress warnings about "deprecated" functions + # Use secure functions by default and suppress warnings about "deprecated" functions set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES=1") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT=1") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CRT_NONSTDC_NO_WARNINGS=1 /D _CRT_SECURE_NO_WARNINGS=1") @@ -12,28 +16,80 @@ elseif ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "AppleClang") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Werror -pedantic") + if(ENABLE_COVERAGE) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O0 -fprofile-arcs -ftest-coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") + endif() endif (MSVC) # zip set(SRC src/miniz.h src/zip.h src/zip.c) add_library(${PROJECT_NAME} ${SRC}) -target_include_directories(${PROJECT_NAME} INTERFACE src) +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $ +) # test if (NOT CMAKE_DISABLE_TESTING) enable_testing() add_subdirectory(test) find_package(Sanitizers) - add_sanitizers(${PROJECT_NAME} test.exe) - add_sanitizers(${PROJECT_NAME} test_miniz.exe) + add_sanitizers(${PROJECT_NAME} ${test_out} ${test_miniz_out}) endif() +#### +# Installation (https://github.com/forexample/package-example) { + +set(CONFIG_INSTALL_DIR "lib/cmake/${PROJECT_NAME}") +set(INCLUDE_INSTALL_DIR "include") + +set(GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") + +# Configuration +set(VERSION_CONFIG "${GENERATED_DIR}/${PROJECT_NAME}ConfigVersion.cmake") +set(PROJECT_CONFIG "${GENERATED_DIR}/${PROJECT_NAME}Config.cmake") +set(TARGETS_EXPORT_NAME "${PROJECT_NAME}Targets") +set(NAMESPACE "${PROJECT_NAME}::") + +# Include module with fuction 'write_basic_package_version_file' +include(CMakePackageConfigHelpers) + +# Note: PROJECT_VERSION is used as a VERSION +write_basic_package_version_file( + "${VERSION_CONFIG}" COMPATIBILITY SameMajorVersion +) + +# Use variables: +# * TARGETS_EXPORT_NAME +# * PROJECT_NAME +configure_package_config_file( + "cmake/Config.cmake.in" + "${PROJECT_CONFIG}" + INSTALL_DESTINATION "${CONFIG_INSTALL_DIR}" +) + +install( + FILES "${PROJECT_CONFIG}" "${VERSION_CONFIG}" + DESTINATION "${CONFIG_INSTALL_DIR}" +) + +install( + EXPORT "${TARGETS_EXPORT_NAME}" + NAMESPACE "${NAMESPACE}" + DESTINATION "${CONFIG_INSTALL_DIR}" +) + +# } + install(TARGETS ${PROJECT_NAME} + EXPORT ${TARGETS_EXPORT_NAME} RUNTIME DESTINATION bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib - COMPONENT library) -install(FILES ${PROJECT_SOURCE_DIR}/src/zip.h DESTINATION include) + INCLUDES DESTINATION ${INCLUDE_INSTALL_DIR} +) +install(FILES ${PROJECT_SOURCE_DIR}/src/zip.h DESTINATION ${INCLUDE_INSTALL_DIR}/zip) # uninstall target (https://gitlab.kitware.com/cmake/community/wikis/FAQ#can-i-do-make-uninstall-with-cmake) if(NOT TARGET uninstall) @@ -45,3 +101,12 @@ if(NOT TARGET uninstall) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake/cmake_uninstall.cmake) endif() + +find_package(Doxygen) +if(DOXYGEN_FOUND) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating API documentation with Doxygen" VERBATIM) +endif() diff --git a/contrib/zip/README.md b/contrib/zip/README.md index d5fb8cd203..14eb9a34c8 100644 --- a/contrib/zip/README.md +++ b/contrib/zip/README.md @@ -71,7 +71,7 @@ int arg = 2; zip_extract("foo.zip", "/tmp", on_extract_entry, &arg); ``` -* Extract a zip entry into memory. +* Extract a zip entry into memory. ```c void *buf = NULL; size_t bufsize; @@ -89,7 +89,7 @@ zip_close(zip); free(buf); ``` -* Extract a zip entry into memory (no internal allocation). +* Extract a zip entry into memory (no internal allocation). ```c unsigned char *buf; size_t bufsize; @@ -110,7 +110,7 @@ zip_close(zip); free(buf); ``` -* Extract a zip entry into memory using callback. +* Extract a zip entry into memory using callback. ```c struct buffer_t { char *data; @@ -144,7 +144,7 @@ free(buf.data); ``` -* Extract a zip entry into a file. +* Extract a zip entry into a file. ```c struct zip_t *zip = zip_open("foo.zip", 0, 'r'); { @@ -157,7 +157,7 @@ struct zip_t *zip = zip_open("foo.zip", 0, 'r'); zip_close(zip); ``` -* List of all zip entries +* List of all zip entries ```c struct zip_t *zip = zip_open("foo.zip", 0, 'r'); int i, n = zip_total_entries(zip); @@ -174,7 +174,7 @@ for (i = 0; i < n; ++i) { zip_close(zip); ``` -## Bindings +# Bindings Compile zip library as a dynamic library. ```shell $ mkdir build diff --git a/contrib/zip/appveyor.yml b/contrib/zip/appveyor.yml index 0be6373ca0..ea17f5de92 100644 --- a/contrib/zip/appveyor.yml +++ b/contrib/zip/appveyor.yml @@ -1,4 +1,4 @@ -version: zip-0.1.9.{build} +version: zip-0.1.15.{build} build_script: - cmd: >- cd c:\projects\zip diff --git a/contrib/zip/src/miniz.h b/contrib/zip/src/miniz.h index 2c27a94d8d..c4fcfb83e6 100644 --- a/contrib/zip/src/miniz.h +++ b/contrib/zip/src/miniz.h @@ -221,6 +221,7 @@ #ifndef MINIZ_HEADER_INCLUDED #define MINIZ_HEADER_INCLUDED +#include #include // Defines to completely disable specific portions of miniz.c: @@ -284,7 +285,8 @@ /* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES only if not set */ #if !defined(MINIZ_USE_UNALIGNED_LOADS_AND_STORES) #if MINIZ_X86_OR_X64_CPU -/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient integer loads and stores from unaligned addresses. */ +/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient + * integer loads and stores from unaligned addresses. */ #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1 #define MINIZ_UNALIGNED_USE_MEMCPY #else @@ -354,6 +356,44 @@ enum { MZ_FIXED = 4 }; +/* miniz error codes. Be sure to update mz_zip_get_error_string() if you add or + * modify this enum. */ +typedef enum { + MZ_ZIP_NO_ERROR = 0, + MZ_ZIP_UNDEFINED_ERROR, + MZ_ZIP_TOO_MANY_FILES, + MZ_ZIP_FILE_TOO_LARGE, + MZ_ZIP_UNSUPPORTED_METHOD, + MZ_ZIP_UNSUPPORTED_ENCRYPTION, + MZ_ZIP_UNSUPPORTED_FEATURE, + MZ_ZIP_FAILED_FINDING_CENTRAL_DIR, + MZ_ZIP_NOT_AN_ARCHIVE, + MZ_ZIP_INVALID_HEADER_OR_CORRUPTED, + MZ_ZIP_UNSUPPORTED_MULTIDISK, + MZ_ZIP_DECOMPRESSION_FAILED, + MZ_ZIP_COMPRESSION_FAILED, + MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE, + MZ_ZIP_CRC_CHECK_FAILED, + MZ_ZIP_UNSUPPORTED_CDIR_SIZE, + MZ_ZIP_ALLOC_FAILED, + MZ_ZIP_FILE_OPEN_FAILED, + MZ_ZIP_FILE_CREATE_FAILED, + MZ_ZIP_FILE_WRITE_FAILED, + MZ_ZIP_FILE_READ_FAILED, + MZ_ZIP_FILE_CLOSE_FAILED, + MZ_ZIP_FILE_SEEK_FAILED, + MZ_ZIP_FILE_STAT_FAILED, + MZ_ZIP_INVALID_PARAMETER, + MZ_ZIP_INVALID_FILENAME, + MZ_ZIP_BUF_TOO_SMALL, + MZ_ZIP_INTERNAL_ERROR, + MZ_ZIP_FILE_NOT_FOUND, + MZ_ZIP_ARCHIVE_TOO_LARGE, + MZ_ZIP_VALIDATION_FAILED, + MZ_ZIP_WRITE_CALLBACK_FAILED, + MZ_ZIP_TOTAL_ERRORS +} mz_zip_error; + // Method #define MZ_DEFLATED 8 @@ -696,6 +736,7 @@ typedef size_t (*mz_file_read_func)(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n); typedef size_t (*mz_file_write_func)(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n); +typedef mz_bool (*mz_file_needs_keepalive)(void *pOpaque); struct mz_zip_internal_state_tag; typedef struct mz_zip_internal_state_tag mz_zip_internal_state; @@ -707,13 +748,27 @@ typedef enum { MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED = 3 } mz_zip_mode; -typedef struct mz_zip_archive_tag { +typedef enum { + MZ_ZIP_TYPE_INVALID = 0, + MZ_ZIP_TYPE_USER, + MZ_ZIP_TYPE_MEMORY, + MZ_ZIP_TYPE_HEAP, + MZ_ZIP_TYPE_FILE, + MZ_ZIP_TYPE_CFILE, + MZ_ZIP_TOTAL_TYPES +} mz_zip_type; + +typedef struct { mz_uint64 m_archive_size; mz_uint64 m_central_directory_file_ofs; - mz_uint m_total_files; + + /* We only support up to UINT32_MAX files in zip64 mode. */ + mz_uint32 m_total_files; mz_zip_mode m_zip_mode; + mz_zip_type m_zip_type; + mz_zip_error m_last_error; - mz_uint m_file_offset_alignment; + mz_uint64 m_file_offset_alignment; mz_alloc_func m_pAlloc; mz_free_func m_pFree; @@ -722,6 +777,7 @@ typedef struct mz_zip_archive_tag { mz_file_read_func m_pRead; mz_file_write_func m_pWrite; + mz_file_needs_keepalive m_pNeeds_keepalive; void *m_pIO_opaque; mz_zip_internal_state *m_pState; @@ -1263,6 +1319,9 @@ mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy); #endif // #ifndef MINIZ_NO_ZLIB_APIS +#define MZ_UINT16_MAX (0xFFFFU) +#define MZ_UINT32_MAX (0xFFFFFFFFU) + #ifdef __cplusplus } #endif @@ -1311,6 +1370,11 @@ typedef unsigned char mz_validate_uint64[sizeof(mz_uint64) == 8 ? 1 : -1]; ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U)) #endif +#define MZ_READ_LE64(p) \ + (((mz_uint64)MZ_READ_LE32(p)) | \ + (((mz_uint64)MZ_READ_LE32((const mz_uint8 *)(p) + sizeof(mz_uint32))) \ + << 32U)) + #ifdef _MSC_VER #define MZ_FORCEINLINE __forceinline #elif defined(__GNUC__) @@ -4160,6 +4224,17 @@ enum { MZ_ZIP_LOCAL_DIR_HEADER_SIZE = 30, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE = 46, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE = 22, + + /* ZIP64 archive identifier and record sizes */ + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06064b50, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50, + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE = 56, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE = 20, + MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID = 0x0001, + MZ_ZIP_DATA_DESCRIPTOR_ID = 0x08074b50, + MZ_ZIP_DATA_DESCRIPTER_SIZE64 = 24, + MZ_ZIP_DATA_DESCRIPTER_SIZE32 = 16, + // Central directory header record offsets MZ_ZIP_CDH_SIG_OFS = 0, MZ_ZIP_CDH_VERSION_MADE_BY_OFS = 4, @@ -4199,6 +4274,31 @@ enum { MZ_ZIP_ECDH_CDIR_SIZE_OFS = 12, MZ_ZIP_ECDH_CDIR_OFS_OFS = 16, MZ_ZIP_ECDH_COMMENT_SIZE_OFS = 20, + + /* ZIP64 End of central directory locator offsets */ + MZ_ZIP64_ECDL_SIG_OFS = 0, /* 4 bytes */ + MZ_ZIP64_ECDL_NUM_DISK_CDIR_OFS = 4, /* 4 bytes */ + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS = 8, /* 8 bytes */ + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS = 16, /* 4 bytes */ + + /* ZIP64 End of central directory header offsets */ + MZ_ZIP64_ECDH_SIG_OFS = 0, /* 4 bytes */ + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS = 4, /* 8 bytes */ + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS = 12, /* 2 bytes */ + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS = 14, /* 2 bytes */ + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS = 16, /* 4 bytes */ + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS = 20, /* 4 bytes */ + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 24, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS = 32, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_SIZE_OFS = 40, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_OFS_OFS = 48, /* 8 bytes */ + MZ_ZIP_VERSION_MADE_BY_DOS_FILESYSTEM_ID = 0, + MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG = 0x10, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED = 1, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG = 32, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION = 64, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED = 8192, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8 = 1 << 11 }; typedef struct { @@ -4211,7 +4311,24 @@ struct mz_zip_internal_state_tag { mz_zip_array m_central_dir; mz_zip_array m_central_dir_offsets; mz_zip_array m_sorted_central_dir_offsets; + + /* The flags passed in when the archive is initially opened. */ + uint32_t m_init_flags; + + /* MZ_TRUE if the archive has a zip64 end of central directory headers, etc. + */ + mz_bool m_zip64; + + /* MZ_TRUE if we found zip64 extended info in the central directory (m_zip64 + * will also be slammed to true too, even if we didn't find a zip64 end of + * central dir header, etc.) */ + mz_bool m_zip64_has_extended_info_fields; + + /* These fields are used by the file, FILE, memory, and memory/heap read/write + * helpers. */ MZ_FILE *m_pFile; + mz_uint64 m_file_archive_start_ofs; + void *m_pMem; size_t m_mem_size; size_t m_mem_capacity; @@ -4363,6 +4480,13 @@ static mz_bool mz_zip_set_file_times(const char *pFilename, time_t access_time, #endif /* #ifndef MINIZ_NO_STDIO */ #endif /* #ifndef MINIZ_NO_TIME */ +static MZ_FORCEINLINE mz_bool mz_zip_set_error(mz_zip_archive *pZip, + mz_zip_error err_num) { + if (pZip) + pZip->m_last_error = err_num; + return MZ_FALSE; +} + static mz_bool mz_zip_reader_init_internal(mz_zip_archive *pZip, mz_uint32 flags) { (void)flags; @@ -4480,127 +4604,346 @@ mz_zip_reader_sort_central_dir_offsets_by_filename(mz_zip_archive *pZip) { } } -static mz_bool mz_zip_reader_read_central_dir(mz_zip_archive *pZip, - mz_uint32 flags) { - mz_uint cdir_size, num_this_disk, cdir_disk_index; - mz_uint64 cdir_ofs; +static mz_bool mz_zip_reader_locate_header_sig(mz_zip_archive *pZip, + mz_uint32 record_sig, + mz_uint32 record_size, + mz_int64 *pOfs) { mz_int64 cur_file_ofs; - const mz_uint8 *p; mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; mz_uint8 *pBuf = (mz_uint8 *)buf_u32; - mz_bool sort_central_dir = - ((flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0); - // Basic sanity checks - reject files which are too small, and check the first - // 4 bytes of the file to make sure a local header is there. - if (pZip->m_archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + + /* Basic sanity checks - reject files which are too small */ + if (pZip->m_archive_size < record_size) return MZ_FALSE; - // Find the end of central directory record by scanning the file from the end - // towards the beginning. + + /* Find the record by scanning the file from the end towards the beginning. */ cur_file_ofs = MZ_MAX((mz_int64)pZip->m_archive_size - (mz_int64)sizeof(buf_u32), 0); for (;;) { int i, n = (int)MZ_MIN(sizeof(buf_u32), pZip->m_archive_size - cur_file_ofs); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, n) != (mz_uint)n) return MZ_FALSE; - for (i = n - 4; i >= 0; --i) - if (MZ_READ_LE32(pBuf + i) == MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG) - break; + + for (i = n - 4; i >= 0; --i) { + mz_uint s = MZ_READ_LE32(pBuf + i); + if (s == record_sig) { + if ((pZip->m_archive_size - (cur_file_ofs + i)) >= record_size) + break; + } + } + if (i >= 0) { cur_file_ofs += i; break; } + + /* Give up if we've searched the entire file, or we've gone back "too far" + * (~64kb) */ if ((!cur_file_ofs) || ((pZip->m_archive_size - cur_file_ofs) >= - (0xFFFF + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE))) + (MZ_UINT16_MAX + record_size))) return MZ_FALSE; + cur_file_ofs = MZ_MAX(cur_file_ofs - (sizeof(buf_u32) - 3), 0); } - // Read and verify the end of central directory record. + + *pOfs = cur_file_ofs; + return MZ_TRUE; +} + +static mz_bool mz_zip_reader_read_central_dir(mz_zip_archive *pZip, + mz_uint flags) { + mz_uint cdir_size = 0, cdir_entries_on_this_disk = 0, num_this_disk = 0, + cdir_disk_index = 0; + mz_uint64 cdir_ofs = 0; + mz_int64 cur_file_ofs = 0; + const mz_uint8 *p; + + mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; + mz_uint8 *pBuf = (mz_uint8 *)buf_u32; + mz_bool sort_central_dir = + ((flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0); + mz_uint32 zip64_end_of_central_dir_locator_u32 + [(MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + sizeof(mz_uint32) - 1) / + sizeof(mz_uint32)]; + mz_uint8 *pZip64_locator = (mz_uint8 *)zip64_end_of_central_dir_locator_u32; + + mz_uint32 zip64_end_of_central_dir_header_u32 + [(MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / + sizeof(mz_uint32)]; + mz_uint8 *pZip64_end_of_central_dir = + (mz_uint8 *)zip64_end_of_central_dir_header_u32; + + mz_uint64 zip64_end_of_central_dir_ofs = 0; + + /* Basic sanity checks - reject files which are too small, and check the first + * 4 bytes of the file to make sure a local header is there. */ + if (pZip->m_archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (!mz_zip_reader_locate_header_sig( + pZip, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG, + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE, &cur_file_ofs)) + return mz_zip_set_error(pZip, MZ_ZIP_FAILED_FINDING_CENTRAL_DIR); + + /* Read and verify the end of central directory record. */ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) - return MZ_FALSE; - if ((MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_SIG_OFS) != - MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG) || - ((pZip->m_total_files = - MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS)) != - MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS))) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_SIG_OFS) != + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (cur_file_ofs >= (MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) { + if (pZip->m_pRead(pZip->m_pIO_opaque, + cur_file_ofs - MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE, + pZip64_locator, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) == + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) { + if (MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_SIG_OFS) == + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG) { + zip64_end_of_central_dir_ofs = MZ_READ_LE64( + pZip64_locator + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS); + if (zip64_end_of_central_dir_ofs > + (pZip->m_archive_size - MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (pZip->m_pRead(pZip->m_pIO_opaque, zip64_end_of_central_dir_ofs, + pZip64_end_of_central_dir, + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) == + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) { + if (MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIG_OFS) == + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG) { + pZip->m_pState->m_zip64 = MZ_TRUE; + } + } + } + } + } + pZip->m_total_files = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS); + cdir_entries_on_this_disk = + MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); num_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS); cdir_disk_index = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS); + cdir_size = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_SIZE_OFS); + cdir_ofs = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_OFS_OFS); + + if (pZip->m_pState->m_zip64) { + mz_uint32 zip64_total_num_of_disks = + MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS); + mz_uint64 zip64_cdir_total_entries = MZ_READ_LE64( + pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS); + mz_uint64 zip64_cdir_total_entries_on_this_disk = MZ_READ_LE64( + pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); + mz_uint64 zip64_size_of_end_of_central_dir_record = MZ_READ_LE64( + pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS); + mz_uint64 zip64_size_of_central_directory = + MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_SIZE_OFS); + + if (zip64_size_of_end_of_central_dir_record < + (MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - 12)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (zip64_total_num_of_disks != 1U) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + /* Check for miniz's practical limits */ + if (zip64_cdir_total_entries > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + pZip->m_total_files = (mz_uint32)zip64_cdir_total_entries; + + if (zip64_cdir_total_entries_on_this_disk > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + cdir_entries_on_this_disk = + (mz_uint32)zip64_cdir_total_entries_on_this_disk; + + /* Check for miniz's current practical limits (sorry, this should be enough + * for millions of files) */ + if (zip64_size_of_central_directory > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + cdir_size = (mz_uint32)zip64_size_of_central_directory; + + num_this_disk = MZ_READ_LE32(pZip64_end_of_central_dir + + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS); + + cdir_disk_index = MZ_READ_LE32(pZip64_end_of_central_dir + + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS); + + cdir_ofs = + MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_OFS_OFS); + } + + if (pZip->m_total_files != cdir_entries_on_this_disk) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + if (((num_this_disk | cdir_disk_index) != 0) && ((num_this_disk != 1) || (cdir_disk_index != 1))) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); - if ((cdir_size = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_SIZE_OFS)) < - pZip->m_total_files * MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) - return MZ_FALSE; + if (cdir_size < pZip->m_total_files * MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); - cdir_ofs = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_OFS_OFS); if ((cdir_ofs + (mz_uint64)cdir_size) > pZip->m_archive_size) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pZip->m_central_directory_file_ofs = cdir_ofs; if (pZip->m_total_files) { mz_uint i, n; - - // Read the entire central directory into a heap block, and allocate another - // heap block to hold the unsorted central dir file record offsets, and - // another to hold the sorted indices. + /* Read the entire central directory into a heap block, and allocate another + * heap block to hold the unsorted central dir file record offsets, and + * possibly another to hold the sorted indices. */ if ((!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir, cdir_size, MZ_FALSE)) || (!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir_offsets, pZip->m_total_files, MZ_FALSE))) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); if (sort_central_dir) { if (!mz_zip_array_resize(pZip, &pZip->m_pState->m_sorted_central_dir_offsets, pZip->m_total_files, MZ_FALSE)) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs, pZip->m_pState->m_central_dir.m_p, cdir_size) != cdir_size) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); - // Now create an index into the central directory file records, do some - // basic sanity checking on each record, and check for zip64 entries (which - // are not yet supported). + /* Now create an index into the central directory file records, do some + * basic sanity checking on each record */ p = (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p; for (n = cdir_size, i = 0; i < pZip->m_total_files; ++i) { - mz_uint total_header_size, comp_size, decomp_size, disk_index; + mz_uint total_header_size, disk_index, bit_flags, filename_size, + ext_data_size; + mz_uint64 comp_size, decomp_size, local_header_ofs; + if ((n < MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) || (MZ_READ_LE32(p) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG)) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, i) = (mz_uint32)(p - (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p); + if (sort_central_dir) MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_sorted_central_dir_offsets, mz_uint32, i) = i; + comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); decomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); - if (((!MZ_READ_LE32(p + MZ_ZIP_CDH_METHOD_OFS)) && - (decomp_size != comp_size)) || - (decomp_size && !comp_size) || (decomp_size == 0xFFFFFFFF) || - (comp_size == 0xFFFFFFFF)) - return MZ_FALSE; + local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); + filename_size = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + ext_data_size = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + if ((!pZip->m_pState->m_zip64_has_extended_info_fields) && + (ext_data_size) && + (MZ_MAX(MZ_MAX(comp_size, decomp_size), local_header_ofs) == + MZ_UINT32_MAX)) { + /* Attempt to find zip64 extended information field in the entry's extra + * data */ + mz_uint32 extra_size_remaining = ext_data_size; + + if (extra_size_remaining) { + const mz_uint8 *pExtra_data; + void *buf = NULL; + + if (MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + ext_data_size > + n) { + buf = MZ_MALLOC(ext_data_size); + if (buf == NULL) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (pZip->m_pRead(pZip->m_pIO_opaque, + cdir_ofs + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + + filename_size, + buf, ext_data_size) != ext_data_size) { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + pExtra_data = (mz_uint8 *)buf; + } else { + pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size; + } + + do { + mz_uint32 field_id; + mz_uint32 field_data_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + + if ((field_data_size + sizeof(mz_uint16) * 2) > + extra_size_remaining) { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) { + /* Ok, the archive didn't have any zip64 headers but it uses a + * zip64 extended information field so mark it as zip64 anyway + * (this can occur with infozip's zip util when it reads + * compresses files from stdin). */ + pZip->m_pState->m_zip64 = MZ_TRUE; + pZip->m_pState->m_zip64_has_extended_info_fields = MZ_TRUE; + break; + } + + pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; + extra_size_remaining = + extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; + } while (extra_size_remaining); + + MZ_FREE(buf); + } + } + + /* I've seen archives that aren't marked as zip64 that uses zip64 ext + * data, argh */ + if ((comp_size != MZ_UINT32_MAX) && (decomp_size != MZ_UINT32_MAX)) { + if (((!MZ_READ_LE32(p + MZ_ZIP_CDH_METHOD_OFS)) && + (decomp_size != comp_size)) || + (decomp_size && !comp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + disk_index = MZ_READ_LE16(p + MZ_ZIP_CDH_DISK_START_OFS); - if ((disk_index != num_this_disk) && (disk_index != 1)) - return MZ_FALSE; - if (((mz_uint64)MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS) + - MZ_ZIP_LOCAL_DIR_HEADER_SIZE + comp_size) > pZip->m_archive_size) - return MZ_FALSE; + if ((disk_index == MZ_UINT16_MAX) || + ((disk_index != num_this_disk) && (disk_index != 1))) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (comp_size != MZ_UINT32_MAX) { + if (((mz_uint64)MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS) + + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + bit_flags = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + if (bit_flags & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + if ((total_header_size = MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS)) > n) - return MZ_FALSE; + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + n -= total_header_size; p += total_header_size; } diff --git a/contrib/zip/src/zip.c b/contrib/zip/src/zip.c index ff3a8fe1e6..1abcfd8fd1 100644 --- a/contrib/zip/src/zip.c +++ b/contrib/zip/src/zip.c @@ -24,7 +24,6 @@ ((((P)[0] >= 'A' && (P)[0] <= 'Z') || ((P)[0] >= 'a' && (P)[0] <= 'z')) && \ (P)[1] == ':') #define FILESYSTEM_PREFIX_LEN(P) (HAS_DEVICE(P) ? 2 : 0) -#define ISSLASH(C) ((C) == '/' || (C) == '\\') #else @@ -48,7 +47,7 @@ int symlink(const char *target, const char *linkpath); // needed on Linux #endif #ifndef ISSLASH -#define ISSLASH(C) ((C) == '/') +#define ISSLASH(C) ((C) == '/' || (C) == '\\') #endif #define CLEANUP(ptr) \ @@ -78,26 +77,34 @@ static const char *base_name(const char *name) { return base; } -static int mkpath(const char *path) { - char const *p; +static int mkpath(char *path) { + char *p; char npath[MAX_PATH + 1]; int len = 0; int has_device = HAS_DEVICE(path); memset(npath, 0, MAX_PATH + 1); - -#ifdef _WIN32 - // only on windows fix the path - npath[0] = path[0]; - npath[1] = path[1]; - len = 2; -#endif // _WIN32 - + if (has_device) { + // only on windows + npath[0] = path[0]; + npath[1] = path[1]; + len = 2; + } for (p = path + len; *p && len < MAX_PATH; p++) { if (ISSLASH(*p) && ((!has_device && len > 0) || (has_device && len > 2))) { - if (MKDIR(npath) == -1) - if (errno != EEXIST) +#if defined(_WIN32) || defined(__WIN32__) || defined(_MSC_VER) || \ + defined(__MINGW32__) +#else + if ('\\' == *p) { + *p = '/'; + } +#endif + + if (MKDIR(npath) == -1) { + if (errno != EEXIST) { return -1; + } + } } npath[len++] = *p; } @@ -279,7 +286,14 @@ int zip_entry_open(struct zip_t *zip, const char *entryname) { zip->entry.header_offset = zip->archive.m_archive_size; memset(zip->entry.header, 0, MZ_ZIP_LOCAL_DIR_HEADER_SIZE * sizeof(mz_uint8)); zip->entry.method = 0; + + // UNIX or APPLE +#if MZ_PLATFORM == 3 || MZ_PLATFORM == 19 + // regular file with rw-r--r-- persmissions + zip->entry.external_attr = (mz_uint32)(0100644) << 16; +#else zip->entry.external_attr = 0; +#endif num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pzip); @@ -660,7 +674,7 @@ ssize_t zip_entry_noallocread(struct zip_t *zip, void *buf, size_t bufsize) { } if (!mz_zip_reader_extract_to_mem_no_alloc(pzip, (mz_uint)zip->entry.index, - buf, bufsize, 0, NULL, 0)) { + buf, bufsize, 0, NULL, 0)) { return -1; } @@ -670,10 +684,7 @@ ssize_t zip_entry_noallocread(struct zip_t *zip, void *buf, size_t bufsize) { int zip_entry_fread(struct zip_t *zip, const char *filename) { mz_zip_archive *pzip = NULL; mz_uint idx; -#if defined(_MSC_VER) -#else mz_uint32 xattr = 0; -#endif mz_zip_archive_file_stat info; if (!zip) { @@ -875,12 +886,19 @@ int zip_extract(const char *zipname, const char *dir, goto out; } - if ((((info.m_version_made_by >> 8) == 3) || ((info.m_version_made_by >> 8) == 19)) // if zip is produced on Unix or macOS (3 and 19 from section 4.4.2.2 of zip standard) - && info.m_external_attr & (0x20 << 24)) { // and has sym link attribute (0x80 is file, 0x40 is directory) + if ((((info.m_version_made_by >> 8) == 3) || + ((info.m_version_made_by >> 8) == + 19)) // if zip is produced on Unix or macOS (3 and 19 from + // section 4.4.2.2 of zip standard) + && info.m_external_attr & + (0x20 << 24)) { // and has sym link attribute (0x80 is file, 0x40 + // is directory) #if defined(_WIN32) || defined(__WIN32__) || defined(_MSC_VER) || \ defined(__MINGW32__) -#else - if (info.m_uncomp_size > MAX_PATH || !mz_zip_reader_extract_to_mem_no_alloc(&zip_archive, i, symlink_to, MAX_PATH, 0, NULL, 0)) { +#else + if (info.m_uncomp_size > MAX_PATH || + !mz_zip_reader_extract_to_mem_no_alloc(&zip_archive, i, symlink_to, + MAX_PATH, 0, NULL, 0)) { goto out; } symlink_to[info.m_uncomp_size] = '\0'; diff --git a/contrib/zip/src/zip.h b/contrib/zip/src/zip.h index 5f39df50ad..a48d64d6de 100644 --- a/contrib/zip/src/zip.h +++ b/contrib/zip/src/zip.h @@ -20,241 +20,240 @@ extern "C" { #endif #if !defined(_SSIZE_T_DEFINED) && !defined(_SSIZE_T_DEFINED_) && \ - !defined(_SSIZE_T) && !defined(_SSIZE_T_) && !defined(__ssize_t_defined) -#define _SSIZE_T + !defined(__DEFINED_ssize_t) && !defined(__ssize_t_defined) && \ + !defined(_SSIZE_T) && !defined(_SSIZE_T_) + // 64-bit Windows is the only mainstream platform // where sizeof(long) != sizeof(void*) #ifdef _WIN64 -typedef long long ssize_t; /* byte count or error */ +typedef long long ssize_t; /* byte count or error */ #else -typedef long ssize_t; /* byte count or error */ +typedef long ssize_t; /* byte count or error */ #endif + +#define _SSIZE_T_DEFINED +#define _SSIZE_T_DEFINED_ +#define __DEFINED_ssize_t +#define __ssize_t_defined +#define _SSIZE_T +#define _SSIZE_T_ + #endif #ifndef MAX_PATH #define MAX_PATH 32767 /* # chars in a path name including NULL */ #endif +/** + * @mainpage + * + * Documenation for @ref zip. + */ + +/** + * @addtogroup zip + * @{ + */ + +/** + * Default zip compression level. + */ + #define ZIP_DEFAULT_COMPRESSION_LEVEL 6 -/* - This data structure is used throughout the library to represent zip archive - - forward declaration. -*/ +/** + * @struct zip_t + * + * This data structure is used throughout the library to represent zip archive - + * forward declaration. + */ struct zip_t; -/* - Opens zip archive with compression level using the given mode. - - Args: - zipname: zip archive file name. - level: compression level (0-9 are the standard zlib-style levels). - mode: file access mode. - 'r': opens a file for reading/extracting (the file must exists). - 'w': creates an empty file for writing. - 'a': appends to an existing archive. - - Returns: - The zip archive handler or NULL on error -*/ +/** + * Opens zip archive with compression level using the given mode. + * + * @param zipname zip archive file name. + * @param level compression level (0-9 are the standard zlib-style levels). + * @param mode file access mode. + * - 'r': opens a file for reading/extracting (the file must exists). + * - 'w': creates an empty file for writing. + * - 'a': appends to an existing archive. + * + * @return the zip archive handler or NULL on error + */ extern struct zip_t *zip_open(const char *zipname, int level, char mode); -/* - Closes the zip archive, releases resources - always finalize. - - Args: - zip: zip archive handler. -*/ +/** + * Closes the zip archive, releases resources - always finalize. + * + * @param zip zip archive handler. + */ extern void zip_close(struct zip_t *zip); -/* - Opens an entry by name in the zip archive. - For zip archive opened in 'w' or 'a' mode the function will append - a new entry. In readonly mode the function tries to locate the entry - in global dictionary. - - Args: - zip: zip archive handler. - entryname: an entry name in local dictionary. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Opens an entry by name in the zip archive. + * + * For zip archive opened in 'w' or 'a' mode the function will append + * a new entry. In readonly mode the function tries to locate the entry + * in global dictionary. + * + * @param zip zip archive handler. + * @param entryname an entry name in local dictionary. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_entry_open(struct zip_t *zip, const char *entryname); -/* - Opens a new entry by index in the zip archive. - This function is only valid if zip archive was opened in 'r' (readonly) mode. - - Args: - zip: zip archive handler. - index: index in local dictionary. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Opens a new entry by index in the zip archive. + * + * This function is only valid if zip archive was opened in 'r' (readonly) mode. + * + * @param zip zip archive handler. + * @param index index in local dictionary. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_entry_openbyindex(struct zip_t *zip, int index); -/* - Closes a zip entry, flushes buffer and releases resources. - - Args: - zip: zip archive handler. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Closes a zip entry, flushes buffer and releases resources. + * + * @param zip zip archive handler. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_entry_close(struct zip_t *zip); -/* - Returns a local name of the current zip entry. - The main difference between user's entry name and local entry name - is optional relative path. - Following .ZIP File Format Specification - the path stored MUST not contain - a drive or device letter, or a leading slash. - All slashes MUST be forward slashes '/' as opposed to backwards slashes '\' - for compatibility with Amiga and UNIX file systems etc. - - Args: - zip: zip archive handler. - - Returns: - The pointer to the current zip entry name, or NULL on error. -*/ +/** + * Returns a local name of the current zip entry. + * + * The main difference between user's entry name and local entry name + * is optional relative path. + * Following .ZIP File Format Specification - the path stored MUST not contain + * a drive or device letter, or a leading slash. + * All slashes MUST be forward slashes '/' as opposed to backwards slashes '\' + * for compatibility with Amiga and UNIX file systems etc. + * + * @param zip: zip archive handler. + * + * @return the pointer to the current zip entry name, or NULL on error. + */ extern const char *zip_entry_name(struct zip_t *zip); -/* - Returns an index of the current zip entry. - - Args: - zip: zip archive handler. - - Returns: - The index on success, negative number (< 0) on error. -*/ +/** + * Returns an index of the current zip entry. + * + * @param zip zip archive handler. + * + * @return the index on success, negative number (< 0) on error. + */ extern int zip_entry_index(struct zip_t *zip); -/* - Determines if the current zip entry is a directory entry. - - Args: - zip: zip archive handler. - - Returns: - The return code - 1 (true), 0 (false), negative number (< 0) on error. -*/ +/** + * Determines if the current zip entry is a directory entry. + * + * @param zip zip archive handler. + * + * @return the return code - 1 (true), 0 (false), negative number (< 0) on + * error. + */ extern int zip_entry_isdir(struct zip_t *zip); -/* - Returns an uncompressed size of the current zip entry. - - Args: - zip: zip archive handler. - - Returns: - The uncompressed size in bytes. -*/ +/** + * Returns an uncompressed size of the current zip entry. + * + * @param zip zip archive handler. + * + * @return the uncompressed size in bytes. + */ extern unsigned long long zip_entry_size(struct zip_t *zip); -/* - Returns CRC-32 checksum of the current zip entry. - - Args: - zip: zip archive handler. - - Returns: - The CRC-32 checksum. -*/ +/** + * Returns CRC-32 checksum of the current zip entry. + * + * @param zip zip archive handler. + * + * @return the CRC-32 checksum. + */ extern unsigned int zip_entry_crc32(struct zip_t *zip); -/* - Compresses an input buffer for the current zip entry. - - Args: - zip: zip archive handler. - buf: input buffer. - bufsize: input buffer size (in bytes). - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Compresses an input buffer for the current zip entry. + * + * @param zip zip archive handler. + * @param buf input buffer. + * @param bufsize input buffer size (in bytes). + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_entry_write(struct zip_t *zip, const void *buf, size_t bufsize); -/* - Compresses a file for the current zip entry. - - Args: - zip: zip archive handler. - filename: input file. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Compresses a file for the current zip entry. + * + * @param zip zip archive handler. + * @param filename input file. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_entry_fwrite(struct zip_t *zip, const char *filename); -/* - Extracts the current zip entry into output buffer. - The function allocates sufficient memory for a output buffer. - - Args: - zip: zip archive handler. - buf: output buffer. - bufsize: output buffer size (in bytes). - - Note: - - remember to release memory allocated for a output buffer. - - for large entries, please take a look at zip_entry_extract function. - - Returns: - The return code - the number of bytes actually read on success. - Otherwise a -1 on error. -*/ +/** + * Extracts the current zip entry into output buffer. + * + * The function allocates sufficient memory for a output buffer. + * + * @param zip zip archive handler. + * @param buf output buffer. + * @param bufsize output buffer size (in bytes). + * + * @note remember to release memory allocated for a output buffer. + * for large entries, please take a look at zip_entry_extract function. + * + * @return the return code - the number of bytes actually read on success. + * Otherwise a -1 on error. + */ extern ssize_t zip_entry_read(struct zip_t *zip, void **buf, size_t *bufsize); -/* - Extracts the current zip entry into a memory buffer using no memory - allocation. - - Args: - zip: zip archive handler. - buf: preallocated output buffer. - bufsize: output buffer size (in bytes). - - Note: - - ensure supplied output buffer is large enough. - - zip_entry_size function (returns uncompressed size for the current entry) - can be handy to estimate how big buffer is needed. - - for large entries, please take a look at zip_entry_extract function. - - Returns: - The return code - the number of bytes actually read on success. - Otherwise a -1 on error (e.g. bufsize is not large enough). -*/ -extern ssize_t zip_entry_noallocread(struct zip_t *zip, void *buf, size_t bufsize); - -/* - Extracts the current zip entry into output file. - - Args: - zip: zip archive handler. - filename: output file. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Extracts the current zip entry into a memory buffer using no memory + * allocation. + * + * @param zip zip archive handler. + * @param buf preallocated output buffer. + * @param bufsize output buffer size (in bytes). + * + * @note ensure supplied output buffer is large enough. + * zip_entry_size function (returns uncompressed size for the current + * entry) can be handy to estimate how big buffer is needed. for large + * entries, please take a look at zip_entry_extract function. + * + * @return the return code - the number of bytes actually read on success. + * Otherwise a -1 on error (e.g. bufsize is not large enough). + */ +extern ssize_t zip_entry_noallocread(struct zip_t *zip, void *buf, + size_t bufsize); + +/** + * Extracts the current zip entry into output file. + * + * @param zip zip archive handler. + * @param filename output file. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_entry_fread(struct zip_t *zip, const char *filename); -/* - Extracts the current zip entry using a callback function (on_extract). - - Args: - zip: zip archive handler. - on_extract: callback function. - arg: opaque pointer (optional argument, - which you can pass to the on_extract callback) - - Returns: - The return code - 0 on success, negative number (< 0) on error. +/** + * Extracts the current zip entry using a callback function (on_extract). + * + * @param zip zip archive handler. + * @param on_extract callback function. + * @param arg opaque pointer (optional argument, which you can pass to the + * on_extract callback) + * + * @return the return code - 0 on success, negative number (< 0) on error. */ extern int zip_entry_extract(struct zip_t *zip, @@ -262,53 +261,49 @@ zip_entry_extract(struct zip_t *zip, const void *data, size_t size), void *arg); -/* - Returns the number of all entries (files and directories) in the zip archive. - - Args: - zip: zip archive handler. - - Returns: - The return code - the number of entries on success, - negative number (< 0) on error. -*/ +/** + * Returns the number of all entries (files and directories) in the zip archive. + * + * @param zip zip archive handler. + * + * @return the return code - the number of entries on success, negative number + * (< 0) on error. + */ extern int zip_total_entries(struct zip_t *zip); -/* - Creates a new archive and puts files into a single zip archive. - - Args: - zipname: zip archive file. - filenames: input files. - len: number of input files. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Creates a new archive and puts files into a single zip archive. + * + * @param zipname zip archive file. + * @param filenames input files. + * @param len: number of input files. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_create(const char *zipname, const char *filenames[], size_t len); -/* - Extracts a zip archive file into directory. - - If on_extract_entry is not NULL, the callback will be called after - successfully extracted each zip entry. - Returning a negative value from the callback will cause abort and return an - error. The last argument (void *arg) is optional, which you can use to pass - data to the on_extract_entry callback. - - Args: - zipname: zip archive file. - dir: output directory. - on_extract_entry: on extract callback. - arg: opaque pointer. - - Returns: - The return code - 0 on success, negative number (< 0) on error. -*/ +/** + * Extracts a zip archive file into directory. + * + * If on_extract_entry is not NULL, the callback will be called after + * successfully extracted each zip entry. + * Returning a negative value from the callback will cause abort and return an + * error. The last argument (void *arg) is optional, which you can use to pass + * data to the on_extract_entry callback. + * + * @param zipname zip archive file. + * @param dir output directory. + * @param on_extract_entry on extract callback. + * @param arg opaque pointer. + * + * @return the return code - 0 on success, negative number (< 0) on error. + */ extern int zip_extract(const char *zipname, const char *dir, int (*on_extract_entry)(const char *filename, void *arg), void *arg); +/** @} */ + #ifdef __cplusplus } #endif diff --git a/contrib/zip/test/CMakeLists.txt b/contrib/zip/test/CMakeLists.txt index 9b2a8db106..cc060b00fe 100644 --- a/contrib/zip/test/CMakeLists.txt +++ b/contrib/zip/test/CMakeLists.txt @@ -1,19 +1,16 @@ cmake_minimum_required(VERSION 2.8) -if ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "AppleClang") - if(ENABLE_COVERAGE) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g ") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-arcs") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ftest-coverage") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") - endif() -endif () - # test -include_directories(../src) -add_executable(test.exe test.c ../src/zip.c) -add_executable(test_miniz.exe test_miniz.c) +set(test_out test.out) +set(test_miniz_out test_miniz.out) + +add_executable(${test_out} test.c) +target_link_libraries(${test_out} zip) +add_executable(${test_miniz_out} test_miniz.c) +target_link_libraries(${test_miniz_out} zip) + +add_test(NAME ${test_out} COMMAND ${test_out}) +add_test(NAME ${test_miniz_out} COMMAND ${test_miniz_out}) -add_test(NAME test COMMAND test.exe) -add_test(NAME test_miniz COMMAND test_miniz.exe) +set(test_out ${test_out} PARENT_SCOPE) +set(test_miniz_out ${test_miniz_out} PARENT_SCOPE) diff --git a/contrib/zip/test/test.c b/contrib/zip/test/test.c index 454430533a..a9b2ddab1e 100644 --- a/contrib/zip/test/test.c +++ b/contrib/zip/test/test.c @@ -29,6 +29,8 @@ #define XFILE "7.txt\0" #define XMODE 0100777 +#define UNIXMODE 0100644 + #define UNUSED(x) (void)x static int total_entries = 0; @@ -102,7 +104,8 @@ static void test_read(void) { assert(0 == zip_entry_close(zip)); free(buf); buf = NULL; - + bufsize = 0; + assert(0 == zip_entry_open(zip, "test/test-2.txt")); assert(strlen(TESTDATA2) == zip_entry_size(zip)); assert(CRC32DATA2 == zip_entry_crc32(zip)); @@ -131,7 +134,8 @@ static void test_read(void) { assert(0 == zip_entry_close(zip)); free(buf); buf = NULL; - + bufsize = 0; + buftmp = strlen(TESTDATA1); buf = calloc(buftmp, sizeof(char)); assert(0 == zip_entry_open(zip, "test/test-1.txt")); @@ -433,6 +437,35 @@ static void test_mtime(void) { remove(ZIPNAME); } +static void test_unix_permissions(void) { +#if defined(_WIN64) || defined(_WIN32) || defined(__WIN32__) +#else + // UNIX or APPLE + struct MZ_FILE_STAT_STRUCT file_stats; + + remove(ZIPNAME); + + struct zip_t *zip = zip_open(ZIPNAME, ZIP_DEFAULT_COMPRESSION_LEVEL, 'w'); + assert(zip != NULL); + + assert(0 == zip_entry_open(zip, RFILE)); + assert(0 == zip_entry_write(zip, TESTDATA1, strlen(TESTDATA1))); + assert(0 == zip_entry_close(zip)); + + zip_close(zip); + + remove(RFILE); + + assert(0 == zip_extract(ZIPNAME, ".", NULL, NULL)); + + assert(0 == MZ_FILE_STAT(RFILE, &file_stats)); + assert(UNIXMODE == file_stats.st_mode); + + remove(RFILE); + remove(ZIPNAME); +#endif +} + int main(int argc, char *argv[]) { UNUSED(argc); UNUSED(argv); @@ -453,6 +486,7 @@ int main(int argc, char *argv[]) { test_write_permissions(); test_exe_permissions(); test_mtime(); + test_unix_permissions(); remove(ZIPNAME); return 0; diff --git a/contrib/zip/test/test_miniz.c b/contrib/zip/test/test_miniz.c index ebc0564dc3..babcaecdb6 100644 --- a/contrib/zip/test/test_miniz.c +++ b/contrib/zip/test/test_miniz.c @@ -23,16 +23,39 @@ int main(int argc, char *argv[]) { uint step = 0; int cmp_status; uLong src_len = (uLong)strlen(s_pStr); - uLong cmp_len = compressBound(src_len); uLong uncomp_len = src_len; + uLong cmp_len; uint8 *pCmp, *pUncomp; + size_t sz; uint total_succeeded = 0; (void)argc, (void)argv; printf("miniz.c version: %s\n", MZ_VERSION); do { + pCmp = (uint8 *)tdefl_compress_mem_to_heap(s_pStr, src_len, &cmp_len, 0); + if (!pCmp) { + printf("tdefl_compress_mem_to_heap failed\n"); + return EXIT_FAILURE; + } + if (src_len <= cmp_len) { + printf("tdefl_compress_mem_to_heap failed: from %u to %u bytes\n", + (mz_uint32)uncomp_len, (mz_uint32)cmp_len); + free(pCmp); + return EXIT_FAILURE; + } + + sz = tdefl_compress_mem_to_mem(pCmp, cmp_len, s_pStr, src_len, 0); + if (sz != cmp_len) { + printf("tdefl_compress_mem_to_mem failed: expected %u, got %u\n", + (mz_uint32)cmp_len, (mz_uint32)sz); + free(pCmp); + return EXIT_FAILURE; + } + // Allocate buffers to hold compressed and uncompressed data. + free(pCmp); + cmp_len = compressBound(src_len); pCmp = (mz_uint8 *)malloc((size_t)cmp_len); pUncomp = (mz_uint8 *)malloc((size_t)src_len); if ((!pCmp) || (!pUncomp)) { From 42fa22f85e20822fb5779fc8338bf7f0d86f4da2 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Wed, 20 Nov 2019 21:26:36 +0100 Subject: [PATCH 73/74] Update Readme.md Add doc for glTF2.0 for supported extenstions --- Readme.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index f02a3b617a..01c5fe33b9 100644 --- a/Readme.md +++ b/Readme.md @@ -60,7 +60,12 @@ __Importers__: - ENFF - [FBX](https://en.wikipedia.org/wiki/FBX) - [glTF 1.0](https://en.wikipedia.org/wiki/GlTF#glTF_1.0) + GLB -- [glTF 2.0](https://en.wikipedia.org/wiki/GlTF#glTF_2.0) +- [glTF 2.0](https://en.wikipedia.org/wiki/GlTF#glTF_2.0): + At the moment for glTF2.0 the following extensions are supported: + + KHR_lights_punctual ( 5.0 ) + + KHR_materials_pbrSpecularGlossiness ( 5.0 ) + + KHR_materials_unlit ( 5.0 ) + + KHR_texture_transform ( 5.1 under test ) - HMB - IFC-STEP - IRR / IRRMESH From 5cfb0fd633372bbbec87f08015139d71d330d4a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc?= Date: Fri, 22 Nov 2019 18:27:34 +0100 Subject: [PATCH 74/74] Add function aiGetVersionPatch() to be able to display Assimp version as in Git tags --- code/Common/Version.cpp | 6 ++++++ include/assimp/version.h | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/code/Common/Version.cpp b/code/Common/Version.cpp index cf1da7d5ba..ea4c996f0a 100644 --- a/code/Common/Version.cpp +++ b/code/Common/Version.cpp @@ -66,6 +66,12 @@ ASSIMP_API const char* aiGetLegalString () { return LEGAL_INFORMATION; } +// ------------------------------------------------------------------------------------------------ +// Get Assimp patch version +ASSIMP_API unsigned int aiGetVersionPatch() { + return VER_PATCH; +} + // ------------------------------------------------------------------------------------------------ // Get Assimp minor version ASSIMP_API unsigned int aiGetVersionMinor () { diff --git a/include/assimp/version.h b/include/assimp/version.h index 2fdd37a43c..90645a38f0 100644 --- a/include/assimp/version.h +++ b/include/assimp/version.h @@ -62,6 +62,13 @@ extern "C" { */ ASSIMP_API const char* aiGetLegalString (void); +// --------------------------------------------------------------------------- +/** @brief Returns the current patch version number of Assimp. + * @return Patch version of the Assimp runtime the application was + * linked/built against + */ +ASSIMP_API unsigned int aiGetVersionPatch(void); + // --------------------------------------------------------------------------- /** @brief Returns the current minor version number of Assimp. * @return Minor version of the Assimp runtime the application was