diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..3fd6967c7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +; Top-most EditorConfig file +root = true + +; 4-column tab indentation +[*.cpp] +indent_style = tab +indent_size = 4 + +[*.c] +indent_style = tab +indent_size = 4 + +[*.h] +indent_style = tab +indent_size = 4 + +[*.py] +indent_style = tab +indent_size = 4 diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 22686bba0..a1f3df8aa 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -147,6 +147,10 @@ IF(WITH_QT) FIND_PACKAGE(Qt4 COMPONENTS QtCore QtGui QtXml QtOpenGL REQUIRED) ENDIF(WITH_QT) +IF(WITH_ASSIMP) + FIND_PACKAGE(assimp REQUIRED) +ENDIF(WITH_ASSIMP) + IF(WITH_NEL) IF(WITH_NEL_TESTS) FIND_PACKAGE(CppTest) diff --git a/code/CMakeModules/Findassimp.cmake b/code/CMakeModules/Findassimp.cmake new file mode 100644 index 000000000..6748cd221 --- /dev/null +++ b/code/CMakeModules/Findassimp.cmake @@ -0,0 +1,25 @@ +FIND_PATH( + assimp_INCLUDE_DIRS + NAMES assimp/postprocess.h assimp/scene.h assimp/version.h assimp/config.h assimp/cimport.h + PATHS /usr/local/include/ +) + +FIND_LIBRARY( + assimp_LIBRARIES + NAMES assimp + PATHS /usr/local/lib/ +) + +IF (assimp_INCLUDE_DIRS AND assimp_LIBRARIES) + SET(assimp_FOUND TRUE) +ENDIF (assimp_INCLUDE_DIRS AND assimp_LIBRARIES) + +IF (assimp_FOUND) + IF (NOT assimp_FIND_QUIETLY) + MESSAGE(STATUS "Found asset importer library: ${assimp_LIBRARIES}") + ENDIF (NOT assimp_FIND_QUIETLY) +ELSE (assimp_FOUND) + IF (assimp_FIND_REQUIRED) + MESSAGE(FATAL_ERROR "Could not find asset importer library") + ENDIF (assimp_FIND_REQUIRED) +ENDIF (assimp_FOUND) diff --git a/code/CMakeModules/nel.cmake b/code/CMakeModules/nel.cmake index d02db8bd5..802458ca3 100644 --- a/code/CMakeModules/nel.cmake +++ b/code/CMakeModules/nel.cmake @@ -274,6 +274,8 @@ MACRO(NL_SETUP_DEFAULT_OPTIONS) OPTION(WITH_STATIC_EXTERNAL "With static external libraries" OFF) OPTION(WITH_INSTALL_LIBRARIES "Install development files." ON ) + OPTION(WITH_ASSIMP "Use assimp exporter" OFF) + ### # GUI toolkits ### diff --git a/code/nel/include/nel/misc/tool_logger.h b/code/nel/include/nel/misc/tool_logger.h new file mode 100644 index 000000000..3ef547045 --- /dev/null +++ b/code/nel/include/nel/misc/tool_logger.h @@ -0,0 +1,210 @@ +/** + * \file tool_logger.h + * \brief CToolLogger + * \date 2012-02-19 10:33GMT + * \author Jan Boon (Kaetemi) + * Tool logger is fully implemented in header so small tools do not + * need to link to this library unnecessarily. + * NOTE: Needs to be changed not to use time_nl and string_common. + */ + +/* + * Copyright (C) 2012 by authors + * + * This file is part of RYZOM CORE PIPELINE. + * RYZOM CORE PIPELINE is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 2 of + * the License, or (at your option) any later version. + * + * RYZOM CORE PIPELINE 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RYZOM CORE PIPELINE; see the file COPYING. If not, see + * . + */ + +#ifndef NLMISC_TOOL_LOGGER_H +#define NLMISC_TOOL_LOGGER_H +#include + +// STL includes +#include +#include +#include + +// NeL includes +#include +#include + +// Project includes + +#ifdef ERROR +#undef ERROR +#endif + +#ifdef NL_DEBUG_H +#define tlerror(toolLogger, path, error, ...) nlwarning(error, __VA_ARGS__), toolLogger.writeError(NLMISC::ERROR, path, error, __VA_ARGS__) +#define tlwarning(toolLogger, path, error, ...) nlwarning(error, __VA_ARGS__), toolLogger.writeError(NLMISC::WARNING, path, error, __VA_ARGS__) +#define tlmessage(toolLogger, path, error, ...) nlinfo(error, __VA_ARGS__), toolLogger.writeError(NLMISC::MESSAGE, path, error, __VA_ARGS__) +#else +#define tlerror(toolLogger, path, error, ...) toolLogger.writeError(NLMISC::ERROR, path, error, __VA_ARGS__) +#define tlwarning(toolLogger, path, error, ...) toolLogger.writeError(NLMISC::WARNING, path, error, __VA_ARGS__) +#define tlmessage(toolLogger, path, error, ...) toolLogger.writeError(NLMISC::MESSAGE, path, error, __VA_ARGS__) +#endif + +namespace NLMISC { + +enum TError +{ + ERROR, + WARNING, + MESSAGE, +}; + +enum TDepend +{ + BUILD, + DIRECTORY, + RUNTIME, +}; + +const std::string s_ErrorHeader = "type\tpath\ttime\terror"; +const std::string s_DependHeader = "type\toutput_file\tinput_file"; + +/** + * \brief CToolLogger + * \date 2012-02-19 10:33GMT + * \author Jan Boon (Kaetemi) + * CToolLogger + */ +class CToolLogger +{ +private: + FILE *m_ErrorLog; + FILE *m_DependLog; + +public: + inline CToolLogger() : m_ErrorLog(NULL), m_DependLog(NULL) + { + + } + + inline ~CToolLogger() + { + release(); + } + + inline void initError(const std::string &errorLog) + { + releaseError(); + + m_ErrorLog = fopen(errorLog.c_str(), "wt"); + fwrite(s_ErrorHeader.c_str(), 1, s_ErrorHeader.length(), m_ErrorLog); + fwrite("\n", 1, 1, m_ErrorLog); + fflush(m_ErrorLog); + + } + + inline void initDepend(const std::string &dependLog) + { + releaseDepend(); + + m_DependLog = fopen(dependLog.c_str(), "wt"); + fwrite(s_DependHeader.c_str(), 1, s_DependHeader.length(), m_DependLog); + fwrite("\n", 1, 1, m_DependLog); + // fflush(m_DependLog); + } + + inline void writeError(TError type, const char *path, const char *error, ...) + { + if (m_ErrorLog) + { + switch (type) + { + case ERROR: + fwrite("ERROR", 1, 5, m_ErrorLog); + break; + case WARNING: + fwrite("WARNING", 1, 7, m_ErrorLog); + break; + case MESSAGE: + fwrite("MESSAGE", 1, 7, m_ErrorLog); + break; + } + fwrite("\t", 1, 1, m_ErrorLog); + fprintf(m_ErrorLog, "%s", path); + fwrite("\t", 1, 1, m_ErrorLog); + std::string time = NLMISC::toString(NLMISC::CTime::getSecondsSince1970()); + fwrite(time.c_str(), 1, time.length(), m_ErrorLog); + fwrite("\t", 1, 1, m_ErrorLog); + va_list args; + va_start(args, error); + vfprintf(m_ErrorLog, error, args); + va_end(args); + fwrite("\n", 1, 1, m_ErrorLog); + fflush(m_ErrorLog); + } + } + + /// inputFile can only be file. [? May be not-yet-existing file for expected input for future build runs. ?] Directories are handled on process level. [? You should call this before calling writeError on inputFile, so the error is also linked from the outputFile. ?] + inline void writeDepend(TDepend type, const char *outputFile, const char *inputFile) + { + if (m_DependLog) + { + switch (type) + { + case BUILD: + fwrite("BUILD", 1, 5, m_DependLog); + break; + case DIRECTORY: + fwrite("DIRECTORY", 1, 9, m_DependLog); + break; + case RUNTIME: + fwrite("RUNTIME", 1, 7, m_DependLog); + break; + } + fwrite("\t", 1, 1, m_DependLog); + fprintf(m_DependLog, "%s", outputFile); + fwrite("\t", 1, 1, m_DependLog); + fprintf(m_DependLog, "%s", inputFile); + fwrite("\n", 1, 1, m_DependLog); + // fflush(m_DependLog); + } + } + + inline void releaseError() + { + if (m_ErrorLog) + { + fflush(m_ErrorLog); + fclose(m_ErrorLog); + m_ErrorLog = NULL; + } + } + + inline void releaseDepend() + { + if (m_DependLog) + { + fflush(m_DependLog); + fclose(m_DependLog); + m_DependLog = NULL; + } + } + + inline void release() + { + releaseError(); + releaseDepend(); + } +}; /* class CToolLogger */ + +} /* namespace NLMISC */ + +#endif /* #ifndef NLMISC_TOOL_LOGGER_H */ + +/* end of file */ diff --git a/code/nel/src/misc/tool_logger.cpp b/code/nel/src/misc/tool_logger.cpp new file mode 100644 index 000000000..e6a9fbf36 --- /dev/null +++ b/code/nel/src/misc/tool_logger.cpp @@ -0,0 +1,45 @@ +/** + * \file tool_logger.cpp + * \brief CToolLogger + * \date 2012-02-19 10:33GMT + * \author Jan Boon (Kaetemi) + * CToolLogger + */ + +/* + * Copyright (C) 2012 by authors + * + * This file is part of RYZOM CORE PIPELINE. + * RYZOM CORE PIPELINE is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 2 of + * the License, or (at your option) any later version. + * + * RYZOM CORE PIPELINE 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RYZOM CORE PIPELINE; see the file COPYING. If not, see + * . + */ + +#include "stdmisc.h" +#include "nel/misc/tool_logger.h" + +// STL includes + +// NeL includes +// #include + +// Project includes + +namespace NLMISC { + +// Tool logger is fully implemented in header so small tools do not need to link to this library unnecessarily. +void dummy_tool_logger_cpp() { } + +} /* namespace NLMISC */ + +/* end of file */ diff --git a/code/nel/tools/3d/CMakeLists.txt b/code/nel/tools/3d/CMakeLists.txt index cb709ffaa..37773e9ee 100644 --- a/code/nel/tools/3d/CMakeLists.txt +++ b/code/nel/tools/3d/CMakeLists.txt @@ -1,6 +1,11 @@ IF(WITH_NEL_TOOLS) IF(WITH_3D) + IF(WITH_ASSIMP) + SUBDIRS( + mesh_utils + mesh_export) + ENDIF() SUBDIRS( anim_builder animation_set_builder @@ -27,7 +32,6 @@ IF(WITH_NEL_TOOLS) zone_dump zviewer) ENDIF() - SUBDIRS( build_interface get_neighbors diff --git a/code/nel/tools/3d/mesh_export/CMakeLists.txt b/code/nel/tools/3d/mesh_export/CMakeLists.txt new file mode 100644 index 000000000..838ce49c2 --- /dev/null +++ b/code/nel/tools/3d/mesh_export/CMakeLists.txt @@ -0,0 +1,12 @@ +FILE(GLOB SRCS *.cpp) +FILE(GLOB HDRS *.h) + +SOURCE_GROUP("" FILES ${SRCS} ${HDRS}) + +ADD_EXECUTABLE(mesh_export ${SRCS} ${HDRS}) + +TARGET_LINK_LIBRARIES(mesh_export mesh_utils nel3d nelmisc) +NL_DEFAULT_PROPS(mesh_export "NeL, Tools, 3D: Mesh Export") +NL_ADD_RUNTIME_FLAGS(mesh_export) + +INSTALL(TARGETS mesh_export RUNTIME DESTINATION ${NL_BIN_PREFIX} COMPONENT tools3d) diff --git a/code/nel/tools/3d/mesh_export/main.cpp b/code/nel/tools/3d/mesh_export/main.cpp new file mode 100644 index 000000000..1503a4b16 --- /dev/null +++ b/code/nel/tools/3d/mesh_export/main.cpp @@ -0,0 +1,77 @@ +// 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/mesh_utils.h" + +#include +#include + +#include +#include + +int printHelp(const NLMISC::CCmdArgs &args) +{ + printf("NeL Mesh Export\n"); + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]) +{ + NLMISC::CApplicationContext app; + + NLMISC::CCmdArgs args; + args.setArgs(argc, (const char **)argv); + + if (args.getArgs().size() == 1 + || args.haveArg('h') + || args.haveLongArg("help")) + return printHelp(args); + + const NLMISC::CSString &filePath = args.getArgs().back(); + if (!NLMISC::CFile::fileExists(filePath)) + { + printHelp(args); + nlerror("File '%s' does not exist", filePath); + return EXIT_FAILURE; + } + + CMeshUtilsSettings settings; + settings.SourceFilePath = filePath; + + if (args.haveArg('d')) + settings.DestinationDirectoryPath = args.getArg('d'); + if (settings.DestinationDirectoryPath.empty()) + settings.DestinationDirectoryPath = args.getLongArg("dst"); + if (settings.DestinationDirectoryPath.empty()) + settings.DestinationDirectoryPath = filePath + "_export"; + settings.DestinationDirectoryPath += "/"; + + settings.ToolDependLog = args.getLongArg("dependlog"); + if (settings.ToolDependLog.empty()) + settings.ToolDependLog = settings.DestinationDirectoryPath + "depend.log"; + settings.ToolErrorLog = args.getLongArg("errorlog"); + if (settings.ToolErrorLog.empty()) + settings.ToolErrorLog = settings.DestinationDirectoryPath + "error.log"; + + NL3D::CScene::registerBasics(); + NL3D::registerSerial3d(); + + return exportScene(settings); +} + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/CMakeLists.txt b/code/nel/tools/3d/mesh_utils/CMakeLists.txt new file mode 100644 index 000000000..a5a9fa952 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/CMakeLists.txt @@ -0,0 +1,16 @@ +FILE(GLOB SRCS *.cpp) +FILE(GLOB HDRS *.h) + +SOURCE_GROUP("" FILES ${SRCS} ${HDRS}) + +INCLUDE_DIRECTORIES(${assimp_INCLUDE_DIRS}) + +NL_TARGET_LIB(mesh_utils ${SRCS} ${HDRS}) + +TARGET_LINK_LIBRARIES(mesh_utils ${assimp_LIBRARIES} nelmisc nel3d) +NL_DEFAULT_PROPS(mesh_utils "NeL, Tools, 3D: Mesh Utils") +NL_ADD_RUNTIME_FLAGS(mesh_utils) + +IF((WITH_INSTALL_LIBRARIES AND WITH_STATIC) OR NOT WITH_STATIC) + INSTALL(TARGETS mesh_utils LIBRARY DESTINATION ${NL_LIB_PREFIX} ARCHIVE DESTINATION ${NL_LIB_PREFIX} COMPONENT tools3d) +ENDIF((WITH_INSTALL_LIBRARIES AND WITH_STATIC) OR NOT WITH_STATIC) diff --git a/code/nel/tools/3d/mesh_utils/assimp_material.cpp b/code/nel/tools/3d/mesh_utils/assimp_material.cpp new file mode 100644 index 000000000..5dbe8643c --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/assimp_material.cpp @@ -0,0 +1,228 @@ +// 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 "assimp_shape.h" + +#include +#include +#include + +#define NL_NODE_INTERNAL_TYPE aiNode +#define NL_SCENE_INTERNAL_TYPE aiScene +#include "scene_context.h" + +#include +#include +#include + +#include +#include + +using namespace std; +using namespace NLMISC; +using namespace NL3D; + +// http://assimp.sourceforge.net/lib_html/materials.html + +inline CRGBA convColor(const aiColor3D &ac, uint8 a = 255) +{ + return CRGBA(ac.r * 255.99f, ac.g * 255.99f, ac.b * 255.99f); +} + +inline CRGBA convColor(const aiColor4D &ac) +{ + return CRGBA(ac.r * 255.99f, ac.g * 255.99f, ac.b * 255.99f, ac.a * 255.99f); +} + +void assimpMaterial(NL3D::CMaterial &mat, CMeshUtilsContext &context, const aiMaterial *am) +{ + aiString amname; + if (am->Get(AI_MATKEY_NAME, amname) != aiReturn_SUCCESS) + amname = ""; + + mat.initLighted(); + mat.setShader(CMaterial::Normal); + + int i; + float f; + aiColor3D c3; + aiColor4D c4; + + if (am->Get(AI_MATKEY_TWOSIDED, i) == aiReturn_SUCCESS) + mat.setDoubleSided(i != 0); + + if (am->Get(AI_MATKEY_BLEND_FUNC, i) == aiReturn_SUCCESS) switch ((aiBlendMode)i) + { + case aiBlendMode_Default: + mat.setSrcBlend(CMaterial::srcalpha); + mat.setDstBlend(CMaterial::invsrcalpha); + break; + case aiBlendMode_Additive: + mat.setSrcBlend(CMaterial::one); + mat.setDstBlend(CMaterial::one); + break; + } + + // Colors follow GL convention + // "While the ambient, diffuse, specular and emission + // "material parameters all have alpha components, only the diffuse" + // "alpha component is used in the lighting computation." + if (am->Get(AI_MATKEY_COLOR_DIFFUSE, c3) == aiReturn_SUCCESS) + mat.setDiffuse(convColor(c3)); + + if (am->Get(AI_MATKEY_OPACITY, f) == aiReturn_SUCCESS) + mat.setOpacity(f * 255.99f); + + if (am->Get(AI_MATKEY_COLOR_AMBIENT, c3) == aiReturn_SUCCESS) + mat.setAmbient(convColor(c3)); + + if (am->Get(AI_MATKEY_SHININESS, f) == aiReturn_SUCCESS) + mat.setShininess(f); // (float)pow(2.0, f * 10.0) * 4.f; + + if (am->Get(AI_MATKEY_COLOR_SPECULAR, c3) == aiReturn_SUCCESS) + mat.setSpecular(convColor(c3)); + + if (am->Get(AI_MATKEY_SHININESS_STRENGTH, f) == aiReturn_SUCCESS) + mat.setSpecular(CRGBAF(mat.getSpecular()) * f); + else + mat.setSpecular(NLMISC::CRGBA::Black); + + if (am->Get(AI_MATKEY_COLOR_EMISSIVE, c3) == aiReturn_SUCCESS) + mat.setEmissive(convColor(c3)); + + // Textures + unsigned int texCount = am->GetTextureCount(aiTextureType_DIFFUSE); + if (texCount > IDRV_MAT_MAXTEXTURES) + { + tlwarning(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Material '%s' has more than %i textures (%i textures found)", amname.C_Str(), IDRV_MAT_MAXTEXTURES, texCount); + texCount = IDRV_MAT_MAXTEXTURES; + } + + for (unsigned int ti = 0; ti < texCount; ++ti) + { + aiString path; + aiTextureMapping mapping; + unsigned int uvindex; + float blend; // Partially supported + aiTextureOp op; + aiTextureMapMode mapmode; + if (am->GetTexture(aiTextureType_DIFFUSE, ti, &path, &mapping, &uvindex, &blend, &op, &mapmode) != aiReturn_SUCCESS) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Failed to get texture %i in material '%s'", ti, amname.C_Str()); + break; + } + + std::string fileName = CFile::getFilename(CPath::standardizePath(path.C_Str(), false)); + std::string knownPath = CPath::lookup(fileName, false, false, false); + if (knownPath.empty()) + { + tlwarning(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Texture '%s' referenced in material '%s' but not found in the database search paths", fileName.c_str(), amname.C_Str()); + } + + // NeL supports bitmap and cubemap, but we import only basic bitmap here. Cubemap can be inserted from the mesh editor tool + // NeL also has fancy multi-bitmap thing to switch between summer and winter and so on. Same story + CSmartPtr tex = new CTextureFile(); + tex->setFileName(fileName); + tex->setWrapS(mapmode == aiTextureMapMode_Clamp ? ITexture::Clamp : ITexture::Repeat); + tex->setWrapT(mapmode == aiTextureMapMode_Clamp ? ITexture::Clamp : ITexture::Repeat); + mat.setTexture(ti, tex); + + // TODO uvindex for uv routing (probably necessary during shape import - if so also need to also ask the uv channel in the editor and store in meta) + + // TODO aiTextureMapping texcoordgen if useful to import + + mat.texEnvArg0Alpha(ti, CMaterial::Texture, CMaterial::SrcAlpha); + mat.texEnvArg0RGB(ti, CMaterial::Texture, CMaterial::SrcColor); + mat.texEnvArg1Alpha(ti, ti == 0 ? CMaterial::Diffuse : CMaterial::Previous, CMaterial::SrcAlpha); + mat.texEnvArg1RGB(ti, ti == 0 ? CMaterial::Diffuse : CMaterial::Previous, CMaterial::SrcColor); + switch (op) + { + case aiTextureOp_Multiply: + default: + mat.texEnvOpAlpha(ti, CMaterial::Modulate); + mat.texEnvOpRGB(ti, CMaterial::Modulate); + break; + case aiTextureOp_Add: + mat.texEnvOpAlpha(ti, CMaterial::Add); + mat.texEnvOpRGB(ti, CMaterial::Add); + break; + case aiTextureOp_Subtract: + mat.texEnvArg0Alpha(ti, CMaterial::Texture, CMaterial::InvSrcAlpha); + mat.texEnvArg0RGB(ti, CMaterial::Texture, CMaterial::InvSrcColor); + mat.texEnvOpAlpha(ti, CMaterial::Add); + mat.texEnvOpRGB(ti, CMaterial::Add); + break; + case aiTextureOp_SignedAdd: + mat.texEnvOpAlpha(ti, CMaterial::AddSigned); + mat.texEnvOpRGB(ti, CMaterial::AddSigned); + break; + } + } +} + +CSmartPtr assimpMaterial(CMeshUtilsContext &context, const aiMaterial *am) +{ + CSmartPtr matp = new CMaterial(); + CMaterial &mat = *matp; + assimpMaterial(mat, context, am); + return matp; +} + +void assimpMaterials(CMeshUtilsContext &context) +{ + set materialNames; + + const aiScene *scene = context.InternalScene; + for (unsigned int mi = 0; mi < scene->mNumMaterials; ++mi) + { + const aiMaterial *am = scene->mMaterials[mi]; + + for (unsigned int pi = 0; pi < am->mNumProperties; ++pi) // DEBUG + { // DEBUG + const aiMaterialProperty *amp = am->mProperties[pi]; + printf("%s\n", amp->mKey.C_Str()); + } // DEBUG + + aiString amname; + if (am->Get(AI_MATKEY_NAME, amname) != aiReturn_SUCCESS) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Material has no name"); + continue; + } + + if (materialNames.find(amname.C_Str()) != materialNames.end()) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Material name '%s' used more than once", amname.C_Str()); + continue; + } + + if (context.SceneMeta.Materials.find(amname.C_Str()) + == context.SceneMeta.Materials.end()) + { + materialNames.insert(amname.C_Str()); + context.SceneMeta.Materials[amname.C_Str()] = assimpMaterial(context, am); + } + } +} + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/assimp_material.h b/code/nel/tools/3d/mesh_utils/assimp_material.h new file mode 100644 index 000000000..062ca95bb --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/assimp_material.h @@ -0,0 +1,28 @@ +// 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 + +namespace NL3D { + class CMaterial; +} + +struct CMeshUtilsContext; +void assimpMaterial(NL3D::CMaterial &mat, CMeshUtilsContext &context, const aiMaterial *am); +void assimpMaterials(CMeshUtilsContext &context); + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/assimp_shape.cpp b/code/nel/tools/3d/mesh_utils/assimp_shape.cpp new file mode 100644 index 000000000..7b404d1e2 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/assimp_shape.cpp @@ -0,0 +1,379 @@ +// 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 "assimp_shape.h" + +#include +#include +#include + +#define NL_NODE_INTERNAL_TYPE aiNode +#define NL_SCENE_INTERNAL_TYPE aiScene +#include "scene_context.h" + +#include +#include +#include + +#include + +#include "assimp_material.h" + +using namespace std; +using namespace NLMISC; +using namespace NL3D; + +// TODO: buildParticleSystem ?? +// TODO: buildWaveMakerShape ?? +// TODO: buildRemanence ?? +// TODO: buildFlare ?? +// Probably specific settings we can only do in meta editor on a dummy node.. +// TODO: pacs prim + +// TODO: buildWaterShape specifics when node has water material + +// TODO: CMeshMultiLod::CMeshMultiLodBuild multiLodBuild; export_mesh.cpp ln 228 +// TODO: LOD MRM + +// TODO: Skinned - reverse transform by skeleton root bone to align? + +/*inline CMatrix convMatrix(const aiMatrix4x4 &tf) +{ + CMatrix m; + for (int i = 0; i < 16; ++i) + m.set(&tf.a1); + return m; +}*/ + +inline CVector convVector(const aiVector3D &av) +{ + return CVector(av.x, av.y, av.z); // COORDINATE CONVERSION +} + +inline CRGBA convColor(const aiColor4D &ac) +{ + return CRGBA(ac.r * 255.99f, ac.g * 255.99f, ac.b * 255.99f, ac.a * 255.99f); +} + +inline CUVW convUvw(const aiVector3D &av) +{ + return CUVW(av.x, -av.y, av.z); // UH OH COORDINATE CONVERSION ?! ONLY FOR TEXTURES !! +} + +inline CQuat convQuat(const aiQuaternion &aq) +{ + return CQuat(aq.x, aq.y, aq.z, aq.w); +} + +void assimpBuildBaseMesh(CMeshBase::CMeshBaseBuild &buildBaseMesh, CMeshUtilsContext &context, CNodeContext &nodeContext) +{ + const aiNode *node = nodeContext.InternalNode; + // Reference CExportNel::buildBaseMeshInterface + + // Load materials + buildBaseMesh.Materials.resize(node->mNumMeshes); + + for (unsigned int mi = 0; mi < node->mNumMeshes; ++mi) + { + const aiMesh *mesh = context.InternalScene->mMeshes[node->mMeshes[mi]]; + const aiMaterial *am = context.InternalScene->mMaterials[mesh->mMaterialIndex]; + + aiString amname; + if (am->Get(AI_MATKEY_NAME, amname) != aiReturn_SUCCESS) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Material used by node '%s' has no name", node->mName.C_Str()); // TODO: Maybe autogen names by index in mesh or node if this is actually a thing + assimpMaterial(buildBaseMesh.Materials[mi], context, am); + } + else + { + buildBaseMesh.Materials[mi] = *context.SceneMeta.Materials[amname.C_Str()]; + } + } + + // Positioning + const aiMatrix4x4 &root = context.InternalScene->mRootNode->mTransformation; + const aiMatrix4x4 &tf = nodeContext.InternalNode->mTransformation; // COORDINATE CONVERSION HERE INSTEAD OF PER VERTEX ?? + aiVector3D scaling; + aiQuaternion rotation; + aiVector3D position; + tf.Decompose(scaling, rotation, position); + buildBaseMesh.DefaultScale = convVector(scaling); + buildBaseMesh.DefaultRotQuat = convQuat(rotation); + buildBaseMesh.DefaultRotEuler = CVector(0, 0, 0); + buildBaseMesh.DefaultPivot = CVector(0, 0, 0); + buildBaseMesh.DefaultPos = convVector(position); + if (buildBaseMesh.DefaultScale.x != 1.0f || buildBaseMesh.DefaultScale.y != 1.0f || buildBaseMesh.DefaultScale.z != 1.0f) + { + tlmessage(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Node '%s' has a scaled transformation. This may be a mistake", node->mName.C_Str()); + } + + // Meta + // dst.CollisionMeshGeneration = src.CollisionMeshGeneration; + + // TODO: Morph +} + +bool assimpBuildMesh(CMesh::CMeshBuild &buildMesh, CMeshBase::CMeshBaseBuild &buildBaseMesh, CMeshUtilsContext &context, CNodeContext &nodeContext) +{ + // TODO + // *** If the mesh is skined, vertices will be exported in world space. + // *** If the mesh is not skined, vertices will be exported in offset space. + + // TODO Support skinning + + const aiNode *node = nodeContext.InternalNode; + nlassert(node->mNumMeshes); + + // Basic validations before processing starts + for (unsigned int mi = 0; mi < node->mNumMeshes; ++mi) + { + // TODO: Maybe needs to be the same count too for all meshes, so compare with mesh 0 + const aiMesh *mesh = context.InternalScene->mMeshes[node->mMeshes[mi]]; + if (mesh->GetNumColorChannels() > 2) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "(%s) mesh->GetNumColorChannels() > 2", node->mName.C_Str()); + return false; + } + if (mesh->GetNumUVChannels() > CVertexBuffer::MaxStage) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "(%s) mesh->GetNumUVChannels() > CVertexBuffer::MaxStage", node->mName.C_Str()); + return false; + } + if (!mesh->HasNormals()) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "(%s) !mesh->HasNormals()", node->mName.C_Str()); + return false; + } + } + + // Default vertex flags + buildMesh.VertexFlags = CVertexBuffer::PositionFlag | CVertexBuffer::NormalFlag; + + // TODO: UV Channels routing to correct texture stage + for (uint i = 0; i < CVertexBuffer::MaxStage; ++i) + buildMesh.UVRouting[i] = i; + + // Meshes in assimp are separated per material, so we need to re-merge them for the mesh build process + // This process also deduplicates vertices + bool cleanupMesh = true; + sint32 numVertices = 0; + for (unsigned int mi = 0; mi < node->mNumMeshes; ++mi) + numVertices += context.InternalScene->mMeshes[node->mMeshes[mi]]->mNumVertices; + buildMesh.Vertices.resize(numVertices); + numVertices = 0; + map vertexIdentifiers; + vector > vertexRemapping; + vertexRemapping.resize(node->mNumMeshes); + for (unsigned int mi = 0; mi < node->mNumMeshes; ++mi) + { + const aiMesh *mesh = context.InternalScene->mMeshes[node->mMeshes[mi]]; + vertexRemapping[mi].resize(mesh->mNumVertices); + for (unsigned int vi = 0; vi < mesh->mNumVertices; ++vi) + { + CVector vec = convVector(mesh->mVertices[vi]); + map::iterator vecit = vertexIdentifiers.find(vec); + if (vecit == vertexIdentifiers.end()) + { + buildMesh.Vertices[numVertices] = vec; + if (cleanupMesh) vertexIdentifiers[vec] = numVertices; // Don't remap if we don't wan't to lose vertex indices + vertexRemapping[mi][vi] = numVertices; + ++numVertices; + } + else + { + vertexRemapping[mi][vi] = vecit->second; + } + } + } + buildMesh.Vertices.resize(numVertices); + + // Process all faces + // WONT IMPLEMENT: Radial faces generation... is linked to smoothing group... + // leave radial normals generation to modeling tool for now... + sint32 numFaces = 0; + for (unsigned int mi = 0; mi < node->mNumMeshes; ++mi) + numFaces += context.InternalScene->mMeshes[node->mMeshes[mi]]->mNumFaces; + buildMesh.Faces.resize(numFaces); + numFaces = 0; + unsigned int refNumColorChannels = context.InternalScene->mMeshes[node->mMeshes[0]]->GetNumColorChannels(); + unsigned int refNumUVChannels = context.InternalScene->mMeshes[node->mMeshes[0]]->GetNumUVChannels(); + for (unsigned int mi = 0; mi < node->mNumMeshes; ++mi) + { + const aiMesh *mesh = context.InternalScene->mMeshes[node->mMeshes[mi]]; + + // Get channel numbers + unsigned int numColorChannels = mesh->GetNumColorChannels(); + if (numColorChannels > 2) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Shape '%s' has too many color channels in mesh %i (%i channels found)", node->mName.C_Str(), mi, numColorChannels); + } + if (numColorChannels > 0) + { + buildMesh.VertexFlags |= CVertexBuffer::PrimaryColorFlag; + if (numColorChannels > 1) + { + buildMesh.VertexFlags |= CVertexBuffer::SecondaryColorFlag; + } + } + unsigned int numUVChannels = mesh->GetNumUVChannels(); + if (numUVChannels > CVertexBuffer::MaxStage) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Shape '%s' has too many uv channels in mesh %i (%i channels found)", node->mName.C_Str(), mi, numUVChannels); + numUVChannels = CVertexBuffer::MaxStage; + } + for (unsigned int ui = 0; ui < numUVChannels; ++ui) + buildMesh.VertexFlags |= (CVertexBuffer::TexCoord0Flag << ui); // TODO: Coord UV tex stage rerouting + + // TODO: Channels do in fact differ between submeshes, so we need to correctly recount and reroute the materials properly + if (numColorChannels != refNumColorChannels) + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Shape '%s' mismatch of nb color channel in mesh '%i', please contact developer", node->mName.C_Str(), mi); + if (numUVChannels != refNumUVChannels) + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Shape '%s' mismatch of nb uv channel in mesh '%i', please contact developer", node->mName.C_Str(), mi); + + for (unsigned int fi = 0; fi < mesh->mNumFaces; ++fi) + { + const aiFace &af = mesh->mFaces[fi]; + if (af.mNumIndices != 3) + { + tlerror(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "(%s) Face %i on mesh %i has %i faces", node->mName.C_Str(), fi, mi, af.mNumIndices); + continue; // return false; Keep going, just drop the face for better user experience + } + if (cleanupMesh) + { + if (vertexRemapping[mi][af.mIndices[0]] == vertexRemapping[mi][af.mIndices[1]] + || vertexRemapping[mi][af.mIndices[1]] == vertexRemapping[mi][af.mIndices[2]] + || vertexRemapping[mi][af.mIndices[2]] == vertexRemapping[mi][af.mIndices[0]]) + continue; // Not a triangle + } + CMesh::CFace &face = buildMesh.Faces[numFaces]; + face.MaterialId = mi; + face.SmoothGroup = 0; // No smoothing groups (bitfield) + face.Corner[0].Vertex = vertexRemapping[mi][af.mIndices[0]]; + face.Corner[1].Vertex = vertexRemapping[mi][af.mIndices[1]]; + face.Corner[2].Vertex = vertexRemapping[mi][af.mIndices[2]]; + face.Corner[0].Normal = convVector(mesh->mNormals[af.mIndices[0]]); + face.Corner[1].Normal = convVector(mesh->mNormals[af.mIndices[1]]); + face.Corner[2].Normal = convVector(mesh->mNormals[af.mIndices[2]]); + // TODO: If we want normal maps, we need to add tangent vectors to CFace and build process + // UV channels + for (unsigned int ui = 0; ui < numUVChannels; ++ui) // TODO: UV Rerouting + { + face.Corner[0].Uvws[ui] = convUvw(mesh->mTextureCoords[ui][af.mIndices[0]]); + face.Corner[1].Uvws[ui] = convUvw(mesh->mTextureCoords[ui][af.mIndices[1]]); + face.Corner[2].Uvws[ui] = convUvw(mesh->mTextureCoords[ui][af.mIndices[2]]); + } + for (unsigned int ui = numUVChannels; ui < CVertexBuffer::MaxStage; ++ui) + { + face.Corner[0].Uvws[ui] = CUVW(0, 0, 0); + face.Corner[1].Uvws[ui] = CUVW(0, 0, 0); + face.Corner[2].Uvws[ui] = CUVW(0, 0, 0); + } + // Primary and secondary color channels + if (numColorChannels > 0) // TODO: Verify + { + face.Corner[0].Color = convColor(mesh->mColors[0][af.mIndices[0]]); + face.Corner[1].Color = convColor(mesh->mColors[0][af.mIndices[1]]); + face.Corner[2].Color = convColor(mesh->mColors[0][af.mIndices[2]]); + } + else + { + face.Corner[0].Color = CRGBA(255, 255, 255, 255); + face.Corner[1].Color = CRGBA(255, 255, 255, 255); + face.Corner[2].Color = CRGBA(255, 255, 255, 255); + } + if (numColorChannels > 1) // TODO: Verify + { + face.Corner[0].Specular = convColor(mesh->mColors[1][af.mIndices[0]]); + face.Corner[1].Specular = convColor(mesh->mColors[1][af.mIndices[1]]); + face.Corner[2].Specular = convColor(mesh->mColors[1][af.mIndices[2]]); + } + else + { + face.Corner[0].Specular = CRGBA(255, 255, 255, 255); + face.Corner[1].Specular = CRGBA(255, 255, 255, 255); + face.Corner[2].Specular = CRGBA(255, 255, 255, 255); + } + // TODO: Color modulate, alpha, use color alpha for vp tree, etc + ++numFaces; + } + } + if (numFaces != buildMesh.Faces.size()) + { + tlmessage(context.ToolLogger, context.Settings.SourceFilePath.c_str(), + "Removed %u degenerate faces in shape '%s'", (uint32)(buildMesh.Faces.size() - numFaces), node->mName.C_Str()); + buildMesh.Faces.resize(numFaces); + } + + // clear for MRM info + buildMesh.Interfaces.clear(); + buildMesh.InterfaceLinks.clear(); + + // TODO: Export VP + buildMesh.MeshVertexProgram = NULL; + + return true; +} + +bool assimpShape(CMeshUtilsContext &context, CNodeContext &nodeContext) +{ + // Reference: export_mesh.cpp, buildShape + nodeContext.Shape = NULL; + + const aiNode *node = nodeContext.InternalNode; + nlassert(node->mNumMeshes); + + // Fill the build interface of CMesh + CMeshBase::CMeshBaseBuild buildBaseMesh; + assimpBuildBaseMesh(buildBaseMesh, context, nodeContext); + + CMesh::CMeshBuild buildMesh; + if (!assimpBuildMesh(buildMesh, buildBaseMesh, context, nodeContext)) + return false; + + // Make a CMesh object + CMesh *mesh = new CMesh(); + + // Build the mesh with the build interface + mesh->build(buildBaseMesh, buildMesh); + + // TODO + // Reference: export_mesh.cpp, buildShape + // Must be done after the build to update vertex links + // Pass to buildMeshMorph if the original mesh is skinned or not + // buildMeshMorph(buildMesh, node, time, nodeMap != NULL); + // mesh->setBlendShapes(buildMesh.BlendShapes); + + // optimize number of material + // mesh->optimizeMaterialUsage(materialRemap); + + // Store mesh in context + nodeContext.Shape = mesh; + return true; +} + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/assimp_shape.h b/code/nel/tools/3d/mesh_utils/assimp_shape.h new file mode 100644 index 000000000..b4bdc2490 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/assimp_shape.h @@ -0,0 +1,25 @@ +// 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 + +struct CMeshUtilsContext; +struct CNodeContext; + +bool assimpShape(CMeshUtilsContext &context, CNodeContext &nodeContext); + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/database_config.cpp b/code/nel/tools/3d/mesh_utils/database_config.cpp new file mode 100644 index 000000000..7e6ea5b37 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/database_config.cpp @@ -0,0 +1,107 @@ +// 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 "database_config.h" + +#include +#include +#include + +using namespace std; +using namespace NLMISC; + +TPathString CDatabaseConfig::s_RootPath; +NLMISC::CConfigFile *CDatabaseConfig::s_ConfigFile = NULL; +CDatabaseConfig CDatabaseConfig::s_Instance; +uint32 CDatabaseConfig::s_ConfigFileModification; + +static std::set s_SearchPaths; + +void CDatabaseConfig::cleanup() +{ + delete CDatabaseConfig::s_ConfigFile; + CDatabaseConfig::s_ConfigFile = NULL; +} + +CDatabaseConfig::~CDatabaseConfig() +{ + cleanup(); +} + +bool CDatabaseConfig::init(const std::string &asset) +{ + // release(); + + TPathString rootPath = NLMISC::CPath::standardizePath(asset, false); + TPathString configPath = rootPath + "/database.cfg"; + while (!CFile::fileExists(configPath)) + { + int sep = CFile::getLastSeparator(rootPath); + if (sep == string::npos) + return false; + + rootPath = rootPath.substr(0, sep); + if (rootPath.empty()) + return false; + + configPath = rootPath + "/database.cfg"; + } + + rootPath += "/"; + uint32 configFileModification = CFile::getFileModificationDate(configPath); + if (rootPath == s_RootPath && s_ConfigFileModification == configFileModification) + return true; // Do not reload + + nldebug("Initializing database config '%s'", configPath.c_str()); + release(); + + s_RootPath = rootPath; + s_ConfigFileModification = configFileModification; + + s_ConfigFile = new CConfigFile(); + s_ConfigFile->load(configPath); + return true; +} + +void CDatabaseConfig::initTextureSearchDirectories() +{ + searchDirectories("TextureSearchDirectories"); +} + +void CDatabaseConfig::searchDirectories(const char *var) +{ + CConfigFile::CVar &paths = s_ConfigFile->getVar(var); + for (uint i = 0; i < paths.size(); i++) + { + TPathString path = paths.asString(i); + if (s_SearchPaths.find(path) == s_SearchPaths.end()) + { + CPath::addSearchPath(s_RootPath + path); + s_SearchPaths.insert(path); + } + } +} + +void CDatabaseConfig::release() +{ + s_SearchPaths.clear(); + CPath::clearMap(); + cleanup(); +} + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/database_config.h b/code/nel/tools/3d/mesh_utils/database_config.h new file mode 100644 index 000000000..25bcc2a00 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/database_config.h @@ -0,0 +1,58 @@ +// 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 + +namespace NLMISC { + class CConfigFile; +} + +#ifdef NL_OS_WINDOWS +#include +typedef NLMISC::CSString TPathString; +#else +typedef std::string TPathString; +#endif + +/// Asset database configuration +class CDatabaseConfig +{ +public: + ~CDatabaseConfig(); + + /// Searches for the configuration for the specified asset path by recursively going through all parent directories looking for 'database.cfg', initializes and applies the configuration. + static bool init(const std::string &asset); + static void release(); + + static void initTextureSearchDirectories(); + + static inline const TPathString &rootPath() { return s_RootPath; } + static inline TPathString configPath() { return s_RootPath + "/database.cfg"; } + +private: + static void cleanup(); + static void searchDirectories(const char *var); + + static CDatabaseConfig s_Instance; + static uint32 s_ConfigFileModification; + + static TPathString s_RootPath; + static NLMISC::CConfigFile *s_ConfigFile; + +}; + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/mesh_utils.cpp b/code/nel/tools/3d/mesh_utils/mesh_utils.cpp new file mode 100644 index 000000000..2ea3800a7 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/mesh_utils.cpp @@ -0,0 +1,348 @@ +// 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::CMeshBase *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 */ diff --git a/code/nel/tools/3d/mesh_utils/mesh_utils.h b/code/nel/tools/3d/mesh_utils/mesh_utils.h new file mode 100644 index 000000000..18be11e60 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/mesh_utils.h @@ -0,0 +1,44 @@ +// 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 . + +#ifndef NL_MESH_UTILS_H +#define NL_MESH_UTILS_H +#include + +#include + +struct CMeshUtilsSettings +{ + CMeshUtilsSettings(); + + // Absolute Paths + std::string SourceFilePath; + std::string DestinationDirectoryPath; + std::string ToolDependLog; + std::string ToolErrorLog; + + // Relative Directories + /*std::string ShapeDirectory; + std::string IGDirectory; + std::string SkelDirectory;*/ +}; + +int exportScene(const CMeshUtilsSettings &settings); + +#endif /* NL_MESH_UTILS_H */ + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/scene_context.cpp b/code/nel/tools/3d/mesh_utils/scene_context.cpp new file mode 100644 index 000000000..6812a312a --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/scene_context.cpp @@ -0,0 +1,30 @@ +// 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 "scene_context.h" + +#include +#include +#include + +using namespace std; +using namespace NLMISC; + +void dummy_scene_context_cpp(); + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/scene_context.h b/code/nel/tools/3d/mesh_utils/scene_context.h new file mode 100644 index 000000000..714e3ff54 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/scene_context.h @@ -0,0 +1,81 @@ +// 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 . + +#ifndef NL_SCENE_CONTEXT_H +#define NL_SCENE_CONTEXT_H +#include + +#include "mesh_utils.h" +#include "scene_meta.h" + +#include +#include +#include +#include + +#include + +#ifndef NL_NODE_INTERNAL_TYPE +#define NL_NODE_INTERNAL_TYPE void +#endif +#ifndef NL_SCENE_INTERNAL_TYPE +#define NL_SCENE_INTERNAL_TYPE void +#endif + +namespace NL3D { + class IShape; + class CMaterial; +} + +struct CNodeContext +{ + CNodeContext() : + InternalNode(NULL), + IsBone(false) + { + + } + + const NL_NODE_INTERNAL_TYPE *InternalNode; + bool IsBone; + + // NLMISC::CMatrix Transform; // TODO + NLMISC::CSmartPtr Shape; +}; + +typedef std::map TNodeContextMap; +struct CMeshUtilsContext +{ + CMeshUtilsContext(const CMeshUtilsSettings &settings) : Settings(settings), InternalScene(NULL) + { + + } + + const CMeshUtilsSettings &Settings; + + NLMISC::CToolLogger ToolLogger; + + const NL_SCENE_INTERNAL_TYPE *InternalScene; + CSceneMeta SceneMeta; + + TNodeContextMap Nodes; // Impl note: Should never end up containing the scene root node. + // std::map MeshNames; // Maps meshes to a node name ********************* todo *************** +}; + +#endif /* NL_SCENE_CONTEXT_H */ + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/scene_meta.cpp b/code/nel/tools/3d/mesh_utils/scene_meta.cpp new file mode 100644 index 000000000..dd7ea0686 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/scene_meta.cpp @@ -0,0 +1,100 @@ +// 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 "scene_meta.h" + +#include +#include +#include + +#include + +using namespace std; +using namespace NLMISC; + +CNodeMeta::CNodeMeta() : + AddToIG(true), + ExportMesh(TMeshShape), + ExportBone(TBoneAuto), + AutoAnim(false) +{ + +} + +void CNodeMeta::serial(NLMISC::IStream &s) +{ + uint version = s.serialVersion(1); + s.serial(AddToIG); + s.serial((uint32 &)ExportMesh); + s.serial((uint32 &)ExportBone); + s.serial(InstanceShape); + s.serial(InstanceName); + s.serial(InstanceGroupName); + s.serial(AutoAnim); +} + +CSceneMeta::CSceneMeta() : + ImportShape(true), + ImportSkel(true), + ImportAnim(true), + ImportCmb(true), + ImportIG(true), + ExportDefaultIG(false), + SkeletonMode(TSkelRoot) +{ + +} + +bool CSceneMeta::load(const std::string &filePath) +{ + m_MetaFilePath = NLMISC::CPath::standardizePath(filePath + ".nelmeta", false); + if (CFile::fileExists(m_MetaFilePath)) + { + CIFile f(m_MetaFilePath); + serial(f); + f.close(); + return true; + } + return false; +} + +void CSceneMeta::save() +{ + COFile f(m_MetaFilePath, false, false, true); + serial(f); + f.close(); +} + +void CSceneMeta::serial(NLMISC::IStream &s) +{ + uint version = s.serialVersion(1); + + s.serial(ImportShape); + s.serial(ImportSkel); + s.serial(ImportAnim); + s.serial(ImportCmb); + s.serial(ImportIG); + + s.serial(ExportDefaultIG); + s.serial((uint32 &)SkeletonMode); + + s.serialCont(Nodes); + s.serialPtrCont(Materials); +} + +/* end of file */ diff --git a/code/nel/tools/3d/mesh_utils/scene_meta.h b/code/nel/tools/3d/mesh_utils/scene_meta.h new file mode 100644 index 000000000..62c185ee4 --- /dev/null +++ b/code/nel/tools/3d/mesh_utils/scene_meta.h @@ -0,0 +1,108 @@ +// 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 . + +#ifndef NL_SCENE_META_H +#define NL_SCENE_META_H +#include + +#include +#include + +#include + +namespace NLMISC { + class IStream; +} + +namespace NL3D { + class CMaterial; +} + +enum TMesh +{ + TMeshDisabled = 0, + TMeshShape = 1, + TMeshCollisionInt = 2, + TMeshCollisionExt = 3, + TMeshZone = 4, + TMeshPortal = 5, + TMeshCluster = 6, +}; + +enum TBone +{ + TBoneAuto = 0, + TBoneForce = 1, // Force this node to be part of a skeleton + TBoneRoot = 2, // Make this node the skeleton root, it will be exported using the scene name. There can only be one (editor should keep track and disable) +}; + +struct CNodeMeta +{ + CNodeMeta(); + + bool AddToIG; // Add this node to an instance group + TMesh ExportMesh; + TBone ExportBone; + + std::string InstanceShape; + std::string InstanceName; + std::string InstanceGroupName; + + bool AutoAnim; + // std::vector Materials; // In case there's an issue with nameless materials in some format... Map to material entirely in the meta editor. + + void serial(NLMISC::IStream &s); +}; + +enum TSkel +{ + TSkelLocal = 0, // Export smallest skeleton possible from connected bones + TSkelRoot = 1, // Export skeleton from a direct child node in the scene root node + TSkelFull = 2, // Include all connected child nodes in the skeleton +}; + +typedef std::map > TMaterialMap; +struct CSceneMeta +{ + CSceneMeta(); + + bool ImportShape; + bool ImportSkel; + bool ImportAnim; + bool ImportCmb; + bool ImportIG; + + bool ExportDefaultIG; // Export a default instance group from nodes the scene that do not have an instance group set + TSkel SkeletonMode; + + std::map Nodes; + TMaterialMap Materials; + + const std::string &metaFilePath() const { return m_MetaFilePath; } + + bool load(const std::string &filePath); + void save(); + void serial(NLMISC::IStream &s); + +private: + std::string m_MetaFilePath; + +}; + +#endif NL_SCENE_META_H + +/* end of file */