1
Fork 0
mirror of https://github.com/SapphireServer/Sapphire.git synced 2025-04-27 06:47:45 +00:00

Merge pull request #507 from NotAdam/develop

bnpc fixes, headless navmesh exporting
This commit is contained in:
Mordred 2019-01-26 16:30:13 +01:00 committed by GitHub
commit 1a17241bae
28 changed files with 1881 additions and 453 deletions

View file

@ -1,6 +1,7 @@
if( UNIX )
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -fPIC" )
set( CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O3")
else()
add_definitions( -D_WIN32_WINNT=0x601 )
add_definitions( -D_CRT_SECURE_NO_WARNINGS )

View file

@ -57,9 +57,16 @@ GameData::GameData(const std::experimental::filesystem::path& path) try :
m_path(path)
{
int maxExLevel = 0;
// msvc has retarded stdlib implementation
#ifdef _WIN32
static constexpr auto sep = "\\";
#else
static constexpr auto sep = std::experimental::filesystem::path::preferred_separator;
#endif
// Determine which expansions are available
while( std::experimental::filesystem::exists( std::experimental::filesystem::path( m_path.string() + "\\ex" + std::to_string( maxExLevel + 1) + "\\ex" + std::to_string( maxExLevel + 1) + ".ver" ) ) )
while( std::experimental::filesystem::exists( std::experimental::filesystem::path( m_path.string() + sep + "ex" + std::to_string( maxExLevel + 1 ) + sep + "ex" + std::to_string( maxExLevel + 1 ) + ".ver" ) ) )
{
maxExLevel++;
}
@ -90,7 +97,7 @@ GameData::GameData(const std::experimental::filesystem::path& path) try :
// Check for expansion
for( int exNum = 1; exNum <= maxExLevel; exNum++ )
{
const std::string path = m_path.string() + "\\" + buildDatStr( "ex" + std::to_string( exNum ), cat_nb, exNum, 0, "win32", "index" );
const std::string path = m_path.string() + sep + buildDatStr( "ex" + std::to_string( exNum ), cat_nb, exNum, 0, "win32", "index" );
if( std::experimental::filesystem::exists( std::experimental::filesystem::path( path ) ) )
{
@ -99,7 +106,7 @@ GameData::GameData(const std::experimental::filesystem::path& path) try :
for(int chunkTest = 0; chunkTest < 256; chunkTest++ )
{
if( std::experimental::filesystem::exists( m_path.string() + "\\" + buildDatStr( "ex" + std::to_string( exNum ), cat_nb, exNum, chunkTest, "win32", "index" ) ) )
if( std::experimental::filesystem::exists( m_path.string() + sep + buildDatStr( "ex" + std::to_string( exNum ), cat_nb, exNum, chunkTest, "win32", "index" ) ) )
{
m_exCats[cat_nb].exNumToChunkMap[exNum].chunkToCatMap[chunkTest] = std::unique_ptr<Cat>();
chunkCount++;

View file

@ -615,6 +615,7 @@ namespace Sapphire::Common
InvincibilityNone,
InvincibilityRefill,
InvincibilityStayAlive,
InvincibilityIgnoreDamage,
};
enum PlayerStateFlag : uint8_t

View file

@ -666,7 +666,7 @@ struct FFXIVIpcActorMove :
{
/* 0000 */ uint8_t rotation;
/* 0001 */ uint8_t unknown_1;
/* 0002 */ uint8_t unknown_2;
/* 0002 */ uint8_t animationType;
/* 0003 */ uint8_t unknown_3;
/* 0004 */ uint16_t unknown_4;
/* 0006 */ uint16_t posX;

View file

@ -3,14 +3,17 @@ cmake_policy(SET CMP0015 NEW)
project(Tool_pcb_reader2)
file(GLOB SERVER_PUBLIC_INCLUDE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*")
file(GLOB SERVER_SOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}*.c*")
file(GLOB SERVER_SOURCE_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
*.c*
nav/*.c*
nav/ext/*.c*)
add_executable(pcb_reader2 ${SERVER_PUBLIC_INCLUDE_FILES} ${SERVER_SOURCE_FILES})
if (UNIX)
target_link_libraries( pcb_reader2 common xivdat pthread mysqlclient dl z stdc++fs Recast Detour )
target_link_libraries( pcb_reader2 common xivdat pthread mysqlclient dl z stdc++fs Recast Detour DetourTileCache )
else()
target_link_libraries( pcb_reader2 common xivdat mysql zlib Recast Detour )
target_link_libraries( pcb_reader2 common xivdat mysql zlib Recast Detour DetourTileCache )
endif()
target_include_directories( pcb_reader2

View file

@ -83,7 +83,7 @@ private:
m_lgbCache.clear();
m_sgbCache.clear();
m_pcbCache.clear();
std::cout << "Purged PCB/SGB/PCB cache \n";
std::cout << "Purged PCB/SGB/LGB cache \n";
m_totalFiles = 1;
}

View file

@ -20,14 +20,14 @@ public:
void exportZone(const ExportedZone& zone, ExportFileType exportFileTypes)
{
if( exportFileTypes & ExportFileType::WavefrontObj )
m_threadpool.queue( [zone, exportFileTypes]()
{
m_threadpool.queue( [zone](){ ObjExporter::exportZone( zone ); } );
}
if( exportFileTypes & ExportFileType::Navmesh )
{
m_threadpool.queue( [zone](){ NavmeshExporter::exportZone( zone ); } );
}
if( exportFileTypes & ExportFileType::WavefrontObj )
ObjExporter::exportZone( zone );
if( exportFileTypes & ExportFileType::Navmesh )
NavmeshExporter::exportZone( zone );
} );
}
void exportGroup( const std::string& zoneName, const ExportedGroup& group, ExportFileType exportFileTypes )
@ -36,10 +36,10 @@ public:
{
m_threadpool.queue( [zoneName, group](){ ObjExporter::exportGroup( zoneName, group ); } );
}
if( exportFileTypes & ExportFileType::Navmesh )
{
m_threadpool.queue( [zoneName, group](){ NavmeshExporter::exportGroup( zoneName, group ); } );
}
// if( exportFileTypes & ExportFileType::Navmesh )
// {
// m_threadpool.queue( [zoneName, group](){ NavmeshExporter::exportGroup( zoneName, group ); } );
// }
}
void waitForTasks()

View file

@ -9,6 +9,10 @@
#include <map>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
#include "matrix4.h"
#include "vec3.h"
#include "sgb.h"
@ -238,6 +242,47 @@ public:
};
};
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;
@ -289,7 +334,11 @@ struct LGB_GROUP
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;
}
}

View file

@ -34,12 +34,15 @@ 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;
@ -61,6 +64,49 @@ void initExd( const std::string& gamePath )
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 )
{
@ -116,7 +162,10 @@ int main( int argc, char* argv[] )
{ return arg == "--dump-all"; } ) != argVec.end();
bool generateNavmesh = std::remove_if( argVec.begin(), argVec.end(), []( auto arg )
{ return arg == "--navmesh"; } ) != argVec.end();
bool splitByGroup = std::remove_if( argVec.begin(), argVec.end(), []( auto arg )
{ return arg == "--split-by-group"; }) != argVec.end();
bool splitByZone = std::remove_if( argVec.begin(), argVec.end(), []( auto arg )
{ return arg == "--split-by-zone"; }) != argVec.end();
int exportFileType = 0;
if( !noObj )
exportFileType |= ExportFileType::WavefrontObj;
@ -137,10 +186,11 @@ int main( int argc, char* argv[] )
try
{
initExd( gamePath );
getEobjSgbPath( 0 );
}
catch( std::exception& e )
{
printf( "Unable to initialise EXD! Usage: pcb_reader <teri> \"path/to/FINAL FANTASY XIV - A REALM REBORN/game/sqpack\" [--no-obj, --dump-all, --navmesh]" );
printf( "Unable to initialise EXD!\n Usage: pcb_reader <teri> \"path/to/FINAL FANTASY XIV - A REALM REBORN/game/sqpack\" [--no-obj, --dump-all, --navmesh]\n" );
return -1;
}
ExportMgr exportMgr;
@ -156,14 +206,19 @@ int main( int argc, char* argv[] )
zoneDumpList.emplace( zoneName );
}
for( const auto& zoneName : zoneDumpList )
for( auto zoneName : zoneDumpList )
{
try
{
const auto& zonePath = zoneNameToPath( zoneName );
if( exportedTeriMap.find( zonePath ) != exportedTeriMap.end() )
continue;
zoneName = zonePath.substr( zonePath.find_last_of( '/' ) );
ExportedZone exportedZone;
exportedZone.name = zoneName;
const auto& zonePath = zoneNameToPath( zoneName );
exportedTeriMap[ zonePath ] = zoneName;
std::string listPcbPath( zonePath + "/collision/list.pcb" );
std::string bgLgbPath( zonePath + "/level/bg.lgb" );
@ -324,8 +379,8 @@ int main( int argc, char* argv[] )
if( auto pPcbFile = pCache->getPcbFile( fileName ) )
buildModelEntry( pPcbFile, exportedTerrainGroup, fileName, zoneName );
}
exportMgr.exportGroup( zoneName, exportedTerrainGroup, ( ExportFileType )exportFileType );
exportedZone.groups.emplace( exportedTerrainGroup.name, exportedTerrainGroup );
for( const auto& lgb : lgbList )
{
for( const auto& group : lgb.groups )
@ -351,12 +406,51 @@ int main( int argc, char* argv[] )
}
return true;
};
auto exportSgbModel = [&]( const std::string& sgbFilePath, LGB_ENTRY* pGimmick, 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() );
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, pSubModel );
}
}
}
}
pcbTransformModel( fileName, &pGimmick->header.scale, &pGimmick->header.rotation,
&pGimmick->header.translation, pModel );
}
}
}
};
switch( pEntry->getType() )
{
case LgbEntryType::BgParts:
{
auto pBgParts = static_cast<LGB_BGPARTS_ENTRY*>(pEntry.get());
auto pBgParts = static_cast< LGB_BGPARTS_ENTRY* >( pEntry.get() );
fileName = pBgParts->collisionFileName;
pcbTransformModel( fileName, &pBgParts->header.scale, &pBgParts->header.rotation,
&pBgParts->header.translation );
@ -366,37 +460,41 @@ int main( int argc, char* argv[] )
// gimmick entry
case LgbEntryType::Gimmick:
{
auto pGimmick = static_cast<LGB_GIMMICK_ENTRY*>( pEntry.get() );
if( auto pSgbFile = pCache->getSgbFile( pGimmick->gimmickFileName ) )
{
const auto& sgbFile = *pSgbFile;
for( const auto& group : sgbFile.entries )
{
for( const auto& pEntry : group.entries )
{
auto pModel = dynamic_cast< SGB_MODEL_ENTRY* >( pEntry.get() );
fileName = pModel->collisionFileName;
pcbTransformModel( fileName, &pGimmick->header.scale, &pGimmick->header.rotation,
&pGimmick->header.translation, pModel );
}
}
}
auto pGimmick = static_cast< LGB_GIMMICK_ENTRY* >( pEntry.get() );
exportSgbModel( pGimmick->gimmickFileName, pGimmick );
}
break;
case LgbEntryType::EventObject:
{
auto pEobj = static_cast< LGB_EOBJ_ENTRY* >( pEntry.get() );
pcbTransformModel( fileName, &pEntry->header.scale, &pEntry->header.rotation, &pEntry->header.translation );
auto sgbPath = getEobjSgbPath( pEobj->header.eobjId );
if ( !sgbPath.empty() )
{
exportSgbModel( sgbPath, pEobj, true );
if( auto pGimmick = pCache->getSgbFile( sgbPath ) )
{
for( const auto& offset1cFile : pGimmick->offset1cObjects )
exportSgbModel( offset1cFile, pEobj, true );
}
}
}
break;
default:
break;
}
}
exportMgr.exportGroup( zoneName, exportedGroup, ( ExportFileType )exportFileType );
//exportedZone.groups.emplace( group.name, exportedGroup );
if( splitByGroup )
exportMgr.exportGroup( zoneName, exportedGroup, ( ExportFileType )exportFileType );
exportedZone.groups.emplace( group.name, exportedGroup );
}
}
//exportMgr.exportZone( exportedZone, ( ExportFileType )exportFileType );
exportMgr.exportZone( exportedZone, static_cast< ExportFileType >( exportFileType ) );
printf( "Exported %s in %lu seconds \n",
@ -416,8 +514,6 @@ int main( int argc, char* argv[] )
printf( "Finished all tasks in %lu seconds\n",
std::chrono::duration_cast< std::chrono::seconds >( std::chrono::high_resolution_clock::now() - startTime ).count() );
getchar();
delete eData;
delete data1;

View file

@ -0,0 +1,581 @@
#include "TiledNavmeshGenerator.h"
#include <experimental/filesystem>
#include <cstring>
#include <recastnavigation/Detour/Include/DetourNavMeshBuilder.h>
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()
{
if( m_mesh )
delete m_mesh;
if( m_chunkyMesh )
delete m_chunkyMesh;
if( m_ctx )
delete m_ctx;
dtFreeNavMesh( m_navMesh );
dtFreeNavMeshQuery( m_navQuery );
}
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( &params );
if( dtStatusFailed( status ) )
{
printf( "[Navmesh] buildTiledNavigation: Could not init navmesh.\n" );
return false;
}
m_navQuery = dtAllocNavMeshQuery();
assert( m_navQuery );
status = m_navQuery->init( m_navMesh, 2048 );
if( dtStatusFailed( status ) )
{
printf( "[Navmesh] buildTiledNavigation: Could not init Detour navmesh query\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( &params, 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( &params, &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;
}

View file

@ -0,0 +1,124 @@
#ifndef SAPPHIRE_TILEDNAVMESHGENERATOR_H
#define SAPPHIRE_TILEDNAVMESHGENERATOR_H
#include <string>
#include <cassert>
#include <cmath>
#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;
dtNavMeshQuery* m_navQuery;
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

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <math.h>
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<size_t>(inum), sizeof( BoundsItem ), compareItemX );
}
else if( axis == 1 )
{
// Sort along y-axis
qsort( items + imin, static_cast<size_t>(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;
}

View file

@ -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

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <cstring>
#define _USE_MATH_DEFINES
#include <math.h>
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;
}

View file

@ -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 <string>
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

View file

@ -9,17 +9,13 @@
#include <chrono>
#include "exporter.h"
/*
#include <recastnavigation/Recast/Include/Recast.h>
#include <recastnavigation/Recast/Include/RecastAlloc.h>
#include <recastnavigation/Detour/Include/DetourNavMesh.h>
#include <recastnavigation/Detour/Include/DetourNavMeshBuilder.h>
#include <recastnavigation/DetourTileCache/Include/DetourTileCache.h>
#include <recastnavigation/DetourTileCache/Include/DetourTileCacheBuilder.h>
#include <recastnavigation/RecastDemo/Include/ChunkyTriMesh.h>
#include <recastnavigation/RecastDemo/Include/InputGeom.h>
#include <recastnavigation/RecastDemo/Include/Sample.h>
*/
#include "obj_exporter.h"
#include "nav/TiledNavmeshGenerator.h"
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
class NavmeshExporter
{
public:
@ -27,11 +23,35 @@ public:
{
auto start = std::chrono::high_resolution_clock::now();
auto fileName = zone.name + ".obj";
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;
std::error_code e;
if( !fs::exists( fileName, e ) )
ObjExporter::exportZone( zone );
TiledNavmeshGenerator gen;
auto objName = fileName + ".obj";
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",
fileName.c_str(),
zone.name.c_str(),
std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() );
}
@ -39,344 +59,38 @@ public:
{
auto start = std::chrono::high_resolution_clock::now();
auto fileName = zoneName + "_" + group.name + ".obj";
static std::string currPath = std::experimental::filesystem::current_path().string();
auto dir = fs::current_path().string() + "/pcb_export/" + zoneName + "/";
auto fileName = dir + zoneName + "_" + group.name;
std::error_code e;
if( !fs::exists( fileName, e ) )
ObjExporter::exportGroup( zoneName, group );
TiledNavmeshGenerator gen;
auto objName = fileName + ".obj";
if( !gen.init( objName ) )
{
printf( "[Navmesh] failed to init TiledNavmeshGenerator for file '%s'\n", fileName.c_str() );
return;
}
if( !gen.buildNavmesh() )
{
printf( "[Navmesh] Failed to build navmesh for '%s'\n", fileName.c_str() );
return;
}
gen.saveNavmesh( fileName );
auto end = std::chrono::high_resolution_clock::now();
printf( "[Navmesh] Finished exporting %s in %lu ms\n",
fileName.c_str(),
std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() );
}
private:
/*/
static unsigned char* buildTileMesh( const ExportedGroup& group, int tx, int ty )
{
unsigned char* navData;
rcConfig cfg;
cfg.ch = 0.2f;
cfg.cs = 0.2f;
cfg.walkableHeight = 2.f;
cfg.walkableRadius = 0.5;
cfg.walkableClimb = 0.6;
cfg.walkableSlopeAngle = 58.f;
cfg.minRegionArea = 8.0f;
cfg.mergeRegionArea = 20.f;
cfg.maxEdgeLen = 12.f;
cfg.maxSimplificationError = 1.4f;
cfg.maxVertsPerPoly = 6.f;
cfg.detailSampleDist = 6.f;
cfg.detailSampleMaxError = 1.f;
cfg.tileSize = 160.f;
cfg.walkableHeight = (int)ceilf( cfg.walkableHeight / cfg.ch );
cfg.walkableClimb = (int)floorf( cfg.walkableClimb / cfg.ch );
cfg.walkableRadius = (int)ceilf( cfg.walkableRadius / cfg.cs );
cfg.maxEdgeLen = (int)( cfg.maxEdgeLen / cfg.cs );
cfg.minRegionArea = (int)rcSqr( cfg.minRegionArea ); // Note: area = size*size
cfg.mergeRegionArea = (int)rcSqr( cfg.mergeRegionArea ); // Note: area = size*size
cfg.borderSize = cfg.walkableRadius + 3; // Reserve enough padding.
cfg.width = cfg.tileSize + cfg.borderSize*2;
cfg.height = cfg.tileSize + cfg.borderSize*2;
cfg.detailSampleDist = cfg.detailSampleDist < 0.9f ? 0 : cfg.cs * cfg.detailSampleDist;
cfg.detailSampleMaxError = cfg.ch * cfg.detailSampleMaxError;
rcContext ctx;
auto hf = rcAllocHeightfield();
auto chf = rcAllocCompactHeightfield();
auto cs = rcAllocContourSet();
auto pmesh = rcAllocPolyMesh();
auto pdetailmesh = rcAllocPolyMeshDetail();
std::vector< float > verts;
std::vector< int > indices;
int i = 0;
int numIndices = 0;
for( const auto& model : group.models )
{
for( const auto& mesh : model.second.meshes )
{
auto size = mesh.verts.size();
rcCalcBounds( mesh.verts.data(), size / 3, &cfg.bmin[0], &cfg.bmax[0] );
verts.reserve( verts.size() + size );
memcpy( &verts[i], mesh.verts.data(), size );
i += size;
size = mesh.indices.size();
indices.reserve( indices.size() + size );
for( auto j = 0; j < mesh.indices.size(); j += 3 )
{
indices[j] = mesh.indices[j] + numIndices;
indices[j + 1] = mesh.indices[j + 1] + numIndices;
indices[j + 2] = mesh.indices[j + 2] + numIndices;
}
numIndices += size;
}
}
auto chunkyMesh = new rcChunkyTriMesh;
rcCreateChunkyTriMesh( &verts[0], &indices[0], verts.size() / 3, 256, chunkyMesh );
if( !rcCreateHeightfield( &ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch ) )
{
}
float tbmin[2], tbmax[2];
tbmin[0] = cfg.bmin[0];
tbmin[1] = cfg.bmin[2];
tbmax[0] = cfg.bmax[0];
tbmax[1] = cfg.bmax[2];
int cid[512];// TODO: Make grow when returning too many items.
const int ncid = rcGetChunksOverlappingRect(chunkyMesh, tbmin, tbmax, cid, 512);
if (!ncid)
return 0;
auto tileTriCount = 0;
auto triareas = new unsigned char[chunkyMesh->maxTrisPerChunk];
for (int i = 0; i < ncid; ++i)
{
const rcChunkyTriMeshNode& node = chunkyMesh->nodes[cid[i]];
const int* ctris = &chunkyMesh->tris[node.i*3];
const int nctris = node.n;
tileTriCount += nctris;
memset(triareas, 0, nctris*sizeof(unsigned char));
rcMarkWalkableTriangles(&ctx, cfg.walkableSlopeAngle,
&verts[0], verts.size() / 3, ctris, nctris, triareas);
if (!rcRasterizeTriangles(&ctx, &verts[0], verts.size() / 3, ctris, triareas, nctris, *hf, cfg.walkableClimb))
return 0;
}
{
delete [] triareas;
triareas = 0;
}
// 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(&ctx, cfg.walkableClimb, *hf);
rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf);
rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf);
// 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.
chf = rcAllocCompactHeightfield();
if (!chf)
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'chf'.");
return 0;
}
if (!rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build compact data.");
return 0;
}
{
rcFreeHeightField(hf);
hf = 0;
}
// Erode the walkable area by agent radius.
if (!rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could not erode.");
return 0;
}
// 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(&ctx, *chf))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build distance field.");
return 0;
}
// Partition the walkable surface into simple regions without holes.
if (!rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build watershed regions.");
return 0;
}
}
//else if (m_partitionType == SAMPLE_PARTITION_MONOTONE)
//{
// // Partition the walkable surface into simple regions without holes.
// // Monotone partitioning does not need distancefield.
// if (!rcBuildRegionsMonotone(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea))
// {
// ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build monotone regions.");
// return 0;
// }
//}
//else // SAMPLE_PARTITION_LAYERS
//{
// // Partition the walkable surface into simple regions without holes.
// if (!rcBuildLayerRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea))
// {
// ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build layer regions.");
// return 0;
// }
//}
// Create contours.
cs = rcAllocContourSet();
if (!cs)
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'cset'.");
return 0;
}
if (!rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could not create contours.");
return 0;
}
if (cs->nconts == 0)
{
return 0;
}
// Build polygon navmesh from the contours.
pmesh = rcAllocPolyMesh();
if (!pmesh)
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'pmesh'.");
return 0;
}
if (!rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pmesh))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could not triangulate contours.");
return 0;
}
// Build detail mesh.
pdetailmesh = rcAllocPolyMeshDetail();
if (!pdetailmesh)
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'dmesh'.");
return 0;
}
if (!rcBuildPolyMeshDetail(&ctx, *pmesh, *chf,
cfg.detailSampleDist, cfg.detailSampleMaxError,
*pdetailmesh))
{
ctx.log(RC_LOG_ERROR, "buildNavigation: Could build polymesh detail.");
return 0;
}
{
rcFreeCompactHeightfield(chf);
chf = 0;
rcFreeContourSet(cs);
cs = 0;
}
unsigned char* navData = 0;
int navDataSize = 0;
if (cfg.maxVertsPerPoly <= DT_VERTS_PER_POLYGON)
{
if (pmesh->nverts >= 0xffff)
{
// The vertex indices are ushorts, and cannot point to more than 0xffff vertices.
ctx.log(RC_LOG_ERROR, "Too many vertices per tile %d (max: %d).", pmesh->nverts, 0xffff);
return 0;
}
// Update poly flags from areas.
for (int i = 0; i < pmesh->npolys; ++i)
{
//pmesh->flags[i] = sampleAreaToFlags(pmesh->areas[i]);
}
dtNavMeshCreateParams params;
memset(&params, 0, sizeof(params));
params.verts = pmesh->verts;
params.vertCount = pmesh->nverts;
params.polys = pmesh->polys;
params.polyAreas = pmesh->areas;
params.polyFlags = pmesh->flags;
params.polyCount = pmesh->npolys;
params.nvp = pmesh->nvp;
params.detailMeshes = pdetailmesh->meshes;
params.detailVerts = pdetailmesh->verts;
params.detailVertsCount = pdetailmesh->nverts;
params.detailTris = pdetailmesh->tris;
params.detailTriCount = pdetailmesh->ntris;
params.offMeshConVerts = 0;
params.offMeshConRad = 0;
params.offMeshConDir = 0;
params.offMeshConAreas = 0;
params.offMeshConFlags = 0;
params.offMeshConUserID = 0;
params.offMeshConCount = 0;
params.walkableHeight = cfg.walkableHeight;
params.walkableRadius = cfg.walkableRadius;
params.walkableClimb = cfg.walkableClimb;
params.tileX = 0;
params.tileY = 0;
params.tileLayer = 0;
rcVcopy(params.bmin, pmesh->bmin);
rcVcopy(params.bmax, pmesh->bmax);
params.cs = cfg.cs;
params.ch = cfg.ch;
params.buildBvTree = true;
if (!dtCreateNavMeshData(&params, &navData, &navDataSize))
{
ctx.log(RC_LOG_ERROR, "Could not build Detour navmesh.");
return 0;
}
}
auto tileMemUsage = navDataSize/1024.0f;
ctx.stopTimer(RC_TIMER_TOTAL);
// Show performance stats.
//duLogBuildTimes(*&ctx, ctx.getAccumulatedTime(RC_TIMER_TOTAL));
ctx.log(RC_LOG_PROGRESS, ">> Polymesh: %d vertices %d polygons", pmesh->nverts, pmesh->npolys);
auto tileBuildTime = ctx.getAccumulatedTime(RC_TIMER_TOTAL)/1000.0f;
auto dataSize = navDataSize;
return navData;
}
//*/
};
#endif // !OBJ_EXPORTER_H

View file

@ -14,23 +14,23 @@
class ObjExporter
{
public:
static void exportZone( const ExportedZone& zone )
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 + "/" + zone.name + "/";
auto fileName = dir + "/" + zone.name + ".obj";
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_directory( dir, e ) )
if( !std::experimental::filesystem::create_directories( dir, e ) )
{
printf( "Unable to create directory '%s'", ( dir ).c_str() );
return;
return "";
}
}
std::ofstream of( fileName, std::ios::trunc );
@ -50,27 +50,29 @@ public:
}
auto end = std::chrono::high_resolution_clock::now();
printf( "[Obj] Finished exporting %s in %lu ms\n",
fileName.c_str(),
std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() );
fileName.substr( fileName.find( "pcb_export" ) - 1 ).c_str(),
std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() );
return fileName;
}
static void exportGroup( const std::string& zoneName, const ExportedGroup& group )
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 + "/" + zoneName + "/";
auto fileName = dir + "/" + group.name + ".obj";
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_directory( dir, e ) )
if( !std::experimental::filesystem::create_directories( dir, e ) )
{
printf( "Unable to create directory '%s'", ( dir ).c_str() );
return;
return "";
}
}
std::ofstream of( fileName, std::ios::trunc );
@ -88,8 +90,10 @@ public:
auto end = std::chrono::high_resolution_clock::now();
printf( "[Obj] Finished exporting %s in %lu ms\n",
fileName.c_str(),
std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() );
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 )

