#include "DetourTileCache.h" #include "DetourTileCacheBuilder.h" #include "DetourNavMeshBuilder.h" #include "DetourNavMesh.h" #include "DetourCommon.h" #include "DetourMath.h" #include "DetourAlloc.h" #include "DetourAssert.h" #include #include dtTileCache* dtAllocTileCache() { void* mem = dtAlloc(sizeof(dtTileCache), DT_ALLOC_PERM); if (!mem) return 0; return new(mem) dtTileCache; } void dtFreeTileCache(dtTileCache* tc) { if (!tc) return; tc->~dtTileCache(); dtFree(tc); } static bool contains(const dtCompressedTileRef* a, const int n, const dtCompressedTileRef v) { for (int i = 0; i < n; ++i) if (a[i] == v) return true; return false; } inline int computeTileHash(int x, int y, const int mask) { const unsigned int h1 = 0x8da6b343; // Large multiplicative constants; const unsigned int h2 = 0xd8163841; // here arbitrarily chosen primes unsigned int n = h1 * x + h2 * y; return (int)(n & mask); } struct NavMeshTileBuildContext { inline NavMeshTileBuildContext(struct dtTileCacheAlloc* a) : layer(0), lcset(0), lmesh(0), alloc(a) {} inline ~NavMeshTileBuildContext() { purge(); } void purge() { dtFreeTileCacheLayer(alloc, layer); layer = 0; dtFreeTileCacheContourSet(alloc, lcset); lcset = 0; dtFreeTileCachePolyMesh(alloc, lmesh); lmesh = 0; } struct dtTileCacheLayer* layer; struct dtTileCacheContourSet* lcset; struct dtTileCachePolyMesh* lmesh; struct dtTileCacheAlloc* alloc; }; dtTileCache::dtTileCache() : m_tileLutSize(0), m_tileLutMask(0), m_posLookup(0), m_nextFreeTile(0), m_tiles(0), m_saltBits(0), m_tileBits(0), m_talloc(0), m_tcomp(0), m_tmproc(0), m_obstacles(0), m_nextFreeObstacle(0), m_nreqs(0), m_nupdate(0) { memset(&m_params, 0, sizeof(m_params)); memset(m_reqs, 0, sizeof(ObstacleRequest) * MAX_REQUESTS); } dtTileCache::~dtTileCache() { for (int i = 0; i < m_params.maxTiles; ++i) { if (m_tiles[i].flags & DT_COMPRESSEDTILE_FREE_DATA) { dtFree(m_tiles[i].data); m_tiles[i].data = 0; } } dtFree(m_obstacles); m_obstacles = 0; dtFree(m_posLookup); m_posLookup = 0; dtFree(m_tiles); m_tiles = 0; m_nreqs = 0; m_nupdate = 0; } const dtCompressedTile* dtTileCache::getTileByRef(dtCompressedTileRef ref) const { if (!ref) return 0; unsigned int tileIndex = decodeTileIdTile(ref); unsigned int tileSalt = decodeTileIdSalt(ref); if ((int)tileIndex >= m_params.maxTiles) return 0; const dtCompressedTile* tile = &m_tiles[tileIndex]; if (tile->salt != tileSalt) return 0; return tile; } dtStatus dtTileCache::init(const dtTileCacheParams* params, dtTileCacheAlloc* talloc, dtTileCacheCompressor* tcomp, dtTileCacheMeshProcess* tmproc) { m_talloc = talloc; m_tcomp = tcomp; m_tmproc = tmproc; m_nreqs = 0; memcpy(&m_params, params, sizeof(m_params)); // Alloc space for obstacles. m_obstacles = (dtTileCacheObstacle*)dtAlloc(sizeof(dtTileCacheObstacle)*m_params.maxObstacles, DT_ALLOC_PERM); if (!m_obstacles) return DT_FAILURE | DT_OUT_OF_MEMORY; memset(m_obstacles, 0, sizeof(dtTileCacheObstacle)*m_params.maxObstacles); m_nextFreeObstacle = 0; for (int i = m_params.maxObstacles-1; i >= 0; --i) { m_obstacles[i].salt = 1; m_obstacles[i].next = m_nextFreeObstacle; m_nextFreeObstacle = &m_obstacles[i]; } // Init tiles m_tileLutSize = dtNextPow2(m_params.maxTiles/4); if (!m_tileLutSize) m_tileLutSize = 1; m_tileLutMask = m_tileLutSize-1; m_tiles = (dtCompressedTile*)dtAlloc(sizeof(dtCompressedTile)*m_params.maxTiles, DT_ALLOC_PERM); if (!m_tiles) return DT_FAILURE | DT_OUT_OF_MEMORY; m_posLookup = (dtCompressedTile**)dtAlloc(sizeof(dtCompressedTile*)*m_tileLutSize, DT_ALLOC_PERM); if (!m_posLookup) return DT_FAILURE | DT_OUT_OF_MEMORY; memset(m_tiles, 0, sizeof(dtCompressedTile)*m_params.maxTiles); memset(m_posLookup, 0, sizeof(dtCompressedTile*)*m_tileLutSize); m_nextFreeTile = 0; for (int i = m_params.maxTiles-1; i >= 0; --i) { m_tiles[i].salt = 1; m_tiles[i].next = m_nextFreeTile; m_nextFreeTile = &m_tiles[i]; } // Init ID generator values. m_tileBits = dtIlog2(dtNextPow2((unsigned int)m_params.maxTiles)); // Only allow 31 salt bits, since the salt mask is calculated using 32bit uint and it will overflow. m_saltBits = dtMin((unsigned int)31, 32 - m_tileBits); if (m_saltBits < 10) return DT_FAILURE | DT_INVALID_PARAM; return DT_SUCCESS; } int dtTileCache::getTilesAt(const int tx, const int ty, dtCompressedTileRef* tiles, const int maxTiles) const { int n = 0; // Find tile based on hash. int h = computeTileHash(tx,ty,m_tileLutMask); dtCompressedTile* tile = m_posLookup[h]; while (tile) { if (tile->header && tile->header->tx == tx && tile->header->ty == ty) { if (n < maxTiles) tiles[n++] = getTileRef(tile); } tile = tile->next; } return n; } dtCompressedTile* dtTileCache::getTileAt(const int tx, const int ty, const int tlayer) { // Find tile based on hash. int h = computeTileHash(tx,ty,m_tileLutMask); dtCompressedTile* tile = m_posLookup[h]; while (tile) { if (tile->header && tile->header->tx == tx && tile->header->ty == ty && tile->header->tlayer == tlayer) { return tile; } tile = tile->next; } return 0; } dtCompressedTileRef dtTileCache::getTileRef(const dtCompressedTile* tile) const { if (!tile) return 0; const unsigned int it = (unsigned int)(tile - m_tiles); return (dtCompressedTileRef)encodeTileId(tile->salt, it); } dtObstacleRef dtTileCache::getObstacleRef(const dtTileCacheObstacle* ob) const { if (!ob) return 0; const unsigned int idx = (unsigned int)(ob - m_obstacles); return encodeObstacleId(ob->salt, idx); } const dtTileCacheObstacle* dtTileCache::getObstacleByRef(dtObstacleRef ref) { if (!ref) return 0; unsigned int idx = decodeObstacleIdObstacle(ref); if ((int)idx >= m_params.maxObstacles) return 0; const dtTileCacheObstacle* ob = &m_obstacles[idx]; unsigned int salt = decodeObstacleIdSalt(ref); if (ob->salt != salt) return 0; return ob; } dtTileCacheMeshProcess::~dtTileCacheMeshProcess() { // Defined out of line to fix the weak v-tables warning } dtStatus dtTileCache::addTile(unsigned char* data, const int dataSize, unsigned char flags, dtCompressedTileRef* result) { // Make sure the data is in right format. dtTileCacheLayerHeader* header = (dtTileCacheLayerHeader*)data; if (header->magic != DT_TILECACHE_MAGIC) return DT_FAILURE | DT_WRONG_MAGIC; if (header->version != DT_TILECACHE_VERSION) return DT_FAILURE | DT_WRONG_VERSION; // Make sure the location is free. if (getTileAt(header->tx, header->ty, header->tlayer)) return DT_FAILURE; // Allocate a tile. dtCompressedTile* tile = 0; if (m_nextFreeTile) { tile = m_nextFreeTile; m_nextFreeTile = tile->next; tile->next = 0; } // Make sure we could allocate a tile. if (!tile) return DT_FAILURE | DT_OUT_OF_MEMORY; // Insert tile into the position lut. int h = computeTileHash(header->tx, header->ty, m_tileLutMask); tile->next = m_posLookup[h]; m_posLookup[h] = tile; // Init tile. const int headerSize = dtAlign4(sizeof(dtTileCacheLayerHeader)); tile->header = (dtTileCacheLayerHeader*)data; tile->data = data; tile->dataSize = dataSize; tile->compressed = tile->data + headerSize; tile->compressedSize = tile->dataSize - headerSize; tile->flags = flags; if (result) *result = getTileRef(tile); return DT_SUCCESS; } dtStatus dtTileCache::removeTile(dtCompressedTileRef ref, unsigned char** data, int* dataSize) { if (!ref) return DT_FAILURE | DT_INVALID_PARAM; unsigned int tileIndex = decodeTileIdTile(ref); unsigned int tileSalt = decodeTileIdSalt(ref); if ((int)tileIndex >= m_params.maxTiles) return DT_FAILURE | DT_INVALID_PARAM; dtCompressedTile* tile = &m_tiles[tileIndex]; if (tile->salt != tileSalt) return DT_FAILURE | DT_INVALID_PARAM; // Remove tile from hash lookup. const int h = computeTileHash(tile->header->tx,tile->header->ty,m_tileLutMask); dtCompressedTile* prev = 0; dtCompressedTile* cur = m_posLookup[h]; while (cur) { if (cur == tile) { if (prev) prev->next = cur->next; else m_posLookup[h] = cur->next; break; } prev = cur; cur = cur->next; } // Reset tile. if (tile->flags & DT_COMPRESSEDTILE_FREE_DATA) { // Owns data dtFree(tile->data); tile->data = 0; tile->dataSize = 0; if (data) *data = 0; if (dataSize) *dataSize = 0; } else { if (data) *data = tile->data; if (dataSize) *dataSize = tile->dataSize; } tile->header = 0; tile->data = 0; tile->dataSize = 0; tile->compressed = 0; tile->compressedSize = 0; tile->flags = 0; // Update salt, salt should never be zero. tile->salt = (tile->salt+1) & ((1<salt == 0) tile->salt++; // Add to free list. tile->next = m_nextFreeTile; m_nextFreeTile = tile; return DT_SUCCESS; } dtStatus dtTileCache::addObstacle(const float* pos, const float radius, const float height, dtObstacleRef* result) { if (m_nreqs >= MAX_REQUESTS) return DT_FAILURE | DT_BUFFER_TOO_SMALL; dtTileCacheObstacle* ob = 0; if (m_nextFreeObstacle) { ob = m_nextFreeObstacle; m_nextFreeObstacle = ob->next; ob->next = 0; } if (!ob) return DT_FAILURE | DT_OUT_OF_MEMORY; unsigned short salt = ob->salt; memset(ob, 0, sizeof(dtTileCacheObstacle)); ob->salt = salt; ob->state = DT_OBSTACLE_PROCESSING; ob->type = DT_OBSTACLE_CYLINDER; dtVcopy(ob->cylinder.pos, pos); ob->cylinder.radius = radius; ob->cylinder.height = height; ObstacleRequest* req = &m_reqs[m_nreqs++]; memset(req, 0, sizeof(ObstacleRequest)); req->action = REQUEST_ADD; req->ref = getObstacleRef(ob); if (result) *result = req->ref; return DT_SUCCESS; } dtStatus dtTileCache::addBoxObstacle(const float* bmin, const float* bmax, dtObstacleRef* result) { if (m_nreqs >= MAX_REQUESTS) return DT_FAILURE | DT_BUFFER_TOO_SMALL; dtTileCacheObstacle* ob = 0; if (m_nextFreeObstacle) { ob = m_nextFreeObstacle; m_nextFreeObstacle = ob->next; ob->next = 0; } if (!ob) return DT_FAILURE | DT_OUT_OF_MEMORY; unsigned short salt = ob->salt; memset(ob, 0, sizeof(dtTileCacheObstacle)); ob->salt = salt; ob->state = DT_OBSTACLE_PROCESSING; ob->type = DT_OBSTACLE_BOX; dtVcopy(ob->box.bmin, bmin); dtVcopy(ob->box.bmax, bmax); ObstacleRequest* req = &m_reqs[m_nreqs++]; memset(req, 0, sizeof(ObstacleRequest)); req->action = REQUEST_ADD; req->ref = getObstacleRef(ob); if (result) *result = req->ref; return DT_SUCCESS; } dtStatus dtTileCache::addBoxObstacle(const float* center, const float* halfExtents, const float yRadians, dtObstacleRef* result) { if (m_nreqs >= MAX_REQUESTS) return DT_FAILURE | DT_BUFFER_TOO_SMALL; dtTileCacheObstacle* ob = 0; if (m_nextFreeObstacle) { ob = m_nextFreeObstacle; m_nextFreeObstacle = ob->next; ob->next = 0; } if (!ob) return DT_FAILURE | DT_OUT_OF_MEMORY; unsigned short salt = ob->salt; memset(ob, 0, sizeof(dtTileCacheObstacle)); ob->salt = salt; ob->state = DT_OBSTACLE_PROCESSING; ob->type = DT_OBSTACLE_ORIENTED_BOX; dtVcopy(ob->orientedBox.center, center); dtVcopy(ob->orientedBox.halfExtents, halfExtents); float coshalf= cosf(0.5f*yRadians); float sinhalf = sinf(-0.5f*yRadians); ob->orientedBox.rotAux[0] = coshalf*sinhalf; ob->orientedBox.rotAux[1] = coshalf*coshalf - 0.5f; ObstacleRequest* req = &m_reqs[m_nreqs++]; memset(req, 0, sizeof(ObstacleRequest)); req->action = REQUEST_ADD; req->ref = getObstacleRef(ob); if (result) *result = req->ref; return DT_SUCCESS; } dtStatus dtTileCache::removeObstacle(const dtObstacleRef ref) { if (!ref) return DT_SUCCESS; if (m_nreqs >= MAX_REQUESTS) return DT_FAILURE | DT_BUFFER_TOO_SMALL; ObstacleRequest* req = &m_reqs[m_nreqs++]; memset(req, 0, sizeof(ObstacleRequest)); req->action = REQUEST_REMOVE; req->ref = ref; return DT_SUCCESS; } dtStatus dtTileCache::queryTiles(const float* bmin, const float* bmax, dtCompressedTileRef* results, int* resultCount, const int maxResults) const { const int MAX_TILES = 32; dtCompressedTileRef tiles[MAX_TILES]; int n = 0; const float tw = m_params.width * m_params.cs; const float th = m_params.height * m_params.cs; const int tx0 = (int)dtMathFloorf((bmin[0]-m_params.orig[0]) / tw); const int tx1 = (int)dtMathFloorf((bmax[0]-m_params.orig[0]) / tw); const int ty0 = (int)dtMathFloorf((bmin[2]-m_params.orig[2]) / th); const int ty1 = (int)dtMathFloorf((bmax[2]-m_params.orig[2]) / th); for (int ty = ty0; ty <= ty1; ++ty) { for (int tx = tx0; tx <= tx1; ++tx) { const int ntiles = getTilesAt(tx,ty,tiles,MAX_TILES); for (int i = 0; i < ntiles; ++i) { const dtCompressedTile* tile = &m_tiles[decodeTileIdTile(tiles[i])]; float tbmin[3], tbmax[3]; calcTightTileBounds(tile->header, tbmin, tbmax); if (dtOverlapBounds(bmin,bmax, tbmin,tbmax)) { if (n < maxResults) results[n++] = tiles[i]; } } } } *resultCount = n; return DT_SUCCESS; } dtStatus dtTileCache::update(const float /*dt*/, dtNavMesh* navmesh, bool* upToDate) { if (m_nupdate == 0) { // Process requests. for (int i = 0; i < m_nreqs; ++i) { ObstacleRequest* req = &m_reqs[i]; unsigned int idx = decodeObstacleIdObstacle(req->ref); if ((int)idx >= m_params.maxObstacles) continue; dtTileCacheObstacle* ob = &m_obstacles[idx]; unsigned int salt = decodeObstacleIdSalt(req->ref); if (ob->salt != salt) continue; if (req->action == REQUEST_ADD) { // Find touched tiles. float bmin[3], bmax[3]; getObstacleBounds(ob, bmin, bmax); int ntouched = 0; queryTiles(bmin, bmax, ob->touched, &ntouched, DT_MAX_TOUCHED_TILES); ob->ntouched = (unsigned char)ntouched; // Add tiles to update list. ob->npending = 0; for (int j = 0; j < ob->ntouched; ++j) { if (m_nupdate < MAX_UPDATE) { if (!contains(m_update, m_nupdate, ob->touched[j])) m_update[m_nupdate++] = ob->touched[j]; ob->pending[ob->npending++] = ob->touched[j]; } } } else if (req->action == REQUEST_REMOVE) { // Prepare to remove obstacle. ob->state = DT_OBSTACLE_REMOVING; // Add tiles to update list. ob->npending = 0; for (int j = 0; j < ob->ntouched; ++j) { if (m_nupdate < MAX_UPDATE) { if (!contains(m_update, m_nupdate, ob->touched[j])) m_update[m_nupdate++] = ob->touched[j]; ob->pending[ob->npending++] = ob->touched[j]; } } } } m_nreqs = 0; } dtStatus status = DT_SUCCESS; // Process updates if (m_nupdate) { // Build mesh const dtCompressedTileRef ref = m_update[0]; status = buildNavMeshTile(ref, navmesh); m_nupdate--; if (m_nupdate > 0) memmove(m_update, m_update+1, m_nupdate*sizeof(dtCompressedTileRef)); // Update obstacle states. for (int i = 0; i < m_params.maxObstacles; ++i) { dtTileCacheObstacle* ob = &m_obstacles[i]; if (ob->state == DT_OBSTACLE_PROCESSING || ob->state == DT_OBSTACLE_REMOVING) { // Remove handled tile from pending list. for (int j = 0; j < (int)ob->npending; j++) { if (ob->pending[j] == ref) { ob->pending[j] = ob->pending[(int)ob->npending-1]; ob->npending--; break; } } // If all pending tiles processed, change state. if (ob->npending == 0) { if (ob->state == DT_OBSTACLE_PROCESSING) { ob->state = DT_OBSTACLE_PROCESSED; } else if (ob->state == DT_OBSTACLE_REMOVING) { ob->state = DT_OBSTACLE_EMPTY; // Update salt, salt should never be zero. ob->salt = (ob->salt+1) & ((1<<16)-1); if (ob->salt == 0) ob->salt++; // Return obstacle to free list. ob->next = m_nextFreeObstacle; m_nextFreeObstacle = ob; } } } } } if (upToDate) *upToDate = m_nupdate == 0 && m_nreqs == 0; return status; } dtStatus dtTileCache::buildNavMeshTilesAt(const int tx, const int ty, dtNavMesh* navmesh) { const int MAX_TILES = 32; dtCompressedTileRef tiles[MAX_TILES]; const int ntiles = getTilesAt(tx,ty,tiles,MAX_TILES); for (int i = 0; i < ntiles; ++i) { dtStatus status = buildNavMeshTile(tiles[i], navmesh); if (dtStatusFailed(status)) return status; } return DT_SUCCESS; } dtStatus dtTileCache::buildNavMeshTile(const dtCompressedTileRef ref, dtNavMesh* navmesh) { dtAssert(m_talloc); dtAssert(m_tcomp); unsigned int idx = decodeTileIdTile(ref); if (idx > (unsigned int)m_params.maxTiles) return DT_FAILURE | DT_INVALID_PARAM; const dtCompressedTile* tile = &m_tiles[idx]; unsigned int salt = decodeTileIdSalt(ref); if (tile->salt != salt) return DT_FAILURE | DT_INVALID_PARAM; m_talloc->reset(); NavMeshTileBuildContext bc(m_talloc); const int walkableClimbVx = (int)(m_params.walkableClimb / m_params.ch); dtStatus status; // Decompress tile layer data. status = dtDecompressTileCacheLayer(m_talloc, m_tcomp, tile->data, tile->dataSize, &bc.layer); if (dtStatusFailed(status)) return status; // Rasterize obstacles. for (int i = 0; i < m_params.maxObstacles; ++i) { const dtTileCacheObstacle* ob = &m_obstacles[i]; if (ob->state == DT_OBSTACLE_EMPTY || ob->state == DT_OBSTACLE_REMOVING) continue; if (contains(ob->touched, ob->ntouched, ref)) { if (ob->type == DT_OBSTACLE_CYLINDER) { dtMarkCylinderArea(*bc.layer, tile->header->bmin, m_params.cs, m_params.ch, ob->cylinder.pos, ob->cylinder.radius, ob->cylinder.height, 0); } else if (ob->type == DT_OBSTACLE_BOX) { dtMarkBoxArea(*bc.layer, tile->header->bmin, m_params.cs, m_params.ch, ob->box.bmin, ob->box.bmax, 0); } else if (ob->type == DT_OBSTACLE_ORIENTED_BOX) { dtMarkBoxArea(*bc.layer, tile->header->bmin, m_params.cs, m_params.ch, ob->orientedBox.center, ob->orientedBox.halfExtents, ob->orientedBox.rotAux, 0); } } } // Build navmesh status = dtBuildTileCacheRegions(m_talloc, *bc.layer, walkableClimbVx); if (dtStatusFailed(status)) return status; bc.lcset = dtAllocTileCacheContourSet(m_talloc); if (!bc.lcset) return DT_FAILURE | DT_OUT_OF_MEMORY; status = dtBuildTileCacheContours(m_talloc, *bc.layer, walkableClimbVx, m_params.maxSimplificationError, *bc.lcset); if (dtStatusFailed(status)) return status; bc.lmesh = dtAllocTileCachePolyMesh(m_talloc); if (!bc.lmesh) return DT_FAILURE | DT_OUT_OF_MEMORY; status = dtBuildTileCachePolyMesh(m_talloc, *bc.lcset, *bc.lmesh); if (dtStatusFailed(status)) return status; // Early out if the mesh tile is empty. if (!bc.lmesh->npolys) { // Remove existing tile. navmesh->removeTile(navmesh->getTileRefAt(tile->header->tx,tile->header->ty,tile->header->tlayer),0,0); return DT_SUCCESS; } dtNavMeshCreateParams params; memset(¶ms, 0, sizeof(params)); params.verts = bc.lmesh->verts; params.vertCount = bc.lmesh->nverts; params.polys = bc.lmesh->polys; params.polyAreas = bc.lmesh->areas; params.polyFlags = bc.lmesh->flags; params.polyCount = bc.lmesh->npolys; params.nvp = DT_VERTS_PER_POLYGON; params.walkableHeight = m_params.walkableHeight; params.walkableRadius = m_params.walkableRadius; params.walkableClimb = m_params.walkableClimb; params.tileX = tile->header->tx; params.tileY = tile->header->ty; params.tileLayer = tile->header->tlayer; params.cs = m_params.cs; params.ch = m_params.ch; params.buildBvTree = false; dtVcopy(params.bmin, tile->header->bmin); dtVcopy(params.bmax, tile->header->bmax); if (m_tmproc) { m_tmproc->process(¶ms, bc.lmesh->areas, bc.lmesh->flags); } unsigned char* navData = 0; int navDataSize = 0; if (!dtCreateNavMeshData(¶ms, &navData, &navDataSize)) return DT_FAILURE; // Remove existing tile. navmesh->removeTile(navmesh->getTileRefAt(tile->header->tx,tile->header->ty,tile->header->tlayer),0,0); // Add new tile, or leave the location empty. if (navData) { // Let the navmesh own the data. status = navmesh->addTile(navData,navDataSize,DT_TILE_FREE_DATA,0,0); if (dtStatusFailed(status)) { dtFree(navData); return status; } } return DT_SUCCESS; } void dtTileCache::calcTightTileBounds(const dtTileCacheLayerHeader* header, float* bmin, float* bmax) const { const float cs = m_params.cs; bmin[0] = header->bmin[0] + header->minx*cs; bmin[1] = header->bmin[1]; bmin[2] = header->bmin[2] + header->miny*cs; bmax[0] = header->bmin[0] + (header->maxx+1)*cs; bmax[1] = header->bmax[1]; bmax[2] = header->bmin[2] + (header->maxy+1)*cs; } void dtTileCache::getObstacleBounds(const struct dtTileCacheObstacle* ob, float* bmin, float* bmax) const { if (ob->type == DT_OBSTACLE_CYLINDER) { const dtObstacleCylinder &cl = ob->cylinder; bmin[0] = cl.pos[0] - cl.radius; bmin[1] = cl.pos[1]; bmin[2] = cl.pos[2] - cl.radius; bmax[0] = cl.pos[0] + cl.radius; bmax[1] = cl.pos[1] + cl.height; bmax[2] = cl.pos[2] + cl.radius; } else if (ob->type == DT_OBSTACLE_BOX) { dtVcopy(bmin, ob->box.bmin); dtVcopy(bmax, ob->box.bmax); } else if (ob->type == DT_OBSTACLE_ORIENTED_BOX) { const dtObstacleOrientedBox &orientedBox = ob->orientedBox; float maxr = 1.41f*dtMax(orientedBox.halfExtents[0], orientedBox.halfExtents[2]); bmin[0] = orientedBox.center[0] - maxr; bmax[0] = orientedBox.center[0] + maxr; bmin[1] = orientedBox.center[1] - orientedBox.halfExtents[1]; bmax[1] = orientedBox.center[1] + orientedBox.halfExtents[1]; bmin[2] = orientedBox.center[2] - maxr; bmax[2] = orientedBox.center[2] + maxr; } }