diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index d7ae59e2..69904de5 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -23,4 +23,5 @@ add_subdirectory( "quest_parser" ) add_subdirectory( "discovery_parser" ) add_subdirectory( "mob_parse" ) add_subdirectory( "pcb_reader" ) +add_subdirectory( "nav_export" ) add_subdirectory( "event_object_parser" ) diff --git a/src/tools/nav_export/CMakeLists.txt b/src/tools/nav_export/CMakeLists.txt new file mode 100644 index 00000000..4aaaa4c7 --- /dev/null +++ b/src/tools/nav_export/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 2.6) +cmake_policy(SET CMP0015 NEW) +project(Tool_nav_export) + +file(GLOB SERVER_PUBLIC_INCLUDE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*") +file(GLOB SERVER_SOURCE_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + *.c* + nav/*.c* + nav/ext/*.c*) + +add_executable( nav_export ${SERVER_PUBLIC_INCLUDE_FILES} ${SERVER_SOURCE_FILES}) + +if (UNIX) + target_link_libraries( nav_export common xivdat pthread mysqlclient dl z stdc++fs Recast Detour DetourTileCache ) +else() + target_link_libraries( nav_export common xivdat mysql zlib Recast Detour DetourTileCache ) +endif() + +target_include_directories( nav_export + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/../../deps/" ) + diff --git a/src/tools/nav_export/README.md b/src/tools/nav_export/README.md new file mode 100644 index 00000000..66fb849f --- /dev/null +++ b/src/tools/nav_export/README.md @@ -0,0 +1,26 @@ +collision data exporter for sapphire + +compile with STANDALONE defined to compile without boost and sapphire dependencies + +usage: +- regular + - compile with root sapphire dir cmakelists + - sapphire/src/tools/bin/pcb_reader2 "" +- standalone + - compile main.cpp with STANDALONE defined in build arg + - download ffxivexplorer + - ffxivexplorer > path/to/ffxiv's/game/sqpack/ffxiv/0a0000.dat + - exd/territorytype.exh > `File > Export` and copy `territorytype.exh.csv` from exproted directory to `pcb_reader.exe` directory + - ffxivexplorer > path/to/ffxiv's/game/sqpack/ffxiv/020000.dat + - ctrl click the following: + - `bg/ffxiv/[REGION]/common/collision` + - `bg/ffxiv/[REGION]/[dun|fld|twn|etc..]/common/collision/` + - `bg/ffxiv/[REGION]/[dun|fld|twn|etc..]/collision/` + - `bg/ffxiv/region/shared/[for_bg|for_hou]/` + - `bg/ffxiv/[REGION]/[dun|fld|twn|etc..]/ZONE/level/` + - `bg/ffxiv/[REGION]/[dun|fld|twn|etc..]/ZONE/collision/` + - `bgcommon/world/sys/shared/for_bg/` + and `File > Export Raw` to pcb_reader exe dir (common and shared files are optional but you will be missing a lot of objects if you skip them) + - note: at this time ffxivexplorer is still missing some hashes, though any tool which can export the exds should work fine + - main "" + diff --git a/src/tools/nav_export/cache.h b/src/tools/nav_export/cache.h new file mode 100644 index 00000000..b9f79226 --- /dev/null +++ b/src/tools/nav_export/cache.h @@ -0,0 +1,135 @@ +#ifndef CACHE_H +#define CACHE_H + +#include +#include +#include +#include +#include + +#include "pcb.h" +#include "lgb.h" +#include "sgb.h" + +#include +#include +#include + +class Cache : public std::enable_shared_from_this< Cache > +{ +public: + Cache( xiv::dat::GameData* pData ) + { + if( !pData ) + throw std::runtime_error( "Unable to initialise cache without game data" ); + m_pData = pData; + } + ~Cache(){} + + std::shared_ptr< SGB_FILE > getSgbFile( const std::string& filepath ) + { + std::scoped_lock lock( m_mutex ); + + auto it = m_sgbCache.find( filepath ); + if( it != m_sgbCache.end() ) + return it->second; + + auto pFile = loadFile< SGB_FILE >( filepath ); + m_sgbCache[ filepath ] = pFile; + return pFile; + } + + std::shared_ptr< LGB_FILE > getLgbFile( const std::string& filepath ) + { + std::scoped_lock lock( m_mutex ); + + auto it = m_lgbCache.find( filepath ); + if( it != m_lgbCache.end() ) + return it->second; + + auto pFile = loadFile< LGB_FILE >( filepath ); + m_lgbCache[ filepath ] = pFile; + return pFile; + } + + std::shared_ptr< PCB_FILE > getPcbFile( const std::string& filepath ) + { + std::scoped_lock lock( m_mutex ); + + auto it = m_pcbCache.find( filepath ); + if( it != m_pcbCache.end() ) + return it->second; + + auto pFile = loadFile< PCB_FILE >( filepath ); + m_pcbCache[ filepath ] = pFile; + return pFile; + } + + void purge() + { + std::scoped_lock lock( m_mutex ); + _purge(); + } + +private: + void _purge() + { + m_lgbCache.clear(); + m_sgbCache.clear(); + m_pcbCache.clear(); + //std::cout << "Purged PCB/SGB/LGB cache \n"; + } + template< typename T > + std::shared_ptr< T > loadFile( const std::string& filepath ) + { + auto buf = getFileBuffer( filepath ); + if( !buf.empty() ) + { + try + { + auto pFile = std::make_shared< T >( &buf[0] ); + + m_totalFiles++; + if( m_totalFiles % 1000 == 0 ) + { + _purge(); + m_totalFiles = 1; + } + + return pFile; + } + catch( std::exception& e ) + { + std::string err( filepath + " " + e.what() ); + std::cout << err << std::endl; + } + } + return nullptr; + } + + std::vector< char > getFileBuffer( const std::string& filepath ) + { + try + { + //std::cout << fileName << " \n"; + auto pFile = m_pData->getFile( filepath ); + auto& sections = pFile->get_data_sections(); + auto& section = sections.at( 0 ); + return section; + } + catch( std::exception& e ) + { + std::vector< char > empty; + return empty; + } + } + + std::mutex m_mutex; + xiv::dat::GameData* m_pData; + std::map< std::string, std::shared_ptr< LGB_FILE > > m_lgbCache; + std::map< std::string, std::shared_ptr< SGB_FILE > > m_sgbCache; + std::map< std::string, std::shared_ptr< PCB_FILE > > m_pcbCache; + int m_totalFiles{0}; +}; + +#endif \ No newline at end of file diff --git a/src/tools/nav_export/exporter.h b/src/tools/nav_export/exporter.h new file mode 100644 index 00000000..5d738cb9 --- /dev/null +++ b/src/tools/nav_export/exporter.h @@ -0,0 +1,55 @@ +#ifndef EXPORTER_H +#define EXPORTER_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "threadpool.h" + +enum ExportFileType : int +{ + WavefrontObj = 0x01, + Navmesh = 0x02, +}; + +enum ExportSplitType +{ + None, + SplitByGroup, + SingleZone +}; + +struct ExportedMesh +{ + std::vector< float > verts; + std::vector< int > indices; +}; + +struct ExportedModel +{ + std::string name; + std::vector< ExportedMesh > meshes; +}; + +struct ExportedGroup +{ + std::string name; + std::map< std::string, ExportedModel > models; +}; + +struct ExportedZone +{ + std::string name; + std::map< std::string, ExportedGroup > groups; +}; + +#endif \ No newline at end of file diff --git a/src/tools/nav_export/exportmgr.h b/src/tools/nav_export/exportmgr.h new file mode 100644 index 00000000..b2f5a67f --- /dev/null +++ b/src/tools/nav_export/exportmgr.h @@ -0,0 +1,51 @@ +#ifndef EXPORTMGR_H +#define EXPORTMGR_H + +#include "exporter.h" +#include "navmesh_exporter.h" +#include "obj_exporter.h" +#include "threadpool.h" + +class ExportMgr +{ +public: + ExportMgr( unsigned int maxJobs = 0 ) + { + m_threadpool.addWorkers( maxJobs ); + } + ~ExportMgr() + { + waitForTasks(); + } + + void restart( bool cancel = false, unsigned int maxJobs = 0 ) + { + if( cancel ) + m_threadpool.cancel(); + + m_threadpool.complete(); + + m_threadpool.addWorkers( maxJobs ); + } + + void exportZone(const ExportedZone& zone, ExportFileType exportFileTypes) + { + m_threadpool.queue( [zone, exportFileTypes]() + { + if( exportFileTypes & ExportFileType::WavefrontObj ) + ObjExporter::exportZone( zone ); + + if( exportFileTypes & ExportFileType::Navmesh ) + NavmeshExporter::exportZone( zone ); + } ); + } + + void waitForTasks() + { + m_threadpool.complete(); + } +private: + ThreadPool m_threadpool; +}; + +#endif \ No newline at end of file diff --git a/src/tools/nav_export/lgb.h b/src/tools/nav_export/lgb.h new file mode 100644 index 00000000..b363dcb3 --- /dev/null +++ b/src/tools/nav_export/lgb.h @@ -0,0 +1,389 @@ +#ifndef _LGB_H +#define _LGB_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "matrix4.h" +#include "vec3.h" +#include "sgb.h" + +// garbage to skip model loading +extern bool noObj; + +// all credit to +// https://github.com/ufx/SaintCoinach/blob/master/SaintCoinach/Graphics/Lgb/ +// this is simply their work ported to c++ since we dont c# +struct LGB_FILE; +struct LGB_FILE_HEADER; +struct LGB_GROUP; +struct LGB_GROUP_HEADER; + +enum class LgbEntryType : + uint32_t +{ + BgParts = 1, + Light = 3, + Vfx = 4, + PositionMarker = 5, + Gimmick = 6, + SharedGroup6 = 6,// secondary variable is set to 2 + Sound = 7, + EventNpc = 8, + BattleNpc = 9, + Aetheryte = 12, + EnvSpace = 13, + Gathering = 14, + SharedGroup15 = 15,// secondary variable is set to 13 + Treasure = 16, + Weapon = 39, + PopRange = 40, + ExitRange = 41, + MapRange = 43, + NaviMeshRange = 44, + EventObject = 45, + EnvLocation = 47, + EventRange = 49, + QuestMarker = 51, + CollisionBox = 57, + DoorRange = 58, + LineVfx = 59, + ClientPath = 65, + ServerPath = 66, + GimmickRange = 67, + TargetMarker = 68, + ChairMarker = 69, + ClickableRange = 70, + PrefetchRange = 71, + FateRange = 72, + SphereCastRange = 75, +}; + +struct LGB_ENTRY_HEADER +{ + LgbEntryType type; + uint32_t unknown; + uint32_t nameOffset; + vec3 translation; + vec3 rotation; + vec3 scale; +}; + +class LGB_ENTRY +{ +public: + char* m_buf; + uint32_t m_offset; + LGB_ENTRY_HEADER header; + + LGB_ENTRY() + { + m_buf = nullptr; + m_offset = 0; + memset( &header, 0, sizeof( header ) ); + }; + + LGB_ENTRY( char* buf, uint32_t offset ) + { + m_buf = buf; + m_offset = offset; + header = *reinterpret_cast< LGB_ENTRY_HEADER* >( buf + offset ); + }; + + const LgbEntryType getType() const + { + return header.type; + }; + + virtual ~LGB_ENTRY() + { + }; +}; + + +struct LGB_BGPARTS_HEADER : + public LGB_ENTRY_HEADER +{ + uint32_t modelFileOffset; + uint32_t collisionFileOffset; + uint32_t unknown4; + uint32_t unknown5; + uint32_t unknown6; + uint32_t unknown7; + uint32_t unknown8; + uint32_t unknown9; +}; + +class LGB_BGPARTS_ENTRY : + public LGB_ENTRY +{ +public: + LGB_BGPARTS_HEADER header; + std::string name; + std::string modelFileName; + std::string collisionFileName; + + LGB_BGPARTS_ENTRY() + { + }; + + LGB_BGPARTS_ENTRY( char* buf, uint32_t offset ) : + LGB_ENTRY( buf, offset ) + { + header = *reinterpret_cast( buf + offset ); + name = std::string( buf + offset + header.nameOffset ); + modelFileName = std::string( buf + offset + header.modelFileOffset ); + collisionFileName = std::string( buf + offset + header.collisionFileOffset ); + }; +}; + +struct LGB_GIMMICK_HEADER : + public LGB_ENTRY_HEADER +{ + uint32_t gimmickFileOffset; + char unknownBytes[100]; +}; + +class LGB_GIMMICK_ENTRY : + public LGB_ENTRY +{ +public: + LGB_GIMMICK_HEADER header; + std::string name; + std::string gimmickFileName; + + LGB_GIMMICK_ENTRY( char* buf, uint32_t offset ) : + LGB_ENTRY( buf, offset ) + { + header = *reinterpret_cast( buf + offset ); + name = std::string( buf + offset + header.nameOffset ); + gimmickFileName = std::string( buf + offset + header.gimmickFileOffset ); + //std::cout << "\t " << gimmickFileName << " unknown: " << header.unknown << "\n"; + }; +}; + +struct LGB_ENPC_HEADER : + public LGB_ENTRY_HEADER +{ + uint32_t enpcId; + uint8_t unknown1[0x24]; +}; + +class LGB_ENPC_ENTRY : + public LGB_ENTRY +{ +public: + LGB_ENPC_HEADER header; + std::string name; + + LGB_ENPC_ENTRY( char* buf, uint32_t offset ) : + LGB_ENTRY( buf, offset ) + { + header = *reinterpret_cast< LGB_ENPC_HEADER* >( buf + offset ); + name = std::string( buf + offset + header.nameOffset ); + //std::cout << "\t ENpc " << header.enpcId << " " << name << "\n"; + }; +}; + +struct LGB_EOBJ_HEADER : + public LGB_ENTRY_HEADER +{ + uint32_t eobjId; + uint32_t levelHierachyId; + uint8_t unknown1[0xC]; +}; + +class LGB_EOBJ_ENTRY : + public LGB_ENTRY +{ +public: + LGB_EOBJ_HEADER header; + std::string name; + + LGB_EOBJ_ENTRY( char* buf, uint32_t offset ) : + LGB_ENTRY( buf, offset ) + { + header = *reinterpret_cast< LGB_EOBJ_HEADER* >( buf + offset ); + //std::cout << "\t " << header.eobjId << " " << name << " unknown: " << header.unknown << "\n"; + name = std::string( buf + offset + header.nameOffset ); + }; +}; + +struct LGB_MAPRANGE_HEADER : + public LGB_ENTRY_HEADER +{ + uint32_t type; + uint16_t unknown2; + uint16_t unknown3; + uint8_t unknown4[0x10]; +}; + +struct LGB_MAPRANGE_ENTRY : + public LGB_ENTRY +{ +public: + LGB_MAPRANGE_HEADER header; + std::string name; + + LGB_MAPRANGE_ENTRY( char* buf, uint32_t offset ) : + LGB_ENTRY( buf, offset ) + { + header = *reinterpret_cast< LGB_MAPRANGE_HEADER* >( buf + offset ); + name = std::string( buf + offset + header.nameOffset ); + }; +}; + +struct LGB_COLLISION_BOX_HEADER : + public LGB_ENTRY_HEADER +{ + uint8_t unk[100]; +}; + +struct LGB_COLLISION_BOX_ENTRY : + public LGB_ENTRY +{ + LGB_COLLISION_BOX_HEADER header; + std::string name; + + LGB_COLLISION_BOX_ENTRY( char* buf, uint32_t offset ) : + LGB_ENTRY( buf, offset ) + { + header = *reinterpret_cast< LGB_COLLISION_BOX_HEADER* >( buf + offset ); + header.type = LgbEntryType::CollisionBox; + name = std::string( buf + offset + header.nameOffset ); + std::stringstream ss; + ss << "\nName: " << name << "Id: " << header.unknown << "\n"; + ss << "Pos: " << header.translation.x << " " << header.translation.y << " " << header.translation.z << "\n"; + ss << "Rot?: " << header.rotation.x << " " << header.rotation.y << " " << header.rotation.z << "\n"; + ss << "Scale?: " << header.scale.x << " " << header.scale.y << " " << header.scale.z << "\n"; + ss << "00 01 02 03 04 05 06 07 | 08 09 0A 0B 0C 0D 0E 0F\n"; + ss << "-------------------------------------------------\n"; + ss << std::hex; + ss << std::setw( 2 ); + ss << std::setfill( '0' ); + + for( auto i = 1; i < sizeof( header.unk ); ++i ) + if( i % 16 == 0 ) + ss << std::setw(2) << (int)header.unk[i - 1] << "\n"; + else if( i % 8 == 0 ) + ss << std::setw(2) << (int)header.unk[i - 1] << " | "; + else + ss << std::setw(2) << (int)header.unk[i - 1] << " "; + ss << "\n"; + std::cout << ss.str(); + } +}; + +struct LGB_GROUP_HEADER +{ + uint32_t unknown; + int32_t groupNameOffset; + int32_t entriesOffset; + int32_t entryCount; + uint32_t unknown2; + uint32_t unknown3; + uint32_t unknown4; + uint32_t unknown5; + uint32_t unknown6; + uint32_t unknown7; + uint32_t unknown8; + uint32_t unknown9; + uint32_t unknown10; +}; + +struct LGB_GROUP +{ + LGB_FILE* parent; + LGB_GROUP_HEADER header; + std::string name; + std::vector< std::shared_ptr< LGB_ENTRY > > entries; + + LGB_GROUP( char* buf, LGB_FILE* parentStruct, uint32_t offset ) + { + parent = parentStruct; + header = *reinterpret_cast< LGB_GROUP_HEADER* >( buf + offset ); + name = std::string( buf + offset + header.groupNameOffset ); + //entries.resize( header.entryCount ); + //std::cout << name << "\n\t unknown: " << header.unknown << "\n"; + const auto entriesOffset = offset + header.entriesOffset; + for( auto i = 0; i < header.entryCount; ++i ) + { + const auto entryOffset = entriesOffset + *reinterpret_cast< int32_t* >( buf + ( entriesOffset + i * 4 ) ); + + try + { + const auto type = *reinterpret_cast( buf + entryOffset ); + // garbage to skip model loading + switch( type ) + { + case LgbEntryType::BgParts: + entries.push_back( std::make_shared< LGB_BGPARTS_ENTRY >( buf, entryOffset ) ); + break; + case LgbEntryType::Gimmick: + entries.push_back( std::make_shared< LGB_GIMMICK_ENTRY >( buf, entryOffset ) ); + break; + case LgbEntryType::EventObject: + entries.push_back( std::make_shared< LGB_EOBJ_ENTRY >( buf, entryOffset ) ); + break; + case LgbEntryType::CollisionBox: + //entries.push_back( std::make_shared< LGB_COLLISION_BOX_ENTRY >( buf, entryOffset ) ); + break; + default: + //std::cout << "\t\tUnknown SGB entry! Group: " << name << " type: " << ( int )type << " index: " << i << " entryOffset: " << entryOffset << "\n"; + break; + } + } + catch( std::exception& e ) + { + std::cout << ( name + " " + e.what() + "\n" ); + } + } + }; +}; + +struct LGB_FILE_HEADER +{ + char magic[4]; // LGB 1 + uint32_t fileSize; + uint32_t unknown; + char magic2[4]; // LGP1 + uint32_t unknown2; + uint32_t unknown3; + uint32_t unknown4; + uint32_t unknown5; + int32_t groupCount; +}; + +struct LGB_FILE +{ + LGB_FILE_HEADER header; + std::vector< LGB_GROUP > groups; + + LGB_FILE( char* buf ) + { + header = *reinterpret_cast< LGB_FILE_HEADER* >( buf ); + if( strncmp( &header.magic[ 0 ], "LGB1", 4 ) != 0 || strncmp( &header.magic2[ 0 ], "LGP1", 4 ) != 0 ) + throw std::runtime_error( "Invalid LGB file!" ); + + //groups.resize(header.groupCount); + + constexpr auto baseOffset = sizeof( header ); + for( auto i = 0; i < header.groupCount; ++i ) + { + const auto groupOffset = baseOffset + *reinterpret_cast< int32_t* >( buf + ( baseOffset + i * 4 ) ); + const auto group = LGB_GROUP( buf, this, groupOffset ); + groups.push_back( group ); + } + }; +}; + +#endif \ No newline at end of file diff --git a/src/tools/nav_export/main.cpp b/src/tools/nav_export/main.cpp new file mode 100644 index 00000000..af3cf261 --- /dev/null +++ b/src/tools/nav_export/main.cpp @@ -0,0 +1,503 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "exporter.h" +#include "exportmgr.h" + +#include "cache.h" +#include "pcb.h" +#include "lgb.h" +#include "sgb.h" + +#include +#include +#include +#include +#include +#include + +// garbage to ignore models +bool noObj = false; + +std::string gamePath( "C:\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\sqpack" ); +std::unordered_map< uint16_t, std::string > zoneNameMap; +std::map< std::string, std::string > exportedTeriMap; + +uint32_t zoneId; +std::set< std::string > zoneDumpList; + +std::shared_ptr< Cache > pCache; +std::map< uint32_t, uint16_t > eobjSgbPaths; + +xiv::dat::GameData* data1 = nullptr; +xiv::exd::ExdData* eData = nullptr; + + +enum class TerritoryTypeExdIndexes : + size_t +{ + TerritoryType = 0, + Path = 1 +}; + +using namespace std::chrono_literals; + +void initExd( const std::string& gamePath ) +{ + data1 = data1 ? data1 : new xiv::dat::GameData( gamePath ); + eData = eData ? eData : new xiv::exd::ExdData( *data1 ); + pCache = std::make_shared< Cache >( data1 ); +} + +void replaceAll( std::string& str, const std::string& from, const std::string& to ) { + if( from.empty() ) + return; + size_t start_pos = 0; + while( ( start_pos = str.find( from, start_pos ) ) != std::string::npos ) { + str.replace( start_pos, from.length(), to ); + start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } +} + +std::string getEobjSgbPath( uint32_t eobjId ) +{ + static std::map< uint16_t, std::string > exportedSgMap; + + if( !exportedSgMap.empty() ) + return exportedSgMap[ eobjSgbPaths[ eobjId ] ]; + + auto& eobjCat = eData->get_category( "EObj" ); + auto eObjExd = static_cast< xiv::exd::Exd >( eobjCat.get_data_ln( xiv::exd::Language::none ) ); + + auto& exportedSgCat = eData->get_category( "ExportedSG" ); + auto exportedSgExd = static_cast< xiv::exd::Exd >( exportedSgCat.get_data_ln( xiv::exd::Language::none ) ); + + for( auto& row : exportedSgExd.get_rows() ) + { + auto id = row.first; + auto& fields = row.second; + + auto path = std::get< std::string >( fields.at( 0 ) ); + exportedSgMap[id] = path; + } + + uint16_t exportedSgId{0}; + + for( auto& row : eObjExd.get_rows() ) + { + auto id = row.first; + auto& fields = row.second; + + eobjSgbPaths[id] = std::get< uint16_t >( fields.at( 11 ) ); + } + return exportedSgMap[exportedSgId]; +} + +std::string zoneNameToPath( const std::string& name ) +{ + std::string path; + bool found = false; + + auto& cat = eData->get_category( "TerritoryType" ); + auto exd = static_cast< xiv::exd::Exd >( cat.get_data_ln( xiv::exd::Language::none ) ); + for( auto& row : exd.get_rows() ) + { + auto& fields = row.second; + auto teriName = std::get< std::string >( + fields.at( static_cast< size_t >( TerritoryTypeExdIndexes::TerritoryType ) ) ); + if( teriName.empty() ) + continue; + auto teriPath = std::get< std::string >( fields.at( static_cast< size_t >( TerritoryTypeExdIndexes::Path ) ) ); + if( !found && ( Sapphire::Util::toLowerCopy( name ) == Sapphire::Util::toLowerCopy( teriName ) ) ) + { + path = teriPath; + found = true; + zoneId = row.first; + } + zoneNameMap[ row.first ] = teriName; + } + + if( found ) + { + //path = path.substr( path.find_first_of( "/" ) + 1, path.size() - path.find_first_of( "/" )); + //path = std::string( "ffxiv/" ) + path; + path = std::string( "bg/" ) + path.substr( 0, path.find( "/level/" ) ); + printf( "[Info] Found path for %s\n", name.c_str() ); + } + else + { + throw std::runtime_error( "Unable to find path for " + name + + ".\n\tPlease double check spelling." ); + } + + return path; +} + +int totalModels = 0; +void buildModelEntry( std::shared_ptr< PCB_FILE > pPcbFile, ExportedGroup& exportedGroup, + const std::string& name, const std::string& groupName, + const vec3* scale = nullptr, + const vec3* rotation = nullptr, + const vec3* translation = nullptr, + const SGB_MODEL_ENTRY* pSgbEntry = nullptr ) +{ + auto& pcb_file = *pPcbFile.get(); + + ExportedModel model; + model.name = name + "_" + std::to_string( totalModels++ ); + model.meshes.resize( pcb_file.entries.size() ); + + uint32_t meshCount = 0; + for( const auto& entry : pcb_file.entries ) + { + ExportedMesh mesh; + + mesh.verts.resize( ( entry.header.num_vertices + entry.header.num_v16 ) * 3 ); + mesh.indices.resize( entry.header.num_indices * 3 ); + + float x_base = abs( float( entry.header.x1 - entry.header.x ) ); + float y_base = abs( float( entry.header.y1 - entry.header.y ) ); + float z_base = abs( float( entry.header.z1 - entry.header.z ) ); + + auto makeTranslation = [ & ]( vec3& v ) + { + if( pSgbEntry ) + { + v.x *= pSgbEntry->header.scale.x; + v.y *= pSgbEntry->header.scale.y; + v.z *= pSgbEntry->header.scale.z; + + v = v * matrix4::rotateX( pSgbEntry->header.rotation.x ); + v = v * matrix4::rotateY( pSgbEntry->header.rotation.y ); + v = v * matrix4::rotateZ( pSgbEntry->header.rotation.z ); + + v.x += pSgbEntry->header.translation.x; + v.y += pSgbEntry->header.translation.y; + v.z += pSgbEntry->header.translation.z; + } + + if( scale ) + { + v.x *= scale->x; + v.y *= scale->y; + v.z *= scale->z; + + v = v * matrix4::rotateX( rotation->x ); + v = v * matrix4::rotateY( rotation->y ); + v = v * matrix4::rotateZ( rotation->z ); + + v.x += translation->x; + v.y += translation->y; + v.z += translation->z; + } + + }; + int verts = 0; + int indices = 0; + + for( auto& vertex : entry.data.vertices ) + { + vec3 v( vertex.x, vertex.y, vertex.z ); + makeTranslation( v ); + + mesh.verts[ verts++ ] = v.x; + mesh.verts[ verts++ ] = v.y; + mesh.verts[ verts++ ] = v.z; + } + + for( const auto& link : entry.data.vertices_i16 ) + { + vec3 v( float( link.x ) / 0xFFFF, float( link.y ) / 0xFFFF, float( link.z ) / 0xFFFF ); + + v.x = v.x * x_base + entry.header.x; + v.y = v.y * y_base + entry.header.y; + v.z = v.z * z_base + entry.header.z; + + makeTranslation( v ); + + mesh.verts[ verts++ ] = v.x; + mesh.verts[ verts++ ] = v.y; + mesh.verts[ verts++ ] = v.z; + } + + for( const auto& index : entry.data.indices ) + { + mesh.indices[ indices++ ] = index.index[ 0 ]; + mesh.indices[ indices++ ] = index.index[ 1 ]; + mesh.indices[ indices++ ] = index.index[ 2 ]; + // std::cout << std::to_string( index.unknown[0] )<< " " << std::to_string( index.unknown[1] )<< " " << std::to_string( index.unknown[2]) << std::endl; + } + model.meshes[ meshCount++ ] = mesh; + } + exportedGroup.models[model.name] = model; +} + +bool pcbTransformModel( const std::string& fileName, const vec3* scale, const vec3* rotation, + const vec3* translation, ExportedGroup& exportgroup, const SGB_MODEL_ENTRY* pModel = nullptr ) +{ + if( auto pPcbFile = pCache->getPcbFile( fileName ) ) + { + buildModelEntry( pPcbFile, exportgroup, fileName, exportgroup.name, scale, rotation, translation, pModel ); + } + return true; +}; + +void exportSgbModel( const std::string& sgbFilePath, LGB_ENTRY* pGimmick, ExportedGroup& exportgroup, bool isEobj = false ) +{ + if( auto pSgbFile = pCache->getSgbFile( sgbFilePath ) ) + { + const auto& sgbFile = *pSgbFile; + for( const auto& group : sgbFile.entries ) + { + for( const auto& pSgbEntry : group.entries ) + { + auto pModel = dynamic_cast< SGB_MODEL_ENTRY* >( pSgbEntry.get() ); + std::string fileName = pModel->collisionFileName; + if( pModel->type == SgbGroupEntryType::Gimmick ) + { + if( auto pSubSgbFile = pCache->getSgbFile( pModel->modelFileName ) ) + { + for( const auto& subGroup : pSubSgbFile->entries ) + { + for( const auto& pSubEntry : subGroup.entries ) + { + auto pSubModel = dynamic_cast< SGB_MODEL_ENTRY* >( pSubEntry.get() ); + std::string subModelFile = pSubModel->modelFileName; + //"bg/ex1/02_dra_d2/alx/common/bgparts/d2a0_a7_btog2.mdl" + //"bg/ex1/02_dra_d2/alx/common/collision/d2a0_a1_twl01.pcb" + replaceAll( subModelFile, "/bgparts/", "/collision/" ); + replaceAll( subModelFile, ".mdl", ".pcb "); + + if( pSubModel && pSubModel->type == SgbGroupEntryType::Model ) + pcbTransformModel( subModelFile, &pGimmick->header.scale, &pGimmick->header.rotation, + &pGimmick->header.translation, exportgroup, pSubModel ); + } + } + } + } + pcbTransformModel( fileName, &pGimmick->header.scale, &pGimmick->header.rotation, + &pGimmick->header.translation, exportgroup, pModel ); + + } + } + } +}; + +int main( int argc, char* argv[] ) +{ + auto startTime = std::chrono::high_resolution_clock::now(); + auto entryStartTime = std::chrono::high_resolution_clock::now(); + + std::vector< std::string > argVec( argv + 1, argv + argc ); + std::string zoneName = "r2t2"; + + bool generateNavmesh = true; + bool dumpAllZones = true; + int nJobs = 4; + + int exportFileType = 0; + if( !noObj ) + exportFileType |= ExportFileType::WavefrontObj; + if( generateNavmesh ) + exportFileType |= ExportFileType::Navmesh; + + try + { + initExd( gamePath ); + getEobjSgbPath( 0 ); + } + catch( std::exception& e ) + { + printf( "Unable to initialise EXD!\n Usage: pcb_reader \"path/to/FINAL FANTASY XIV - A REALM REBORN/game/sqpack\"\n" ); + return -1; + } + ExportMgr exportMgr( nJobs ); + zoneNameToPath( zoneName ); + + if( dumpAllZones ) + { + for( const auto& zone : zoneNameMap ) + zoneDumpList.emplace( zone.second ); + } + else + { + zoneDumpList.emplace( zoneName ); + } + + int zoneCount = 0; + for( auto& zoneName : zoneDumpList ) + { + try + { + const auto& zonePath = zoneNameToPath( zoneName ); + if( exportedTeriMap.find( zonePath ) != exportedTeriMap.end() ) + continue; + + std::string zoneNameShort = zonePath.substr( zonePath.find_last_of( '/' ) ); + + ExportedZone exportedZone; + exportedZone.name = zoneNameShort; + exportedTeriMap[ zonePath ] = zoneNameShort; + + std::string listPcbPath( zonePath + "/collision/list.pcb" ); + std::string bgLgbPath( zonePath + "/level/bg.lgb" ); + std::string planmapLgbPath( zonePath + "/level/planmap.lgb" ); + std::string collisionFilePath( zonePath + "/collision/" ); + std::vector< char > section; + std::vector< char > section1; + std::vector< char > section2; + + const xiv::dat::Cat& test = data1->getCategory( "bg" ); + + auto test_file = data1->getFile( bgLgbPath ); + section = test_file->access_data_sections().at( 0 ); + + auto planmap_file = data1->getFile( planmapLgbPath ); + section2 = planmap_file->access_data_sections().at( 0 ); + + auto test_file1 = data1->getFile( listPcbPath ); + section1 = test_file1->access_data_sections().at( 0 ); + + std::vector< std::string > stringList; + + int totalGroups = 0; + int totalEntries = 0; + + uint32_t offset1 = 0x20; + + { + for( ;; ) + { + if( offset1 >= section1.size() ) + { + break; + } + uint16_t trId = *( uint16_t* ) §ion1[ offset1 ]; + + char someString[200]; + sprintf( someString, "%str%04d.pcb", collisionFilePath.c_str(), trId ); + stringList.push_back( std::string( someString ) ); + //std::cout << someString << "\n"; + offset1 += 0x20; + + + } + } + LGB_FILE bgLgb( §ion[ 0 ] ); + LGB_FILE planmapLgb( §ion2[ 0 ] ); + + std::vector< LGB_FILE > lgbList{ bgLgb, planmapLgb }; + + ExportedGroup exportedTerrainGroup; + exportedTerrainGroup.name = zoneName + "_terrain"; + for( const auto& fileName : stringList ) + { + if( auto pPcbFile = pCache->getPcbFile( fileName ) ) + buildModelEntry( pPcbFile, exportedTerrainGroup, fileName, zoneNameShort ); + } + exportedZone.groups.emplace( exportedTerrainGroup.name, exportedTerrainGroup ); + + for( const auto& lgb : lgbList ) + { + for( const auto& group : lgb.groups ) + { + ExportedGroup exportedGroup; + exportedGroup.name = group.name; + + //std::cout << "\t" << group.name << " Size " << group.header.entryCount << "\n"; + for( const auto& pEntry : group.entries ) + { + std::string fileName( "" ); + fileName.resize( 256 ); + + // write files + switch( pEntry->getType() ) + { + case LgbEntryType::BgParts: + { + auto pBgParts = static_cast< LGB_BGPARTS_ENTRY* >( pEntry.get() ); + fileName = pBgParts->collisionFileName; + pcbTransformModel( fileName, &pBgParts->header.scale, &pBgParts->header.rotation, + &pBgParts->header.translation, exportedGroup ); + } + break; + + // gimmick entry + case LgbEntryType::Gimmick: + { + auto pGimmick = static_cast< LGB_GIMMICK_ENTRY* >( pEntry.get() ); + + exportSgbModel( pGimmick->gimmickFileName, pGimmick, exportedGroup ); + } + break; + + case LgbEntryType::EventObject: + { + auto pEobj = static_cast< LGB_EOBJ_ENTRY* >( pEntry.get() ); + pcbTransformModel( fileName, &pEntry->header.scale, &pEntry->header.rotation, &pEntry->header.translation, exportedGroup ); + + auto sgbPath = getEobjSgbPath( pEobj->header.eobjId ); + if ( !sgbPath.empty() ) + { + exportSgbModel( sgbPath, pEobj, exportedGroup, true ); + + if( auto pGimmick = pCache->getSgbFile( sgbPath ) ) + { + for( const auto& offset1cFile : pGimmick->offset1cObjects ) + exportSgbModel( offset1cFile, pEobj, exportedGroup, true ); + } + } + + } + break; + default: + break; + } + } + exportedZone.groups.emplace( group.name, exportedGroup ); + } + } + exportMgr.exportZone( exportedZone, static_cast< ExportFileType >( exportFileType ) ); + exportedZone.groups.clear(); + + printf( "Built export struct for %s in %lu seconds \n", + zoneName.c_str(), + std::chrono::duration_cast< std::chrono::seconds >( std::chrono::high_resolution_clock::now() - entryStartTime ).count() ); + //if( zoneCount++ % nJobs == 0 ) + { + exportMgr.restart(); + pCache->purge(); + } + } + catch( std::exception& e ) + { + printf( "%s", ( std::string( e.what() ) + "\n" ).c_str() ); + printf( "Unable to extract collision data.\n" ); + printf( "Usage: pcb_reader2 territory \"path/to/game/sqpack/ffxiv\"\n" ); + } + } + pCache->purge(); + exportMgr.waitForTasks(); + std::cout << "\n\n\n"; + + printf( "Finished all tasks in %lu seconds\n", + std::chrono::duration_cast< std::chrono::seconds >( std::chrono::high_resolution_clock::now() - startTime ).count() ); + + delete eData; + delete data1; + + return 0; +} diff --git a/src/tools/nav_export/matrix4.h b/src/tools/nav_export/matrix4.h new file mode 100644 index 00000000..fdcee84c --- /dev/null +++ b/src/tools/nav_export/matrix4.h @@ -0,0 +1,111 @@ +#ifndef _MATRIX4_H +#define _MATRIX4_H + +#include +#include + +// https://github.com/jpd002/Play--Framework/tree/master/include/math +struct matrix4 +{ + // 4x4 + float grid[16]; + + matrix4() + { + memset( &grid[ 0 ], 0, sizeof( grid ) ); + } + + float operator()( int row, int col ) const + { + return grid[ ( row * 4 ) + col ]; + } + + float& operator()( int row, int col ) + { + return grid[ ( row * 4 ) + col ]; + } + + static matrix4 rotateX( float angle ) + { + matrix4 ret = matrix4(); + ret( 0, 0 ) = 1.000000000f; + ret( 1, 1 ) = cos( angle ); + ret( 1, 2 ) = -sin( angle ); + ret( 2, 1 ) = sin( angle ); + ret( 2, 2 ) = cos( angle ); + ret( 3, 3 ) = 1.000000000f; + return ret; + } + + static matrix4 rotateY( float angle ) + { + matrix4 ret = matrix4(); + ret( 0, 0 ) = cos( angle ); + ret( 0, 2 ) = sin( angle ); + ret( 1, 1 ) = 1.000000000f; + ret( 2, 0 ) = -sin( angle ); + ret( 2, 2 ) = cos( angle ); + ret( 3, 3 ) = 1.000000000f; + return ret; + } + + static matrix4 rotateZ( float angle ) + { + matrix4 ret = matrix4(); + ret( 0, 0 ) = cos( angle ); + ret( 0, 1 ) = -sin( angle ); + ret( 1, 0 ) = sin( angle ); + ret( 1, 1 ) = cos( angle ); + ret( 2, 2 ) = 1.000000000f; + ret( 3, 3 ) = 1.000000000f; + return ret; + } + + static matrix4 scale( float x, float y, float z ) + { + matrix4 ret = matrix4(); + ret( 0, 0 ) = x; + ret( 1, 1 ) = y; + ret( 2, 2 ) = z; + ret( 3, 3 ) = 1; + + return ret; + } + + static matrix4 translate( float x, float y, float z ) + { + matrix4 ret = matrix4(); + ret( 0, 0 ) = 1; + ret( 1, 1 ) = 1; + ret( 2, 2 ) = 1; + ret( 3, 3 ) = 1; + + ret( 3, 0 ) = x; + ret( 3, 1 ) = y; + ret( 3, 2 ) = z; + return ret; + } + + matrix4 operator*( const matrix4& rhs ) const + { + matrix4 ret; + for( unsigned int i = 0; i < 4; i++ ) + { + ret( i, 0 ) = + ( *this )( i, 0 ) * rhs( 0, 0 ) + ( *this )( i, 1 ) * rhs( 1, 0 ) + ( *this )( i, 2 ) * rhs( 2, 0 ) + + ( *this )( i, 3 ) * rhs( 3, 0 ); + ret( i, 1 ) = + ( *this )( i, 0 ) * rhs( 0, 1 ) + ( *this )( i, 1 ) * rhs( 1, 1 ) + ( *this )( i, 2 ) * rhs( 2, 1 ) + + ( *this )( i, 3 ) * rhs( 3, 1 ); + ret( i, 2 ) = + ( *this )( i, 0 ) * rhs( 0, 2 ) + ( *this )( i, 1 ) * rhs( 1, 2 ) + ( *this )( i, 2 ) * rhs( 2, 2 ) + + ( *this )( i, 3 ) * rhs( 3, 2 ); + ret( i, 3 ) = + ( *this )( i, 0 ) * rhs( 0, 3 ) + ( *this )( i, 1 ) * rhs( 1, 3 ) + ( *this )( i, 2 ) * rhs( 2, 3 ) + + ( *this )( i, 3 ) * rhs( 3, 3 ); + } + return ret; + } +}; + +#endif diff --git a/src/tools/nav_export/nav/TiledNavmeshGenerator.cpp b/src/tools/nav_export/nav/TiledNavmeshGenerator.cpp new file mode 100644 index 00000000..0204910f --- /dev/null +++ b/src/tools/nav_export/nav/TiledNavmeshGenerator.cpp @@ -0,0 +1,574 @@ +#include "TiledNavmeshGenerator.h" + +#include +#include + +#include + +namespace fs = std::experimental::filesystem; + + +inline unsigned int nextPow2( uint32_t v ) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; +} + +inline unsigned int ilog2( uint32_t v ) +{ + uint32_t r; + uint32_t shift; + r = (v > 0xffff) << 4; v >>= r; + shift = (v > 0xff) << 3; v >>= shift; r |= shift; + shift = (v > 0xf) << 2; v >>= shift; r |= shift; + shift = (v > 0x3) << 1; v >>= shift; r |= shift; + r |= (v >> 1); + return r; +} + +bool TiledNavmeshGenerator::init( const std::string& path ) +{ + if( !fs::exists( path ) ) + throw std::runtime_error( "what" ); + + // ignore logging/bullshit/etc + m_ctx = new rcContext( false ); + + printf( "[Navmesh] loading obj: %s\n", path.substr( path.find( "pcb_export" ) - 1 ).c_str() ); + + m_mesh = new rcMeshLoaderObj; + assert( m_mesh ); + + if( !m_mesh->load( path ) ) + { + printf( "[Navmesh] Failed to allocate rcMeshLoaderObj\n" ); + return false; + } + + rcCalcBounds( m_mesh->getVerts(), m_mesh->getVertCount(), m_meshBMin, m_meshBMax ); + + m_chunkyMesh = new rcChunkyTriMesh; + assert( m_chunkyMesh ); + + if( !rcCreateChunkyTriMesh( m_mesh->getVerts(), m_mesh->getTris(), m_mesh->getTriCount(), 256, m_chunkyMesh ) ) + { + printf( "[Navmesh] buildTiledNavigation: Failed to build chunky mesh.\n" ); + return false; + } + + // todo: load some bullshit settings from exd + + int gw = 0, gh = 0; + rcCalcGridSize( m_meshBMin, m_meshBMax, m_cellSize, &gw, &gh ); + + auto ts = static_cast< uint32_t >( m_tileSize ); + const uint32_t tw = ( gw + ts - 1 ) / ts; + const uint32_t th = ( gh + ts - 1 ) / ts; + + printf( "[Navmesh] - Tiles %d x %d\n", tw, th ); + + int tileBits = rcMin( ( int ) ilog2( nextPow2( tw * th ) ), 14 ); + if( tileBits > 14 ) + tileBits = 14; + int polyBits = 22 - tileBits; + m_maxTiles = 1 << tileBits; + m_maxPolysPerTile = 1 << polyBits; + + printf( "[Navmesh] - %.1fK verts, %.1fK tris\n", m_mesh->getVertCount() / 1000.0f, m_mesh->getTriCount() / 1000.0f ); + + return true; +} + +TiledNavmeshGenerator::~TiledNavmeshGenerator() +{ + delete m_mesh; + delete m_chunkyMesh; + + if( m_triareas ) + delete[] m_triareas; + delete m_ctx; + + rcFreeContourSet( m_cset ); + rcFreeHeightField( m_solid ); + rcFreeCompactHeightfield(m_chf); + rcFreePolyMesh( m_pmesh ); + rcFreePolyMeshDetail( m_dmesh ); + dtFreeNavMesh( m_navMesh ); +} + +void TiledNavmeshGenerator::saveNavmesh( const std::string& name ) +{ + assert( m_navMesh ); + + // fuck this gay earth + auto mesh = const_cast< const dtNavMesh* >( m_navMesh ); + + auto dir = fs::current_path().string() + "/pcb_export/" + name + "/"; + auto fileName = dir + name + ".nav"; + + fs::create_directories( dir ); + + FILE* fp = fopen( fileName.c_str(), "wb" ); + if( !fp ) + return; + + // Store header. + NavMeshSetHeader header; + header.magic = NAVMESHSET_MAGIC; + header.version = NAVMESHSET_VERSION; + header.numTiles = 0; + for( int i = 0; i < mesh->getMaxTiles(); ++i ) + { + auto tile = mesh->getTile( i ); + if( !tile || !tile->header || !tile->dataSize ) + continue; + + header.numTiles++; + } + + memcpy( &header.params, mesh->getParams(), sizeof( dtNavMeshParams ) ); + fwrite( &header, sizeof( NavMeshSetHeader ), 1, fp ); + + // Store tiles. + for( int i = 0; i < mesh->getMaxTiles(); ++i ) + { + auto tile = mesh->getTile( i ); + if( !tile || !tile->header || !tile->dataSize ) + continue; + + NavMeshTileHeader tileHeader; + tileHeader.tileRef = mesh->getTileRef( tile ); + tileHeader.dataSize = tile->dataSize; + fwrite( &tileHeader, sizeof( tileHeader ), 1, fp ); + + fwrite( tile->data, tile->dataSize, 1, fp ); + } + + fclose( fp ); + + auto pos = fileName.find( "pcb_export" ); + fileName = fileName.substr( pos - 1 ); + + printf( "[Navmesh] Saved navmesh to '%s'\n", fileName.c_str() ); +} + +bool TiledNavmeshGenerator::buildNavmesh() +{ + assert( m_mesh ); + + m_navMesh = dtAllocNavMesh(); + if( !m_navMesh ) + { + printf( "[Navmesh] buildTiledNavigation: Could not allocate navmesh.\n" ); + return false; + } + + dtNavMeshParams params{}; + rcVcopy( params.orig, m_meshBMin ); + params.tileWidth = m_tileSize * m_cellSize; + params.tileHeight = m_tileSize * m_cellSize; + params.maxTiles = m_maxTiles; + params.maxPolys = m_maxPolysPerTile; + + dtStatus status; + + status = m_navMesh->init( ¶ms ); + if( dtStatusFailed( status ) ) + { + printf( "[Navmesh] buildTiledNavigation: Could not init navmesh.\n" ); + return false; + } + + // todo: duplicated from above, we can probably cache all this and only do it once + int gw = 0, gh = 0; + rcCalcGridSize( m_meshBMin, m_meshBMax, m_cellSize, &gw, &gh ); + auto ts = static_cast< uint32_t >( m_tileSize ); + const int tw = ( gw + ts - 1 ) / ts; + const int th = ( gh + ts - 1 ) / ts; + const float tcs = m_tileSize * m_cellSize; + + for( int y = 0; y < th; y++ ) + { + for( int x = 0; x < tw; x++ ) + { + m_lastBuiltTileBmin[ 0 ] = m_meshBMin[ 0 ] + x * tcs; + m_lastBuiltTileBmin[ 1 ] = m_meshBMin[ 1 ]; + m_lastBuiltTileBmin[ 2 ] = m_meshBMin[ 2 ] + y * tcs; + + m_lastBuiltTileBmax[ 0 ] = m_meshBMin[ 0 ] + ( x + 1 ) * tcs; + m_lastBuiltTileBmax[ 1 ] = m_meshBMax[ 1 ]; + m_lastBuiltTileBmax[ 2 ] = m_meshBMin[ 2 ] + ( y + 1 ) * tcs; + + int dataSize = 0; + + unsigned char* data = buildTileMesh( x, y, m_lastBuiltTileBmin, m_lastBuiltTileBmax, dataSize ); + if( data ) + { + // Remove any previous data (navmesh owns and deletes the data). + m_navMesh->removeTile( m_navMesh->getTileRefAt( x, y, 0 ), nullptr, nullptr ); + + // Let the navmesh own the data. + status = m_navMesh->addTile( data, dataSize, DT_TILE_FREE_DATA, 0, nullptr ); + + if( dtStatusFailed( status ) ) + { + dtFree( data ); + } + } + } + } + + return true; +} + + +unsigned char* TiledNavmeshGenerator::buildTileMesh( const int tx, const int ty, const float* bmin, const float* bmax, + int& dataSize ) +{ + const float* verts = m_mesh->getVerts(); + const int nverts = m_mesh->getVertCount(); + const int ntris = m_mesh->getTriCount(); + + // Init build configuration from GUI + memset( &m_cfg, 0, sizeof( m_cfg ) ); + m_cfg.cs = m_cellSize; + m_cfg.ch = m_cellHeight; + m_cfg.walkableSlopeAngle = m_agentMaxSlope; + m_cfg.walkableHeight = static_cast< int >( ceilf( m_agentHeight / m_cfg.ch ) ); + m_cfg.walkableClimb = static_cast< int >( floorf( m_agentMaxClimb / m_cfg.ch ) ); + m_cfg.walkableRadius = static_cast< int >( ceilf( m_agentRadius / m_cfg.cs ) ); + m_cfg.maxEdgeLen = static_cast< int >( m_edgeMaxLen / m_cellSize ); + m_cfg.maxSimplificationError = m_edgeMaxError; + m_cfg.minRegionArea = static_cast< int >( rcSqr( m_regionMinSize ) ); // Note: area = size*size + m_cfg.mergeRegionArea = static_cast< int >( rcSqr( m_regionMergeSize ) ); // Note: area = size*size + m_cfg.maxVertsPerPoly = static_cast< int >( m_vertsPerPoly ); + m_cfg.tileSize = static_cast< int >( m_tileSize ); + m_cfg.borderSize = m_cfg.walkableRadius + 3; // Reserve enough padding. + m_cfg.width = m_cfg.tileSize + m_cfg.borderSize * 2; + m_cfg.height = m_cfg.tileSize + m_cfg.borderSize * 2; + m_cfg.detailSampleDist = m_detailSampleDist < 0.9f ? 0 : m_cellSize * m_detailSampleDist; + m_cfg.detailSampleMaxError = m_cellHeight * m_detailSampleMaxError; + + // Expand the heighfield bounding box by border size to find the extents of geometry we need to build this tile. + // + // This is done in order to make sure that the navmesh tiles connect correctly at the borders, + // and the obstacles close to the border work correctly with the dilation process. + // No polygons (or contours) will be created on the border area. + // + // IMPORTANT! + // + // :''''''''': + // : +-----+ : + // : | | : + // : | |<--- tile to build + // : | | : + // : +-----+ :<-- geometry needed + // :.........: + // + // You should use this bounding box to query your input geometry. + // + // For example if you build a navmesh for terrain, and want the navmesh tiles to match the terrain tile size + // you will need to pass in data from neighbour terrain tiles too! In a simple case, just pass in all the 8 neighbours, + // or use the bounding box below to only pass in a sliver of each of the 8 neighbours. + rcVcopy( m_cfg.bmin, bmin ); + rcVcopy( m_cfg.bmax, bmax ); + m_cfg.bmin[ 0 ] -= m_cfg.borderSize * m_cfg.cs; + m_cfg.bmin[ 2 ] -= m_cfg.borderSize * m_cfg.cs; + m_cfg.bmax[ 0 ] += m_cfg.borderSize * m_cfg.cs; + m_cfg.bmax[ 2 ] += m_cfg.borderSize * m_cfg.cs; + + m_solid = rcAllocHeightfield(); + if( !m_solid ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'solid'.\n" ); + return nullptr; + } + + if( !rcCreateHeightfield( m_ctx, *m_solid, m_cfg.width, m_cfg.height, m_cfg.bmin, m_cfg.bmax, m_cfg.cs, m_cfg.ch ) ) + { + printf( "[Navmesh] buildNavigation: Could not create solid heightfield.\n" ); + return nullptr; + } + + // Allocate array that can hold triangle flags. + // If you have multiple meshes you need to process, allocate + // and array which can hold the max number of triangles you need to process. + m_triareas = new unsigned char[ m_chunkyMesh->maxTrisPerChunk ]; + if( !m_triareas ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'm_triareas' (%d).\n", m_chunkyMesh->maxTrisPerChunk ); + return nullptr; + } + + float tbmin[ 2 ]; + float tbmax[ 2 ]; + tbmin[ 0 ] = m_cfg.bmin[ 0 ]; + tbmin[ 1 ] = m_cfg.bmin[ 2 ]; + tbmax[ 0 ] = m_cfg.bmax[ 0 ]; + tbmax[ 1 ] = m_cfg.bmax[ 2 ]; + + int cid[512];// TODO: Make grow when returning too many items. + const int ncid = rcGetChunksOverlappingRect( m_chunkyMesh, tbmin, tbmax, cid, 512 ); + + if( !ncid ) + return nullptr; + + m_tileTriCount = 0; + + for( int i = 0; i < ncid; ++i ) + { + const rcChunkyTriMeshNode& node = m_chunkyMesh->nodes[ cid[ i ] ]; + const int* ctris = &m_chunkyMesh->tris[ node.i * 3 ]; + const int nctris = node.n; + + m_tileTriCount += nctris; + + memset( m_triareas, 0, nctris * sizeof( unsigned char ) ); + rcMarkWalkableTriangles( m_ctx, m_cfg.walkableSlopeAngle, verts, nverts, ctris, nctris, m_triareas ); + if( !rcRasterizeTriangles( m_ctx, verts, nverts, ctris, m_triareas, nctris, *m_solid, m_cfg.walkableClimb ) ) + return nullptr; + } + + delete[] m_triareas; + m_triareas = nullptr; + + // Once all geometry is rasterized, we do initial pass of filtering to + // remove unwanted overhangs caused by the conservative rasterization + // as well as filter spans where the character cannot possibly stand. + rcFilterLowHangingWalkableObstacles( m_ctx, m_cfg.walkableClimb, *m_solid ); + rcFilterLedgeSpans( m_ctx, m_cfg.walkableHeight, m_cfg.walkableClimb, *m_solid ); + rcFilterWalkableLowHeightSpans( m_ctx, m_cfg.walkableHeight, *m_solid ); + + // Compact the heightfield so that it is faster to handle from now on. + // This will result more cache coherent data as well as the neighbours + // between walkable cells will be calculated. + m_chf = rcAllocCompactHeightfield(); + if( !m_chf ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'chf'." ); + return nullptr; + } + if( !rcBuildCompactHeightfield( m_ctx, m_cfg.walkableHeight, m_cfg.walkableClimb, *m_solid, *m_chf ) ) + { + printf( "[Navmesh] buildNavigation: Could not build compact data." ); + return nullptr; + } + + rcFreeHeightField( m_solid ); + m_solid = nullptr; + + // Erode the walkable area by agent radius. + if( !rcErodeWalkableArea( m_ctx, m_cfg.walkableRadius, *m_chf ) ) + { + printf( "[Navmesh] buildNavigation: Could not erode." ); + return nullptr; + } + + // (Optional) Mark areas. +// const ConvexVolume* vols = m_mesh->getConvexVolumes(); +// for (int i = 0; i < m_geom->getConvexVolumeCount(); ++i) +// rcMarkConvexPolyArea(m_ctx, vols[i].verts, vols[i].nverts, vols[i].hmin, vols[i].hmax, (unsigned char)vols[i].area, *m_chf); + + // Partition the heightfield so that we can use simple algorithm later to triangulate the walkable areas. + // There are 3 martitioning methods, each with some pros and cons: + // 1) Watershed partitioning + // - the classic Recast partitioning + // - creates the nicest tessellation + // - usually slowest + // - partitions the heightfield into nice regions without holes or overlaps + // - the are some corner cases where this method creates produces holes and overlaps + // - holes may appear when a small obstacles is close to large open area (triangulation can handle this) + // - overlaps may occur if you have narrow spiral corridors (i.e stairs), this make triangulation to fail + // * generally the best choice if you precompute the nacmesh, use this if you have large open areas + // 2) Monotone partioning + // - fastest + // - partitions the heightfield into regions without holes and overlaps (guaranteed) + // - creates long thin polygons, which sometimes causes paths with detours + // * use this if you want fast navmesh generation + // 3) Layer partitoining + // - quite fast + // - partitions the heighfield into non-overlapping regions + // - relies on the triangulation code to cope with holes (thus slower than monotone partitioning) + // - produces better triangles than monotone partitioning + // - does not have the corner cases of watershed partitioning + // - can be slow and create a bit ugly tessellation (still better than monotone) + // if you have large open areas with small obstacles (not a problem if you use tiles) + // * good choice to use for tiled navmesh with medium and small sized tiles + + if( m_partitionType == SAMPLE_PARTITION_WATERSHED ) + { + // Prepare for region partitioning, by calculating distance field along the walkable surface. + if( !rcBuildDistanceField( m_ctx, *m_chf ) ) + { + printf( "[Navmesh] buildNavigation: Could not build distance field." ); + return nullptr; + } + + // Partition the walkable surface into simple regions without holes. + if( !rcBuildRegions( m_ctx, *m_chf, m_cfg.borderSize, m_cfg.minRegionArea, m_cfg.mergeRegionArea ) ) + { + printf( "[Navmesh] buildNavigation: Could not build watershed regions." ); + return nullptr; + } + } + else if( m_partitionType == SAMPLE_PARTITION_MONOTONE ) + { + // Partition the walkable surface into simple regions without holes. + // Monotone partitioning does not need distancefield. + if( !rcBuildRegionsMonotone( m_ctx, *m_chf, m_cfg.borderSize, m_cfg.minRegionArea, m_cfg.mergeRegionArea ) ) + { + printf( "[Navmesh] buildNavigation: Could not build monotone regions." ); + return nullptr; + } + } + else // SAMPLE_PARTITION_LAYERS + { + // Partition the walkable surface into simple regions without holes. + if( !rcBuildLayerRegions( m_ctx, *m_chf, m_cfg.borderSize, m_cfg.minRegionArea ) ) + { + printf( "[Navmesh] buildNavigation: Could not build layer regions." ); + return nullptr; + } + } + + // Create contours. + m_cset = rcAllocContourSet(); + if( !m_cset ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'cset'." ); + return nullptr; + } + if( !rcBuildContours( m_ctx, *m_chf, m_cfg.maxSimplificationError, m_cfg.maxEdgeLen, *m_cset ) ) + { + printf( "[Navmesh] buildNavigation: Could not create contours." ); + return nullptr; + } + + if( m_cset->nconts == 0 ) + { + return nullptr; + } + + // Build polygon navmesh from the contours. + m_pmesh = rcAllocPolyMesh(); + if( !m_pmesh ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'pmesh'." ); + return nullptr; + } + if( !rcBuildPolyMesh( m_ctx, *m_cset, m_cfg.maxVertsPerPoly, *m_pmesh ) ) + { + printf( "[Navmesh] buildNavigation: Could not triangulate contours." ); + return nullptr; + } + + // Build detail mesh. + m_dmesh = rcAllocPolyMeshDetail(); + if( !m_dmesh ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'dmesh'." ); + return nullptr; + } + + if( !rcBuildPolyMeshDetail( m_ctx, *m_pmesh, *m_chf, + m_cfg.detailSampleDist, m_cfg.detailSampleMaxError, + *m_dmesh ) ) + { + printf( "[Navmesh] buildNavigation: Could build polymesh detail." ); + return nullptr; + } + + rcFreeCompactHeightfield( m_chf ); + rcFreeContourSet( m_cset ); + m_chf = nullptr; + m_cset = nullptr; + + unsigned char* navData = 0; + int navDataSize = 0; + if( m_cfg.maxVertsPerPoly <= DT_VERTS_PER_POLYGON ) + { + if( m_pmesh->nverts >= 0xffff ) + { + // The vertex indices are ushorts, and cannot point to more than 0xffff vertices. + printf( "[Navmesh] Too many vertices per tile %d (max: %d).", m_pmesh->nverts, 0xffff ); + return nullptr; + } + + // Update poly flags from areas. + for( int i = 0; i < m_pmesh->npolys; ++i ) + { + if( m_pmesh->areas[ i ] == RC_WALKABLE_AREA ) + m_pmesh->areas[ i ] = SAMPLE_POLYAREA_GROUND; + + if( m_pmesh->areas[ i ] == SAMPLE_POLYAREA_GROUND || + m_pmesh->areas[ i ] == SAMPLE_POLYAREA_GRASS || + m_pmesh->areas[ i ] == SAMPLE_POLYAREA_ROAD ) + { + m_pmesh->flags[ i ] = SAMPLE_POLYFLAGS_WALK; + } + else if( m_pmesh->areas[ i ] == SAMPLE_POLYAREA_WATER ) + { + m_pmesh->flags[ i ] = SAMPLE_POLYFLAGS_SWIM; + } + else if( m_pmesh->areas[ i ] == SAMPLE_POLYAREA_DOOR ) + { + m_pmesh->flags[ i ] = SAMPLE_POLYFLAGS_WALK | SAMPLE_POLYFLAGS_DOOR; + } + } + + dtNavMeshCreateParams params; + memset( ¶ms, 0, sizeof( params ) ); + params.verts = m_pmesh->verts; + params.vertCount = m_pmesh->nverts; + params.polys = m_pmesh->polys; + params.polyAreas = m_pmesh->areas; + params.polyFlags = m_pmesh->flags; + params.polyCount = m_pmesh->npolys; + params.nvp = m_pmesh->nvp; + params.detailMeshes = m_dmesh->meshes; + params.detailVerts = m_dmesh->verts; + params.detailVertsCount = m_dmesh->nverts; + params.detailTris = m_dmesh->tris; + params.detailTriCount = m_dmesh->ntris; + + params.offMeshConVerts = nullptr; + params.offMeshConRad = nullptr; + params.offMeshConDir = nullptr; + params.offMeshConAreas = nullptr; + params.offMeshConFlags = nullptr; + params.offMeshConUserID = nullptr; + params.offMeshConCount = 0; + + params.walkableHeight = m_agentHeight; + params.walkableRadius = m_agentRadius; + params.walkableClimb = m_agentMaxClimb; + params.tileX = tx; + params.tileY = ty; + params.tileLayer = 0; + rcVcopy( params.bmin, m_pmesh->bmin ); + rcVcopy( params.bmax, m_pmesh->bmax ); + params.cs = m_cfg.cs; + params.ch = m_cfg.ch; + params.buildBvTree = true; + + if( !dtCreateNavMeshData( ¶ms, &navData, &navDataSize ) ) + { + printf( "[Navmesh] Could not build Detour navmesh." ); + return nullptr; + } + } + + rcFreePolyMesh( m_pmesh ); + rcFreePolyMeshDetail( m_dmesh ); + m_pmesh = nullptr; + m_dmesh = nullptr; + + dataSize = navDataSize; + return navData; +} diff --git a/src/tools/nav_export/nav/TiledNavmeshGenerator.h b/src/tools/nav_export/nav/TiledNavmeshGenerator.h new file mode 100644 index 00000000..15039c95 --- /dev/null +++ b/src/tools/nav_export/nav/TiledNavmeshGenerator.h @@ -0,0 +1,123 @@ +#ifndef SAPPHIRE_TILEDNAVMESHGENERATOR_H +#define SAPPHIRE_TILEDNAVMESHGENERATOR_H + +#include +#include +#include + +#include "ext/MeshLoaderObj.h" +#include "ext/ChunkyTriMesh.h" + +#include "recastnavigation/Detour/Include/DetourNavMesh.h" +#include "recastnavigation/Detour/Include/DetourNavMeshQuery.h" +#include "recastnavigation/Recast/Include/Recast.h" + +class TiledNavmeshGenerator +{ +public: + enum SamplePartitionType + { + SAMPLE_PARTITION_WATERSHED, + SAMPLE_PARTITION_MONOTONE, + SAMPLE_PARTITION_LAYERS, + }; + + enum SamplePolyAreas + { + SAMPLE_POLYAREA_GROUND, + SAMPLE_POLYAREA_WATER, + SAMPLE_POLYAREA_ROAD, + SAMPLE_POLYAREA_DOOR, + SAMPLE_POLYAREA_GRASS, + SAMPLE_POLYAREA_JUMP, + }; + enum SamplePolyFlags + { + SAMPLE_POLYFLAGS_WALK = 0x01, // Ability to walk (ground, grass, road) + SAMPLE_POLYFLAGS_SWIM = 0x02, // Ability to swim (water). + SAMPLE_POLYFLAGS_DOOR = 0x04, // Ability to move through doors. + SAMPLE_POLYFLAGS_JUMP = 0x08, // Ability to jump. + SAMPLE_POLYFLAGS_DISABLED = 0x10, // Disabled polygon + SAMPLE_POLYFLAGS_ALL = 0xffff // All abilities. + }; + + static const int NAVMESHSET_MAGIC = 'M'<<24 | 'S'<<16 | 'E'<<8 | 'T'; //'MSET'; + static const int NAVMESHSET_VERSION = 1; + + struct NavMeshSetHeader + { + int magic; + int version; + int numTiles; + dtNavMeshParams params; + }; + + struct NavMeshTileHeader + { + dtTileRef tileRef; + int dataSize; + }; + + + TiledNavmeshGenerator() = default; + ~TiledNavmeshGenerator(); + + bool init( const std::string& path ); + unsigned char* buildTileMesh( const int tx, const int ty, const float* bmin, const float* bmax, int& dataSize ); + bool buildNavmesh(); + void saveNavmesh( const std::string& name ); + +private: + rcConfig m_cfg; + + rcMeshLoaderObj* m_mesh; + rcChunkyTriMesh* m_chunkyMesh; + + rcContext* m_ctx; + dtNavMesh* m_navMesh; + rcHeightfield* m_solid; + rcContourSet* m_cset; + rcPolyMesh* m_pmesh; + rcPolyMeshDetail* m_dmesh; + + rcCompactHeightfield* m_chf; + + unsigned char* m_triareas; + + int m_maxTiles = 0; + int m_maxPolysPerTile = 0; + + int m_tileTriCount = 0; + + int m_partitionType = SamplePartitionType::SAMPLE_PARTITION_WATERSHED; + + float m_meshBMin[ 3 ]; + float m_meshBMax[ 3 ]; + + float m_lastBuiltTileBmin[ 3 ]; + float m_lastBuiltTileBmax[ 3 ]; + + // options + float m_tileSize = 160.f; + float m_cellSize = 0.2f; + float m_cellHeight = 0.2f; + + float m_agentMaxSlope = 56.f; + float m_agentHeight = 2.f; + float m_agentMaxClimb = 0.6f; + float m_agentRadius = 0.5f; + + float m_regionMinSize = 8.f; + float m_regionMergeSize = 20.f; + + float m_edgeMaxLen = 12.f; + float m_edgeMaxError = 1.4f; + float m_vertsPerPoly = 6.f; + + float m_detailSampleDist = 6.f; + float m_detailSampleMaxError = 1.f; + +}; + + +#endif //SAPPHIRE_TILEDNAVMESHGENERATOR_H diff --git a/src/tools/nav_export/nav/ext/ChunkyTriMesh.cpp b/src/tools/nav_export/nav/ext/ChunkyTriMesh.cpp new file mode 100644 index 00000000..7b5ef0d6 --- /dev/null +++ b/src/tools/nav_export/nav/ext/ChunkyTriMesh.cpp @@ -0,0 +1,331 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#include "ChunkyTriMesh.h" +#include +#include +#include + +struct BoundsItem +{ + float bmin[ 2 ]; + float bmax[ 2 ]; + int i; +}; + +static int compareItemX( const void* va, const void* vb ) +{ + const BoundsItem* a = ( const BoundsItem* ) va; + const BoundsItem* b = ( const BoundsItem* ) vb; + if( a->bmin[ 0 ] < b->bmin[ 0 ] ) + return -1; + if( a->bmin[ 0 ] > b->bmin[ 0 ] ) + return 1; + return 0; +} + +static int compareItemY( const void* va, const void* vb ) +{ + const BoundsItem* a = ( const BoundsItem* ) va; + const BoundsItem* b = ( const BoundsItem* ) vb; + if( a->bmin[ 1 ] < b->bmin[ 1 ] ) + return -1; + if( a->bmin[ 1 ] > b->bmin[ 1 ] ) + return 1; + return 0; +} + +static void calcExtends( const BoundsItem* items, const int /*nitems*/, + const int imin, const int imax, + float* bmin, float* bmax ) +{ + bmin[ 0 ] = items[ imin ].bmin[ 0 ]; + bmin[ 1 ] = items[ imin ].bmin[ 1 ]; + + bmax[ 0 ] = items[ imin ].bmax[ 0 ]; + bmax[ 1 ] = items[ imin ].bmax[ 1 ]; + + for( int i = imin + 1; i < imax; ++i ) + { + const BoundsItem& it = items[ i ]; + if( it.bmin[ 0 ] < bmin[ 0 ] ) + bmin[ 0 ] = it.bmin[ 0 ]; + if( it.bmin[ 1 ] < bmin[ 1 ] ) + bmin[ 1 ] = it.bmin[ 1 ]; + + if( it.bmax[ 0 ] > bmax[ 0 ] ) + bmax[ 0 ] = it.bmax[ 0 ]; + if( it.bmax[ 1 ] > bmax[ 1 ] ) + bmax[ 1 ] = it.bmax[ 1 ]; + } +} + +inline int longestAxis( float x, float y ) +{ + return y > x ? 1 : 0; +} + +static void subdivide( BoundsItem* items, int nitems, int imin, int imax, int trisPerChunk, + int& curNode, rcChunkyTriMeshNode* nodes, const int maxNodes, + int& curTri, int* outTris, const int* inTris ) +{ + int inum = imax - imin; + int icur = curNode; + + if( curNode > maxNodes ) + return; + + rcChunkyTriMeshNode& node = nodes[ curNode++ ]; + + if( inum <= trisPerChunk ) + { + // Leaf + calcExtends( items, nitems, imin, imax, node.bmin, node.bmax ); + + // Copy triangles. + node.i = curTri; + node.n = inum; + + for( int i = imin; i < imax; ++i ) + { + const int* src = &inTris[ items[ i ].i * 3 ]; + int* dst = &outTris[ curTri * 3 ]; + curTri++; + dst[ 0 ] = src[ 0 ]; + dst[ 1 ] = src[ 1 ]; + dst[ 2 ] = src[ 2 ]; + } + } + else + { + // Split + calcExtends( items, nitems, imin, imax, node.bmin, node.bmax ); + + int axis = longestAxis( node.bmax[ 0 ] - node.bmin[ 0 ], + node.bmax[ 1 ] - node.bmin[ 1 ] ); + + if( axis == 0 ) + { + // Sort along x-axis + qsort( items + imin, static_cast(inum), sizeof( BoundsItem ), compareItemX ); + } + else if( axis == 1 ) + { + // Sort along y-axis + qsort( items + imin, static_cast(inum), sizeof( BoundsItem ), compareItemY ); + } + + int isplit = imin + inum / 2; + + // Left + subdivide( items, nitems, imin, isplit, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris ); + // Right + subdivide( items, nitems, isplit, imax, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris ); + + int iescape = curNode - icur; + // Negative index means escape. + node.i = -iescape; + } +} + +bool rcCreateChunkyTriMesh( const float* verts, const int* tris, int ntris, + int trisPerChunk, rcChunkyTriMesh* cm ) +{ + int nchunks = ( ntris + trisPerChunk - 1 ) / trisPerChunk; + + cm->nodes = new rcChunkyTriMeshNode[nchunks * 4]; + if( !cm->nodes ) + return false; + + cm->tris = new int[ntris * 3]; + if( !cm->tris ) + return false; + + cm->ntris = ntris; + + // Build tree + BoundsItem* items = new BoundsItem[ntris]; + if( !items ) + return false; + + for( int i = 0; i < ntris; i++ ) + { + const int* t = &tris[ i * 3 ]; + BoundsItem& it = items[ i ]; + it.i = i; + // Calc triangle XZ bounds. + it.bmin[ 0 ] = it.bmax[ 0 ] = verts[ t[ 0 ] * 3 + 0 ]; + it.bmin[ 1 ] = it.bmax[ 1 ] = verts[ t[ 0 ] * 3 + 2 ]; + for( int j = 1; j < 3; ++j ) + { + const float* v = &verts[ t[ j ] * 3 ]; + if( v[ 0 ] < it.bmin[ 0 ] ) + it.bmin[ 0 ] = v[ 0 ]; + if( v[ 2 ] < it.bmin[ 1 ] ) + it.bmin[ 1 ] = v[ 2 ]; + + if( v[ 0 ] > it.bmax[ 0 ] ) + it.bmax[ 0 ] = v[ 0 ]; + if( v[ 2 ] > it.bmax[ 1 ] ) + it.bmax[ 1 ] = v[ 2 ]; + } + } + + int curTri = 0; + int curNode = 0; + subdivide( items, ntris, 0, ntris, trisPerChunk, curNode, cm->nodes, nchunks * 4, curTri, cm->tris, tris ); + + delete[] items; + + cm->nnodes = curNode; + + // Calc max tris per node. + cm->maxTrisPerChunk = 0; + for( int i = 0; i < cm->nnodes; ++i ) + { + rcChunkyTriMeshNode& node = cm->nodes[ i ]; + const bool isLeaf = node.i >= 0; + if( !isLeaf ) + continue; + if( node.n > cm->maxTrisPerChunk ) + cm->maxTrisPerChunk = node.n; + } + + return true; +} + + +inline bool checkOverlapRect( const float amin[2], const float amax[2], + const float bmin[2], const float bmax[2] ) +{ + bool overlap = true; + overlap = ( amin[ 0 ] > bmax[ 0 ] || amax[ 0 ] < bmin[ 0 ] ) ? false : overlap; + overlap = ( amin[ 1 ] > bmax[ 1 ] || amax[ 1 ] < bmin[ 1 ] ) ? false : overlap; + return overlap; +} + +int rcGetChunksOverlappingRect( const rcChunkyTriMesh* cm, + float bmin[2], float bmax[2], + int* ids, const int maxIds ) +{ + // Traverse tree + int i = 0; + int n = 0; + while( i < cm->nnodes ) + { + const rcChunkyTriMeshNode* node = &cm->nodes[ i ]; + const bool overlap = checkOverlapRect( bmin, bmax, node->bmin, node->bmax ); + const bool isLeafNode = node->i >= 0; + + if( isLeafNode && overlap ) + { + if( n < maxIds ) + { + ids[ n ] = i; + n++; + } + } + + if( overlap || isLeafNode ) + i++; + else + { + const int escapeIndex = -node->i; + i += escapeIndex; + } + } + + return n; +} + + +static bool checkOverlapSegment( const float p[2], const float q[2], + const float bmin[2], const float bmax[2] ) +{ + static const float EPSILON = 1e-6f; + + float tmin = 0; + float tmax = 1; + float d[2]; + d[ 0 ] = q[ 0 ] - p[ 0 ]; + d[ 1 ] = q[ 1 ] - p[ 1 ]; + + for( int i = 0; i < 2; i++ ) + { + if( fabsf( d[ i ] ) < EPSILON ) + { + // Ray is parallel to slab. No hit if origin not within slab + if( p[ i ] < bmin[ i ] || p[ i ] > bmax[ i ] ) + return false; + } + else + { + // Compute intersection t value of ray with near and far plane of slab + float ood = 1.0f / d[ i ]; + float t1 = ( bmin[ i ] - p[ i ] ) * ood; + float t2 = ( bmax[ i ] - p[ i ] ) * ood; + if( t1 > t2 ) + { + float tmp = t1; + t1 = t2; + t2 = tmp; + } + if( t1 > tmin ) + tmin = t1; + if( t2 < tmax ) + tmax = t2; + if( tmin > tmax ) + return false; + } + } + return true; +} + +int rcGetChunksOverlappingSegment( const rcChunkyTriMesh* cm, + float p[2], float q[2], + int* ids, const int maxIds ) +{ + // Traverse tree + int i = 0; + int n = 0; + while( i < cm->nnodes ) + { + const rcChunkyTriMeshNode* node = &cm->nodes[ i ]; + const bool overlap = checkOverlapSegment( p, q, node->bmin, node->bmax ); + const bool isLeafNode = node->i >= 0; + + if( isLeafNode && overlap ) + { + if( n < maxIds ) + { + ids[ n ] = i; + n++; + } + } + + if( overlap || isLeafNode ) + i++; + else + { + const int escapeIndex = -node->i; + i += escapeIndex; + } + } + + return n; +} diff --git a/src/tools/nav_export/nav/ext/ChunkyTriMesh.h b/src/tools/nav_export/nav/ext/ChunkyTriMesh.h new file mode 100644 index 00000000..24eb5890 --- /dev/null +++ b/src/tools/nav_export/nav/ext/ChunkyTriMesh.h @@ -0,0 +1,68 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef CHUNKYTRIMESH_H +#define CHUNKYTRIMESH_H + +struct rcChunkyTriMeshNode +{ + float bmin[ 2 ]; + float bmax[ 2 ]; + int i; + int n; +}; + +struct rcChunkyTriMesh +{ + inline rcChunkyTriMesh() : + nodes( 0 ), nnodes( 0 ), tris( 0 ), ntris( 0 ), maxTrisPerChunk( 0 ) + { + }; + + inline ~rcChunkyTriMesh() + { + delete[] nodes; + delete[] tris; + } + + rcChunkyTriMeshNode* nodes; + int nnodes; + int* tris; + int ntris; + int maxTrisPerChunk; + +private: + // Explicitly disabled copy constructor and copy assignment operator. + rcChunkyTriMesh( const rcChunkyTriMesh& ); + + rcChunkyTriMesh& operator=( const rcChunkyTriMesh& ); +}; + +/// Creates partitioned triangle mesh (AABB tree), +/// where each node contains at max trisPerChunk triangles. +bool rcCreateChunkyTriMesh( const float* verts, const int* tris, int ntris, + int trisPerChunk, rcChunkyTriMesh* cm ); + +/// Returns the chunk indices which overlap the input rectable. +int rcGetChunksOverlappingRect( const rcChunkyTriMesh* cm, float bmin[2], float bmax[2], int* ids, const int maxIds ); + +/// Returns the chunk indices which overlap the input segment. +int rcGetChunksOverlappingSegment( const rcChunkyTriMesh* cm, float p[2], float q[2], int* ids, const int maxIds ); + + +#endif // CHUNKYTRIMESH_H diff --git a/src/tools/nav_export/nav/ext/MeshLoaderObj.cpp b/src/tools/nav_export/nav/ext/MeshLoaderObj.cpp new file mode 100644 index 00000000..08c9c7f1 --- /dev/null +++ b/src/tools/nav_export/nav/ext/MeshLoaderObj.cpp @@ -0,0 +1,252 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#include "MeshLoaderObj.h" +#include +#include +#include + +#define _USE_MATH_DEFINES + +#include + +rcMeshLoaderObj::rcMeshLoaderObj() : + m_scale( 1.0f ), + m_verts( 0 ), + m_tris( 0 ), + m_normals( 0 ), + m_vertCount( 0 ), + m_triCount( 0 ) +{ +} + +rcMeshLoaderObj::~rcMeshLoaderObj() +{ + delete[] m_verts; + delete[] m_normals; + delete[] m_tris; +} + +void rcMeshLoaderObj::addVertex( float x, float y, float z, int& cap ) +{ + if( m_vertCount + 1 > cap ) + { + cap = !cap ? 8 : cap * 2; + float* nv = new float[cap * 3]; + if( m_vertCount ) + memcpy( nv, m_verts, m_vertCount * 3 * sizeof( float ) ); + delete[] m_verts; + m_verts = nv; + } + float* dst = &m_verts[ m_vertCount * 3 ]; + *dst++ = x * m_scale; + *dst++ = y * m_scale; + *dst++ = z * m_scale; + m_vertCount++; +} + +void rcMeshLoaderObj::addTriangle( int a, int b, int c, int& cap ) +{ + if( m_triCount + 1 > cap ) + { + cap = !cap ? 8 : cap * 2; + int* nv = new int[cap * 3]; + if( m_triCount ) + memcpy( nv, m_tris, m_triCount * 3 * sizeof( int ) ); + delete[] m_tris; + m_tris = nv; + } + int* dst = &m_tris[ m_triCount * 3 ]; + *dst++ = a; + *dst++ = b; + *dst++ = c; + m_triCount++; +} + +static char* parseRow( char* buf, char* bufEnd, char* row, int len ) +{ + bool start = true; + bool done = false; + int n = 0; + while( !done && buf < bufEnd ) + { + char c = *buf; + buf++; + // multirow + switch( c ) + { + case '\\': + break; + case '\n': + if( start ) + break; + done = true; + break; + case '\r': + break; + case '\t': + case ' ': + if( start ) + break; + // else falls through + default: + start = false; + row[ n++ ] = c; + if( n >= len - 1 ) + done = true; + break; + } + } + row[ n ] = '\0'; + return buf; +} + +static int parseFace( char* row, int* data, int n, int vcnt ) +{ + int j = 0; + while( *row != '\0' ) + { + // Skip initial white space + while( *row != '\0' && ( *row == ' ' || *row == '\t' ) ) + row++; + char* s = row; + // Find vertex delimiter and terminated the string there for conversion. + while( *row != '\0' && *row != ' ' && *row != '\t' ) + { + if( *row == '/' ) + *row = '\0'; + row++; + } + if( *s == '\0' ) + continue; + int vi = atoi( s ); + data[ j++ ] = vi < 0 ? vi + vcnt : vi - 1; + if( j >= n ) + return j; + } + return j; +} + +bool rcMeshLoaderObj::load( const std::string& filename ) +{ + char* buf = 0; + FILE* fp = fopen( filename.c_str(), "rb" ); + if( !fp ) + return false; + if( fseek( fp, 0, SEEK_END ) != 0 ) + { + fclose( fp ); + return false; + } + long bufSize = ftell( fp ); + if( bufSize < 0 ) + { + fclose( fp ); + return false; + } + if( fseek( fp, 0, SEEK_SET ) != 0 ) + { + fclose( fp ); + return false; + } + buf = new char[bufSize]; + if( !buf ) + { + fclose( fp ); + return false; + } + size_t readLen = fread( buf, bufSize, 1, fp ); + fclose( fp ); + + if( readLen != 1 ) + { + delete[] buf; + return false; + } + + char* src = buf; + char* srcEnd = buf + bufSize; + char row[512]; + int face[32]; + float x, y, z; + int nv; + int vcap = 0; + int tcap = 0; + + while( src < srcEnd ) + { + // Parse one row + row[ 0 ] = '\0'; + src = parseRow( src, srcEnd, row, sizeof( row ) / sizeof( char ) ); + // Skip comments + if( row[ 0 ] == '#' ) + continue; + if( row[ 0 ] == 'v' && row[ 1 ] != 'n' && row[ 1 ] != 't' ) + { + // Vertex pos + sscanf( row + 1, "%f %f %f", &x, &y, &z ); + addVertex( x, y, z, vcap ); + } + if( row[ 0 ] == 'f' ) + { + // Faces + nv = parseFace( row + 1, face, 32, m_vertCount ); + for( int i = 2; i < nv; ++i ) + { + const int a = face[ 0 ]; + const int b = face[ i - 1 ]; + const int c = face[ i ]; + if( a < 0 || a >= m_vertCount || b < 0 || b >= m_vertCount || c < 0 || c >= m_vertCount ) + continue; + addTriangle( a, b, c, tcap ); + } + } + } + + delete[] buf; + + // Calculate normals. + m_normals = new float[m_triCount * 3]; + for( int i = 0; i < m_triCount * 3; i += 3 ) + { + const float* v0 = &m_verts[ m_tris[ i ] * 3 ]; + const float* v1 = &m_verts[ m_tris[ i + 1 ] * 3 ]; + const float* v2 = &m_verts[ m_tris[ i + 2 ] * 3 ]; + float e0[3], e1[3]; + for( int j = 0; j < 3; ++j ) + { + e0[ j ] = v1[ j ] - v0[ j ]; + e1[ j ] = v2[ j ] - v0[ j ]; + } + float* n = &m_normals[ i ]; + n[ 0 ] = e0[ 1 ] * e1[ 2 ] - e0[ 2 ] * e1[ 1 ]; + n[ 1 ] = e0[ 2 ] * e1[ 0 ] - e0[ 0 ] * e1[ 2 ]; + n[ 2 ] = e0[ 0 ] * e1[ 1 ] - e0[ 1 ] * e1[ 0 ]; + float d = sqrtf( n[ 0 ] * n[ 0 ] + n[ 1 ] * n[ 1 ] + n[ 2 ] * n[ 2 ] ); + if( d > 0 ) + { + d = 1.0f / d; + n[ 0 ] *= d; + n[ 1 ] *= d; + n[ 2 ] *= d; + } + } + + m_filename = filename; + return true; +} diff --git a/src/tools/nav_export/nav/ext/MeshLoaderObj.h b/src/tools/nav_export/nav/ext/MeshLoaderObj.h new file mode 100644 index 00000000..1b3f9c56 --- /dev/null +++ b/src/tools/nav_export/nav/ext/MeshLoaderObj.h @@ -0,0 +1,82 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef MESHLOADER_OBJ +#define MESHLOADER_OBJ + +#include + +class rcMeshLoaderObj +{ +public: + rcMeshLoaderObj(); + + ~rcMeshLoaderObj(); + + bool load( const std::string& fileName ); + + const float* getVerts() const + { + return m_verts; + } + + const float* getNormals() const + { + return m_normals; + } + + const int* getTris() const + { + return m_tris; + } + + int getVertCount() const + { + return m_vertCount; + } + + int getTriCount() const + { + return m_triCount; + } + + const std::string& getFileName() const + { + return m_filename; + } + +private: + // Explicitly disabled copy constructor and copy assignment operator. + rcMeshLoaderObj( const rcMeshLoaderObj& ); + + rcMeshLoaderObj& operator=( const rcMeshLoaderObj& ); + + void addVertex( float x, float y, float z, int& cap ); + + void addTriangle( int a, int b, int c, int& cap ); + + std::string m_filename; + float m_scale; + float* m_verts; + int* m_tris; + float* m_normals; + int m_vertCount; + int m_triCount; +}; + +#endif // MESHLOADER_OBJ diff --git a/src/tools/nav_export/navmesh_exporter.h b/src/tools/nav_export/navmesh_exporter.h new file mode 100644 index 00000000..6035b552 --- /dev/null +++ b/src/tools/nav_export/navmesh_exporter.h @@ -0,0 +1,59 @@ +#ifndef NAVMESH_EXPORTER_H +#define NAVMESH_EXPORTER_H + +#include +#include +#include +#include + +#include + +#include "exporter.h" +#include "obj_exporter.h" +#include "nav/TiledNavmeshGenerator.h" + +#include + +namespace fs = std::experimental::filesystem; + +class NavmeshExporter +{ +public: + static void exportZone( const ExportedZone& zone ) + { + auto start = std::chrono::high_resolution_clock::now(); + + static std::string currPath = std::experimental::filesystem::current_path().string(); + + auto dir = fs::current_path().string() + "/pcb_export/" + zone.name + "/"; + auto fileName = dir + zone.name; + auto objName = fileName + ".obj"; + + std::error_code e; + if( !fs::exists( objName, e ) ) + ObjExporter::exportZone( zone ); + + TiledNavmeshGenerator gen; + + if( !gen.init( objName ) ) + { + printf( "[Navmesh] failed to init TiledNavmeshGenerator for file '%s'\n", zone.name.c_str() ); + return; + } + + if( !gen.buildNavmesh() ) + { + printf( "[Navmesh] Failed to build navmesh for '%s'\n", zone.name.c_str() ); + return; + } + + gen.saveNavmesh( zone.name ); + + + auto end = std::chrono::high_resolution_clock::now(); + printf( "[Navmesh] Finished exporting %s in %lu ms\n", zone.name.c_str(), + std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); + } + +}; +#endif // !OBJ_EXPORTER_H diff --git a/src/tools/nav_export/obj_exporter.h b/src/tools/nav_export/obj_exporter.h new file mode 100644 index 00000000..b01ad970 --- /dev/null +++ b/src/tools/nav_export/obj_exporter.h @@ -0,0 +1,137 @@ +#ifndef OBJ_EXPORTER_H +#define OBJ_EXPORTER_H + +#include +#include +#include +#include +#include + + +#include "exporter.h" + + +class ObjExporter +{ +public: + static std::string exportZone( const ExportedZone& zone ) + { + static std::string currPath = std::experimental::filesystem::current_path().string(); + + auto start = std::chrono::high_resolution_clock::now(); + + auto dir = currPath + "/pcb_export/" + zone.name + "/"; + auto fileName = dir + zone.name + ".obj"; + + std::error_code e; + + if( !std::experimental::filesystem::exists( dir, e ) ) + { + if( !std::experimental::filesystem::create_directories( dir, e ) ) + { + printf( "Unable to create directory '%s'", ( dir ).c_str() ); + return ""; + } + } + std::ofstream of( fileName, std::ios::trunc ); + int indicesOffset = 0; + int meshesCount = 0; + + if( of.good() ) + { + of.close(); + of.open( fileName, std::ios::app ); + for( const auto& group : zone.groups ) + { + exportGroup( group.second, of, indicesOffset, meshesCount ); + } + of.flush(); + of.close(); + } + + auto end = std::chrono::high_resolution_clock::now(); + + printf( "[Obj] Finished exporting %s in %lu ms\n", + fileName.substr( fileName.find( "pcb_export" ) - 1 ).c_str(), + std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); + return fileName; + } + + static std::string exportGroup( const std::string& zoneName, const ExportedGroup& group ) + { + static std::string currPath = std::experimental::filesystem::current_path().string(); + + auto start = std::chrono::high_resolution_clock::now(); + + auto dir = currPath + "/pcb_export/" + zoneName + "/groups/"; + auto fileName = dir + group.name + ".obj"; + + std::error_code e; + if( !std::experimental::filesystem::exists( dir, e ) ) + { + if( !std::experimental::filesystem::create_directories( dir, e ) ) + { + printf( "Unable to create directory '%s'", ( dir ).c_str() ); + return ""; + } + } + std::ofstream of( fileName, std::ios::trunc ); + int indicesOffset = 0; + int modelCount = 0; + + if( of.good() ) + { + of.close(); + of.open( fileName, std::ios::app ); + exportGroup( group, of, indicesOffset, modelCount ); + of.flush(); + of.close(); + } + + auto end = std::chrono::high_resolution_clock::now(); + printf( "[Obj] Finished exporting %s in %lu ms\n", + fileName.substr( fileName.find( "pcb_export" ) - 1 ).c_str(), + std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); + + return fileName; + } +private: + static void exportGroup( const ExportedGroup& group, std::ofstream& of, int& indicesOffset, int& modelCount ) + { + int currModelCount = modelCount; + + of << "o " << group.name << '_' << std::to_string( currModelCount ) << '\n'; + for( const auto& model : group.models ) + { + modelCount++; + of << "o " << model.second.name << '_' << std::to_string( currModelCount ) << '_' << std::to_string( modelCount ) << '\n'; + + int meshCount = 0; + for( const auto& mesh : model.second.meshes ) + { + for( int i = 0; i < mesh.verts.size(); i += 3 ) + { + of << "v " << + std::to_string( mesh.verts[ i ] ) << ' ' << + std::to_string( mesh.verts[ i + 1 ] ) << ' ' << + std::to_string( mesh.verts[ i + 2 ] ) << '\n'; + } + + of << "g " << + model.second.name << '_' << + std::to_string( currModelCount ) << '_' << std::to_string( modelCount ) << '_' << std::to_string( meshCount++ ) << '\n'; + + for( int i = 0; i < mesh.indices.size(); i += 3 ) + { + of << "f " << + std::to_string( mesh.indices[ i ] + indicesOffset + 1 ) << ' ' << + std::to_string( mesh.indices[ i + 1 ] + indicesOffset + 1 ) << ' ' + + std::to_string( mesh.indices[ i + 2 ] + indicesOffset + 1 ) << '\n'; + } + indicesOffset += mesh.verts.size() / 3; + } + } + //of.flush(); + } +}; +#endif // !OBJ_EXPORTER_H diff --git a/src/tools/nav_export/pcb.h b/src/tools/nav_export/pcb.h new file mode 100644 index 00000000..647c56ca --- /dev/null +++ b/src/tools/nav_export/pcb.h @@ -0,0 +1,176 @@ +#ifndef _PCB_H +#define _PCB_H + +#include +#include + +struct PCB_HEADER +{ + uint32_t unknown_1; + uint32_t unknown_2; + uint32_t num_entries; // count starts at 0 + uint32_t total_indices; + uint64_t padding; +}; + +struct PCB_BLOCK_HEADER +{ + uint32_t type; // 0 for entry, 0x30 for group + uint32_t group_size; // when group size in bytes for the group block + // bounding box + float x; + float y; + float z; + float x1; + float y1; + float z1; + // number of vertices packed into 16 bit + uint16_t num_v16; + // number of indices + uint16_t num_indices; + // number of normal floar vertices + uint32_t num_vertices; +}; + +struct PCB_VERTEXDATA +{ + float x; + float y; + float z; +}; + +struct PCB_INDEXDATA +{ + uint8_t index[3]; + uint8_t unknown[3]; + uint8_t unknown1[6]; +}; + +struct PCB_VERTEXDATAI16 +{ + uint16_t x; + uint16_t y; + uint16_t z; +}; + +struct PCB_BLOCK_DATA +{ + std::vector< PCB_VERTEXDATA > vertices; + std::vector< PCB_VERTEXDATAI16 > vertices_i16; + std::vector< PCB_INDEXDATA > indices; +}; + +struct PCB_BLOCK_ENTRY +{ + PCB_BLOCK_HEADER header; + PCB_BLOCK_DATA data; +}; + +struct PCB_FILE +{ + PCB_HEADER header; + std::vector< PCB_BLOCK_ENTRY > entries; + + PCB_FILE( char* buf ) + { + uint32_t offset = 0; + memcpy( &header, buf, sizeof( header )); + offset += sizeof( header ); + entries.resize( header.num_entries ); + bool isgroup = true; + while( isgroup ) + { + PCB_BLOCK_ENTRY block_entry; + memcpy( &block_entry.header, buf + offset, sizeof( block_entry.header ) ); + isgroup = block_entry.header.type == 0x30; + + //printf( "BLOCKHEADER_%X: type: %i, group_size: %i\n", offset, block_entry.header.type, block_entry.header.group_size ); + // + if( isgroup ) + { + parseBlockEntry( buf + offset + 0x30, entries, offset); + offset += block_entry.header.group_size; + } + else + { + parseBlockEntry( buf + offset, entries, offset ); + } + } + } + + int parseBlockEntry( char* data, std::vector< PCB_BLOCK_ENTRY >& entries, int gOff ) + { + int offset = 0; + bool isgroup = true; + while( isgroup ) + { + PCB_BLOCK_ENTRY block_entry; + memcpy( &block_entry.header, data + offset, sizeof( block_entry.header ) ); + isgroup = block_entry.header.type == 0x30; + + //printf( " BLOCKHEADER_%X: type: %i, group_size: %i\n", gOff + offset, block_entry.header.type, block_entry.header.group_size ); + + if( isgroup ) + { + parseBlockEntry( data + offset + 0x30, entries, gOff + offset ); + offset += block_entry.header.group_size; + } + else + { + /* printf( "\tnum_v16: %i, num_indices: %i, num_vertices: %i\n\n", + block_entry.header.num_v16, block_entry.header.num_indices, block_entry.header.num_vertices );*/ + int doffset = sizeof( block_entry.header ) + offset; + uint16_t block_size = sizeof( block_entry.header ) + + block_entry.header.num_vertices * 3 * 4 + + block_entry.header.num_v16 * 6 + + block_entry.header.num_indices * 6; + + if( block_entry.header.num_vertices != 0 ) + { + block_entry.data.vertices.resize( block_entry.header.num_vertices ); + + int32_t size_vertexbuffer = block_entry.header.num_vertices * 3; + memcpy( &block_entry.data.vertices[ 0 ], data + doffset, size_vertexbuffer * 4 ); + doffset += size_vertexbuffer * 4; + } + if( block_entry.header.num_v16 != 0 ) + { + block_entry.data.vertices_i16.resize( block_entry.header.num_v16 ); + int32_t size_unknownbuffer = block_entry.header.num_v16 * 6; + memcpy( &block_entry.data.vertices_i16[ 0 ], data + doffset, size_unknownbuffer ); + doffset += block_entry.header.num_v16 * 6; + } + if( block_entry.header.num_indices != 0 ) + { + block_entry.data.indices.resize( block_entry.header.num_indices ); + int32_t size_indexbuffer = block_entry.header.num_indices * 12; + memcpy( &block_entry.data.indices[ 0 ], data + doffset, size_indexbuffer ); + doffset += size_indexbuffer; + } + entries.push_back( block_entry ); + } + } + + return 0; + } + +}; + +struct PCB_LIST_ENTRY +{ + uint32_t id; + float x, y, z, x2, y2, z2, rot; +}; + +struct PCB_LIST_BASE_ENTRY +{ + float x, y, z, x2, y2, z2, rot; +}; + +struct PCB_LIST_FILE +{ + uint32_t count; + PCB_LIST_BASE_ENTRY entry; + std::vector< PCB_LIST_ENTRY > entries; +}; +#endif \ No newline at end of file diff --git a/src/tools/nav_export/sgb.h b/src/tools/nav_export/sgb.h new file mode 100644 index 00000000..b3264055 --- /dev/null +++ b/src/tools/nav_export/sgb.h @@ -0,0 +1,276 @@ +#ifndef _SGB_H +#define _SGB_H + +#include +#include +#include +#include +#include +#include +#include + +#include "vec3.h" + +// garbage to skip model loading +extern bool noObj; + +// +// ported from https://github.com/ufx/SaintCoinach/blob/master/SaintCoinach/Graphics/Sgb/SgbDataType.cs + +struct SGB_FILE; +struct SGB_HEADER; +struct SGB_MODEL_ENTRY; +struct SGB_MODEL_HEADER; +struct SGB_GROUP; +struct SGB_GROUP_HEADER; + + +enum SgbDataType : + uint32_t +{ + Unknown0008 = 0x0008, + Group = 0x0100, +}; + +enum SgbGroupEntryType : + uint32_t +{ + Model = 0x01, + Gimmick = 0x06, +}; + +struct SGB_GROUP_HEADER +{ + SgbDataType type; + int32_t nameOffset; + uint32_t unknown08; + uint32_t unknown0C; + + uint32_t unknown10; + uint32_t unknown14; + uint32_t unknown18; + uint32_t unknown1C; + + int32_t entryCount; + uint32_t unknown24; + uint32_t unknown28; + uint32_t unknown2C; + + uint32_t unknown30; + uint32_t unknown34; + uint32_t unknown38; + uint32_t unknown3C; + + uint32_t unknown40; + uint32_t unknown44; +}; + +struct SGB_GROUP1C_HEADER +{ + SgbDataType type; + int32_t nameOffset; + uint32_t unknown08; + + int32_t entryCount; + uint32_t unknown14; + int32_t modelFileOffset; + vec3 unknownFloat3; + vec3 unknownFloat3_2; + int32_t stateOffset; + int32_t modelFileOffset2; + uint32_t unknown3; + float unknown4; + int32_t nameOffset2; + vec3 unknownFloat3_3; +}; + +struct SGB_GROUP1C_ENTRY +{ + uint32_t unk; + uint32_t unk2; + int32_t nameOffset; + uint32_t index; + uint32_t unk3; + int32_t modelFileOffset; +}; + +struct SGB_GROUP_ENTRY +{ +public: + char* m_buf; + uint32_t m_offset; + + SGB_GROUP_ENTRY() + { + m_buf = nullptr; + m_offset = 0; + }; + + SGB_GROUP_ENTRY( char* buf, uint32_t offset ) + { + m_buf = buf; + m_offset = offset; + }; + + virtual ~SGB_GROUP_ENTRY() + { + }; +}; + +struct SGB_ENTRY_HEADER +{ + SgbGroupEntryType type; + uint32_t unknown2; + int32_t nameOffset; + vec3 translation; + vec3 rotation; + vec3 scale; +}; + +struct SGB_MODEL_HEADER : + public SGB_ENTRY_HEADER +{ + int32_t modelFileOffset; + int32_t collisionFileOffset; +}; + +struct SGB_MODEL_ENTRY : + public SGB_GROUP_ENTRY +{ + SGB_MODEL_HEADER header; + SgbGroupEntryType type; + std::string name; + std::string modelFileName; + std::string collisionFileName; + + SGB_MODEL_ENTRY( char* buf, uint32_t offset, SgbGroupEntryType type ) + { + this->type = type; + header = *reinterpret_cast< SGB_MODEL_HEADER* >( buf + offset ); + name = std::string( buf + offset + header.nameOffset ); + modelFileName = std::string( buf + offset + header.modelFileOffset ); + collisionFileName = std::string( buf + offset + header.collisionFileOffset ); + } +}; + +struct SGB_GROUP +{ + SGB_GROUP_HEADER header; + std::string name; + SGB_FILE* parent; + std::vector< std::shared_ptr< SGB_GROUP_ENTRY > > entries; + + SGB_GROUP( char* buf, SGB_FILE* file, std::set< std::string >* offset1cObjects, uint32_t fileSize, uint32_t offset, bool isOffset1C = false ) + { + parent = file; + + + if( isOffset1C ) + { + auto header1c = *reinterpret_cast< SGB_GROUP1C_HEADER* >( buf + offset ); + + auto entriesOffset = offset + sizeof( header1c ); + + auto entryCount = header1c.entryCount; + for( auto i = 0; i < entryCount; ++i ) + { + auto entryOffset = entriesOffset + ( i * 24 ); + auto entry = *reinterpret_cast< SGB_GROUP1C_ENTRY* >( buf + entryOffset ); + + std::string entryModelFile( buf + entryOffset + entry.modelFileOffset + 9 ); + if( entryModelFile.find( ".sgb" ) != std::string::npos ) + { + offset1cObjects->emplace( entryModelFile ); + } + } + return; + } + auto entriesOffset = offset + sizeof( header ); + + header = *reinterpret_cast< SGB_GROUP_HEADER* >( buf + offset ); + name = std::string( buf + offset + header.nameOffset ); + + for( auto i = 0; i < header.entryCount; ++i ) + { + auto entryOffset = entriesOffset + *reinterpret_cast< uint32_t* >( buf + ( entriesOffset + ( i * 4 ) ) ); + if( entryOffset > fileSize ) + throw std::runtime_error( "SGB_GROUP entry offset was larger than SGB file size!" ); + auto type = *reinterpret_cast< uint32_t* >( buf + entryOffset ); + if( type == SgbGroupEntryType::Model || type == SgbGroupEntryType::Gimmick ) + { + entries.push_back( std::make_shared< SGB_MODEL_ENTRY >( buf, entryOffset, ( SgbGroupEntryType )type ) ); + } + else + { + // std::cout << "\t\tUnknown SGB entry! Group: " << name << " type: " << type << " index: " << i << " entryOffset: " << entryOffset << "\n"; + } + } + } +}; + +struct SGB_HEADER +{ + char magic[4]; // SGB1 + uint32_t fileSize; + uint32_t unknown1; + char magic2[4]; // SCN1 + + uint32_t unknown10; + int32_t sharedOffset; + uint32_t unknown18; + int32_t offset1C; + + uint32_t unknown20; + uint32_t unknown24; + uint32_t unknown28; + uint32_t unknown2C; + + uint32_t unknown30; + uint32_t unknown34; + uint32_t unknown38; + uint32_t unknown3C; + + uint32_t unknown40; + uint32_t unknown44; + uint32_t unknown48; + uint32_t unknown4C; + + uint32_t unknown50; + uint32_t unknown54; +}; + +struct SGB_FILE +{ + SGB_HEADER header; + std::vector< SGB_GROUP > entries; + std::set< std::string > offset1cObjects; + + SGB_FILE() + { + memset( &header, 0, sizeof( header ) ); + } + + SGB_FILE( char* buf ) + { + constexpr int baseOffset = 0x14; + header = *reinterpret_cast< SGB_HEADER* >( buf ); + + if( strncmp( &header.magic[ 0 ], "SGB1", 4 ) != 0 || strncmp( &header.magic2[ 0 ], "SCN1", 4 ) != 0 ) + throw std::runtime_error( "Unable to load SGB File!" ); + + try + { + auto group = SGB_GROUP( buf, this, &offset1cObjects, header.fileSize, baseOffset + header.sharedOffset ); + entries.push_back( group ); + auto group2 = SGB_GROUP( buf, this, &offset1cObjects, header.fileSize, baseOffset+ header.offset1C, true ); + entries.push_back( group2 ); + } + catch( std::exception& e ) + { + std::cout << ( std::string( e.what() ) + "\n" ); + } + }; +}; + + +#endif // !_SGB_H diff --git a/src/tools/nav_export/threadpool.h b/src/tools/nav_export/threadpool.h new file mode 100644 index 00000000..78657bd3 --- /dev/null +++ b/src/tools/nav_export/threadpool.h @@ -0,0 +1,108 @@ +#ifndef THREADPOOL_H +#define THREADPOOL_H + +#include +#include +#include +#include +#include +#include +#include +#include + +// credit to +// https://riptutorial.com/cplusplus/example/15806/create-a-simple-thread-pool + +class ThreadPool +{ +public: + ThreadPool() + { + + } + + ~ThreadPool() + { + complete(); + } + + void addWorkers( unsigned int num ) + { + + std::unique_lock lock( m_mutex ); + m_runFlag = true; + if( num == 0 ) + num = std::thread::hardware_concurrency() - 1; + + for( auto i = 0; i < num; ++i ) + { + m_workers.push_back( std::async( std::launch::async, [this]{ run(); } ) ); + } + } + + template< class Func, class Ret = std::result_of_t< Func&() > > + std::future< Ret > queue( Func&& f ) + { + std::packaged_task< Ret() > task( std::forward< Func >( f ) ); + auto ret = task.get_future(); + { + std::unique_lock lock( m_mutex ); + m_pendingJobs.emplace_back( std::move( task ) ); + } + m_cv.notify_one(); + return ret; + } + + void cancel() + { + { + std::unique_lock lock( m_mutex ); + m_pendingJobs.clear(); + } + complete(); + } + + bool complete() + { + { + std::unique_lock lock( m_mutex ); + for( auto&& worker : m_workers ) + { + m_pendingJobs.push_back( {} ); + } + } + m_cv.notify_all(); + m_workers.clear(); + return true; + } +private: + void run() + { + while( 1 ) + { + std::packaged_task< void() > func; + { + std::unique_lock lock( m_mutex ); + if( m_pendingJobs.empty() ) + { + m_cv.wait( lock, [&](){ return !m_pendingJobs.empty(); } ); + } + func = std::move( m_pendingJobs.front() ); + m_pendingJobs.pop_front(); + } + if( !func.valid() ) + { + return; + } + func(); + } + } + + bool m_runFlag{ true }; + std::mutex m_mutex; + std::condition_variable m_cv; + std::deque< std::packaged_task< void() > > m_pendingJobs; + std::vector< std::future< void > > m_workers; +}; + +#endif \ No newline at end of file diff --git a/src/tools/nav_export/vec3.h b/src/tools/nav_export/vec3.h new file mode 100644 index 00000000..2ff35fa2 --- /dev/null +++ b/src/tools/nav_export/vec3.h @@ -0,0 +1,34 @@ +#ifndef _VEC3_H +#define _VEC3_H + +#include +#include "matrix4.h" + +struct vec3 +{ + float x, y, z; + + vec3() + { + x = 0.0f; + y = 0.0f; + z = 0.0f; + } + + vec3( float x, float y, float z ) + { + this->x = x; + this->y = y; + this->z = z; + }; +}; + +static vec3 operator*( const vec3& lhs, const matrix4& rhs ) +{ + vec3 ret; + ret.x = rhs( 0, 0 ) * lhs.x + rhs( 0, 1 ) * lhs.y + rhs( 0, 2 ) * lhs.z; + ret.y = rhs( 1, 0 ) * lhs.x + rhs( 1, 1 ) * lhs.y + rhs( 1, 2 ) * lhs.z; + ret.z = rhs( 2, 0 ) * lhs.x + rhs( 2, 1 ) * lhs.y + rhs( 2, 2 ) * lhs.z; + return ret; +}; +#endif \ No newline at end of file