diff --git a/src/tools/pcb_reader/nav/TiledNavmeshGenerator.cpp b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.cpp new file mode 100644 index 00000000..6aedfa70 --- /dev/null +++ b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.cpp @@ -0,0 +1,572 @@ +#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; +} + +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; + } + + 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( ¶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/pcb_reader/nav/TiledNavmeshGenerator.h b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.h index 93d3051e..05b4744d 100644 --- a/src/tools/pcb_reader/nav/TiledNavmeshGenerator.h +++ b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.h @@ -3,7 +3,7 @@ #include #include -#include +#include #include "ext/MeshLoaderObj.h" #include "ext/ChunkyTriMesh.h" @@ -12,147 +12,112 @@ #include "recastnavigation/Detour/Include/DetourNavMeshQuery.h" #include "recastnavigation/Recast/Include/Recast.h" -namespace fs = std::experimental::filesystem; - 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; - float m_meshBMin[ 3 ]; - float m_meshBMax[ 3 ]; - - float m_tileSize = 160.f; - float m_cellSize = 0.2f; + unsigned char* m_triareas; int m_maxTiles = 0; int m_maxPolysPerTile = 0; - 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; - } + int m_tileTriCount = 0; - 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; - } + int m_partitionType = SamplePartitionType::SAMPLE_PARTITION_WATERSHED; + // options + float m_meshBMin[ 3 ]; + float m_meshBMax[ 3 ]; -public: - explicit TiledNavmeshGenerator( const std::string& path ) - { - if( !fs::exists( path ) ) - throw std::runtime_error( "what" ); + float m_lastBuiltTileBmin[3]; + float m_lastBuiltTileBmax[3]; - printf( "[Navmesh] loading obj: %s\n", path.c_str() ); + float m_tileSize = 160.f; + float m_cellSize = 0.2f; + float m_cellHeight = 0.2f; - m_mesh = new rcMeshLoaderObj; - assert( m_mesh ); + float m_agentMaxSlope = 56.f; + float m_agentHeight = 2.f; + float m_agentMaxClimb = 0.6f; + float m_agentRadius = 0.5f; - if( !m_mesh->load( path ) ) - { - printf( "[Navmesh] Failed to allocate rcMeshLoaderObj\n" ); - return; - } + float m_edgeMaxLen = 12.f; + float m_edgeMaxError = 1.4f; - rcCalcBounds( m_mesh->getVerts(), m_mesh->getVertCount(), m_meshBMin, m_meshBMax ); + float m_regionMinSize = 8.f; + float m_regionMergeSize = 20.f; - 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; - } - - printf( "[Navmesh] loaded obj, verts: %i tris: %i\n", m_mesh->getVertCount(), m_mesh->getTriCount() ); - - // 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] Max Tiles: %d\tMax Polys: %d\n", m_maxTiles, m_maxPolysPerTile ); - } - - bool 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( "[Navigation] 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( "[Navigation] buildTiledNavigation: Could not init Detour navmesh query\n" ); - return false; - } - - } - - ~TiledNavmeshGenerator() - { - delete m_mesh; - delete m_chunkyMesh; - } + float m_vertsPerPoly = 6.f; + float m_detailSampleDist = 6.f; + float m_detailSampleMaxError = 1.f; }; diff --git a/src/tools/pcb_reader/navmesh_exporter.h b/src/tools/pcb_reader/navmesh_exporter.h index ec06a0cd..6c4865c8 100644 --- a/src/tools/pcb_reader/navmesh_exporter.h +++ b/src/tools/pcb_reader/navmesh_exporter.h @@ -11,6 +11,10 @@ #include "exporter.h" #include "nav/TiledNavmeshGenerator.h" +#include + +namespace fs = std::experimental::filesystem; + class NavmeshExporter { public: @@ -18,10 +22,16 @@ public: { auto start = std::chrono::high_resolution_clock::now(); - auto dir = std::experimental::filesystem::current_path().string() + "/pcb_export/" + zone.name + "/"; + auto dir = fs::current_path().string() + "/pcb_export/" + zone.name + "/"; auto fileName = dir + zone.name + ".obj"; - TiledNavmeshGenerator gen( fileName ); + TiledNavmeshGenerator gen; + + if( !gen.init( fileName ) ) + { + printf( "[Navmesh] failed to init TiledNavmeshGenerator for file '%s'\n", fileName.c_str() ); + return; + } if( !gen.buildNavmesh() ) { @@ -29,6 +39,8 @@ public: 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(), @@ -39,336 +51,5 @@ public: { } -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(¶ms, 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(¶ms, &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 diff --git a/src/tools/pcb_reader/obj_exporter.h b/src/tools/pcb_reader/obj_exporter.h index f5c1361b..17e9fb51 100644 --- a/src/tools/pcb_reader/obj_exporter.h +++ b/src/tools/pcb_reader/obj_exporter.h @@ -51,8 +51,8 @@ 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() ); } static void exportGroup( const std::string& zoneName, const ExportedGroup& group ) @@ -88,8 +88,8 @@ 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() ); } private: static void exportGroup( const ExportedGroup& group, std::ofstream& of, int& indicesOffset, int& modelCount )