// NeL - MMORPG Framework // Copyright (C) 2015 Winch Gate Property Limited // Author: Jan Boon // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include #include "mesh_utils.h" #include #include #include #include #include #include #include #include #include "database_config.h" #include "scene_meta.h" #include #include #include #define NL_NODE_INTERNAL_TYPE aiNode #define NL_SCENE_INTERNAL_TYPE aiScene #include "scene_context.h" #include "assimp_material.h" #include "assimp_shape.h" CMeshUtilsSettings::CMeshUtilsSettings() { /*ShapeDirectory = "shape"; IGDirectory = "ig"; SkelDirectory = "skel";*/ } void importShapes(CMeshUtilsContext &context, const aiNode *node) { if (node != context.InternalScene->mRootNode) { CNodeContext &nodeContext = context.Nodes[node->mName.C_Str()]; CNodeMeta &nodeMeta = context.SceneMeta.Nodes[node->mName.C_Str()]; if (nodeMeta.ExportMesh == TMeshShape && nodeMeta.InstanceName.empty()) { if (node->mNumMeshes) { nldebug("Shape '%s' found containing '%u' meshes", node->mName.C_Str(), node->mNumMeshes); assimpShape(context, nodeContext); } } } for (unsigned int i = 0; i < node->mNumChildren; ++i) importShapes(context, node->mChildren[i]); } void validateInternalNodeNames(CMeshUtilsContext &context, const aiNode *node) { if (!node->mParent || node == context.InternalScene->mRootNode) { // do nothing } else if (node->mName.length == 0) { tlwarning(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Node has no name"); } else { CNodeContext &nodeContext = context.Nodes[node->mName.C_Str()]; if (nodeContext.InternalNode && nodeContext.InternalNode != node) { tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Node name '%s' appears multiple times", node->mName.C_Str()); } else { nodeContext.InternalNode = node; } } for (unsigned int i = 0; i < node->mNumChildren; ++i) validateInternalNodeNames(context, node->mChildren[i]); } void flagAssimpBones(CMeshUtilsContext &context) { // Find out which nodes are bones by checking the mesh meta info const aiScene *scene = context.InternalScene; for (unsigned int i = 0; i < scene->mNumMeshes; ++i) { // nldebug("FOUND MESH '%s'\n", scene->mMeshes[i]->mName.C_Str()); const aiMesh *mesh = scene->mMeshes[i]; for (unsigned int j = 0; j < mesh->mNumBones; ++j) { CNodeContext &nodeContext = context.Nodes[mesh->mBones[j]->mName.C_Str()]; if (!nodeContext.InternalNode) { tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Bone '%s' has no associated node", mesh->mBones[j]->mName.C_Str()); } else { // Flag as bone nodeContext.IsBone = true; // Flag all parents as bones /*const aiNode *parent = nodeContext.InternalNode; while (parent = parent->mParent) if (parent->mName.length) { context.Nodes[parent->mName.C_Str()].IsBone = true; }*/ } } } // Find out which nodes are bones by checking the animation info // TODO } void flagRecursiveBones(CMeshUtilsContext &context, CNodeContext &nodeContext, bool autoStop = false) { nodeContext.IsBone = true; const aiNode *node = nodeContext.InternalNode; nlassert(node); for (unsigned int i = 0; i < node->mNumChildren; ++i) { CNodeContext &ctx = context.Nodes[node->mName.C_Str()]; if (autoStop && ctx.IsBone) continue; flagRecursiveBones(context, ctx); } } void flagMetaBones(CMeshUtilsContext &context) { for (TNodeContextMap::iterator it(context.Nodes.begin()), end(context.Nodes.end()); it != end; ++it) { CNodeContext &ctx = it->second; CNodeMeta &meta = context.SceneMeta.Nodes[it->first]; if (meta.ExportBone == TBoneForce) ctx.IsBone = true; else if (meta.ExportBone == TBoneRoot) flagRecursiveBones(context, ctx); } } void flagLocalParentBones(CMeshUtilsContext &context, CNodeContext &nodeContext) { const aiNode *node = nodeContext.InternalNode; } void flagAllParentBones(CMeshUtilsContext &context, CNodeContext &nodeContext, bool autoStop = false) { const aiNode *parent = nodeContext.InternalNode; while (parent = parent->mParent) if (parent->mName.length && parent != context.InternalScene->mRootNode) { CNodeContext &ctx = context.Nodes[parent->mName.C_Str()]; if (autoStop && ctx.IsBone) break; ctx.IsBone = true; } } bool hasIndirectParentBone(CMeshUtilsContext &context, CNodeContext &nodeContext) { const aiNode *parent = nodeContext.InternalNode; while (parent = parent->mParent) if (parent->mName.length && parent != context.InternalScene->mRootNode) if (context.Nodes[parent->mName.C_Str()].IsBone) return true; return false; } void flagExpandedBones(CMeshUtilsContext &context) { switch (context.SceneMeta.SkeletonMode) { case TSkelLocal: for (TNodeContextMap::iterator it(context.Nodes.begin()), end(context.Nodes.end()); it != end; ++it) { CNodeContext &nodeContext = it->second; if (nodeContext.IsBone && hasIndirectParentBone(context, nodeContext)) flagAllParentBones(context, nodeContext, true); } break; case TSkelRoot: for (TNodeContextMap::iterator it(context.Nodes.begin()), end(context.Nodes.end()); it != end; ++it) { CNodeContext &nodeContext = it->second; if (nodeContext.IsBone) flagAllParentBones(context, nodeContext, true); } break; case TSkelFull: for (TNodeContextMap::iterator it(context.Nodes.begin()), end(context.Nodes.end()); it != end; ++it) { CNodeContext &nodeContext = it->second; if (nodeContext.IsBone) flagAllParentBones(context, nodeContext, true); } for (TNodeContextMap::iterator it(context.Nodes.begin()), end(context.Nodes.end()); it != end; ++it) { CNodeContext &nodeContext = it->second; if (nodeContext.IsBone) flagRecursiveBones(context, nodeContext, true); } break; } } void exportShapes(CMeshUtilsContext &context) { for (TNodeContextMap::iterator it(context.Nodes.begin()), end(context.Nodes.end()); it != end; ++it) { CNodeContext &nodeContext = it->second; if (nodeContext.Shape) { std::string shapePath = NLMISC::CPath::standardizePath(context.Settings.DestinationDirectoryPath, true) + it->first + ".shape"; context.ToolLogger.writeDepend(NLMISC::BUILD, shapePath.c_str(), "*"); NLMISC::COFile f; if (f.open(shapePath, false, false, true)) { try { NL3D::CShapeStream shapeStream(nodeContext.Shape); shapeStream.serial(f); f.close(); } catch (...) { tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Shape '%s' serialization failed!", it->first.c_str()); } } if (NL3D::CMesh *mesh = dynamic_cast(nodeContext.Shape.getPtr())) { for (uint mi = 0; mi < mesh->getNbMaterial(); ++mi) { NL3D::CMaterial &mat = mesh->getMaterial(mi); for (uint ti = 0; ti < NL3D::IDRV_MAT_MAXTEXTURES; ++ti) { if (NL3D::ITexture *itex = mat.getTexture(ti)) { if (NL3D::CTextureFile *tex = dynamic_cast(itex)) { std::string fileName = tex->getFileName(); std::string knownPath = NLMISC::CPath::lookup(fileName, false, false, false); if (!knownPath.empty()) { context.ToolLogger.writeDepend(NLMISC::RUNTIME, shapePath.c_str(), knownPath.c_str()); } else { // TODO: Move this warning into nelmeta serialization so it's shown before export tlwarning(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Texture '%s' referenced in material but not found in the database search paths", fileName.c_str()); } } } } } } } } } // TODO: Separate load scene and save scene functions int exportScene(const CMeshUtilsSettings &settings) { CMeshUtilsContext context(settings); NLMISC::CFile::createDirectoryTree(settings.DestinationDirectoryPath); if (!settings.ToolDependLog.empty()) context.ToolLogger.initDepend(settings.ToolDependLog); if (!settings.ToolErrorLog.empty()) context.ToolLogger.initError(settings.ToolErrorLog); context.ToolLogger.writeDepend(NLMISC::BUILD, "*", NLMISC::CPath::standardizePath(context.Settings.SourceFilePath, false).c_str()); // Base input file // Apply database configuration CDatabaseConfig::init(settings.SourceFilePath); CDatabaseConfig::initTextureSearchDirectories(); Assimp::Importer importer; const aiScene *scene = importer.ReadFile(settings.SourceFilePath, 0 | aiProcess_Triangulate | aiProcess_ValidateDataStructure | aiProcess_GenNormals // Or GenSmoothNormals? TODO: Validate smoothness between material boundaries! ); // aiProcess_SplitLargeMeshes | aiProcess_LimitBoneWeights if (!scene) { const char *errs = importer.GetErrorString(); if (errs) tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Assimp failed to load the scene: '%s'", errs); else tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), "Unable to load scene"); return EXIT_FAILURE; } // aiProcess_Triangulate // aiProcess_ValidateDataStructure: TODO: Catch Assimp error output stream // aiProcess_RemoveRedundantMaterials: Not used because we may override materials with NeL Material from meta // aiProcess_ImproveCacheLocality: TODO: Verify this does not modify vertex indices //scene->mRootNode->mMetaData context.InternalScene = scene; if (context.SceneMeta.load(context.Settings.SourceFilePath)) context.ToolLogger.writeDepend(NLMISC::BUILD, "*", context.SceneMeta.metaFilePath().c_str()); // Meta input file validateInternalNodeNames(context, context.InternalScene->mRootNode); // -- SKEL FLAG -- flagAssimpBones(context); flagMetaBones(context); flagExpandedBones(context); // TODO // [ // Only necessary in TSkelLocal // For each shape test if all the bones have the same root bones for their skeleton // 1) Iterate each until a different is found // 2) When a different root is found, connect the two to the nearest common bone // ] // -- SKEL FLAG -- // First import materials assimpMaterials(context); // Import shapes importShapes(context, context.InternalScene->mRootNode); // Export shapes exportShapes(context); return EXIT_SUCCESS; } /* end of file */