diff --git a/.changeset/moody-years-end.md b/.changeset/moody-years-end.md new file mode 100644 index 00000000..30e46895 --- /dev/null +++ b/.changeset/moody-years-end.md @@ -0,0 +1,8 @@ +--- +'recast-navigation': minor +'@recast-navigation/generators': minor +--- + +feat: add optional 'bounds' config to navmesh generators + +If provided, it will be used as the bounds for the navmesh heightfield during generation. If not provided, the bounds will be calculated from the input geometry. If the bounds are known ahead of time, providing them can save some time during generation. diff --git a/packages/recast-navigation-generators/src/generators/generate-solo-nav-mesh.ts b/packages/recast-navigation-generators/src/generators/generate-solo-nav-mesh.ts index a51404e7..0f9e2597 100644 --- a/packages/recast-navigation-generators/src/generators/generate-solo-nav-mesh.ts +++ b/packages/recast-navigation-generators/src/generators/generate-solo-nav-mesh.ts @@ -13,6 +13,7 @@ import { TriangleAreasArray, TrianglesArray, UnsignedCharArray, + Vector3Tuple, VerticesArray, allocCompactHeightfield, allocContourSet, @@ -52,6 +53,12 @@ export type SoloNavMeshGeneratorConfig = Pretty< * @default true */ buildBvTree?: boolean; + + /** + * The minimum and maximum bounds of the heightfield's AABB in world units. + * If not provided, the bounding box will be calculated from the input positions and indices + */ + bounds?: [bbMin: Vector3Tuple, bbMax: Vector3Tuple]; } >; @@ -158,7 +165,19 @@ export const generateSoloNavMeshData = ( const trianglesArray = new TrianglesArray(); trianglesArray.copy(triangles); - const { bbMin, bbMax } = getBoundingBox(positions, indices); + let bbMin: Vector3Tuple; + let bbMax: Vector3Tuple; + + if (navMeshGeneratorConfig.bounds) { + bbMin = navMeshGeneratorConfig.bounds[0]; + bbMax = navMeshGeneratorConfig.bounds[1]; + } else { + const boundingBox = getBoundingBox(positions, indices); + bbMin = boundingBox.bbMin; + bbMax = boundingBox.bbMax; + console.log('bbMin', bbMin); + console.log('bbMax', bbMax); + } // // Step 1. Initialize build config. diff --git a/packages/recast-navigation-generators/src/generators/generate-tile-cache.ts b/packages/recast-navigation-generators/src/generators/generate-tile-cache.ts index 2d4d5d40..1ab78e69 100644 --- a/packages/recast-navigation-generators/src/generators/generate-tile-cache.ts +++ b/packages/recast-navigation-generators/src/generators/generate-tile-cache.ts @@ -54,6 +54,12 @@ type TileCacheRecastConfig = Omit; export type TileCacheGeneratorConfig = Pretty< TileCacheRecastConfig & { + /** + * The minimum and maximum bounds of the heightfield's AABB in world units. + * If not provided, the bounding box will be calculated from the input positions and indices + */ + bounds?: [bbMin: Vector3Tuple, bbMax: Vector3Tuple]; + /** * How many layers (or "floors") each navmesh tile is expected to have. */ @@ -165,7 +171,17 @@ export const generateTileCache = ( const trianglesArray = new TrianglesArray(); trianglesArray.copy(triangles); - const { bbMin, bbMax } = getBoundingBox(positions, indices); + let bbMin: Vector3Tuple; + let bbMax: Vector3Tuple; + + if (navMeshGeneratorConfig.bounds) { + bbMin = navMeshGeneratorConfig.bounds[0]; + bbMax = navMeshGeneratorConfig.bounds[1]; + } else { + const boundingBox = getBoundingBox(positions, indices); + bbMin = boundingBox.bbMin; + bbMax = boundingBox.bbMax; + } const { expectedLayersPerTile, maxObstacles, ...recastConfig } = { ...tileCacheGeneratorConfigDefaults, diff --git a/packages/recast-navigation-generators/src/generators/generate-tiled-nav-mesh.ts b/packages/recast-navigation-generators/src/generators/generate-tiled-nav-mesh.ts index 6e367038..7f4bddbc 100644 --- a/packages/recast-navigation-generators/src/generators/generate-tiled-nav-mesh.ts +++ b/packages/recast-navigation-generators/src/generators/generate-tiled-nav-mesh.ts @@ -128,6 +128,12 @@ export const buildTiledNavMeshRcConfig = ({ export type TiledNavMeshGeneratorConfig = Pretty< RecastConfig & OffMeshConnectionGeneratorParams & { + /** + * The minimum and maximum bounds of the heightfield's AABB in world units. + * If not provided, the bounding box will be calculated from the input positions and indices + */ + bounds?: [bbMin: Vector3Tuple, bbMax: Vector3Tuple]; + /** * @default 128 */ @@ -669,8 +675,17 @@ export const generateTiledNavMesh = ( ...navMeshGeneratorConfig, }; - /* get input bounding box */ - const { bbMin, bbMax } = getBoundingBox(positions, indices); + let bbMin: Vector3Tuple; + let bbMax: Vector3Tuple; + + if (navMeshGeneratorConfig.bounds) { + bbMin = navMeshGeneratorConfig.bounds[0]; + bbMax = navMeshGeneratorConfig.bounds[1]; + } else { + const boundingBox = getBoundingBox(positions, indices); + bbMin = boundingBox.bbMin; + bbMax = boundingBox.bbMax; + } const { config: rcConfig, diff --git a/packages/recast-navigation/.storybook/stories/advanced/custom-areas-generator.ts b/packages/recast-navigation/.storybook/stories/advanced/custom-areas-generator.ts index 17318175..47013848 100644 --- a/packages/recast-navigation/.storybook/stories/advanced/custom-areas-generator.ts +++ b/packages/recast-navigation/.storybook/stories/advanced/custom-areas-generator.ts @@ -386,9 +386,9 @@ export const generateNavMesh = ( navMeshCreateParams.setPolyMeshCreateParams(polyMesh); navMeshCreateParams.setPolyMeshDetailCreateParams(polyMeshDetail); - navMeshCreateParams.setWalkableHeight(config.walkableHeight); - navMeshCreateParams.setWalkableRadius(config.walkableRadius); - navMeshCreateParams.setWalkableClimb(config.walkableClimb); + navMeshCreateParams.setWalkableHeight(config.walkableHeight * config.ch); + navMeshCreateParams.setWalkableRadius(config.walkableRadius * config.cs); + navMeshCreateParams.setWalkableClimb(config.walkableClimb * config.ch); navMeshCreateParams.setCellSize(config.cs); navMeshCreateParams.setCellHeight(config.ch); diff --git a/packages/recast-navigation/.storybook/stories/nav-mesh/custom-bounds.stories.tsx b/packages/recast-navigation/.storybook/stories/nav-mesh/custom-bounds.stories.tsx new file mode 100644 index 00000000..29eb60fe --- /dev/null +++ b/packages/recast-navigation/.storybook/stories/nav-mesh/custom-bounds.stories.tsx @@ -0,0 +1,76 @@ +import { OrbitControls } from '@react-three/drei'; +import { NavMesh, NavMeshQuery, Vector3Tuple } from '@recast-navigation/core'; +import { threeToSoloNavMesh } from '@recast-navigation/three'; +import React, { useEffect, useState } from 'react'; +import { Box3, Group, Mesh } from 'three'; +import { Debug } from '../../common/debug'; +import { NavTestEnvironment } from '../../common/nav-test-environment'; +import { decorators } from '../../decorators'; +import { parameters } from '../../parameters'; + +export default { + title: 'NavMesh / Custom Bounds', + decorators, + parameters, +}; + +const BOUNDS = new Box3(); +BOUNDS.min.set(-3, -1, -5); +BOUNDS.max.set(9, 5, 3); + +export const CustomBounds = () => { + const [group, setGroup] = useState(null); + + const [navMesh, setNavMesh] = useState(); + const [navMeshQuery, setNavMeshQuery] = useState(); + + useEffect(() => { + if (!group) return; + + const meshes: Mesh[] = []; + + group.traverse((child) => { + if (child instanceof Mesh) { + meshes.push(child); + } + }); + + const walkableRadiusWorld = 0.1; + const cellSize = 0.05; + + const { success, navMesh } = threeToSoloNavMesh(meshes, { + cs: cellSize, + ch: 0.2, + walkableRadius: Math.ceil(walkableRadiusWorld / cellSize), + bounds: [BOUNDS.min.toArray(), BOUNDS.max.toArray()], + }); + + if (!success) { + return; + } + + setNavMesh(navMesh); + setNavMeshQuery(navMeshQuery); + + return () => { + navMesh.destroy(); + + setNavMesh(undefined); + setNavMeshQuery(undefined); + }; + }, [group]); + + return ( + <> + + + + + + + + + + + ); +};