void buildTile(uint mapID, uint tileX, uint tileY, dtNavMesh navMesh) { Console.WriteLine($"{m_totalTilesProcessed * 100 / m_totalTiles}% [Map {mapID:D4}] Building tile [{tileX:D2},{tileY:D2}]]"); MeshData meshData = new MeshData(); // get heightmap data m_terrainBuilder.loadMap(mapID, tileX, tileY, meshData, _vmapManager); // get model data m_terrainBuilder.loadVMap(mapID, tileY, tileX, meshData); // if there is no data, give up now if (meshData.solidVerts.Count == 0 && meshData.liquidVerts.Count == 0) { return; } // remove unused vertices TerrainBuilder.cleanVertices(meshData.solidVerts, meshData.solidTris); TerrainBuilder.cleanVertices(meshData.liquidVerts, meshData.liquidTris); // gather all mesh data for final data check, and bounds calculation float[] allVerts = new float[meshData.liquidVerts.Count + meshData.solidVerts.Count]; Array.Copy(meshData.liquidVerts.ToArray(), allVerts, meshData.liquidVerts.Count); Array.Copy(meshData.solidVerts.ToArray(), 0, allVerts, meshData.liquidVerts.Count, meshData.solidVerts.Count); if (allVerts.Length == 0) { return; } // get bounds of current tile getTileBounds(tileX, tileY, allVerts, allVerts.Length / 3, out float[] bmin, out float[] bmax); m_terrainBuilder.loadOffMeshConnections(mapID, tileX, tileY, meshData, null); // build navmesh tile buildMoveMapTile(mapID, tileX, tileY, meshData, bmin, bmax, navMesh); }
void buildTile(uint mapID, uint tileX, uint tileY, dtNavMesh navMesh) { MeshData meshData = new MeshData(); // get heightmap data m_terrainBuilder.loadMap(mapID, tileX, tileY, meshData, _vmapManager); // get model data m_terrainBuilder.loadVMap(mapID, tileY, tileX, meshData); // if there is no data, give up now if (meshData.solidVerts.Count == 0 && meshData.liquidVerts.Count == 0) { return; } // remove unused vertices TerrainBuilder.cleanVertices(meshData.solidVerts, meshData.solidTris); TerrainBuilder.cleanVertices(meshData.liquidVerts, meshData.liquidTris); // gather all mesh data for final data check, and bounds calculation List <float> allVerts = new List <float>(); allVerts.AddRange(meshData.liquidVerts); allVerts.AddRange(meshData.solidVerts); if (allVerts.Count == 0) { return; } // get bounds of current tile getTileBounds(tileX, tileY, allVerts.ToArray(), allVerts.Count / 3, out float[] bmin, out float[] bmax); m_terrainBuilder.loadOffMeshConnections(mapID, tileX, tileY, meshData, null); // build navmesh tile buildMoveMapTile(mapID, tileX, tileY, meshData, bmin, bmax, navMesh); }
void buildMoveMapTile(uint mapID, uint tileX, uint tileY, MeshData meshData, float[] bmin, float[] bmax, dtNavMesh navMesh) { // console output string tileString = $"[Map: {mapID:D4}] [{tileX:D2},{tileY:D2}]: "; float[] tVerts = meshData.solidVerts.ToArray(); int tVertCount = meshData.solidVerts.Count / 3; int[] tTris = meshData.solidTris.ToArray(); int tTriCount = meshData.solidTris.Count / 3; float[] lVerts = meshData.liquidVerts.ToArray(); int lVertCount = meshData.liquidVerts.Count / 3; int[] lTris = meshData.liquidTris.ToArray(); int lTriCount = meshData.liquidTris.Count / 3; byte[] lTriFlags = meshData.liquidType.ToArray(); // these are WORLD UNIT based metrics // this are basic unit dimentions // value have to divide GRID_SIZE(533.3333f) ( aka: 0.5333, 0.2666, 0.3333, 0.1333, etc ) float BASE_UNIT_DIM = 0.2666666f;// m_bigBaseUnit ? 0.5333333f : 0.2666666f; // All are in UNIT metrics! int VERTEX_PER_MAP = (int)(SharedConst.GRID_SIZE / BASE_UNIT_DIM + 0.5f); int VERTEX_PER_TILE = 80;// m_bigBaseUnit ? 40 : 80; // must divide VERTEX_PER_MAP int TILES_PER_MAP = VERTEX_PER_MAP / VERTEX_PER_TILE; rcConfig config = new rcConfig(); for (var i = 0; i < 3; ++i) { config.bmin[i] = bmin[i]; config.bmax[i] = bmax[i]; } config.maxVertsPerPoly = SharedConst.DT_VERTS_PER_POLYGON; config.cs = BASE_UNIT_DIM; config.ch = BASE_UNIT_DIM; config.walkableSlopeAngle = m_maxWalkableAngle; config.tileSize = VERTEX_PER_TILE; config.walkableRadius = 2; // m_bigBaseUnit ? 1 : 2; config.borderSize = config.walkableRadius + 3; config.maxEdgeLen = VERTEX_PER_TILE + 1; // anything bigger than tileSize config.walkableHeight = 6; // m_bigBaseUnit ? 3 : 6; // a value >= 3|6 allows npcs to walk over some fences // a value >= 4|8 allows npcs to walk over all fences config.walkableClimb = 8;// m_bigBaseUnit ? 4 : 8; config.minRegionArea = (60 * 60); config.mergeRegionArea = (50 * 50); config.maxSimplificationError = 1.8f; // eliminates most jagged edges (tiny polygons) config.detailSampleDist = config.cs * 64; config.detailSampleMaxError = config.ch * 2; // this sets the dimensions of the heightfield - should maybe happen before border padding int width, height; rcCalcGridSize(config.bmin, config.bmax, config.cs, out width, out height); config.width = width; config.height = height; // allocate subregions : tiles Tile[] tiles = new Tile[TILES_PER_MAP * TILES_PER_MAP]; // Initialize per tile config. rcConfig tileCfg = new rcConfig(config); tileCfg.width = config.tileSize + config.borderSize * 2; tileCfg.height = config.tileSize + config.borderSize * 2; // merge per tile poly and detail meshes rcPolyMesh[] pmmerge = new rcPolyMesh[TILES_PER_MAP * TILES_PER_MAP]; rcPolyMeshDetail[] dmmerge = new rcPolyMeshDetail[TILES_PER_MAP * TILES_PER_MAP]; int nmerge = 0; // build all tiles for (int y = 0; y < TILES_PER_MAP; ++y) { for (int x = 0; x < TILES_PER_MAP; ++x) { Tile tile = tiles[x + y * TILES_PER_MAP]; // Calculate the per tile bounding box. tileCfg.bmin[0] = config.bmin[0] + (float)(x * config.tileSize - config.borderSize) * config.cs; tileCfg.bmin[2] = config.bmin[2] + (float)(y * config.tileSize - config.borderSize) * config.cs; tileCfg.bmax[0] = config.bmin[0] + (float)((x + 1) * config.tileSize + config.borderSize) * config.cs; tileCfg.bmax[2] = config.bmin[2] + (float)((y + 1) * config.tileSize + config.borderSize) * config.cs; // build heightfield tile.solid = new rcHeightfield(); if (!rcCreateHeightfield(m_rcContext, tile.solid, tileCfg.width, tileCfg.height, tileCfg.bmin, tileCfg.bmax, tileCfg.cs, tileCfg.ch)) { //Console.WriteLine($"{tileString} Failed building heightfield! "); continue; } // mark all walkable tiles, both liquids and solids byte[] triFlags = new byte[tTriCount]; for (var i = 0; i < tTriCount; ++i) { triFlags[i] = (byte)NavArea.Ground; } rcClearUnwalkableTriangles(m_rcContext, tileCfg.walkableSlopeAngle, tVerts, tVertCount, tTris, tTriCount, triFlags); rcRasterizeTriangles(m_rcContext, tVerts, tVertCount, tTris, triFlags, tTriCount, tile.solid, config.walkableClimb); rcFilterLowHangingWalkableObstacles(m_rcContext, config.walkableClimb, tile.solid); rcFilterLedgeSpans(m_rcContext, tileCfg.walkableHeight, tileCfg.walkableClimb, tile.solid); rcFilterWalkableLowHeightSpans(m_rcContext, tileCfg.walkableHeight, tile.solid); rcRasterizeTriangles(m_rcContext, lVerts, lVertCount, lTris, lTriFlags, lTriCount, tile.solid, config.walkableClimb); // compact heightfield spans tile.chf = new rcCompactHeightfield(); if (!rcBuildCompactHeightfield(m_rcContext, tileCfg.walkableHeight, tileCfg.walkableClimb, tile.solid, tile.chf)) { //Console.WriteLine($"{tileString} Failed compacting heightfield!"); continue; } // build polymesh intermediates if (!rcErodeWalkableArea(m_rcContext, config.walkableRadius, tile.chf)) { //Console.WriteLine($"{tileString} Failed eroding area!"); continue; } if (!rcBuildDistanceField(m_rcContext, tile.chf)) { //Console.WriteLine($"{tileString} Failed building distance field!"); continue; } if (!rcBuildRegions(m_rcContext, tile.chf, tileCfg.borderSize, tileCfg.minRegionArea, tileCfg.mergeRegionArea)) { //Console.WriteLine($"{tileString} Failed building regions!"); continue; } tile.cset = new rcContourSet(); if (!rcBuildContours(m_rcContext, tile.chf, tileCfg.maxSimplificationError, tileCfg.maxEdgeLen, tile.cset)) { //Console.WriteLine($"{tileString} Failed building contours!"); continue; } // build polymesh tile.pmesh = new rcPolyMesh(); if (!rcBuildPolyMesh(m_rcContext, tile.cset, tileCfg.maxVertsPerPoly, tile.pmesh)) { //Console.WriteLine($"{tileString} Failed building polymesh!"); continue; } tile.dmesh = new rcPolyMeshDetail(); if (!rcBuildPolyMeshDetail(m_rcContext, tile.pmesh, tile.chf, tileCfg.detailSampleDist, tileCfg.detailSampleMaxError, tile.dmesh)) { //Console.WriteLine($"{tileString} Failed building polymesh detail!"); continue; } // free those up // we may want to keep them in the future for debug // but right now, we don't have the code to merge them tile.solid = null; tile.chf = null; tile.cset = null; pmmerge[nmerge] = tile.pmesh; dmmerge[nmerge] = tile.dmesh; nmerge++; } } rcPolyMesh pmesh = new rcPolyMesh(); rcMergePolyMeshes(m_rcContext, pmmerge, nmerge, pmesh); rcPolyMeshDetail dmesh = new rcPolyMeshDetail(); rcMergePolyMeshDetails(m_rcContext, dmmerge, nmerge, dmesh); // set polygons as walkable // TODO: special flags for DYNAMIC polygons, ie surfaces that can be turned on and off for (int i = 0; i < pmesh.npolys; ++i) { byte area = (byte)(pmesh.areas[i] & SharedConst.RC_WALKABLE_AREA); if (area != 0) { if (area >= (byte)NavArea.MagmaSlime) { pmesh.flags[i] = (ushort)(1 << (63 - area)); } else { pmesh.flags[i] = (byte)NavTerrainFlag.Ground; // TODO: these will be dynamic in future } } } // setup mesh parameters dtNavMeshCreateParams createParams = new dtNavMeshCreateParams(); createParams.verts = pmesh.verts; createParams.vertCount = pmesh.nverts; createParams.polys = pmesh.polys; createParams.polyAreas = pmesh.areas; createParams.polyFlags = pmesh.flags; createParams.polyCount = pmesh.npolys; createParams.nvp = pmesh.nvp; createParams.detailMeshes = dmesh.meshes; createParams.detailVerts = dmesh.verts; createParams.detailVertsCount = dmesh.nverts; createParams.detailTris = dmesh.tris; createParams.detailTriCount = dmesh.ntris; createParams.offMeshConVerts = meshData.offMeshConnections.ToArray(); createParams.offMeshConCount = meshData.offMeshConnections.Count / 6; createParams.offMeshConRad = meshData.offMeshConnectionRads.ToArray(); createParams.offMeshConDir = meshData.offMeshConnectionDirs.ToArray(); createParams.offMeshConAreas = meshData.offMeshConnectionsAreas.ToArray(); createParams.offMeshConFlags = meshData.offMeshConnectionsFlags.ToArray(); createParams.walkableHeight = BASE_UNIT_DIM * config.walkableHeight; // agent height createParams.walkableRadius = BASE_UNIT_DIM * config.walkableRadius; // agent radius createParams.walkableClimb = BASE_UNIT_DIM * config.walkableClimb; // keep less that walkableHeight (aka agent height)! createParams.tileX = (int)((((bmin[0] + bmax[0]) / 2) - navMesh.getParams().orig[0]) / SharedConst.GRID_SIZE); createParams.tileY = (int)((((bmin[2] + bmax[2]) / 2) - navMesh.getParams().orig[2]) / SharedConst.GRID_SIZE); createParams.bmin = bmin; createParams.bmax = bmax; createParams.cs = config.cs; createParams.ch = config.ch; createParams.tileLayer = 0; createParams.buildBvTree = true; // will hold final navmesh dtRawTileData dtRawTile; do { // these values are checked within dtCreateNavMeshData - handle them here // so we have a clear error message if (createParams.nvp > SharedConst.DT_VERTS_PER_POLYGON) { //Console.WriteLine($"{tileString} Invalid verts-per-polygon value!"); break; } if (createParams.vertCount >= 0xffff) { //Console.WriteLine($"{tileString} Too many vertices!"); break; } if (createParams.vertCount == 0 || createParams.verts == null) { // occurs mostly when adjacent tiles have models // loaded but those models don't span into this tile // message is an annoyance break; } if (createParams.polyCount == 0 || createParams.polys == null || TILES_PER_MAP * TILES_PER_MAP == createParams.polyCount) { // we have flat tiles with no actual geometry - don't build those, its useless // keep in mind that we do output those into debug info // drop tiles with only exact count - some tiles may have geometry while having less tiles //Console.WriteLine($"{tileString} No polygons to build on tile! "); break; } if (createParams.detailMeshes == null || createParams.detailVerts == null || createParams.detailTris == null) { //Console.WriteLine($"{tileString} No detail mesh to build tile!"); break; } if (!dtCreateNavMeshData(createParams, out dtRawTile)) { //Console.WriteLine($"{tileString} Failed building navmesh tile!"); break; } ulong tileRef = 0; // DT_TILE_FREE_DATA tells detour to unallocate memory when the tile // is removed via removeTile() if (dtStatusFailed(navMesh.addTile(dtRawTile, 1, 0, ref tileRef)) || tileRef == 0) { //Console.WriteLine($"{tileString} Failed adding tile to navmesh!"); break; } // file output string fileName = $"mmaps/{mapID:D4}{tileY:D2}{tileX:D2}.mmtile"; using (BinaryWriter binaryWriter = new BinaryWriter(File.Open(fileName, FileMode.Create, FileAccess.Write))) { var navData = dtRawTile.ToBytes(); // write header MmapTileHeader header = new MmapTileHeader(); header.mmapMagic = SharedConst.MMAP_MAGIC; header.dtVersion = SharedConst.DT_NAVMESH_VERSION; header.mmapVersion = SharedConst.MMAP_VERSION; header.usesLiquids = m_terrainBuilder.usesLiquids(); header.size = (uint)navData.Length; binaryWriter.WriteStruct(header); // write data binaryWriter.Write(navData); } // now that tile is written to disk, we can unload it navMesh.removeTile(tileRef, out dtRawTile); }while (false); if (_debugMaps) { generateObjFile(mapID, tileX, tileY, meshData); } }
void buildNavMesh(uint mapID, out dtNavMesh navMesh) { // if map has a parent we use that to generate dtNavMeshParams - worldserver will load all missing tiles from that map int navMeshParamsMapId = _vmapManager.GetParentMapId(mapID); if (navMeshParamsMapId == -1) { navMeshParamsMapId = (int)mapID; } SortedSet <uint> tiles = getTileList((uint)navMeshParamsMapId); // old code for non-statically assigned bitmask sizes: ///*** calculate number of bits needed to store tiles & polys ***/ //int tileBits = dtIlog2(dtNextPow2(tiles.size())); //if (tileBits < 1) tileBits = 1; // need at least one bit! //int polyBits = sizeof(dtPolyRef)*8 - SALT_MIN_BITS - tileBits; int polyBits = SharedConst.DT_POLY_BITS; int maxTiles = tiles.Count; int maxPolysPerTile = 1 << polyBits; /*** calculate bounds of map ***/ uint tileXMin = 64, tileYMin = 64, tileXMax = 0, tileYMax = 0; foreach (var it in tiles) { StaticMapTree.UnpackTileID(it, out uint tileX, out uint tileY); if (tileX > tileXMax) { tileXMax = tileX; } else if (tileX < tileXMin) { tileXMin = tileX; } if (tileY > tileYMax) { tileYMax = tileY; } else if (tileY < tileYMin) { tileYMin = tileY; } } // use Max because '32 - tileX' is negative for values over 32 float[] bmin; float[] bmax; getTileBounds(tileXMax, tileYMax, null, 0, out bmin, out bmax); /*** now create the navmesh ***/ // navmesh creation params dtNavMeshParams navMeshParams = new dtNavMeshParams(); navMeshParams.tileWidth = SharedConst.GRID_SIZE; navMeshParams.tileHeight = SharedConst.GRID_SIZE; rcVcopy(navMeshParams.orig, bmin); navMeshParams.maxTiles = maxTiles; navMeshParams.maxPolys = maxPolysPerTile; navMesh = new dtNavMesh(); if (dtStatusFailed(navMesh.init(navMeshParams))) { Console.WriteLine($"[Map: {mapID:D4}] Failed creating navmesh!"); return; } string fileName = $"mmaps/{mapID:D4}.mmap"; using (BinaryWriter writer = new BinaryWriter(File.Open(fileName, FileMode.Create, FileAccess.Write))) { // now that we know navMesh params are valid, we can write them to file writer.Write(bmin[0]); writer.Write(bmin[1]); writer.Write(bmin[2]); writer.Write(SharedConst.GRID_SIZE); writer.Write(SharedConst.GRID_SIZE); writer.Write(maxTiles); writer.Write(maxPolysPerTile); } }
/// @par /// /// Must be the first function called after construction, before other /// functions are used. /// /// This function can be used multiple times. /// Initializes the query object. /// @param[in] nav Pointer to the dtNavMesh object to use for all queries. /// @param[in] maxNodes Maximum number of search nodes. [Limits: 0 < value <= 65536] /// @returns The status flags for the query. public dtStatus init(dtNavMesh nav, int maxNodes) { m_nav = nav; if (m_nodePool == null || m_nodePool.getMaxNodes() < maxNodes) { if (m_nodePool != null) { //m_nodePool.~dtNodePool(); //dtFree(m_nodePool); m_nodePool = null; } m_nodePool = new dtNodePool(maxNodes, (int) dtNextPow2((uint)(maxNodes/4)));//(dtAlloc(sizeof(dtNodePool), DT_ALLOC_PERM)) dtNodePool(maxNodes, dtNextPow2(maxNodes/4)); if (m_nodePool == null) return DT_FAILURE | DT_OUT_OF_MEMORY; } else { m_nodePool.clear(); } if (m_tinyNodePool == null) { m_tinyNodePool = new dtNodePool(64, 32);//(dtAlloc(sizeof(dtNodePool), DT_ALLOC_PERM)) dtNodePool(64, 32); if (m_tinyNodePool == null) return DT_FAILURE | DT_OUT_OF_MEMORY; } else { m_tinyNodePool.clear(); } // TODO: check the open list size too. if (m_openList == null || m_openList.getCapacity() < maxNodes) { if (m_openList != null) { //m_openList.~dtNodeQueue(); //dtFree(m_openList); m_openList = null; } m_openList = new dtNodeQueue(maxNodes);//(dtAlloc(sizeof(dtNodeQueue), DT_ALLOC_PERM)) dtNodeQueue(maxNodes); if (m_openList == null) return DT_FAILURE | DT_OUT_OF_MEMORY; } else { m_openList.clear(); } return DT_SUCCESS; }