View file

@ -36,6 +36,7 @@ enum SgbGroupEntryType :
uint32_t
{
Model = 0x01,
Gimmick = 0x06,
};
struct SGB_GROUP_HEADER
@ -64,6 +65,35 @@ struct SGB_GROUP_HEADER
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:
@ -113,8 +143,9 @@ struct SGB_MODEL_ENTRY :
std::string modelFileName;
std::string collisionFileName;
SGB_MODEL_ENTRY( char* buf, uint32_t offset )
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 );
@ -129,23 +160,45 @@ struct SGB_GROUP
SGB_FILE* parent;
std::vector< std::shared_ptr< SGB_GROUP_ENTRY > > entries;
SGB_GROUP( char* buf, SGB_FILE* file, uint32_t fileSize, uint32_t offset )
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 );
auto entriesOffset = offset + sizeof( header );
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 )
if( type == SgbGroupEntryType::Model || type == SgbGroupEntryType::Gimmick )
{
entries.push_back( std::make_shared< SGB_MODEL_ENTRY >( buf, entryOffset ) );
entries.push_back( std::make_shared< SGB_MODEL_ENTRY >( buf, entryOffset, ( SgbGroupEntryType )type ) );
}
else
{
@ -190,6 +243,7 @@ struct SGB_FILE
{
SGB_HEADER header;
std::vector< SGB_GROUP > entries;
std::set< std::string > offset1cObjects;
SGB_FILE()
{
@ -206,9 +260,9 @@ struct SGB_FILE
try
{
auto group = SGB_GROUP( buf, this, header.fileSize, baseOffset + header.sharedOffset );
auto group = SGB_GROUP( buf, this, &offset1cObjects, header.fileSize, baseOffset + header.sharedOffset );
entries.push_back( group );
auto group2 = SGB_GROUP( buf, this, header.fileSize, baseOffset + header.offset1C );
auto group2 = SGB_GROUP( buf, this, &offset1cObjects, header.fileSize, baseOffset+ header.offset1C, true );
entries.push_back( group2 );
}
catch( std::exception& e )

View file

@ -10,6 +10,9 @@
#include <mutex>
#include <thread>
// credit to
// https://riptutorial.com/cplusplus/example/15806/create-a-simple-thread-pool
class ThreadPool
{
public:
@ -55,23 +58,20 @@ public:
{
std::unique_lock lock( m_mutex );
m_pendingJobs.clear();
for( auto&& worker : m_workers )
{
m_pendingJobs.emplace( {} );
}
}
m_cv.notify_all();
m_workers.clear();
complete();
}
bool complete()
{
m_cv.notify_all();
{
std::unique_lock lock( m_mutex );
m_runFlag = false;
m_cv.wait( lock, [&]{ return m_pendingJobs.empty(); } );
for( auto&& worker : m_workers )
{
m_pendingJobs.push_back( {} );
}
}
m_cv.notify_all();
m_workers.clear();
return true;
}
@ -85,11 +85,6 @@ private:
std::unique_lock lock( m_mutex );
if( m_pendingJobs.empty() )
{
if( !m_runFlag )
{
m_cv.notify_all();
return;
}
m_cv.wait( lock, [&](){ return !m_pendingJobs.empty(); } );
}
func = std::move( m_pendingJobs.front() );

View file

@ -174,9 +174,9 @@ void Sapphire::Entity::BNpc::step()
// This is probably not a good way to do it but works fine for now
float angle = Util::calcAngFrom( getPos().x, getPos().z, stepPos.x, stepPos.z ) + PI;
auto x = ( cosf( angle ) * 1.7f );
auto x = ( cosf( angle ) * .5f );
auto y = stepPos.y;
auto z = ( sinf( angle ) * 1.7f );
auto z = ( sinf( angle ) * .5f );
face( stepPos );
setPos( { getPos().x + x, y, getPos().z + z } );
@ -229,7 +229,14 @@ bool Sapphire::Entity::BNpc::moveTo( const FFXIVARR_POSITION3& pos )
void Sapphire::Entity::BNpc::sendPositionUpdate()
{
auto movePacket = std::make_shared< MoveActorPacket >( *getAsChara(), 0x3A, 0, 0, 0x5A );
uint8_t unk1 = 0x3a;
uint8_t animationType = 2;
if( m_state == BNpcState::Combat )
animationType = 0;
auto movePacket = std::make_shared< MoveActorPacket >( *getAsChara(), unk1, animationType, 0, 0x5A );
sendToInRangeSet( movePacket );
}
@ -363,8 +370,39 @@ void Sapphire::Entity::BNpc::update( int64_t currTime )
case BNpcState::Retreat:
{
setInvincibilityType( InvincibilityType::InvincibilityIgnoreDamage );
// slowly restore hp every tick
if( std::difftime( currTime, m_lastTickTime ) > 3000 )
{
m_lastTickTime = currTime;
if( m_hp < getMaxHp() )
{
auto addHp = static_cast< uint32_t >( getMaxHp() * 0.1f + 1 );
if( m_hp + addHp < getMaxHp() )
m_hp += addHp;
else
m_hp = getMaxHp();
}
sendStatusUpdate();
}
if( moveTo( m_spawnPos ) )
{
setInvincibilityType( InvincibilityType::InvincibilityNone );
// retail doesn't seem to roam straight after retreating
// todo: perhaps requires more investigation?
m_lastRoamTargetReached = Util::getTimeSeconds();
setHp( getMaxHp() );
m_state = BNpcState::Idle;
}
}
break;
@ -468,7 +506,6 @@ void Sapphire::Entity::BNpc::update( int64_t currTime )
}
}
}
}
void Sapphire::Entity::BNpc::onActionHostile( Sapphire::Entity::CharaPtr pSource )

