#include #include #include #include #include #include #include #include #include #include static const char* xmlHeader = R"()"; struct Mesh { uint32_t index = 0xFFFFFFFF; ///< Mesh index in the scene std::vector transforms; uint32_t mtlIndex = 0xFFFFFFFF; }; struct Material { uint32_t index = 0xFFFFFFFF; std::vector meshIndices; }; struct Config { std::string inputFname; std::string outDir; std::string rpath; std::string texpath; bool flipyz = false; std::vector meshes; std::vector materials; }; Config config; //============================================================================== // Log and errors #define STR(s) #s #define XSTR(s) STR(s) #define LOGI(...) \ printf("[I] (" __FILE__ ":" XSTR(__LINE__) ") " __VA_ARGS__) #define ERROR(...) \ do { \ fprintf(stderr, "[E] (" __FILE__ ":" XSTR(__LINE__) ") " __VA_ARGS__); \ exit(0); \ } while(0) #define LOGW(...) \ fprintf(stderr, "[W] (" __FILE__ ":" XSTR(__LINE__) ") " __VA_ARGS__) //============================================================================== static std::string replaceAllString( const std::string& str, const std::string& from, const std::string& to) { if(from.empty()) { return str; } std::string out = str; size_t start_pos = 0; while((start_pos = out.find(from, start_pos)) != std::string::npos) { out.replace(start_pos, from.length(), to); start_pos += to.length(); } return out; } //============================================================================== static std::string getFilename(const std::string& path) { std::string out; const size_t last = path.find_last_of("/"); if(std::string::npos != last) { out.insert(out.end(), path.begin() + last + 1, path.end()); } else { out = path; } return out; } //============================================================================== static aiMatrix4x4 toAnkiMatrix(const aiMatrix4x4& in) { static const aiMatrix4x4 toLeftHanded( 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1); static const aiMatrix4x4 toLeftHandedInv( 1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1); if(config.flipyz) { return toLeftHanded * in * toLeftHandedInv; } else { return in; } } //============================================================================== static aiMatrix3x3 toAnkiMatrix(const aiMatrix3x3& in) { static const aiMatrix3x3 toLeftHanded( 1, 0, 0, 0, 0, 1, 0, -1, 0); static const aiMatrix3x3 toLeftHandedInv( 1, 0, 0, 0, 0, -1, 0, 1, 0); if(config.flipyz) { return toLeftHanded * in; } else { return in; } } //============================================================================== static void parseConfig(int argc, char** argv) { static const char* usage = R"(Usage: %s in_file out_dir [options] Options: -rpath : Append a string to the meshes and materials -texrpath : Append a string to the textures paths -flipyz : Flip y with z (For blender exports) )"; // Parse config if(argc < 3) { goto error; } config.inputFname = argv[1]; config.outDir = argv[2] + std::string("/"); for(int i = 3; i < argc; i++) { if(strcmp(argv[i], "-texrpath") == 0) { ++i; if(i < argc) { config.texpath = argv[i] + std::string("/"); } else { goto error; } } else if(strcmp(argv[i], "-rpath") == 0) { ++i; if(i < argc) { config.rpath = argv[i] + std::string("/"); } else { goto error; } } else if(strcmp(argv[i], "-flipyz") == 0) { config.flipyz = true; } else { goto error; } } if(config.rpath.empty()) { config.rpath = config.outDir; } if(config.texpath.empty()) { config.texpath = config.outDir; } return; error: printf(usage, argv[0]); exit(0); } //============================================================================== /// Load the scene static const aiScene& load( const std::string& filename, Assimp::Importer& importer) { LOGI("Loading file %s\n", filename.c_str()); const aiScene* scene = importer.ReadFile(filename, 0 //| aiProcess_FindInstances | aiProcess_Triangulate | aiProcess_JoinIdenticalVertices //| aiProcess_SortByPType | aiProcess_ImproveCacheLocality | aiProcess_OptimizeMeshes | aiProcess_RemoveRedundantMaterials ); if(!scene) { ERROR("%s\n", importer.GetErrorString()); } LOGI("File loaded successfully!\n"); return *scene; } //============================================================================== static const uint32_t MAX_BONES_PER_VERTEX = 4; /// Bone/weight info per vertex struct Vw { uint32_t boneIds[MAX_BONES_PER_VERTEX]; float weigths[MAX_BONES_PER_VERTEX]; uint32_t bonesCount; }; //============================================================================== static void exportMesh( const aiMesh& mesh, const std::string* name_, const aiMatrix4x4* transform) { std::string name = (name_) ? *name_ : mesh.mName.C_Str(); std::fstream file; LOGI("Exporting mesh %s\n", name.c_str()); uint32_t vertsCount = mesh.mNumVertices; // Open file file.open(config.outDir + name + ".ankimesh", std::ios::out | std::ios::binary); // Write magic word file.write("ANKIMESH", 8); // Write the name uint32_t size = name.size(); file.write((char*)&size, sizeof(uint32_t)); file.write(&name[0], size); // Write positions file.write((char*)&vertsCount, sizeof(uint32_t)); for(uint32_t i = 0; i < mesh.mNumVertices; i++) { aiVector3D pos = mesh.mVertices[i]; // Transform if(transform) { pos = (*transform) * pos; } // flip if(config.flipyz) { static const aiMatrix4x4 toLefthanded( 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1); pos = toLefthanded * pos; } for(uint32_t j = 0; j < 3; j++) { file.write((char*)&pos[j], sizeof(float)); } } // Write the indices file.write((char*)&mesh.mNumFaces, sizeof(uint32_t)); for(uint32_t i = 0; i < mesh.mNumFaces; i++) { const aiFace& face = mesh.mFaces[i]; if(face.mNumIndices != 3) { ERROR("For some reason the assimp didn't triangulate\n"); } for(uint32_t j = 0; j < 3; j++) { uint32_t index = face.mIndices[j]; file.write((char*)&index, sizeof(uint32_t)); } } // Write the tex coords file.write((char*)&vertsCount, sizeof(uint32_t)); // For all channels for(uint32_t ch = 0; ch < mesh.GetNumUVChannels(); ch++) { if(mesh.mNumUVComponents[ch] != 2) { ERROR("Incorrect number of UV components\n"); } // For all tex coords of this channel for(uint32_t i = 0; i < vertsCount; i++) { aiVector3D texCoord = mesh.mTextureCoords[ch][i]; for(uint32_t j = 0; j < 2; j++) { file.write((char*)&texCoord[j], sizeof(float)); } } } // Write bone weigths count if(mesh.HasBones()) { #if 0 // Write file file.write((char*)&vertsCount, sizeof(uint32_t)); // Gather info for each vertex std::vector vw; vw.resize(vertsCount); memset(&vw[0], 0, sizeof(Vw) * vertsCount); // For all bones for(uint32_t i = 0; i < mesh.mNumBones; i++) { const aiBone& bone = *mesh.mBones[i]; // for every weights of the bone for(uint32_t j = 0; j < bone.mWeightsCount; j++) { const aiVertexWeight& weigth = bone.mWeights[j]; // Sanity check if(weight.mVertexId >= vertCount) { ERROR("Out of bounds vert ID"); } Vm& a = vm[weight.mVertexId]; // Check out of bounds if(a.bonesCount >= MAX_BONES_PER_VERTEX) { LOGW("Too many bones for vertex %d\n", weigth.mVertexId); continue; } // Write to vertex a.boneIds[a.bonesCount] = i; a.weigths[a.bonesCount] = weigth.mWeigth; ++a.bonesCount; } // Now write the file } #endif } else { uint32_t num = 0; file.write((char*)&num, sizeof(uint32_t)); } } //============================================================================== static void exportSkeleton(const aiMesh& mesh) { assert(mesh.HasBones()); std::string name = mesh.mName.C_Str(); std::fstream file; LOGI("Exporting skeleton %s\n", name.c_str()); // Open file file.open(config.outDir + name + ".skel", std::ios::out); file << xmlHeader << "\n"; file << "\n"; file << "\t\n"; bool rootBoneFound = false; for(uint32_t i = 0; i < mesh.mNumBones; i++) { const aiBone& bone = *mesh.mBones[i]; file << "\t\t\n"; // file << "\t\t\t" << bone.mName.C_Str() << "\n"; if(strcmp(bone.mName.C_Str(), "root") == 0) { rootBoneFound = true; } // file << "\t\t\t"; for(uint32_t j = 0; j < 16; j++) { file << bone.mOffsetMatrix[j] << " "; } file << "\n"; file << "\t\t\n"; } if(!rootBoneFound) { ERROR("There should be one bone named \"root\"\n"); } file << "\t\n"; file << "\n"; } //============================================================================== static std::string getMaterialName(const aiMaterial& mtl) { aiString ainame; std::string name; if(mtl.Get(AI_MATKEY_NAME, ainame) == AI_SUCCESS) { name = ainame.C_Str(); } else { ERROR("Material's name is missing\n"); } return name; } //============================================================================== static void exportMaterial( const aiScene& scene, const aiMaterial& mtl, bool instanced, const std::string* name_) { std::string diffTex; std::string normTex; std::string name; if(name_) { name = *name_; } else { name = getMaterialName(mtl); } LOGI("Exporting material %s\n", name.c_str()); // Diffuse texture if(mtl.GetTextureCount(aiTextureType_DIFFUSE) < 1) { ERROR("Material has no diffuse textures\n"); } aiString path; if(mtl.GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) { diffTex = getFilename(path.C_Str()); } else { ERROR("Failed to retrieve texture\n"); } // Normal texture if(mtl.GetTextureCount(aiTextureType_NORMALS) > 0) { if(mtl.GetTexture(aiTextureType_NORMALS, 0, &path) == AI_SUCCESS) { normTex = getFilename(path.C_Str()); } else { ERROR("Failed to retrieve texture\n"); } } // Write file static const char* diffMtlStr = #include "diffTemplateMtl.h" ; static const char* diffNormMtlStr = #include "diffNormTemplateMtl.h" ; std::fstream file; file.open(config.outDir + name + ".ankimtl", std::ios::out); // Chose the correct template std::string str; if(normTex.size() == 0) { str = diffMtlStr; } else { str = replaceAllString(diffNormMtlStr, "%normalMap%", config.texpath + normTex); } str = replaceAllString(str, "%instanced%", (instanced) ? "1" : "0"); str = replaceAllString(str, "%diffuseMap%", config.texpath + diffTex); file << str; } //============================================================================== static void exportLight( const aiLight& light, std::fstream& file) { if(light.mType != aiLightSource_POINT || light.mType != aiLightSource_SPOT) { LOGW("Skipping light %s. Unsupported type\n", light.mName.C_Str()); return; } file << "\t\n"; file << "\t\t" << light.mName.C_Str() << "\n"; file << "\t\t" << light.mColorDiffuse[0] << " " << light.mColorDiffuse[1] << " " << light.mColorDiffuse[2] << " " << light.mColorDiffuse[3] << "\n"; file << "\t\t" << light.mColorSpecular[0] << " " << light.mColorSpecular[1] << " " << light.mColorSpecular[2] << " " << light.mColorSpecular[3] << "\n"; aiMatrix4x4 trf; aiMatrix4x4::Translation(light.mPosition, trf); switch(light.mType) { case aiLightSource_POINT: { file << "\t\tpoint\n"; // At this point I want the radius and have the attenuation factors // att = Ac + Al*d + Aq*d^2. When d = r then att = 0.0. Also if we // assume that Ac is 0 then: // 0 = Al*r + Aq*r^2. Solving by r is easy float r = -light.mAttenuationLinear / light.mAttenuationQuadratic; file << "\t\t" << r << "\n"; break; } case aiLightSource_SPOT: file << "\t\tspot\n"; break; default: assert(0); break; } // file << "\t\t"; for(uint32_t i = 0; i < 16; i++) { file << trf[i] << " "; } file << "\n"; file << "\t\n"; } //============================================================================== static void exportModel( const aiScene& scene, const aiNode& node) { if(node.mNumMeshes == 0) { return; } std::string name = node.mName.C_Str(); LOGI("Exporting model %s\n", name.c_str()); std::fstream file; file.open(config.outDir + name + ".ankimdl", std::ios::out); file << xmlHeader << '\n'; file << "\n"; file << "\t\n"; for(uint32_t i = 0; i < node.mNumMeshes; i++) { uint32_t meshIndex = node.mMeshes[i]; const aiMesh& mesh = *scene.mMeshes[meshIndex]; // start file << "\t\t\n"; // Write mesh file << "\t\t\t" << config.rpath << mesh.mName.C_Str() << ".ankimesh\n"; // Write material const aiMaterial& mtl = *scene.mMaterials[mesh.mMaterialIndex]; aiString mtlname; mtl.Get(AI_MATKEY_NAME, mtlname); file << "\t\t\t" << config.rpath << mtlname.C_Str() << ".ankimtl\n"; // end file << "\t\t\n"; } file << "\t\n"; file << "\n"; } //============================================================================== static void exportAnimation( const aiAnimation& anim, uint32_t index, const aiScene& scene) { // Get name std::string name = anim.mName.C_Str(); if(name.size() == 0) { name = std::string("animation_") + std::to_string(index); } // Find if it's skeleton animation /*bool isSkeletalAnimation = false; for(uint32_t i = 0; i < scene.mNumMeshes; i++) { const aiMesh& mesh = *scene.mMeshes[i]; if(mesh.HasBones()) { } }*/ std::fstream file; LOGI("Exporting animation %s\n", name.c_str()); file.open(config.outDir + name + ".ankianim", std::ios::out); file << xmlHeader << "\n"; file << "\n"; file << "\t" << anim.mDuration << "\n"; file << "\t\n"; for(uint32_t i = 0; i < anim.mNumChannels; i++) { const aiNodeAnim& nAnim = *anim.mChannels[i]; file << "\t\t\n"; // Name file << "\t\t\t" << nAnim.mNodeName.C_Str() << "\n"; // Positions file << "\t\t\t\n"; for(uint32_t j = 0; j < nAnim.mNumPositionKeys; j++) { const aiVectorKey& key = nAnim.mPositionKeys[j]; if(config.flipyz) { file << "\t\t\t\t" << "" << key.mValue[0] << " " << key.mValue[2] << " " << -key.mValue[1] << "\n"; } else { file << "\t\t\t\t" << "" << key.mValue[0] << " " << key.mValue[1] << " " << key.mValue[2] << "\n"; } } file << "\t\t\t\n"; // Rotations file << "\t\t\t\n"; for(uint32_t j = 0; j < nAnim.mNumRotationKeys; j++) { const aiQuatKey& key = nAnim.mRotationKeys[j]; aiMatrix3x3 mat = toAnkiMatrix(key.mValue.GetMatrix()); aiQuaternion quat(mat); //aiQuaternion quat(key.mValue); file << "\t\t\t\t" << "" << quat.x << " " << quat.y << " " << quat.z << " " << quat.w << "\n"; } file << "\t\t\t\n"; // Scale file << "\t\t\t\n"; for(uint32_t j = 0; j < nAnim.mNumScalingKeys; j++) { const aiVectorKey& key = nAnim.mScalingKeys[j]; // Note: only uniform scale file << "\t\t\t\t" << "" << ((key.mValue[0] + key.mValue[1] + key.mValue[2]) / 3.0) << "\n"; } file << "\t\t\t\n"; file << "\t\t\n"; } file << "\t\n"; file << "\n"; } //============================================================================== static void visitNode(const aiNode* node, const aiScene& scene) { if(node == nullptr) { return; } // For every mesh of this node for(uint32_t i = 0; i < node->mNumMeshes; i++) { uint32_t meshIndex = node->mMeshes[i]; const aiMesh& mesh = *scene.mMeshes[meshIndex]; // Is material set? if(config.meshes[meshIndex].mtlIndex == 0xFFFFFFFF) { // Connect mesh with material config.meshes[meshIndex].mtlIndex = mesh.mMaterialIndex; // Connect material with mesh config.materials[mesh.mMaterialIndex].meshIndices.push_back( meshIndex); } else if(config.meshes[meshIndex].mtlIndex != mesh.mMaterialIndex) { ERROR("Previous material index conflict\n"); } config.meshes[meshIndex].transforms.push_back(node->mTransformation); } // Go to children for(uint32_t i = 0; i < node->mNumChildren; i++) { visitNode(node->mChildren[i], scene); } } //============================================================================== static void exportScene(const aiScene& scene) { LOGI("Exporting scene to %s\n", config.outDir.c_str()); // // Open scene file // std::ofstream file; file.open(config.outDir + "master.ankiscene"); file << xmlHeader << "\n" << "\n"; // // Get all the data // config.meshes.resize(scene.mNumMeshes); config.materials.resize(scene.mNumMaterials); int i = 0; for(Mesh& mesh : config.meshes) { mesh.index = i++; } i = 0; for(Material& mtl : config.materials) { mtl.index = i++; } const aiNode* node = scene.mRootNode; visitNode(node, scene); #if 0 // // Export non instanced static meshes // for(uint32_t i = 0; i < config.meshes.size(); i++) { // Check if instance is one if(config.meshes[i].transforms.size() == 1) { continue; } // Export the material aiMaterial& aimtl = *scene.mMaterials[mesh.mtlIndex]; std::string mtlName = getMaterialName(aimtl); exportMaterial(scene, aimtl, false, &mtlName); // Export mesh std::string meshName = std::string(scene.mMeshes[i]->mName.C_Str()) + "_static_" + std::to_string(i); exportMesh(*scene.mMeshes[i], &meshName, nullptr); for(uint32_t t = 0; t < config.meshes[i].transforms.size(); t++) { std::string nname = name + "_" + std::to_string(t); exportMesh(*scene.mMeshes[i], &nname, &config.meshes[i].transforms[t], config); } } #endif // // Write the instanced meshes // for(uint32_t i = 0; i < config.meshes.size(); i++) { const Mesh& mesh = config.meshes[i]; // Skip meshes that are not instance candidates if(mesh.transforms.size() == 0) { continue; } // Export the material aiMaterial& aimtl = *scene.mMaterials[mesh.mtlIndex]; std::string mtlName = getMaterialName(aimtl) + "_instanced"; exportMaterial(scene, aimtl, true, &mtlName); // Export mesh std::string meshName = std::string(scene.mMeshes[i]->mName.C_Str()) + "_instanced_" + std::to_string(i); exportMesh(*scene.mMeshes[i], &meshName, nullptr); // Write model file std::string modelName = mtlName + "_" + std::to_string(i); { std::ofstream file; file.open( config.outDir + modelName + ".ankimdl"); file << xmlHeader << "\n" << "\n" << "\t\n" << "\t\t\n" << "\t\t\t" << config.rpath << meshName << ".ankimesh\n" << "\t\t\t" << config.rpath << mtlName << ".ankimtl\n" << "\t\t\n" << "\t\n" << "\n"; } // Node name std::string nodeName = getMaterialName(aimtl) + "_instanced_" + std::to_string(i); // Write the scene file file << "\t\n" << "\t\t" << nodeName << "\n" << "\t\t" << config.rpath << modelName << ".ankimdl\n" << "\t\t" << mesh.transforms.size() << "\n"; for(uint32_t j = 0; j < mesh.transforms.size(); j++) { file << "\t\t"; aiMatrix4x4 trf = toAnkiMatrix(mesh.transforms[j]); for(uint32_t a = 0; a < 4; a++) { for(uint32_t b = 0; b < 4; b++) { file << trf[a][b] << " "; } } file << "\n"; } file << "\t\n"; } #if 0 // Write bmeshes for(uint32_t mtlId = 0; mtlId < config.materials.size(); mtlId++) { const Material& mtl = config.materials[mtlId]; // Check if used if(mtl.meshIndices.size() < 1) { continue; } std::string name = getMaterialName(*scene.mMaterials[mtlId]) + ".bmesh"; std::fstream file; file.open(config.outDir + name, std::ios::out); file << xmlHeader << "\n"; file << "\n"; file << "\t\n"; for(uint32_t j = 0; j < mtl.meshIndices.size(); j++) { uint32_t meshId = mtl.meshIndices[j]; const Mesh& mesh = config.meshes[meshId]; for(uint32_t k = 0; k < mesh.transforms.size(); k++) { file << "\t\t" << config.rpath << "mesh_" + std::to_string(meshId) << "_" << std::to_string(k) << ".mesh\n"; } } file << "\t\n"; file << "\n"; } // Create the master model std::fstream file; file.open(config.outDir + "static_geometry.mdl", std::ios::out); file << xmlHeader << "\n"; file << "\n"; file << "\t\n"; for(uint32_t i = 0; i < config.materials.size(); i++) { // Check if used if(config.materials[i].meshIndices.size() < 1) { continue; } file << "\t\t\n"; file << "\t\t\t" << config.rpath << getMaterialName(*scene.mMaterials[i]) << ".bmesh\n"; file << "\t\t\t" << config.rpath << getMaterialName(*scene.mMaterials[i]) << ".mtl\n"; file << "\t\t\n"; } file << "\t\n"; file << "\n"; #endif // // Animations // for(unsigned i = 0; i < scene.mNumAnimations; i++) { exportAnimation(*scene.mAnimations[i], i, scene); } file << "\n"; LOGI("Done exporting scene!\n"); } //============================================================================== int main(int argc, char** argv) { try { parseConfig(argc, argv); // Load Assimp::Importer importer; const aiScene& scene = load(config.inputFname, importer); // Export exportScene(scene); } catch(std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; } }