| /* |
| --------------------------------------------------------------------------- |
| Open Asset Import Library (assimp) |
| --------------------------------------------------------------------------- |
| |
| Copyright (c) 2006-2017, 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. |
| --------------------------------------------------------------------------- |
| */ |
| |
| /** @file MD3Loader.cpp |
| * @brief Implementation of the MD3 importer class |
| * |
| * Sources: |
| * http://www.gamers.org/dEngine/quake3/UQ3S |
| * http://linux.ucla.edu/~phaethon/q3/formats/md3format.html |
| * http://www.heppler.com/shader/shader/ |
| */ |
| |
| |
| #ifndef ASSIMP_BUILD_NO_MD3_IMPORTER |
| |
| #include "MD3Loader.h" |
| #include <assimp/SceneCombiner.h> |
| #include "GenericProperty.h" |
| #include "RemoveComments.h" |
| #include "ParsingUtils.h" |
| #include "Importer.h" |
| #include <assimp/DefaultLogger.hpp> |
| #include <memory> |
| #include <assimp/IOSystem.hpp> |
| #include <assimp/material.h> |
| #include <assimp/scene.h> |
| #include <assimp/importerdesc.h> |
| #include <cctype> |
| |
| using namespace Assimp; |
| |
| static const aiImporterDesc desc = { |
| "Quake III Mesh Importer", |
| "", |
| "", |
| "", |
| aiImporterFlags_SupportBinaryFlavour, |
| 0, |
| 0, |
| 0, |
| 0, |
| "md3" |
| }; |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Convert a Q3 shader blend function to the appropriate enum value |
| Q3Shader::BlendFunc StringToBlendFunc(const std::string& m) |
| { |
| if (m == "GL_ONE") { |
| return Q3Shader::BLEND_GL_ONE; |
| } |
| if (m == "GL_ZERO") { |
| return Q3Shader::BLEND_GL_ZERO; |
| } |
| if (m == "GL_SRC_ALPHA") { |
| return Q3Shader::BLEND_GL_SRC_ALPHA; |
| } |
| if (m == "GL_ONE_MINUS_SRC_ALPHA") { |
| return Q3Shader::BLEND_GL_ONE_MINUS_SRC_ALPHA; |
| } |
| if (m == "GL_ONE_MINUS_DST_COLOR") { |
| return Q3Shader::BLEND_GL_ONE_MINUS_DST_COLOR; |
| } |
| DefaultLogger::get()->error("Q3Shader: Unknown blend function: " + m); |
| return Q3Shader::BLEND_NONE; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Load a Quake 3 shader |
| bool Q3Shader::LoadShader(ShaderData& fill, const std::string& pFile,IOSystem* io) |
| { |
| std::unique_ptr<IOStream> file( io->Open( pFile, "rt")); |
| if (!file.get()) |
| return false; // if we can't access the file, don't worry and return |
| |
| DefaultLogger::get()->info("Loading Quake3 shader file " + pFile); |
| |
| // read file in memory |
| const size_t s = file->FileSize(); |
| std::vector<char> _buff(s+1); |
| file->Read(&_buff[0],s,1); |
| _buff[s] = 0; |
| |
| // remove comments from it (C++ style) |
| CommentRemover::RemoveLineComments("//",&_buff[0]); |
| const char* buff = &_buff[0]; |
| |
| Q3Shader::ShaderDataBlock* curData = NULL; |
| Q3Shader::ShaderMapBlock* curMap = NULL; |
| |
| // read line per line |
| for (;SkipSpacesAndLineEnd(&buff);SkipLine(&buff)) { |
| |
| if (*buff == '{') { |
| ++buff; |
| |
| // append to last section, if any |
| if (!curData) { |
| DefaultLogger::get()->error("Q3Shader: Unexpected shader section token \'{\'"); |
| return true; // still no failure, the file is there |
| } |
| |
| // read this data section |
| for (;SkipSpacesAndLineEnd(&buff);SkipLine(&buff)) { |
| if (*buff == '{') { |
| ++buff; |
| // add new map section |
| curData->maps.push_back(Q3Shader::ShaderMapBlock()); |
| curMap = &curData->maps.back(); |
| |
| for (;SkipSpacesAndLineEnd(&buff);SkipLine(&buff)) { |
| // 'map' - Specifies texture file name |
| if (TokenMatchI(buff,"map",3) || TokenMatchI(buff,"clampmap",8)) { |
| curMap->name = GetNextToken(buff); |
| } |
| // 'blendfunc' - Alpha blending mode |
| else if (TokenMatchI(buff,"blendfunc",9)) { |
| const std::string blend_src = GetNextToken(buff); |
| if (blend_src == "add") { |
| curMap->blend_src = Q3Shader::BLEND_GL_ONE; |
| curMap->blend_dest = Q3Shader::BLEND_GL_ONE; |
| } |
| else if (blend_src == "filter") { |
| curMap->blend_src = Q3Shader::BLEND_GL_DST_COLOR; |
| curMap->blend_dest = Q3Shader::BLEND_GL_ZERO; |
| } |
| else if (blend_src == "blend") { |
| curMap->blend_src = Q3Shader::BLEND_GL_SRC_ALPHA; |
| curMap->blend_dest = Q3Shader::BLEND_GL_ONE_MINUS_SRC_ALPHA; |
| } |
| else { |
| curMap->blend_src = StringToBlendFunc(blend_src); |
| curMap->blend_dest = StringToBlendFunc(GetNextToken(buff)); |
| } |
| } |
| // 'alphafunc' - Alpha testing mode |
| else if (TokenMatchI(buff,"alphafunc",9)) { |
| const std::string at = GetNextToken(buff); |
| if (at == "GT0") { |
| curMap->alpha_test = Q3Shader::AT_GT0; |
| } |
| else if (at == "LT128") { |
| curMap->alpha_test = Q3Shader::AT_LT128; |
| } |
| else if (at == "GE128") { |
| curMap->alpha_test = Q3Shader::AT_GE128; |
| } |
| } |
| else if (*buff == '}') { |
| ++buff; |
| // close this map section |
| curMap = NULL; |
| break; |
| } |
| } |
| |
| } |
| else if (*buff == '}') { |
| ++buff; |
| curData = NULL; |
| break; |
| } |
| |
| // 'cull' specifies culling behaviour for the model |
| else if (TokenMatchI(buff,"cull",4)) { |
| SkipSpaces(&buff); |
| if (!ASSIMP_strincmp(buff,"back",4)) { |
| curData->cull = Q3Shader::CULL_CCW; |
| } |
| else if (!ASSIMP_strincmp(buff,"front",5)) { |
| curData->cull = Q3Shader::CULL_CW; |
| } |
| else if (!ASSIMP_strincmp(buff,"none",4) || !ASSIMP_strincmp(buff,"disable",7)) { |
| curData->cull = Q3Shader::CULL_NONE; |
| } |
| else DefaultLogger::get()->error("Q3Shader: Unrecognized cull mode"); |
| } |
| } |
| } |
| |
| else { |
| // add new section |
| fill.blocks.push_back(Q3Shader::ShaderDataBlock()); |
| curData = &fill.blocks.back(); |
| |
| // get the name of this section |
| curData->name = GetNextToken(buff); |
| } |
| } |
| return true; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Load a Quake 3 skin |
| bool Q3Shader::LoadSkin(SkinData& fill, const std::string& pFile,IOSystem* io) |
| { |
| std::unique_ptr<IOStream> file( io->Open( pFile, "rt")); |
| if (!file.get()) |
| return false; // if we can't access the file, don't worry and return |
| |
| DefaultLogger::get()->info("Loading Quake3 skin file " + pFile); |
| |
| // read file in memory |
| const size_t s = file->FileSize(); |
| std::vector<char> _buff(s+1);const char* buff = &_buff[0]; |
| file->Read(&_buff[0],s,1); |
| _buff[s] = 0; |
| |
| // remove commas |
| std::replace(_buff.begin(),_buff.end(),',',' '); |
| |
| // read token by token and fill output table |
| for (;*buff;) { |
| SkipSpacesAndLineEnd(&buff); |
| |
| // get first identifier |
| std::string ss = GetNextToken(buff); |
| |
| // ignore tokens starting with tag_ |
| if (!::strncmp(&ss[0],"tag_",std::min((size_t)4, ss.length()))) |
| continue; |
| |
| fill.textures.push_back(SkinData::TextureEntry()); |
| SkinData::TextureEntry& s = fill.textures.back(); |
| |
| s.first = ss; |
| s.second = GetNextToken(buff); |
| } |
| return true; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Convert Q3Shader to material |
| void Q3Shader::ConvertShaderToMaterial(aiMaterial* out, const ShaderDataBlock& shader) |
| { |
| ai_assert(NULL != out); |
| |
| /* IMPORTANT: This is not a real conversion. Actually we're just guessing and |
| * hacking around to build an aiMaterial that looks nearly equal to the |
| * original Quake 3 shader. We're missing some important features like |
| * animatable material properties in our material system, but at least |
| * multiple textures should be handled correctly. |
| */ |
| |
| // Two-sided material? |
| if (shader.cull == Q3Shader::CULL_NONE) { |
| const int twosided = 1; |
| out->AddProperty(&twosided,1,AI_MATKEY_TWOSIDED); |
| } |
| |
| unsigned int cur_emissive = 0, cur_diffuse = 0, cur_lm =0; |
| |
| // Iterate through all textures |
| for (std::list< Q3Shader::ShaderMapBlock >::const_iterator it = shader.maps.begin(); it != shader.maps.end();++it) { |
| |
| // CONVERSION BEHAVIOUR: |
| // |
| // |
| // If the texture is additive |
| // - if it is the first texture, assume additive blending for the whole material |
| // - otherwise register it as emissive texture. |
| // |
| // If the texture is using standard blend (or if the blend mode is unknown) |
| // - if first texture: assume default blending for material |
| // - in any case: set it as diffuse texture |
| // |
| // If the texture is using 'filter' blending |
| // - take as lightmap |
| // |
| // Textures with alpha funcs |
| // - aiTextureFlags_UseAlpha is set (otherwise aiTextureFlags_NoAlpha is explicitly set) |
| aiString s((*it).name); |
| aiTextureType type; unsigned int index; |
| |
| if ((*it).blend_src == Q3Shader::BLEND_GL_ONE && (*it).blend_dest == Q3Shader::BLEND_GL_ONE) { |
| if (it == shader.maps.begin()) { |
| const int additive = aiBlendMode_Additive; |
| out->AddProperty(&additive,1,AI_MATKEY_BLEND_FUNC); |
| |
| index = cur_diffuse++; |
| type = aiTextureType_DIFFUSE; |
| } |
| else { |
| index = cur_emissive++; |
| type = aiTextureType_EMISSIVE; |
| } |
| } |
| else if ((*it).blend_src == Q3Shader::BLEND_GL_DST_COLOR && (*it).blend_dest == Q3Shader::BLEND_GL_ZERO) { |
| index = cur_lm++; |
| type = aiTextureType_LIGHTMAP; |
| } |
| else { |
| const int blend = aiBlendMode_Default; |
| out->AddProperty(&blend,1,AI_MATKEY_BLEND_FUNC); |
| |
| index = cur_diffuse++; |
| type = aiTextureType_DIFFUSE; |
| } |
| |
| // setup texture |
| out->AddProperty(&s,AI_MATKEY_TEXTURE(type,index)); |
| |
| // setup texture flags |
| const int use_alpha = ((*it).alpha_test != Q3Shader::AT_NONE ? aiTextureFlags_UseAlpha : aiTextureFlags_IgnoreAlpha); |
| out->AddProperty(&use_alpha,1,AI_MATKEY_TEXFLAGS(type,index)); |
| } |
| // If at least one emissive texture was set, set the emissive base color to 1 to ensure |
| // the texture is actually displayed. |
| if (0 != cur_emissive) { |
| aiColor3D one(1.f,1.f,1.f); |
| out->AddProperty(&one,1,AI_MATKEY_COLOR_EMISSIVE); |
| } |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Constructor to be privately used by Importer |
| MD3Importer::MD3Importer() |
| : configFrameID (0) |
| , configHandleMP (true) |
| , configSpeedFlag() |
| , pcHeader() |
| , mBuffer() |
| , fileSize() |
| , mScene() |
| , mIOHandler() |
| {} |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Destructor, private as well |
| MD3Importer::~MD3Importer() |
| {} |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Returns whether the class can handle the format of the given file. |
| bool MD3Importer::CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const |
| { |
| const std::string extension = GetExtension(pFile); |
| if (extension == "md3") |
| return true; |
| |
| // if check for extension is not enough, check for the magic tokens |
| if (!extension.length() || checkSig) { |
| uint32_t tokens[1]; |
| tokens[0] = AI_MD3_MAGIC_NUMBER_LE; |
| return CheckMagicToken(pIOHandler,pFile,tokens,1); |
| } |
| return false; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| void MD3Importer::ValidateHeaderOffsets() |
| { |
| // Check magic number |
| if (pcHeader->IDENT != AI_MD3_MAGIC_NUMBER_BE && |
| pcHeader->IDENT != AI_MD3_MAGIC_NUMBER_LE) |
| throw DeadlyImportError( "Invalid MD3 file: Magic bytes not found"); |
| |
| // Check file format version |
| if (pcHeader->VERSION > 15) |
| DefaultLogger::get()->warn( "Unsupported MD3 file version. Continuing happily ..."); |
| |
| // Check some offset values whether they are valid |
| if (!pcHeader->NUM_SURFACES) |
| throw DeadlyImportError( "Invalid md3 file: NUM_SURFACES is 0"); |
| |
| if (pcHeader->OFS_FRAMES >= fileSize || pcHeader->OFS_SURFACES >= fileSize || |
| pcHeader->OFS_EOF > fileSize) { |
| throw DeadlyImportError("Invalid MD3 header: some offsets are outside the file"); |
| } |
| |
| if (pcHeader->NUM_SURFACES > AI_MAX_ALLOC(MD3::Surface)) { |
| throw DeadlyImportError("Invalid MD3 header: too many surfaces, would overflow"); |
| } |
| |
| if (pcHeader->OFS_SURFACES + pcHeader->NUM_SURFACES * sizeof(MD3::Surface) >= fileSize) { |
| throw DeadlyImportError("Invalid MD3 header: some surfaces are outside the file"); |
| } |
| |
| if (pcHeader->NUM_FRAMES <= configFrameID ) |
| throw DeadlyImportError("The requested frame is not existing the file"); |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| void MD3Importer::ValidateSurfaceHeaderOffsets(const MD3::Surface* pcSurf) |
| { |
| // Calculate the relative offset of the surface |
| const int32_t ofs = int32_t((const unsigned char*)pcSurf-this->mBuffer); |
| |
| // Check whether all data chunks are inside the valid range |
| if (pcSurf->OFS_TRIANGLES + ofs + pcSurf->NUM_TRIANGLES * sizeof(MD3::Triangle) > fileSize || |
| pcSurf->OFS_SHADERS + ofs + pcSurf->NUM_SHADER * sizeof(MD3::Shader) > fileSize || |
| pcSurf->OFS_ST + ofs + pcSurf->NUM_VERTICES * sizeof(MD3::TexCoord) > fileSize || |
| pcSurf->OFS_XYZNORMAL + ofs + pcSurf->NUM_VERTICES * sizeof(MD3::Vertex) > fileSize) { |
| |
| throw DeadlyImportError("Invalid MD3 surface header: some offsets are outside the file"); |
| } |
| |
| // Check whether all requirements for Q3 files are met. We don't |
| // care, but probably someone does. |
| if (pcSurf->NUM_TRIANGLES > AI_MD3_MAX_TRIANGLES) { |
| DefaultLogger::get()->warn("MD3: Quake III triangle limit exceeded"); |
| } |
| |
| if (pcSurf->NUM_SHADER > AI_MD3_MAX_SHADERS) { |
| DefaultLogger::get()->warn("MD3: Quake III shader limit exceeded"); |
| } |
| |
| if (pcSurf->NUM_VERTICES > AI_MD3_MAX_VERTS) { |
| DefaultLogger::get()->warn("MD3: Quake III vertex limit exceeded"); |
| } |
| |
| if (pcSurf->NUM_FRAMES > AI_MD3_MAX_FRAMES) { |
| DefaultLogger::get()->warn("MD3: Quake III frame limit exceeded"); |
| } |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| const aiImporterDesc* MD3Importer::GetInfo () const |
| { |
| return &desc; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Setup configuration properties |
| void MD3Importer::SetupProperties(const Importer* pImp) |
| { |
| // The |
| // AI_CONFIG_IMPORT_MD3_KEYFRAME option overrides the |
| // AI_CONFIG_IMPORT_GLOBAL_KEYFRAME option. |
| configFrameID = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_MD3_KEYFRAME,-1); |
| if(static_cast<unsigned int>(-1) == configFrameID) { |
| configFrameID = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_GLOBAL_KEYFRAME,0); |
| } |
| |
| // AI_CONFIG_IMPORT_MD3_HANDLE_MULTIPART |
| configHandleMP = (0 != pImp->GetPropertyInteger(AI_CONFIG_IMPORT_MD3_HANDLE_MULTIPART,1)); |
| |
| // AI_CONFIG_IMPORT_MD3_SKIN_NAME |
| configSkinFile = (pImp->GetPropertyString(AI_CONFIG_IMPORT_MD3_SKIN_NAME,"default")); |
| |
| // AI_CONFIG_IMPORT_MD3_SHADER_SRC |
| configShaderFile = (pImp->GetPropertyString(AI_CONFIG_IMPORT_MD3_SHADER_SRC,"")); |
| |
| // AI_CONFIG_FAVOUR_SPEED |
| configSpeedFlag = (0 != pImp->GetPropertyInteger(AI_CONFIG_FAVOUR_SPEED,0)); |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Try to read the skin for a MD3 file |
| void MD3Importer::ReadSkin(Q3Shader::SkinData& fill) const |
| { |
| // skip any postfixes (e.g. lower_1.md3) |
| std::string::size_type s = filename.find_last_of('_'); |
| if (s == std::string::npos) { |
| s = filename.find_last_of('.'); |
| if (s == std::string::npos) { |
| s = filename.size(); |
| } |
| } |
| ai_assert(s != std::string::npos); |
| |
| const std::string skin_file = path + filename.substr(0,s) + "_" + configSkinFile + ".skin"; |
| Q3Shader::LoadSkin(fill,skin_file,mIOHandler); |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Try to read the shader for a MD3 file |
| void MD3Importer::ReadShader(Q3Shader::ShaderData& fill) const |
| { |
| // Determine Q3 model name from given path |
| const std::string::size_type s = path.find_last_of("\\/",path.length()-2); |
| const std::string model_file = path.substr(s+1,path.length()-(s+2)); |
| |
| // If no specific dir or file is given, use our default search behaviour |
| if (!configShaderFile.length()) { |
| if(!Q3Shader::LoadShader(fill,path + "..\\..\\..\\scripts\\" + model_file + ".shader",mIOHandler)) { |
| Q3Shader::LoadShader(fill,path + "..\\..\\..\\scripts\\" + filename + ".shader",mIOHandler); |
| } |
| } |
| else { |
| // If the given string specifies a file, load this file. |
| // Otherwise it's a directory. |
| const std::string::size_type st = configShaderFile.find_last_of('.'); |
| if (st == std::string::npos) { |
| |
| if(!Q3Shader::LoadShader(fill,configShaderFile + model_file + ".shader",mIOHandler)) { |
| Q3Shader::LoadShader(fill,configShaderFile + filename + ".shader",mIOHandler); |
| } |
| } |
| else { |
| Q3Shader::LoadShader(fill,configShaderFile,mIOHandler); |
| } |
| } |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Tiny helper to remove a single node from its parent' list |
| void RemoveSingleNodeFromList(aiNode* nd) |
| { |
| if (!nd || nd->mNumChildren || !nd->mParent)return; |
| aiNode* par = nd->mParent; |
| for (unsigned int i = 0; i < par->mNumChildren;++i) { |
| if (par->mChildren[i] == nd) { |
| --par->mNumChildren; |
| for (;i < par->mNumChildren;++i) { |
| par->mChildren[i] = par->mChildren[i+1]; |
| } |
| delete nd; |
| break; |
| } |
| } |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Read a multi-part Q3 player model |
| bool MD3Importer::ReadMultipartFile() |
| { |
| // check whether the file name contains a common postfix, e.g lower_2.md3 |
| std::string::size_type s = filename.find_last_of('_'), t = filename.find_last_of('.'); |
| |
| if (t == std::string::npos) |
| t = filename.size(); |
| if (s == std::string::npos) |
| s = t; |
| |
| const std::string mod_filename = filename.substr(0,s); |
| const std::string suffix = filename.substr(s,t-s); |
| |
| if (mod_filename == "lower" || mod_filename == "upper" || mod_filename == "head"){ |
| const std::string lower = path + "lower" + suffix + ".md3"; |
| const std::string upper = path + "upper" + suffix + ".md3"; |
| const std::string head = path + "head" + suffix + ".md3"; |
| |
| aiScene* scene_upper = NULL; |
| aiScene* scene_lower = NULL; |
| aiScene* scene_head = NULL; |
| std::string failure; |
| |
| aiNode* tag_torso, *tag_head; |
| std::vector<AttachmentInfo> attach; |
| |
| DefaultLogger::get()->info("Multi part MD3 player model: lower, upper and head parts are joined"); |
| |
| // ensure we won't try to load ourselves recursively |
| BatchLoader::PropertyMap props; |
| SetGenericProperty( props.ints, AI_CONFIG_IMPORT_MD3_HANDLE_MULTIPART, 0); |
| |
| // now read these three files |
| BatchLoader batch(mIOHandler); |
| const unsigned int _lower = batch.AddLoadRequest(lower,0,&props); |
| const unsigned int _upper = batch.AddLoadRequest(upper,0,&props); |
| const unsigned int _head = batch.AddLoadRequest(head,0,&props); |
| batch.LoadAll(); |
| |
| // now construct a dummy scene to place these three parts in |
| aiScene* master = new aiScene(); |
| aiNode* nd = master->mRootNode = new aiNode(); |
| nd->mName.Set("<MD3_Player>"); |
| |
| // ... and get them. We need all of them. |
| scene_lower = batch.GetImport(_lower); |
| if (!scene_lower) { |
| DefaultLogger::get()->error("M3D: Failed to read multi part model, lower.md3 fails to load"); |
| failure = "lower"; |
| goto error_cleanup; |
| } |
| |
| scene_upper = batch.GetImport(_upper); |
| if (!scene_upper) { |
| DefaultLogger::get()->error("M3D: Failed to read multi part model, upper.md3 fails to load"); |
| failure = "upper"; |
| goto error_cleanup; |
| } |
| |
| scene_head = batch.GetImport(_head); |
| if (!scene_head) { |
| DefaultLogger::get()->error("M3D: Failed to read multi part model, head.md3 fails to load"); |
| failure = "head"; |
| goto error_cleanup; |
| } |
| |
| // build attachment infos. search for typical Q3 tags |
| |
| // original root |
| scene_lower->mRootNode->mName.Set("lower"); |
| attach.push_back(AttachmentInfo(scene_lower, nd)); |
| |
| // tag_torso |
| tag_torso = scene_lower->mRootNode->FindNode("tag_torso"); |
| if (!tag_torso) { |
| DefaultLogger::get()->error("M3D: Failed to find attachment tag for multi part model: tag_torso expected"); |
| goto error_cleanup; |
| } |
| scene_upper->mRootNode->mName.Set("upper"); |
| attach.push_back(AttachmentInfo(scene_upper,tag_torso)); |
| |
| // tag_head |
| tag_head = scene_upper->mRootNode->FindNode("tag_head"); |
| if (!tag_head) { |
| DefaultLogger::get()->error("M3D: Failed to find attachment tag for multi part model: tag_head expected"); |
| goto error_cleanup; |
| } |
| scene_head->mRootNode->mName.Set("head"); |
| attach.push_back(AttachmentInfo(scene_head,tag_head)); |
| |
| // Remove tag_head and tag_torso from all other model parts ... |
| // this ensures (together with AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES_IF_NECESSARY) |
| // that tag_torso/tag_head is also the name of the (unique) output node |
| RemoveSingleNodeFromList (scene_upper->mRootNode->FindNode("tag_torso")); |
| RemoveSingleNodeFromList (scene_head-> mRootNode->FindNode("tag_head" )); |
| |
| // Undo the rotations which we applied to the coordinate systems. We're |
| // working in global Quake space here |
| scene_head->mRootNode->mTransformation = aiMatrix4x4(); |
| scene_lower->mRootNode->mTransformation = aiMatrix4x4(); |
| scene_upper->mRootNode->mTransformation = aiMatrix4x4(); |
| |
| // and merge the scenes |
| SceneCombiner::MergeScenes(&mScene,master, attach, |
| AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES | |
| AI_INT_MERGE_SCENE_GEN_UNIQUE_MATNAMES | |
| AI_INT_MERGE_SCENE_RESOLVE_CROSS_ATTACHMENTS | |
| (!configSpeedFlag ? AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES_IF_NECESSARY : 0)); |
| |
| // Now rotate the whole scene 90 degrees around the x axis to convert to internal coordinate system |
| mScene->mRootNode->mTransformation = aiMatrix4x4(1.f,0.f,0.f,0.f, |
| 0.f,0.f,1.f,0.f,0.f,-1.f,0.f,0.f,0.f,0.f,0.f,1.f); |
| |
| return true; |
| |
| error_cleanup: |
| delete scene_upper; |
| delete scene_lower; |
| delete scene_head; |
| delete master; |
| |
| if (failure == mod_filename) { |
| throw DeadlyImportError("MD3: failure to read multipart host file"); |
| } |
| } |
| return false; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Convert a MD3 path to a proper value |
| void MD3Importer::ConvertPath(const char* texture_name, const char* header_name, std::string& out) const |
| { |
| // If the MD3's internal path itself and the given path are using |
| // the same directory, remove it completely to get right output paths. |
| const char* end1 = ::strrchr(header_name,'\\'); |
| if (!end1)end1 = ::strrchr(header_name,'/'); |
| |
| const char* end2 = ::strrchr(texture_name,'\\'); |
| if (!end2)end2 = ::strrchr(texture_name,'/'); |
| |
| // HACK: If the paths starts with "models", ignore the |
| // next two hierarchy levels, it specifies just the model name. |
| // Ignored by Q3, it might be not equal to the real model location. |
| if (end2) { |
| |
| size_t len2; |
| const size_t len1 = (size_t)(end1 - header_name); |
| if (!ASSIMP_strincmp(texture_name,"models",6) && (texture_name[6] == '/' || texture_name[6] == '\\')) { |
| len2 = 6; // ignore the seventh - could be slash or backslash |
| |
| if (!header_name[0]) { |
| // Use the file name only |
| out = end2+1; |
| return; |
| } |
| } |
| else len2 = std::min (len1, (size_t)(end2 - texture_name )); |
| if (!ASSIMP_strincmp(texture_name,header_name,static_cast<unsigned int>(len2))) { |
| // Use the file name only |
| out = end2+1; |
| return; |
| } |
| } |
| // Use the full path |
| out = texture_name; |
| } |
| |
| // ------------------------------------------------------------------------------------------------ |
| // Imports the given file into the given scene structure. |
| void MD3Importer::InternReadFile( const std::string& pFile, |
| aiScene* pScene, IOSystem* pIOHandler) |
| { |
| mFile = pFile; |
| mScene = pScene; |
| mIOHandler = pIOHandler; |
| |
| // get base path and file name |
| // todo ... move to PathConverter |
| std::string::size_type s = mFile.find_last_of("/\\"); |
| if (s == std::string::npos) { |
| s = 0; |
| } |
| else ++s; |
| filename = mFile.substr(s), path = mFile.substr(0,s); |
| for( std::string::iterator it = filename .begin(); it != filename.end(); ++it) |
| *it = tolower( *it); |
| |
| // Load multi-part model file, if necessary |
| if (configHandleMP) { |
| if (ReadMultipartFile()) |
| return; |
| } |
| |
| std::unique_ptr<IOStream> file( pIOHandler->Open( pFile)); |
| |
| // Check whether we can read from the file |
| if( file.get() == NULL) |
| throw DeadlyImportError( "Failed to open MD3 file " + pFile + "."); |
| |
| // Check whether the md3 file is large enough to contain the header |
| fileSize = (unsigned int)file->FileSize(); |
| if( fileSize < sizeof(MD3::Header)) |
| throw DeadlyImportError( "MD3 File is too small."); |
| |
| // Allocate storage and copy the contents of the file to a memory buffer |
| std::vector<unsigned char> mBuffer2 (fileSize); |
| file->Read( &mBuffer2[0], 1, fileSize); |
| mBuffer = &mBuffer2[0]; |
| |
| pcHeader = (BE_NCONST MD3::Header*)mBuffer; |
| |
| // Ensure correct endianness |
| #ifdef AI_BUILD_BIG_ENDIAN |
| |
| AI_SWAP4(pcHeader->VERSION); |
| AI_SWAP4(pcHeader->FLAGS); |
| AI_SWAP4(pcHeader->IDENT); |
| AI_SWAP4(pcHeader->NUM_FRAMES); |
| AI_SWAP4(pcHeader->NUM_SKINS); |
| AI_SWAP4(pcHeader->NUM_SURFACES); |
| AI_SWAP4(pcHeader->NUM_TAGS); |
| AI_SWAP4(pcHeader->OFS_EOF); |
| AI_SWAP4(pcHeader->OFS_FRAMES); |
| AI_SWAP4(pcHeader->OFS_SURFACES); |
| AI_SWAP4(pcHeader->OFS_TAGS); |
| |
| #endif |
| |
| // Validate the file header |
| ValidateHeaderOffsets(); |
| |
| // Navigate to the list of surfaces |
| BE_NCONST MD3::Surface* pcSurfaces = (BE_NCONST MD3::Surface*)(mBuffer + pcHeader->OFS_SURFACES); |
| |
| // Navigate to the list of tags |
| BE_NCONST MD3::Tag* pcTags = (BE_NCONST MD3::Tag*)(mBuffer + pcHeader->OFS_TAGS); |
| |
| // Allocate output storage |
| pScene->mNumMeshes = pcHeader->NUM_SURFACES; |
| if (pcHeader->NUM_SURFACES == 0) { |
| throw DeadlyImportError("MD3: No surfaces"); |
| } else if (pcHeader->NUM_SURFACES > AI_MAX_ALLOC(aiMesh)) { |
| // We allocate pointers but check against the size of aiMesh |
| // since those pointers will eventually have to point to real objects |
| throw DeadlyImportError("MD3: Too many surfaces, would run out of memory"); |
| } |
| pScene->mMeshes = new aiMesh*[pScene->mNumMeshes]; |
| |
| pScene->mNumMaterials = pcHeader->NUM_SURFACES; |
| pScene->mMaterials = new aiMaterial*[pScene->mNumMeshes]; |
| |
| // Set arrays to zero to ensue proper destruction if an exception is raised |
| ::memset(pScene->mMeshes,0,pScene->mNumMeshes*sizeof(aiMesh*)); |
| ::memset(pScene->mMaterials,0,pScene->mNumMaterials*sizeof(aiMaterial*)); |
| |
| // Now read possible skins from .skin file |
| Q3Shader::SkinData skins; |
| ReadSkin(skins); |
| |
| // And check whether we can locate a shader file for this model |
| Q3Shader::ShaderData shaders; |
| ReadShader(shaders); |
| |
| // Adjust all texture paths in the shader |
| const char* header_name = pcHeader->NAME; |
| if (!shaders.blocks.empty()) { |
| for (std::list< Q3Shader::ShaderDataBlock >::iterator dit = shaders.blocks.begin(); dit != shaders.blocks.end(); ++dit) { |
| ConvertPath((*dit).name.c_str(),header_name,(*dit).name); |
| |
| for (std::list< Q3Shader::ShaderMapBlock >::iterator mit = (*dit).maps.begin(); mit != (*dit).maps.end(); ++mit) { |
| ConvertPath((*mit).name.c_str(),header_name,(*mit).name); |
| } |
| } |
| } |
| |
| // Read all surfaces from the file |
| unsigned int iNum = pcHeader->NUM_SURFACES; |
| unsigned int iNumMaterials = 0; |
| while (iNum-- > 0) { |
| |
| // Ensure correct endianness |
| #ifdef AI_BUILD_BIG_ENDIAN |
| |
| AI_SWAP4(pcSurfaces->FLAGS); |
| AI_SWAP4(pcSurfaces->IDENT); |
| AI_SWAP4(pcSurfaces->NUM_FRAMES); |
| AI_SWAP4(pcSurfaces->NUM_SHADER); |
| AI_SWAP4(pcSurfaces->NUM_TRIANGLES); |
| AI_SWAP4(pcSurfaces->NUM_VERTICES); |
| AI_SWAP4(pcSurfaces->OFS_END); |
| AI_SWAP4(pcSurfaces->OFS_SHADERS); |
| AI_SWAP4(pcSurfaces->OFS_ST); |
| AI_SWAP4(pcSurfaces->OFS_TRIANGLES); |
| AI_SWAP4(pcSurfaces->OFS_XYZNORMAL); |
| |
| #endif |
| |
| // Validate the surface header |
| ValidateSurfaceHeaderOffsets(pcSurfaces); |
| |
| // Navigate to the vertex list of the surface |
| BE_NCONST MD3::Vertex* pcVertices = (BE_NCONST MD3::Vertex*) |
| (((uint8_t*)pcSurfaces) + pcSurfaces->OFS_XYZNORMAL); |
| |
| // Navigate to the triangle list of the surface |
| BE_NCONST MD3::Triangle* pcTriangles = (BE_NCONST MD3::Triangle*) |
| (((uint8_t*)pcSurfaces) + pcSurfaces->OFS_TRIANGLES); |
| |
| // Navigate to the texture coordinate list of the surface |
| BE_NCONST MD3::TexCoord* pcUVs = (BE_NCONST MD3::TexCoord*) |
| (((uint8_t*)pcSurfaces) + pcSurfaces->OFS_ST); |
| |
| // Navigate to the shader list of the surface |
| BE_NCONST MD3::Shader* pcShaders = (BE_NCONST MD3::Shader*) |
| (((uint8_t*)pcSurfaces) + pcSurfaces->OFS_SHADERS); |
| |
| // If the submesh is empty ignore it |
| if (0 == pcSurfaces->NUM_VERTICES || 0 == pcSurfaces->NUM_TRIANGLES) |
| { |
| pcSurfaces = (BE_NCONST MD3::Surface*)(((uint8_t*)pcSurfaces) + pcSurfaces->OFS_END); |
| pScene->mNumMeshes--; |
| continue; |
| } |
| |
| // Allocate output mesh |
| pScene->mMeshes[iNum] = new aiMesh(); |
| aiMesh* pcMesh = pScene->mMeshes[iNum]; |
| |
| std::string _texture_name; |
| const char* texture_name = NULL; |
| |
| // Check whether we have a texture record for this surface in the .skin file |
| std::list< Q3Shader::SkinData::TextureEntry >::iterator it = std::find( |
| skins.textures.begin(), skins.textures.end(), pcSurfaces->NAME ); |
| |
| if (it != skins.textures.end()) { |
| texture_name = &*( _texture_name = (*it).second).begin(); |
| DefaultLogger::get()->debug("MD3: Assigning skin texture " + (*it).second + " to surface " + pcSurfaces->NAME); |
| (*it).resolved = true; // mark entry as resolved |
| } |
| |
| // Get the first shader (= texture?) assigned to the surface |
| if (!texture_name && pcSurfaces->NUM_SHADER) { |
| texture_name = pcShaders->NAME; |
| } |
| |
| std::string convertedPath; |
| if (texture_name) { |
| ConvertPath(texture_name,header_name,convertedPath); |
| } |
| |
| const Q3Shader::ShaderDataBlock* shader = NULL; |
| |
| // Now search the current shader for a record with this name ( |
| // excluding texture file extension) |
| if (!shaders.blocks.empty()) { |
| |
| std::string::size_type s = convertedPath.find_last_of('.'); |
| if (s == std::string::npos) |
| s = convertedPath.length(); |
| |
| const std::string without_ext = convertedPath.substr(0,s); |
| std::list< Q3Shader::ShaderDataBlock >::const_iterator dit = std::find(shaders.blocks.begin(),shaders.blocks.end(),without_ext); |
| if (dit != shaders.blocks.end()) { |
| // Hurra, wir haben einen. Tolle Sache. |
| shader = &*dit; |
| DefaultLogger::get()->info("Found shader record for " +without_ext ); |
| } |
| else DefaultLogger::get()->warn("Unable to find shader record for " +without_ext ); |
| } |
| |
| aiMaterial* pcHelper = new aiMaterial(); |
| |
| const int iMode = (int)aiShadingMode_Gouraud; |
| pcHelper->AddProperty<int>(&iMode, 1, AI_MATKEY_SHADING_MODEL); |
| |
| // Add a small ambient color value - Quake 3 seems to have one |
| aiColor3D clr; |
| clr.b = clr.g = clr.r = 0.05f; |
| pcHelper->AddProperty<aiColor3D>(&clr, 1,AI_MATKEY_COLOR_AMBIENT); |
| |
| clr.b = clr.g = clr.r = 1.0f; |
| pcHelper->AddProperty<aiColor3D>(&clr, 1,AI_MATKEY_COLOR_DIFFUSE); |
| pcHelper->AddProperty<aiColor3D>(&clr, 1,AI_MATKEY_COLOR_SPECULAR); |
| |
| // use surface name + skin_name as material name |
| aiString name; |
| name.Set("MD3_[" + configSkinFile + "][" + pcSurfaces->NAME + "]"); |
| pcHelper->AddProperty(&name,AI_MATKEY_NAME); |
| |
| if (!shader) { |
| // Setup dummy texture file name to ensure UV coordinates are kept during postprocessing |
| aiString szString; |
| if (convertedPath.length()) { |
| szString.Set(convertedPath); |
| } |
| else { |
| DefaultLogger::get()->warn("Texture file name has zero length. Using default name"); |
| szString.Set("dummy_texture.bmp"); |
| } |
| pcHelper->AddProperty(&szString,AI_MATKEY_TEXTURE_DIFFUSE(0)); |
| |
| // prevent transparency by default |
| int no_alpha = aiTextureFlags_IgnoreAlpha; |
| pcHelper->AddProperty(&no_alpha,1,AI_MATKEY_TEXFLAGS_DIFFUSE(0)); |
| } |
| else { |
| Q3Shader::ConvertShaderToMaterial(pcHelper,*shader); |
| } |
| |
| pScene->mMaterials[iNumMaterials] = (aiMaterial*)pcHelper; |
| pcMesh->mMaterialIndex = iNumMaterials++; |
| |
| // Ensure correct endianness |
| #ifdef AI_BUILD_BIG_ENDIAN |
| |
| for (uint32_t i = 0; i < pcSurfaces->NUM_VERTICES;++i) { |
| AI_SWAP2( pcVertices[i].NORMAL ); |
| AI_SWAP2( pcVertices[i].X ); |
| AI_SWAP2( pcVertices[i].Y ); |
| AI_SWAP2( pcVertices[i].Z ); |
| |
| AI_SWAP4( pcUVs[i].U ); |
| AI_SWAP4( pcUVs[i].U ); |
| } |
| for (uint32_t i = 0; i < pcSurfaces->NUM_TRIANGLES;++i) { |
| AI_SWAP4(pcTriangles[i].INDEXES[0]); |
| AI_SWAP4(pcTriangles[i].INDEXES[1]); |
| AI_SWAP4(pcTriangles[i].INDEXES[2]); |
| } |
| |
| #endif |
| |
| // Fill mesh information |
| pcMesh->mPrimitiveTypes = aiPrimitiveType_TRIANGLE; |
| |
| pcMesh->mNumVertices = pcSurfaces->NUM_TRIANGLES*3; |
| pcMesh->mNumFaces = pcSurfaces->NUM_TRIANGLES; |
| pcMesh->mFaces = new aiFace[pcSurfaces->NUM_TRIANGLES]; |
| pcMesh->mNormals = new aiVector3D[pcMesh->mNumVertices]; |
| pcMesh->mVertices = new aiVector3D[pcMesh->mNumVertices]; |
| pcMesh->mTextureCoords[0] = new aiVector3D[pcMesh->mNumVertices]; |
| pcMesh->mNumUVComponents[0] = 2; |
| |
| // Fill in all triangles |
| unsigned int iCurrent = 0; |
| for (unsigned int i = 0; i < (unsigned int)pcSurfaces->NUM_TRIANGLES;++i) { |
| pcMesh->mFaces[i].mIndices = new unsigned int[3]; |
| pcMesh->mFaces[i].mNumIndices = 3; |
| |
| //unsigned int iTemp = iCurrent; |
| for (unsigned int c = 0; c < 3;++c,++iCurrent) { |
| pcMesh->mFaces[i].mIndices[c] = iCurrent; |
| |
| // Read vertices |
| aiVector3D& vec = pcMesh->mVertices[iCurrent]; |
| uint32_t index = pcTriangles->INDEXES[c]; |
| if (index >= pcSurfaces->NUM_VERTICES) { |
| throw DeadlyImportError( "MD3: Invalid vertex index"); |
| } |
| vec.x = pcVertices[index].X*AI_MD3_XYZ_SCALE; |
| vec.y = pcVertices[index].Y*AI_MD3_XYZ_SCALE; |
| vec.z = pcVertices[index].Z*AI_MD3_XYZ_SCALE; |
| |
| // Convert the normal vector to uncompressed float3 format |
| aiVector3D& nor = pcMesh->mNormals[iCurrent]; |
| LatLngNormalToVec3(pcVertices[index].NORMAL,(ai_real*)&nor); |
| |
| // Read texture coordinates |
| pcMesh->mTextureCoords[0][iCurrent].x = pcUVs[index].U; |
| pcMesh->mTextureCoords[0][iCurrent].y = 1.0f-pcUVs[index].V; |
| } |
| // Flip face order if necessary |
| if (!shader || shader->cull == Q3Shader::CULL_CW) { |
| std::swap(pcMesh->mFaces[i].mIndices[2],pcMesh->mFaces[i].mIndices[1]); |
| } |
| pcTriangles++; |
| } |
| |
| // Go to the next surface |
| pcSurfaces = (BE_NCONST MD3::Surface*)(((unsigned char*)pcSurfaces) + pcSurfaces->OFS_END); |
| } |
| |
| // For debugging purposes: check whether we found matches for all entries in the skins file |
| if (!DefaultLogger::isNullLogger()) { |
| for (std::list< Q3Shader::SkinData::TextureEntry>::const_iterator it = skins.textures.begin();it != skins.textures.end(); ++it) { |
| if (!(*it).resolved) { |
| DefaultLogger::get()->error("MD3: Failed to match skin " + (*it).first + " to surface " + (*it).second); |
| } |
| } |
| } |
| |
| if (!pScene->mNumMeshes) |
| throw DeadlyImportError( "MD3: File contains no valid mesh"); |
| pScene->mNumMaterials = iNumMaterials; |
| |
| // Now we need to generate an empty node graph |
| pScene->mRootNode = new aiNode("<MD3Root>"); |
| pScene->mRootNode->mNumMeshes = pScene->mNumMeshes; |
| pScene->mRootNode->mMeshes = new unsigned int[pScene->mNumMeshes]; |
| |
| // Attach tiny children for all tags |
| if (pcHeader->NUM_TAGS) { |
| pScene->mRootNode->mNumChildren = pcHeader->NUM_TAGS; |
| pScene->mRootNode->mChildren = new aiNode*[pcHeader->NUM_TAGS]; |
| |
| for (unsigned int i = 0; i < pcHeader->NUM_TAGS; ++i, ++pcTags) { |
| |
| aiNode* nd = pScene->mRootNode->mChildren[i] = new aiNode(); |
| nd->mName.Set((const char*)pcTags->NAME); |
| nd->mParent = pScene->mRootNode; |
| |
| AI_SWAP4(pcTags->origin.x); |
| AI_SWAP4(pcTags->origin.y); |
| AI_SWAP4(pcTags->origin.z); |
| |
| // Copy local origin, again flip z,y |
| nd->mTransformation.a4 = pcTags->origin.x; |
| nd->mTransformation.b4 = pcTags->origin.y; |
| nd->mTransformation.c4 = pcTags->origin.z; |
| |
| // Copy rest of transformation (need to transpose to match row-order matrix) |
| for (unsigned int a = 0; a < 3;++a) { |
| for (unsigned int m = 0; m < 3;++m) { |
| nd->mTransformation[m][a] = pcTags->orientation[a][m]; |
| AI_SWAP4(nd->mTransformation[m][a]); |
| } |
| } |
| } |
| } |
| |
| for (unsigned int i = 0; i < pScene->mNumMeshes;++i) |
| pScene->mRootNode->mMeshes[i] = i; |
| |
| // Now rotate the whole scene 90 degrees around the x axis to convert to internal coordinate system |
| pScene->mRootNode->mTransformation = aiMatrix4x4(1.f,0.f,0.f,0.f, |
| 0.f,0.f,1.f,0.f,0.f,-1.f,0.f,0.f,0.f,0.f,0.f,1.f); |
| } |
| |
| #endif // !! ASSIMP_BUILD_NO_MD3_IMPORTER |