View file

@ -88,7 +88,7 @@ namespace Sapphire::Entity
void onDeath() override;
uint32_t getTimeOfDeath() const;
void setTimeOfDeath( uint32_t timeOfDeath);
void setTimeOfDeath( uint32_t timeOfDeath );
private:
uint32_t m_bNpcBaseId;

View file

@ -151,35 +151,35 @@ uint32_t Sapphire::Entity::Chara::getMaxMp() const
void Sapphire::Entity::Chara::resetHp()
{
m_hp = getMaxHp();
sendStatusUpdate( true );
sendStatusUpdate();
}
/*! \return reset mp to current max mp */
void Sapphire::Entity::Chara::resetMp()
{
m_mp = getMaxMp();
sendStatusUpdate( true );
sendStatusUpdate();
}
/*! \param hp amount to set ( caps to maxHp ) */
void Sapphire::Entity::Chara::setHp( uint32_t hp )
{
m_hp = hp < getMaxHp() ? hp : getMaxHp();
sendStatusUpdate( true );
sendStatusUpdate();
}
/*! \param mp amount to set ( caps to maxMp ) */
void Sapphire::Entity::Chara::setMp( uint32_t mp )
{
m_mp = mp < getMaxMp() ? mp : getMaxMp();
sendStatusUpdate( true );
sendStatusUpdate();
}
/*! \param gp amount to set*/
void Sapphire::Entity::Chara::setGp( uint32_t gp )
{
m_gp = gp;
sendStatusUpdate( true );
sendStatusUpdate();
}
/*! \param type invincibility type to set */
@ -325,12 +325,14 @@ void Sapphire::Entity::Chara::takeDamage( uint32_t damage )
case InvincibilityStayAlive:
setHp( 0 );
break;
case InvincibilityIgnoreDamage:
break;
}
}
else
m_hp -= damage;
sendStatusUpdate( false );
sendStatusUpdate();
}
/*!
@ -349,7 +351,7 @@ void Sapphire::Entity::Chara::heal( uint32_t amount )
else
m_hp += amount;
sendStatusUpdate( false );
sendStatusUpdate();
}
/*!
@ -359,7 +361,7 @@ so players can have their own version and we can abolish the param.
\param true if the update should also be sent to the actor ( player ) himself
*/
void Sapphire::Entity::Chara::sendStatusUpdate( bool toSelf )
void Sapphire::Entity::Chara::sendStatusUpdate()
{
FFXIVPacketBasePtr packet = std::make_shared< UpdateHpMpTpPacket >( *this );
sendToInRangeSet( packet );
@ -391,6 +393,7 @@ void Sapphire::Entity::Chara::autoAttack( CharaPtr pTarget )
uint64_t tick = Util::getTimeMs();
// todo: this needs to use the auto attack delay for the equipped weapon
if( ( tick - m_lastAttack ) > 2500 )
{
pTarget->onActionHostile( getAsChara() );

View file

@ -229,7 +229,7 @@ namespace Sapphire::Entity
virtual uint8_t getLevel() const;
virtual void sendStatusUpdate( bool toSelf = true );
virtual void sendStatusUpdate();
virtual void takeDamage( uint32_t damage );

View file

@ -739,7 +739,7 @@ void Sapphire::Entity::Player::gainLevel()
}
void Sapphire::Entity::Player::sendStatusUpdate( bool toSelf )
void Sapphire::Entity::Player::sendStatusUpdate()
{
sendToInRangeSet( std::make_shared< UpdateHpMpTpPacket >( *this ), true );
}
@ -810,7 +810,7 @@ void Sapphire::Entity::Player::setClassJob( Common::ClassJob classJob )
sendToInRangeSet( makeActorControl142( getId(), ClassJobChange, 0x04 ), true );
sendStatusUpdate( true );
sendStatusUpdate();
}
void Sapphire::Entity::Player::setLevel( uint8_t level )

View file

@ -707,7 +707,7 @@ namespace Sapphire::Entity
void sendStateFlags();
/*! send status update */
void sendStatusUpdate( bool toSelf = true ) override;
void sendStatusUpdate() override;
/*! send the entire inventory sequence */
void sendInventory();

View file

@ -275,8 +275,13 @@ void Sapphire::Network::GameConnection::gm1Handler( FrameworkPtr pFw,
}
case GmCommand::Hp:
{
targetPlayer->setHp( param1 );
player.sendNotice( "Hp for {0} was set to {1}", targetPlayer->getName(), param1 );
auto chara = targetActor->getAsChara();
if( chara )
{
chara->setHp( param1 );
player.sendNotice( "Hp for {0} was set to {1}", chara->getName(), param1 );
}
break;
}
case GmCommand::Mp:

View file

@ -19,19 +19,19 @@ namespace Sapphire::Network::Packets::Server
public ZoneChannelPacket< FFXIVIpcActorMove >
{
public:
MoveActorPacket( Entity::Chara& actor, uint8_t unk1, uint8_t unk2, uint8_t unk3, uint16_t unk4 ) :
MoveActorPacket( Entity::Chara& actor, uint8_t unk1, uint8_t animationType, uint8_t unk3, uint16_t unk4 ) :
ZoneChannelPacket< FFXIVIpcActorMove >( actor.getId(), actor.getId() )
{
initialize( actor, unk1, unk2, unk3, unk4 );
initialize( actor, unk1, animationType, unk3, unk4 );
};
private:
void initialize( Entity::Chara& actor, uint8_t unk1, uint8_t unk2, uint8_t unk3, uint16_t unk4 )
void initialize( Entity::Chara& actor, uint8_t unk1, uint8_t animationType, uint8_t unk3, uint16_t unk4 )
{
m_data.rotation = Util::floatToUInt8Rot( actor.getRot() );
m_data.unknown_1 = unk1;
m_data.unknown_2 = unk2;
m_data.animationType = animationType;
m_data.unknown_3 = unk3;
m_data.unknown_4 = unk4;
m_data.posX = Util::floatToUInt16( actor.getPos().x );

View file

@ -405,8 +405,29 @@ void Sapphire::Zone::updateBNpcs( int64_t tickCount )
removeActor( pBNpc );
break;
}
}
pBNpc->update( tickCount );
for( uint32_t x = 0; x < _sizeX; x++ )
{
for( uint32_t y = 0; y < _sizeY; ++y )
{
auto cell = getCellPtr( x, y );
if( !cell )
continue;
// todo: this is a pretty shit because we will visit the same cells multiple times over
// ideally we run a pass every tick and cache active cells during that initial pass over every cell
// that way we don't have an expensive lookup for every actor
if( !isCellActive( x, y ) )
continue;
for( const auto& actor : cell->m_actors )
{
if( actor->isBattleNpc() )
actor->getAsBNpc()->update( tickCount );
}
}
}
}