From 0baec5f0bd726dc2cd40317abc1015bd6add6c93 Mon Sep 17 00:00:00 2001 From: bzt Date: Tue, 29 Oct 2019 14:14:00 +0100 Subject: [PATCH 01/13] 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 f749993fd..f02a3b617 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 eec805b54..910a43562 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 34d49c472..4ce1a2bd8 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 32ac3b416..b9f28f035 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 000000000..35cae078a --- /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 000000000..dfcff8bc9 --- /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 000000000..76ba51364 --- /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 000000000..06cc757b6 --- /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 000000000..86a802021 --- /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 000000000..bb58a6d64 --- /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 000000000..144d1ec64 --- /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 02/13] 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 35cae078a..d25b91891 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 76ba51364..d34cd982f 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 bb58a6d64..000000000 --- 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 000000000..e7eccc5b2 --- /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 5b18baf88351f2e1a8f9ee88088fac7d2298d73b Mon Sep 17 00:00:00 2001 From: bzt Date: Wed, 30 Oct 2019 00:55:54 +0100 Subject: [PATCH 03/13] 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 d25b91891..9e774fa7f 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 d34cd982f..d732387ce 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 e7eccc5b2..82238f6d6 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 @@ asciiend: 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 04/13] 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 9e774fa7f..9ba48b125 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 d732387ce..873c51f97 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 05/13] 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 82238f6d6..d8d0602f6 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 06/13] 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 873c51f97..980171f73 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 07/13] 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 86a802021..b3c91ab7a 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 08/13] 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 980171f73..fcff49df7 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 d8d0602f6..b43f0721c 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 09/13] 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 b43f0721c..cccfa1a0f 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 10/13] 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 cccfa1a0f..eed66d3e5 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 8ebd48442eab97ea1567ff819c082d84abd22852 Mon Sep 17 00:00:00 2001 From: bzt Date: Fri, 1 Nov 2019 03:39:36 +0100 Subject: [PATCH 11/13] 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 eed66d3e5..7218f83e7 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 @@ postprocess: 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 @@ postprocess: } } } +#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 7d9e9aadbbceb8be0aaf50c7fcd6990715f32242 Mon Sep 17 00:00:00 2001 From: bzt Date: Fri, 1 Nov 2019 17:21:24 +0100 Subject: [PATCH 12/13] 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 9ba48b125..c22943396 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 b3c91ab7a..fa02cf42b 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 7218f83e7..b28dd5d2a 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 913813c3b..c0babe9b7 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 144d1ec64..000000000 --- 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 000000000..c3a0fb08c --- /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 13/13] 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 b28dd5d2a..9ace802ef 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: