@recast-navigation/generators 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright © 2022 Isaac Mason
3
+ Copyright © 2025 Isaac Mason
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +1,35 @@
1
- import { NavMesh, RecastBuildContext, RecastCompactHeightfield, RecastConfig, RecastContourSet, RecastHeightfield, RecastPolyMesh, RecastPolyMeshDetail } from '@recast-navigation/core';
1
+ import { NavMesh, RecastBuildContext, RecastCompactHeightfield, RecastConfig, RecastContourSet, RecastHeightfield, RecastPolyMesh, RecastPolyMeshDetail, UnsignedCharArray, Vector3Tuple } from '@recast-navigation/core';
2
2
  import { Pretty } from '../types';
3
3
  import { OffMeshConnectionGeneratorParams } from './common';
4
- export type SoloNavMeshGeneratorConfig = Pretty<Omit<RecastConfig, 'tileSize'> & OffMeshConnectionGeneratorParams>;
5
- export declare const soloNavMeshGeneratorConfigDefaults: SoloNavMeshGeneratorConfig;
4
+ export type SoloNavMeshGeneratorConfig = Pretty<Omit<RecastConfig, 'tileSize'> & OffMeshConnectionGeneratorParams & {
5
+ /**
6
+ * @default true
7
+ */
8
+ buildBvTree?: boolean;
9
+ /**
10
+ * The minimum and maximum bounds of the heightfield's AABB in world units.
11
+ * If not provided, the bounding box will be calculated from the input positions and indices
12
+ */
13
+ bounds?: [bbMin: Vector3Tuple, bbMax: Vector3Tuple];
14
+ }>;
15
+ export declare const soloNavMeshGeneratorConfigDefaults: {
16
+ buildBvTree: true;
17
+ borderSize: number;
18
+ tileSize: number;
19
+ cs: number;
20
+ ch: number;
21
+ walkableSlopeAngle: number;
22
+ walkableHeight: number;
23
+ walkableClimb: number;
24
+ walkableRadius: number;
25
+ maxEdgeLen: number;
26
+ maxSimplificationError: number;
27
+ minRegionArea: number;
28
+ mergeRegionArea: number;
29
+ maxVertsPerPoly: number;
30
+ detailSampleDist: number;
31
+ detailSampleMaxError: number;
32
+ };
6
33
  export type SoloNavMeshGeneratorIntermediates = {
7
34
  type: 'solo';
8
35
  buildContext: RecastBuildContext;
@@ -12,18 +39,38 @@ export type SoloNavMeshGeneratorIntermediates = {
12
39
  polyMesh?: RecastPolyMesh;
13
40
  polyMeshDetail?: RecastPolyMeshDetail;
14
41
  };
15
- type SoloNavMeshGeneratorSuccessResult = {
42
+ type GenerateSoloNavMeshDataSuccessResult = {
43
+ navMeshData: UnsignedCharArray;
44
+ success: true;
45
+ intermediates: SoloNavMeshGeneratorIntermediates;
46
+ };
47
+ type GenerateSoloNavMeshDataFailResult = {
48
+ navMeshData: undefined;
49
+ success: false;
50
+ intermediates: SoloNavMeshGeneratorIntermediates;
51
+ error: string;
52
+ };
53
+ export type GenerateSoloNavMeshDataResult = GenerateSoloNavMeshDataSuccessResult | GenerateSoloNavMeshDataFailResult;
54
+ /**
55
+ * Builds Solo NavMesh data from the given positions and indices.
56
+ * @param positions a flat array of positions
57
+ * @param indices a flat array of indices
58
+ * @param navMeshGeneratorConfig optional configuration for the NavMesh generator
59
+ * @param keepIntermediates if true intermediates will be returned
60
+ */
61
+ export declare const generateSoloNavMeshData: (positions: ArrayLike<number>, indices: ArrayLike<number>, navMeshGeneratorConfig?: Partial<SoloNavMeshGeneratorConfig>, keepIntermediates?: boolean) => GenerateSoloNavMeshDataResult;
62
+ type GenerateSoloNavMeshSuccessResult = {
16
63
  navMesh: NavMesh;
17
64
  success: true;
18
65
  intermediates: SoloNavMeshGeneratorIntermediates;
19
66
  };
20
- type SoloNavMeshGeneratorFailResult = {
67
+ type GenerateSoloNavMeshFailResult = {
21
68
  navMesh: undefined;
22
69
  success: false;
23
70
  intermediates: SoloNavMeshGeneratorIntermediates;
24
71
  error: string;
25
72
  };
26
- export type SoloNavMeshGeneratorResult = SoloNavMeshGeneratorSuccessResult | SoloNavMeshGeneratorFailResult;
73
+ export type GenerateSoloNavMeshResult = GenerateSoloNavMeshSuccessResult | GenerateSoloNavMeshFailResult;
27
74
  /**
28
75
  * Builds a Solo NavMesh from the given positions and indices.
29
76
  * @param positions a flat array of positions
@@ -31,5 +78,5 @@ export type SoloNavMeshGeneratorResult = SoloNavMeshGeneratorSuccessResult | Sol
31
78
  * @param navMeshGeneratorConfig optional configuration for the NavMesh generator
32
79
  * @param keepIntermediates if true intermediates will be returned
33
80
  */
34
- export declare const generateSoloNavMesh: (positions: ArrayLike<number>, indices: ArrayLike<number>, navMeshGeneratorConfig?: Partial<SoloNavMeshGeneratorConfig>, keepIntermediates?: boolean) => SoloNavMeshGeneratorResult;
81
+ export declare const generateSoloNavMesh: (positions: ArrayLike<number>, indices: ArrayLike<number>, navMeshGeneratorConfig?: Partial<SoloNavMeshGeneratorConfig>, keepIntermediates?: boolean) => GenerateSoloNavMeshResult;
35
82
  export {};
@@ -1,7 +1,12 @@
1
- import { NavMesh, RecastBuildContext, RecastChunkyTriMesh, RecastCompactHeightfield, RecastConfig, RecastHeightfield, RecastHeightfieldLayerSet, TileCache, TileCacheMeshProcess } from '@recast-navigation/core';
1
+ import { NavMesh, RecastBuildContext, RecastChunkyTriMesh, RecastCompactHeightfield, RecastConfig, RecastHeightfield, RecastHeightfieldLayerSet, TileCache, TileCacheMeshProcess, Vector3Tuple } from '@recast-navigation/core';
2
2
  import { Pretty } from '../types';
3
3
  type TileCacheRecastConfig = Omit<RecastConfig, 'minRegionArea' | 'maxEdgeLen'>;
4
4
  export type TileCacheGeneratorConfig = Pretty<TileCacheRecastConfig & {
5
+ /**
6
+ * The minimum and maximum bounds of the heightfield's AABB in world units.
7
+ * If not provided, the bounding box will be calculated from the input positions and indices
8
+ */
9
+ bounds?: [bbMin: Vector3Tuple, bbMax: Vector3Tuple];
5
10
  /**
6
11
  * How many layers (or "floors") each navmesh tile is expected to have.
7
12
  */
@@ -1,35 +1,105 @@
1
- import { NavMesh, RecastBuildContext, RecastChunkyTriMesh, RecastCompactHeightfield, RecastConfig, RecastContourSet, RecastHeightfield, RecastPolyMesh, RecastPolyMeshDetail } from '@recast-navigation/core';
1
+ import { FloatArray, IntArray, NavMesh, OffMeshConnectionParams, RawModule, RecastBuildContext, RecastChunkyTriMesh, RecastCompactHeightfield, RecastConfig, RecastContourSet, RecastHeightfield, RecastPolyMesh, RecastPolyMeshDetail, UnsignedCharArray, Vector3Tuple } from '@recast-navigation/core';
2
2
  import { Pretty } from '../types';
3
3
  import { OffMeshConnectionGeneratorParams } from './common';
4
- export type TiledNavMeshGeneratorConfig = Pretty<RecastConfig & OffMeshConnectionGeneratorParams>;
5
- export declare const tiledNavMeshGeneratorConfigDefaults: TiledNavMeshGeneratorConfig;
4
+ export declare const buildTiledNavMeshRcConfig: ({ recastConfig, navMeshBounds: [navMeshBoundsMin, navMeshBoundsMax], }: {
5
+ recastConfig: RecastConfig;
6
+ navMeshBounds: [min: Vector3Tuple, max: Vector3Tuple];
7
+ }) => {
8
+ config: RawModule.rcConfig;
9
+ gridSize: RawModule.RecastCalcGridSizeResult;
10
+ tileSize: number;
11
+ tileWidth: number;
12
+ tileHeight: number;
13
+ tcs: number;
14
+ orig: {
15
+ x: number;
16
+ y: number;
17
+ z: number;
18
+ };
19
+ maxTiles: number;
20
+ maxPolysPerTile: number;
21
+ };
22
+ export type TiledNavMeshGeneratorConfig = Pretty<RecastConfig & OffMeshConnectionGeneratorParams & {
23
+ /**
24
+ * The minimum and maximum bounds of the heightfield's AABB in world units.
25
+ * If not provided, the bounding box will be calculated from the input positions and indices
26
+ */
27
+ bounds?: [bbMin: Vector3Tuple, bbMax: Vector3Tuple];
28
+ /**
29
+ * @default 128
30
+ */
31
+ chunkyTriMeshTrisPerChunk?: number;
32
+ /**
33
+ * @default true
34
+ */
35
+ buildBvTree?: boolean;
36
+ }>;
37
+ export declare const tiledNavMeshGeneratorConfigDefaults: {
38
+ chunkyTriMeshTrisPerChunk: number;
39
+ buildBvTree: true;
40
+ borderSize: number;
41
+ tileSize: number;
42
+ cs: number;
43
+ ch: number;
44
+ walkableSlopeAngle: number;
45
+ walkableHeight: number;
46
+ walkableClimb: number;
47
+ walkableRadius: number;
48
+ maxEdgeLen: number;
49
+ maxSimplificationError: number;
50
+ minRegionArea: number;
51
+ mergeRegionArea: number;
52
+ maxVertsPerPoly: number;
53
+ detailSampleDist: number;
54
+ detailSampleMaxError: number;
55
+ };
6
56
  type TileIntermediates = {
7
- tileX: number;
8
- tileY: number;
57
+ x: number;
58
+ y: number;
9
59
  heightfield?: RecastHeightfield;
10
60
  compactHeightfield?: RecastCompactHeightfield;
11
61
  contourSet?: RecastContourSet;
12
62
  polyMesh?: RecastPolyMesh;
13
63
  polyMeshDetail?: RecastPolyMeshDetail;
14
64
  };
65
+ type GenerateTileNavMeshDataSuccessResult = {
66
+ success: true;
67
+ data?: UnsignedCharArray;
68
+ intermediates: TileIntermediates;
69
+ };
70
+ type GenerateTileNavMeshDataFailResult = {
71
+ success: false;
72
+ error: string;
73
+ intermediates: TileIntermediates;
74
+ };
75
+ export type GenerateTileNavMeshDataResult = GenerateTileNavMeshDataSuccessResult | GenerateTileNavMeshDataFailResult;
76
+ export declare const generateTileNavMeshData: (positions: FloatArray, indices: IntArray, rcConfig: RawModule.rcConfig, chunkyTriMesh: RecastChunkyTriMesh, tile: {
77
+ x: number;
78
+ y: number;
79
+ bmin: Vector3Tuple;
80
+ bmax: Vector3Tuple;
81
+ }, options?: {
82
+ offMeshConnections?: OffMeshConnectionParams[];
83
+ buildBvTree?: boolean;
84
+ }, keepIntermediates?: boolean, buildContext?: RecastBuildContext) => GenerateTileNavMeshDataResult;
15
85
  export type TiledNavMeshGeneratorIntermediates = {
16
86
  type: 'tiled';
17
87
  buildContext: RecastBuildContext;
18
88
  chunkyTriMesh?: RecastChunkyTriMesh;
19
89
  tileIntermediates: TileIntermediates[];
20
90
  };
21
- type TiledNavMeshGeneratorSuccessResult = {
91
+ type GenerateTiledNavMeshSuccessResult = {
22
92
  navMesh: NavMesh;
23
93
  success: true;
24
94
  intermediates: TiledNavMeshGeneratorIntermediates;
25
95
  };
26
- type TiledNavMeshGeneratorFailResult = {
96
+ type GenerateTiledNavMeshFailResult = {
27
97
  navMesh: undefined;
28
98
  success: false;
29
99
  intermediates: TiledNavMeshGeneratorIntermediates;
30
100
  error: string;
31
101
  };
32
- export type TiledNavMeshGeneratorResult = TiledNavMeshGeneratorSuccessResult | TiledNavMeshGeneratorFailResult;
102
+ export type GenerateTiledNavMeshResult = GenerateTiledNavMeshSuccessResult | GenerateTiledNavMeshFailResult;
33
103
  /**
34
104
  * Builds a Tiled NavMesh
35
105
  * @param positions a flat array of positions
@@ -37,5 +107,5 @@ export type TiledNavMeshGeneratorResult = TiledNavMeshGeneratorSuccessResult | T
37
107
  * @param navMeshGeneratorConfig optional configuration for the NavMesh generator
38
108
  * @param keepIntermediates if true intermediates will be returned
39
109
  */
40
- export declare const generateTiledNavMesh: (positions: ArrayLike<number>, indices: ArrayLike<number>, navMeshGeneratorConfig?: Partial<TiledNavMeshGeneratorConfig>, keepIntermediates?: boolean) => TiledNavMeshGeneratorResult;
110
+ export declare const generateTiledNavMesh: (positions: ArrayLike<number>, indices: ArrayLike<number>, navMeshGeneratorConfig?: Partial<TiledNavMeshGeneratorConfig>, keepIntermediates?: boolean) => GenerateTiledNavMeshResult;
41
111
  export {};
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { vec3, recastConfigDefaults, Raw, RecastBuildContext, NavMesh, VerticesArray, TrianglesArray, createRcConfig, calcGridSize, allocHeightfield, createHeightfield, TriangleAreasArray, markWalkableTriangles, rasterizeTriangles, filterLowHangingWalkableObstacles, filterLedgeSpans, filterWalkableLowHeightSpans, allocCompactHeightfield, buildCompactHeightfield, freeHeightfield, erodeWalkableArea, buildDistanceField, buildRegions, allocContourSet, buildContours, Recast, allocPolyMesh, buildPolyMesh, allocPolyMeshDetail, buildPolyMeshDetail, freeCompactHeightfield, freeContourSet, NavMeshCreateParams, createNavMeshData, freePolyMesh, freePolyMeshDetail, TileCacheMeshProcess, TileCache, DetourTileCacheParams, NavMeshParams, RecastChunkyTriMesh, statusFailed, freeHeightfieldLayerSet, cloneRcConfig, ChunkIdsArray, allocHeightfieldLayerSet, buildHeightfieldLayers, TileCacheData, Detour, getHeightfieldLayerHeights, getHeightfieldLayerAreas, getHeightfieldLayerCons, buildTileCacheLayer, statusToReadableString } from '@recast-navigation/core';
1
+ import { vec3, recastConfigDefaults, RecastBuildContext, VerticesArray, TrianglesArray, createRcConfig, calcGridSize, allocHeightfield, createHeightfield, TriangleAreasArray, markWalkableTriangles, rasterizeTriangles, filterLowHangingWalkableObstacles, filterLedgeSpans, filterWalkableLowHeightSpans, allocCompactHeightfield, buildCompactHeightfield, freeHeightfield, erodeWalkableArea, buildDistanceField, buildRegions, allocContourSet, buildContours, Recast, allocPolyMesh, buildPolyMesh, allocPolyMeshDetail, buildPolyMeshDetail, freeCompactHeightfield, freeContourSet, NavMeshCreateParams, createNavMeshData, Raw, NavMesh, freePolyMesh, freePolyMeshDetail, TileCacheMeshProcess, TileCache, DetourTileCacheParams, NavMeshParams, RecastChunkyTriMesh, statusFailed, freeHeightfieldLayerSet, cloneRcConfig, ChunkIdsArray, allocHeightfieldLayerSet, buildHeightfieldLayers, TileCacheData, Detour, getHeightfieldLayerHeights, getHeightfieldLayerAreas, getHeightfieldLayerCons, buildTileCacheLayer, statusToReadableString } from '@recast-navigation/core';
2
2
 
3
3
  const getBoundingBox = (positions, indices) => {
4
4
  const bbMin = {
@@ -57,25 +57,22 @@ const dtNextPow2 = v => {
57
57
  };
58
58
 
59
59
  const soloNavMeshGeneratorConfigDefaults = {
60
- ...recastConfigDefaults
60
+ ...recastConfigDefaults,
61
+ buildBvTree: true
61
62
  };
62
63
  /**
63
- * Builds a Solo NavMesh from the given positions and indices.
64
+ * Builds Solo NavMesh data from the given positions and indices.
64
65
  * @param positions a flat array of positions
65
66
  * @param indices a flat array of indices
66
67
  * @param navMeshGeneratorConfig optional configuration for the NavMesh generator
67
68
  * @param keepIntermediates if true intermediates will be returned
68
69
  */
69
- const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, keepIntermediates = false) => {
70
- if (!Raw.Module) {
71
- throw new Error('"init" must be called before using any recast-navigation-js APIs. See: https://github.com/isaac-mason/recast-navigation-js?tab=readme-ov-file#initialization');
72
- }
70
+ const generateSoloNavMeshData = (positions, indices, navMeshGeneratorConfig = {}, keepIntermediates = false) => {
73
71
  const buildContext = new RecastBuildContext();
74
72
  const intermediates = {
75
73
  type: 'solo',
76
74
  buildContext
77
75
  };
78
- const navMesh = new NavMesh();
79
76
  const cleanup = () => {
80
77
  if (keepIntermediates) return;
81
78
  if (intermediates.heightfield) {
@@ -101,9 +98,8 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
101
98
  };
102
99
  const fail = error => {
103
100
  cleanup();
104
- navMesh.destroy();
105
101
  return {
106
- navMesh: undefined,
102
+ navMeshData: undefined,
107
103
  success: false,
108
104
  intermediates,
109
105
  error
@@ -118,31 +114,38 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
118
114
  const numTriangles = indices.length / 3;
119
115
  const trianglesArray = new TrianglesArray();
120
116
  trianglesArray.copy(triangles);
121
- const {
122
- bbMin,
123
- bbMax
124
- } = getBoundingBox(positions, indices);
117
+ let bbMin;
118
+ let bbMax;
119
+ if (navMeshGeneratorConfig.bounds) {
120
+ bbMin = navMeshGeneratorConfig.bounds[0];
121
+ bbMax = navMeshGeneratorConfig.bounds[1];
122
+ } else {
123
+ const boundingBox = getBoundingBox(positions, indices);
124
+ bbMin = boundingBox.bbMin;
125
+ bbMax = boundingBox.bbMax;
126
+ }
125
127
  //
126
128
  // Step 1. Initialize build config.
127
129
  //
128
- const config = createRcConfig({
130
+ const config = {
129
131
  ...soloNavMeshGeneratorConfigDefaults,
130
132
  ...navMeshGeneratorConfig
131
- });
132
- config.minRegionArea = config.minRegionArea * config.minRegionArea; // Note: area = size*size
133
- config.mergeRegionArea = config.mergeRegionArea * config.mergeRegionArea; // Note: area = size*size
134
- config.detailSampleDist = config.detailSampleDist < 0.9 ? 0 : config.cs * config.detailSampleDist;
135
- config.detailSampleMaxError = config.ch * config.detailSampleMaxError;
136
- const gridSize = calcGridSize(bbMin, bbMax, config.cs);
137
- config.width = gridSize.width;
138
- config.height = gridSize.height;
133
+ };
134
+ const rcConfig = createRcConfig(config);
135
+ rcConfig.minRegionArea = rcConfig.minRegionArea * rcConfig.minRegionArea; // Note: area = size*size
136
+ rcConfig.mergeRegionArea = rcConfig.mergeRegionArea * rcConfig.mergeRegionArea; // Note: area = size*size
137
+ rcConfig.detailSampleDist = rcConfig.detailSampleDist < 0.9 ? 0 : rcConfig.cs * rcConfig.detailSampleDist;
138
+ rcConfig.detailSampleMaxError = rcConfig.ch * rcConfig.detailSampleMaxError;
139
+ const gridSize = calcGridSize(bbMin, bbMax, rcConfig.cs);
140
+ rcConfig.width = gridSize.width;
141
+ rcConfig.height = gridSize.height;
139
142
  //
140
143
  // Step 2. Rasterize input polygon soup.
141
144
  //
142
145
  // Allocate voxel heightfield where we rasterize our input data to.
143
146
  const heightfield = allocHeightfield();
144
147
  intermediates.heightfield = heightfield;
145
- if (!createHeightfield(buildContext, heightfield, config.width, config.height, bbMin, bbMax, config.cs, config.ch)) {
148
+ if (!createHeightfield(buildContext, heightfield, rcConfig.width, rcConfig.height, bbMin, bbMax, rcConfig.cs, rcConfig.ch)) {
146
149
  return fail('Could not create heightfield');
147
150
  }
148
151
  // Find triangles which are walkable based on their slope and rasterize them.
@@ -150,8 +153,8 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
150
153
  // the are type for each of the meshes and rasterize them.
151
154
  const triangleAreasArray = new TriangleAreasArray();
152
155
  triangleAreasArray.resize(numTriangles);
153
- markWalkableTriangles(buildContext, config.walkableSlopeAngle, verticesArray, numVertices, trianglesArray, numTriangles, triangleAreasArray);
154
- if (!rasterizeTriangles(buildContext, verticesArray, numVertices, trianglesArray, triangleAreasArray, numTriangles, heightfield, config.walkableClimb)) {
156
+ markWalkableTriangles(buildContext, rcConfig.walkableSlopeAngle, verticesArray, numVertices, trianglesArray, numTriangles, triangleAreasArray);
157
+ if (!rasterizeTriangles(buildContext, verticesArray, numVertices, trianglesArray, triangleAreasArray, numTriangles, heightfield, rcConfig.walkableClimb)) {
155
158
  return fail('Could not rasterize triangles');
156
159
  }
157
160
  triangleAreasArray.destroy();
@@ -163,9 +166,9 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
163
166
  // Once all geoemtry is rasterized, we do initial pass of filtering to
164
167
  // remove unwanted overhangs caused by the conservative rasterization
165
168
  // as well as filter spans where the character cannot possibly stand.
166
- filterLowHangingWalkableObstacles(buildContext, config.walkableClimb, heightfield);
167
- filterLedgeSpans(buildContext, config.walkableHeight, config.walkableClimb, heightfield);
168
- filterWalkableLowHeightSpans(buildContext, config.walkableHeight, heightfield);
169
+ filterLowHangingWalkableObstacles(buildContext, rcConfig.walkableClimb, heightfield);
170
+ filterLedgeSpans(buildContext, rcConfig.walkableHeight, rcConfig.walkableClimb, heightfield);
171
+ filterWalkableLowHeightSpans(buildContext, rcConfig.walkableHeight, heightfield);
169
172
  //
170
173
  // Step 4. Partition walkable surface to simple regions.
171
174
  //
@@ -174,7 +177,7 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
174
177
  // between walkable cells will be calculated.
175
178
  const compactHeightfield = allocCompactHeightfield();
176
179
  intermediates.compactHeightfield = compactHeightfield;
177
- if (!buildCompactHeightfield(buildContext, config.walkableHeight, config.walkableClimb, heightfield, compactHeightfield)) {
180
+ if (!buildCompactHeightfield(buildContext, rcConfig.walkableHeight, rcConfig.walkableClimb, heightfield, compactHeightfield)) {
178
181
  return fail('Failed to build compact data');
179
182
  }
180
183
  if (!keepIntermediates) {
@@ -182,7 +185,7 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
182
185
  intermediates.heightfield = undefined;
183
186
  }
184
187
  // Erode the walkable area by agent radius.
185
- if (!erodeWalkableArea(buildContext, config.walkableRadius, compactHeightfield)) {
188
+ if (!erodeWalkableArea(buildContext, rcConfig.walkableRadius, compactHeightfield)) {
186
189
  return fail('Failed to erode walkable area');
187
190
  }
188
191
  // (Optional) Mark areas
@@ -192,7 +195,7 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
192
195
  return fail('Failed to build distance field');
193
196
  }
194
197
  // Partition the walkable surface into simple regions without holes.
195
- if (!buildRegions(buildContext, compactHeightfield, config.borderSize, config.minRegionArea, config.mergeRegionArea)) {
198
+ if (!buildRegions(buildContext, compactHeightfield, rcConfig.borderSize, rcConfig.minRegionArea, rcConfig.mergeRegionArea)) {
196
199
  return fail('Failed to build regions');
197
200
  }
198
201
  //
@@ -200,7 +203,7 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
200
203
  //
201
204
  const contourSet = allocContourSet();
202
205
  intermediates.contourSet = contourSet;
203
- if (!buildContours(buildContext, compactHeightfield, config.maxSimplificationError, config.maxEdgeLen, contourSet, Recast.RC_CONTOUR_TESS_WALL_EDGES)) {
206
+ if (!buildContours(buildContext, compactHeightfield, rcConfig.maxSimplificationError, rcConfig.maxEdgeLen, contourSet, Recast.RC_CONTOUR_TESS_WALL_EDGES)) {
204
207
  return fail('Failed to create contours');
205
208
  }
206
209
  //
@@ -208,7 +211,7 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
208
211
  //
209
212
  const polyMesh = allocPolyMesh();
210
213
  intermediates.polyMesh = polyMesh;
211
- if (!buildPolyMesh(buildContext, contourSet, config.maxVertsPerPoly, polyMesh)) {
214
+ if (!buildPolyMesh(buildContext, contourSet, rcConfig.maxVertsPerPoly, polyMesh)) {
212
215
  return fail('Failed to triangulate contours');
213
216
  }
214
217
  //
@@ -216,7 +219,7 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
216
219
  //
217
220
  const polyMeshDetail = allocPolyMeshDetail();
218
221
  intermediates.polyMeshDetail = polyMeshDetail;
219
- if (!buildPolyMeshDetail(buildContext, polyMesh, compactHeightfield, config.detailSampleDist, config.detailSampleMaxError, polyMeshDetail)) {
222
+ if (!buildPolyMeshDetail(buildContext, polyMesh, compactHeightfield, rcConfig.detailSampleDist, rcConfig.detailSampleMaxError, polyMeshDetail)) {
220
223
  return fail('Failed to build detail mesh');
221
224
  }
222
225
  if (!keepIntermediates) {
@@ -239,12 +242,12 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
239
242
  const navMeshCreateParams = new NavMeshCreateParams();
240
243
  navMeshCreateParams.setPolyMeshCreateParams(polyMesh);
241
244
  navMeshCreateParams.setPolyMeshDetailCreateParams(polyMeshDetail);
242
- navMeshCreateParams.setWalkableHeight(config.walkableHeight * config.ch);
243
- navMeshCreateParams.setWalkableRadius(config.walkableRadius * config.cs);
244
- navMeshCreateParams.setWalkableClimb(config.walkableClimb * config.ch);
245
- navMeshCreateParams.setCellSize(config.cs);
246
- navMeshCreateParams.setCellHeight(config.ch);
247
- navMeshCreateParams.setBuildBvTree(true);
245
+ navMeshCreateParams.setWalkableHeight(rcConfig.walkableHeight * rcConfig.ch);
246
+ navMeshCreateParams.setWalkableRadius(rcConfig.walkableRadius * rcConfig.cs);
247
+ navMeshCreateParams.setWalkableClimb(rcConfig.walkableClimb * rcConfig.ch);
248
+ navMeshCreateParams.setCellSize(rcConfig.cs);
249
+ navMeshCreateParams.setCellHeight(rcConfig.ch);
250
+ navMeshCreateParams.setBuildBvTree(config.buildBvTree);
248
251
  if (navMeshGeneratorConfig.offMeshConnections) {
249
252
  navMeshCreateParams.setOffMeshConnections(navMeshGeneratorConfig.offMeshConnections);
250
253
  }
@@ -252,16 +255,50 @@ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, ke
252
255
  if (!createNavMeshDataResult.success) {
253
256
  return fail('Failed to create Detour navmesh data');
254
257
  }
258
+ cleanup();
259
+ return {
260
+ navMeshData: createNavMeshDataResult.navMeshData,
261
+ success: true,
262
+ intermediates
263
+ };
264
+ };
265
+ /**
266
+ * Builds a Solo NavMesh from the given positions and indices.
267
+ * @param positions a flat array of positions
268
+ * @param indices a flat array of indices
269
+ * @param navMeshGeneratorConfig optional configuration for the NavMesh generator
270
+ * @param keepIntermediates if true intermediates will be returned
271
+ */
272
+ const generateSoloNavMesh = (positions, indices, navMeshGeneratorConfig = {}, keepIntermediates = false) => {
273
+ if (!Raw.Module) {
274
+ throw new Error('"init" must be called before using any recast-navigation-js APIs. See: https://github.com/isaac-mason/recast-navigation-js?tab=readme-ov-file#initialization');
275
+ }
276
+ const createNavMeshDataResult = generateSoloNavMeshData(positions, indices, navMeshGeneratorConfig, keepIntermediates);
277
+ if (!createNavMeshDataResult.success) {
278
+ return {
279
+ navMesh: undefined,
280
+ success: false,
281
+ intermediates: createNavMeshDataResult.intermediates,
282
+ error: createNavMeshDataResult.error
283
+ };
284
+ }
255
285
  const {
256
286
  navMeshData
257
287
  } = createNavMeshDataResult;
288
+ const navMesh = new NavMesh();
258
289
  if (!navMesh.initSolo(navMeshData)) {
259
- return fail('Failed to create Detour navmesh');
290
+ navMeshData.destroy();
291
+ return {
292
+ navMesh: undefined,
293
+ success: false,
294
+ intermediates: createNavMeshDataResult.intermediates,
295
+ error: 'Failed to initialize solo NavMesh'
296
+ };
260
297
  }
261
298
  return {
262
299
  success: true,
263
300
  navMesh,
264
- intermediates
301
+ intermediates: createNavMeshDataResult.intermediates
265
302
  };
266
303
  };
267
304
 
@@ -307,10 +344,16 @@ const generateTileCache = (positions, indices, navMeshGeneratorConfig = {}, keep
307
344
  const numTriangles = indices.length / 3;
308
345
  const trianglesArray = new TrianglesArray();
309
346
  trianglesArray.copy(triangles);
310
- const {
311
- bbMin,
312
- bbMax
313
- } = getBoundingBox(positions, indices);
347
+ let bbMin;
348
+ let bbMax;
349
+ if (navMeshGeneratorConfig.bounds) {
350
+ bbMin = navMeshGeneratorConfig.bounds[0];
351
+ bbMax = navMeshGeneratorConfig.bounds[1];
352
+ } else {
353
+ const boundingBox = getBoundingBox(positions, indices);
354
+ bbMin = boundingBox.bbMin;
355
+ bbMax = boundingBox.bbMax;
356
+ }
314
357
  const {
315
358
  expectedLayersPerTile,
316
359
  maxObstacles,
@@ -594,8 +637,271 @@ const generateTileCache = (positions, indices, navMeshGeneratorConfig = {}, keep
594
637
  };
595
638
  };
596
639
 
640
+ const buildTiledNavMeshRcConfig = ({
641
+ recastConfig,
642
+ navMeshBounds: [navMeshBoundsMin, navMeshBoundsMax]
643
+ }) => {
644
+ //
645
+ // Initialize build config.
646
+ //
647
+ const config = createRcConfig(recastConfig);
648
+ /* grid size */
649
+ const gridSize = calcGridSize(navMeshBoundsMin, navMeshBoundsMax, config.cs);
650
+ config.width = gridSize.width;
651
+ config.height = gridSize.height;
652
+ config.minRegionArea = config.minRegionArea * config.minRegionArea; // Note: area = size*size
653
+ config.mergeRegionArea = config.mergeRegionArea * config.mergeRegionArea; // Note: area = size*size
654
+ config.tileSize = Math.floor(config.tileSize);
655
+ config.borderSize = config.walkableRadius + 3; // Reserve enough padding.
656
+ config.width = config.tileSize + config.borderSize * 2;
657
+ config.height = config.tileSize + config.borderSize * 2;
658
+ config.detailSampleDist = config.detailSampleDist < 0.9 ? 0 : config.cs * config.detailSampleDist;
659
+ config.detailSampleMaxError = config.ch * config.detailSampleMaxError;
660
+ // tile size
661
+ const tileSize = Math.floor(config.tileSize);
662
+ const tileWidth = Math.floor((gridSize.width + tileSize - 1) / tileSize);
663
+ const tileHeight = Math.floor((gridSize.height + tileSize - 1) / tileSize);
664
+ const tcs = config.tileSize * config.cs;
665
+ /* Create dtNavMeshParams, initialise nav mesh for tiled use */
666
+ const orig = vec3.fromArray(navMeshBoundsMin);
667
+ // Max tiles and max polys affect how the tile IDs are caculated.
668
+ // There are 22 bits available for identifying a tile and a polygon.
669
+ let tileBits = Math.min(Math.floor(dtIlog2(dtNextPow2(tileWidth * tileHeight))), 14);
670
+ if (tileBits > 14) tileBits = 14;
671
+ const polyBits = 22 - tileBits;
672
+ const maxTiles = 1 << tileBits;
673
+ const maxPolysPerTile = 1 << polyBits;
674
+ return {
675
+ config,
676
+ gridSize,
677
+ tileSize,
678
+ tileWidth,
679
+ tileHeight,
680
+ tcs,
681
+ orig,
682
+ maxTiles,
683
+ maxPolysPerTile
684
+ };
685
+ };
597
686
  const tiledNavMeshGeneratorConfigDefaults = {
598
- ...recastConfigDefaults
687
+ ...recastConfigDefaults,
688
+ chunkyTriMeshTrisPerChunk: 256,
689
+ buildBvTree: true
690
+ };
691
+ const generateTileNavMeshData = (positions, indices, rcConfig, chunkyTriMesh, tile, options = {}, keepIntermediates = false, buildContext = new RecastBuildContext()) => {
692
+ const tileIntermediate = {
693
+ x: tile.x,
694
+ y: tile.y
695
+ };
696
+ const cleanup = () => {
697
+ if (keepIntermediates) return;
698
+ if (tileIntermediate.compactHeightfield) {
699
+ freeCompactHeightfield(tileIntermediate.compactHeightfield);
700
+ tileIntermediate.compactHeightfield = undefined;
701
+ }
702
+ if (tileIntermediate.heightfield) {
703
+ freeHeightfield(tileIntermediate.heightfield);
704
+ tileIntermediate.heightfield = undefined;
705
+ }
706
+ if (tileIntermediate.contourSet) {
707
+ freeContourSet(tileIntermediate.contourSet);
708
+ tileIntermediate.contourSet = undefined;
709
+ }
710
+ if (tileIntermediate.polyMesh) {
711
+ freePolyMesh(tileIntermediate.polyMesh);
712
+ tileIntermediate.polyMesh = undefined;
713
+ }
714
+ if (tileIntermediate.polyMeshDetail) {
715
+ freePolyMeshDetail(tileIntermediate.polyMeshDetail);
716
+ tileIntermediate.polyMeshDetail = undefined;
717
+ }
718
+ };
719
+ const failTileMesh = error => {
720
+ buildContext.log(Recast.RC_LOG_ERROR, error);
721
+ cleanup();
722
+ return {
723
+ success: false,
724
+ error,
725
+ intermediates: tileIntermediate
726
+ };
727
+ };
728
+ const tileConfig = cloneRcConfig(rcConfig);
729
+ // Expand the heightfield bounding box by border size to find the extents of geometry we need to build this tile.
730
+ //
731
+ // This is done in order to make sure that the navmesh tiles connect correctly at the borders,
732
+ // and the obstacles close to the border work correctly with the dilation process.
733
+ // No polygons (or contours) will be created on the border area.
734
+ //
735
+ // IMPORTANT!
736
+ //
737
+ // :''''''''':
738
+ // : +-----+ :
739
+ // : | | :
740
+ // : | |<--- tile to build
741
+ // : | | :
742
+ // : +-----+ :<-- geometry needed
743
+ // :.........:
744
+ //
745
+ // You should use this bounding box to query your input geometry.
746
+ //
747
+ // For example if you build a navmesh for terrain, and want the navmesh tiles to match the terrain tile size
748
+ // you will need to pass in data from neighbour terrain tiles too! In a simple case, just pass in all the 8 neighbours,
749
+ // or use the bounding box below to only pass in a sliver of each of the 8 neighbours.
750
+ const expandedTileBoundsMin = [...tile.bmin];
751
+ const expandedTileBoundsMax = [...tile.bmax];
752
+ expandedTileBoundsMin[0] -= tileConfig.borderSize * tileConfig.cs;
753
+ expandedTileBoundsMin[2] -= tileConfig.borderSize * tileConfig.cs;
754
+ expandedTileBoundsMax[0] += tileConfig.borderSize * tileConfig.cs;
755
+ expandedTileBoundsMax[2] += tileConfig.borderSize * tileConfig.cs;
756
+ tileConfig.set_bmin(0, expandedTileBoundsMin[0]);
757
+ tileConfig.set_bmin(1, expandedTileBoundsMin[1]);
758
+ tileConfig.set_bmin(2, expandedTileBoundsMin[2]);
759
+ tileConfig.set_bmax(0, expandedTileBoundsMax[0]);
760
+ tileConfig.set_bmax(1, expandedTileBoundsMax[1]);
761
+ tileConfig.set_bmax(2, expandedTileBoundsMax[2]);
762
+ // Reset build timer
763
+ buildContext.resetTimers();
764
+ // Start the build process
765
+ buildContext.startTimer(Recast.RC_TIMER_TOTAL);
766
+ buildContext.log(Recast.RC_LOG_PROGRESS, `Building tile at x: ${tile.x}, y: ${tile.y}`);
767
+ buildContext.log(Recast.RC_LOG_PROGRESS, ` - ${tileConfig.width} x ${tileConfig.height} cells`);
768
+ buildContext.log(Recast.RC_LOG_PROGRESS, ` - ${positions.size / 3 / 1000}K verts, ${indices.size / 3 / 1000}K tris`);
769
+ // Allocate voxel heightfield where we rasterize our input data to.
770
+ const heightfield = allocHeightfield();
771
+ tileIntermediate.heightfield = heightfield;
772
+ if (!createHeightfield(buildContext, heightfield, tileConfig.width, tileConfig.height, expandedTileBoundsMin, expandedTileBoundsMax, tileConfig.cs, tileConfig.ch)) {
773
+ return failTileMesh('Could not create heightfield');
774
+ }
775
+ // Allocate array that can hold triangle flags.
776
+ // If you have multiple meshes you need to process, allocate
777
+ // and array which can hold the max number of triangles you need to process.
778
+ const triAreas = new TriangleAreasArray();
779
+ triAreas.resize(chunkyTriMesh.maxTrisPerChunk());
780
+ const tbmin = [expandedTileBoundsMin[0], expandedTileBoundsMin[2]];
781
+ const tbmax = [expandedTileBoundsMax[0], expandedTileBoundsMax[2]];
782
+ // TODO: Make grow when returning too many items.
783
+ const maxChunkIds = 512;
784
+ const chunkIdsArray = new ChunkIdsArray();
785
+ chunkIdsArray.resize(maxChunkIds);
786
+ const nChunksOverlapping = chunkyTriMesh.getChunksOverlappingRect(tbmin, tbmax, chunkIdsArray, maxChunkIds);
787
+ if (nChunksOverlapping === 0) {
788
+ return {
789
+ success: true,
790
+ intermediates: tileIntermediate
791
+ };
792
+ }
793
+ for (let i = 0; i < nChunksOverlapping; ++i) {
794
+ const nodeId = chunkIdsArray.get(i);
795
+ const node = chunkyTriMesh.nodes(nodeId);
796
+ const nNodeTris = node.n;
797
+ const nodeTrianglesArray = chunkyTriMesh.getNodeTris(nodeId);
798
+ const triangleAreasArray = new TriangleAreasArray();
799
+ triangleAreasArray.resize(nNodeTris);
800
+ // Find triangles which are walkable based on their slope and rasterize them.
801
+ // If your input data is multiple meshes, you can transform them here, calculate
802
+ // the are type for each of the meshes and rasterize them.
803
+ markWalkableTriangles(buildContext, tileConfig.walkableSlopeAngle, positions, indices.size, nodeTrianglesArray, nNodeTris, triangleAreasArray);
804
+ const success = rasterizeTriangles(buildContext, positions, indices.size, nodeTrianglesArray, triangleAreasArray, nNodeTris, heightfield, tileConfig.walkableClimb);
805
+ triangleAreasArray.destroy();
806
+ if (!success) {
807
+ return failTileMesh('Could not rasterize triangles');
808
+ }
809
+ }
810
+ // Once all geometry is rasterized, we do initial pass of filtering to
811
+ // remove unwanted overhangs caused by the conservative rasterization
812
+ // as well as filter spans where the character cannot possibly stand.
813
+ filterLowHangingWalkableObstacles(buildContext, tileConfig.walkableClimb, heightfield);
814
+ filterLedgeSpans(buildContext, tileConfig.walkableHeight, tileConfig.walkableClimb, heightfield);
815
+ filterWalkableLowHeightSpans(buildContext, tileConfig.walkableHeight, heightfield);
816
+ // Compact the heightfield so that it is faster to handle from now on.
817
+ // This will result more cache coherent data as well as the neighbours
818
+ // between walkable cells will be calculated.
819
+ const compactHeightfield = allocCompactHeightfield();
820
+ tileIntermediate.compactHeightfield = compactHeightfield;
821
+ if (!buildCompactHeightfield(buildContext, tileConfig.walkableHeight, tileConfig.walkableClimb, heightfield, compactHeightfield)) {
822
+ return failTileMesh('Could not build compact heightfield');
823
+ }
824
+ if (!keepIntermediates) {
825
+ freeHeightfield(tileIntermediate.heightfield);
826
+ tileIntermediate.heightfield = undefined;
827
+ }
828
+ // Erode the walkable area by agent radius
829
+ if (!erodeWalkableArea(buildContext, tileConfig.walkableRadius, compactHeightfield)) {
830
+ return failTileMesh('Could not erode walkable area');
831
+ }
832
+ // (Optional) Mark areas
833
+ // markConvexPolyArea(...)
834
+ // Prepare for region partitioning, by calculating Distance field along the walkable surface.
835
+ if (!buildDistanceField(buildContext, compactHeightfield)) {
836
+ return failTileMesh('Failed to build distance field');
837
+ }
838
+ // Partition the walkable surface into simple regions without holes.
839
+ if (!buildRegions(buildContext, compactHeightfield, tileConfig.borderSize, tileConfig.minRegionArea, tileConfig.mergeRegionArea)) {
840
+ return failTileMesh('Failed to build regions');
841
+ }
842
+ //
843
+ // Trace and simplify region contours.
844
+ //
845
+ const contourSet = allocContourSet();
846
+ tileIntermediate.contourSet = contourSet;
847
+ if (!buildContours(buildContext, compactHeightfield, tileConfig.maxSimplificationError, tileConfig.maxEdgeLen, contourSet, Recast.RC_CONTOUR_TESS_WALL_EDGES)) {
848
+ return failTileMesh('Failed to create contours');
849
+ }
850
+ //
851
+ // Build polygons mesh from contours.
852
+ //
853
+ const polyMesh = allocPolyMesh();
854
+ tileIntermediate.polyMesh = polyMesh;
855
+ if (!buildPolyMesh(buildContext, contourSet, tileConfig.maxVertsPerPoly, polyMesh)) {
856
+ return failTileMesh('Failed to triangulate contours');
857
+ }
858
+ //
859
+ // Create detail mesh which allows to access approximate height on each polygon.
860
+ //
861
+ const polyMeshDetail = allocPolyMeshDetail();
862
+ tileIntermediate.polyMeshDetail = polyMeshDetail;
863
+ if (!buildPolyMeshDetail(buildContext, polyMesh, compactHeightfield, tileConfig.detailSampleDist, tileConfig.detailSampleMaxError, polyMeshDetail)) {
864
+ return failTileMesh('Failed to build detail mesh');
865
+ }
866
+ if (!keepIntermediates) {
867
+ freeCompactHeightfield(compactHeightfield);
868
+ tileIntermediate.compactHeightfield = undefined;
869
+ freeContourSet(contourSet);
870
+ tileIntermediate.contourSet = undefined;
871
+ }
872
+ // Update poly flags from areas.
873
+ for (let i = 0; i < polyMesh.npolys(); i++) {
874
+ if (polyMesh.areas(i) == Recast.RC_WALKABLE_AREA) {
875
+ polyMesh.setAreas(i, 0);
876
+ }
877
+ if (polyMesh.areas(i) == 0) {
878
+ polyMesh.setFlags(i, 1);
879
+ }
880
+ }
881
+ const navMeshCreateParams = new NavMeshCreateParams();
882
+ navMeshCreateParams.setPolyMeshCreateParams(polyMesh);
883
+ navMeshCreateParams.setPolyMeshDetailCreateParams(polyMeshDetail);
884
+ navMeshCreateParams.setWalkableHeight(tileConfig.walkableHeight * tileConfig.ch);
885
+ navMeshCreateParams.setWalkableRadius(tileConfig.walkableRadius * tileConfig.cs);
886
+ navMeshCreateParams.setWalkableClimb(tileConfig.walkableClimb * tileConfig.ch);
887
+ navMeshCreateParams.setCellSize(tileConfig.cs);
888
+ navMeshCreateParams.setCellHeight(tileConfig.ch);
889
+ navMeshCreateParams.setBuildBvTree(options.buildBvTree ?? tiledNavMeshGeneratorConfigDefaults.buildBvTree);
890
+ if (options.offMeshConnections) {
891
+ navMeshCreateParams.setOffMeshConnections(options.offMeshConnections);
892
+ }
893
+ navMeshCreateParams.setTileX(tile.x);
894
+ navMeshCreateParams.setTileY(tile.y);
895
+ const createNavMeshDataResult = createNavMeshData(navMeshCreateParams);
896
+ if (!createNavMeshDataResult.success) {
897
+ return failTileMesh('Failed to create Detour navmesh data');
898
+ }
899
+ buildContext.log(Recast.RC_LOG_PROGRESS, `>> Polymesh: ${polyMesh.nverts()} vertices ${polyMesh.npolys()} polygons`);
900
+ return {
901
+ success: true,
902
+ data: createNavMeshDataResult.navMeshData,
903
+ intermediates: tileIntermediate
904
+ };
599
905
  };
600
906
  /**
601
907
  * Builds a Tiled NavMesh
@@ -618,7 +924,6 @@ const generateTiledNavMesh = (positions, indices, navMeshGeneratorConfig = {}, k
618
924
  const navMesh = new NavMesh();
619
925
  /* input geometry */
620
926
  const vertices = positions;
621
- const numVertices = indices.length;
622
927
  const verticesArray = new VerticesArray();
623
928
  verticesArray.copy(vertices);
624
929
  const triangles = indices;
@@ -629,29 +934,6 @@ const generateTiledNavMesh = (positions, indices, navMeshGeneratorConfig = {}, k
629
934
  verticesArray.destroy();
630
935
  trianglesArray.destroy();
631
936
  if (keepIntermediates) return;
632
- for (let i = 0; i < intermediates.tileIntermediates.length; i++) {
633
- const tileIntermediate = intermediates.tileIntermediates[i];
634
- if (tileIntermediate.compactHeightfield) {
635
- freeCompactHeightfield(tileIntermediate.compactHeightfield);
636
- tileIntermediate.compactHeightfield = undefined;
637
- }
638
- if (tileIntermediate.heightfield) {
639
- freeHeightfield(tileIntermediate.heightfield);
640
- tileIntermediate.heightfield = undefined;
641
- }
642
- if (tileIntermediate.contourSet) {
643
- freeContourSet(tileIntermediate.contourSet);
644
- tileIntermediate.contourSet = undefined;
645
- }
646
- if (tileIntermediate.polyMesh) {
647
- freePolyMesh(tileIntermediate.polyMesh);
648
- tileIntermediate.polyMesh = undefined;
649
- }
650
- if (tileIntermediate.polyMeshDetail) {
651
- freePolyMeshDetail(tileIntermediate.polyMeshDetail);
652
- tileIntermediate.polyMeshDetail = undefined;
653
- }
654
- }
655
937
  if (intermediates.chunkyTriMesh) {
656
938
  intermediates.chunkyTriMesh = undefined;
657
939
  }
@@ -669,45 +951,36 @@ const generateTiledNavMesh = (positions, indices, navMeshGeneratorConfig = {}, k
669
951
  //
670
952
  // Initialize build config.
671
953
  //
672
- const config = createRcConfig({
954
+ const generatorConfig = {
673
955
  ...tiledNavMeshGeneratorConfigDefaults,
674
956
  ...navMeshGeneratorConfig
675
- });
676
- /* get input bounding box */
957
+ };
958
+ let bbMin;
959
+ let bbMax;
960
+ if (navMeshGeneratorConfig.bounds) {
961
+ bbMin = navMeshGeneratorConfig.bounds[0];
962
+ bbMax = navMeshGeneratorConfig.bounds[1];
963
+ } else {
964
+ const boundingBox = getBoundingBox(positions, indices);
965
+ bbMin = boundingBox.bbMin;
966
+ bbMax = boundingBox.bbMax;
967
+ }
677
968
  const {
678
- bbMin,
679
- bbMax
680
- } = getBoundingBox(positions, indices);
681
- /* grid size */
682
- const gridSize = calcGridSize(bbMin, bbMax, config.cs);
683
- config.width = gridSize.width;
684
- config.height = gridSize.height;
685
- config.minRegionArea = config.minRegionArea * config.minRegionArea; // Note: area = size*size
686
- config.mergeRegionArea = config.mergeRegionArea * config.mergeRegionArea; // Note: area = size*size
687
- config.tileSize = Math.floor(config.tileSize);
688
- config.borderSize = config.walkableRadius + 3; // Reserve enough padding.
689
- config.width = config.tileSize + config.borderSize * 2;
690
- config.height = config.tileSize + config.borderSize * 2;
691
- config.detailSampleDist = config.detailSampleDist < 0.9 ? 0 : config.cs * config.detailSampleDist;
692
- config.detailSampleMaxError = config.ch * config.detailSampleMaxError;
693
- // tile size
694
- const tileSize = Math.floor(config.tileSize);
695
- const tileWidth = Math.floor((gridSize.width + tileSize - 1) / tileSize);
696
- const tileHeight = Math.floor((gridSize.height + tileSize - 1) / tileSize);
697
- const tcs = config.tileSize * config.cs;
698
- /* Create dtNavMeshParams, initialise nav mesh for tiled use */
699
- const orig = vec3.fromArray(bbMin);
700
- // Max tiles and max polys affect how the tile IDs are caculated.
701
- // There are 22 bits available for identifying a tile and a polygon.
702
- let tileBits = Math.min(Math.floor(dtIlog2(dtNextPow2(tileWidth * tileHeight))), 14);
703
- if (tileBits > 14) tileBits = 14;
704
- const polyBits = 22 - tileBits;
705
- const maxTiles = 1 << tileBits;
706
- const maxPolysPerTile = 1 << polyBits;
969
+ config: rcConfig,
970
+ tileWidth,
971
+ tileHeight,
972
+ tcs,
973
+ orig,
974
+ maxTiles,
975
+ maxPolysPerTile
976
+ } = buildTiledNavMeshRcConfig({
977
+ recastConfig: generatorConfig,
978
+ navMeshBounds: [bbMin, bbMax]
979
+ });
707
980
  const navMeshParams = NavMeshParams.create({
708
981
  orig,
709
- tileWidth: config.tileSize * config.cs,
710
- tileHeight: config.tileSize * config.cs,
982
+ tileWidth: generatorConfig.tileSize * generatorConfig.cs,
983
+ tileHeight: generatorConfig.tileSize * generatorConfig.cs,
711
984
  maxTiles,
712
985
  maxPolys: maxPolysPerTile
713
986
  });
@@ -717,199 +990,9 @@ const generateTiledNavMesh = (positions, indices, navMeshGeneratorConfig = {}, k
717
990
  /* create chunky tri mesh */
718
991
  const chunkyTriMesh = new RecastChunkyTriMesh();
719
992
  intermediates.chunkyTriMesh = chunkyTriMesh;
720
- if (!chunkyTriMesh.init(verticesArray, trianglesArray, numTriangles, 256)) {
993
+ if (!chunkyTriMesh.init(verticesArray, trianglesArray, numTriangles, generatorConfig.chunkyTriMeshTrisPerChunk)) {
721
994
  return fail('Failed to build chunky triangle mesh');
722
995
  }
723
- /** @internal */
724
- const buildTileMesh = (tileX, tileY, tileBoundsMin, tileBoundsMax) => {
725
- const failTileMesh = error => {
726
- buildContext.log(Recast.RC_LOG_ERROR, error);
727
- return {
728
- success: false,
729
- error
730
- };
731
- };
732
- const tileIntermediate = {
733
- tileX,
734
- tileY
735
- };
736
- intermediates.tileIntermediates.push(tileIntermediate);
737
- const tileConfig = cloneRcConfig(config);
738
- // Expand the heightfield bounding box by border size to find the extents of geometry we need to build this tile.
739
- //
740
- // This is done in order to make sure that the navmesh tiles connect correctly at the borders,
741
- // and the obstacles close to the border work correctly with the dilation process.
742
- // No polygons (or contours) will be created on the border area.
743
- //
744
- // IMPORTANT!
745
- //
746
- // :''''''''':
747
- // : +-----+ :
748
- // : | | :
749
- // : | |<--- tile to build
750
- // : | | :
751
- // : +-----+ :<-- geometry needed
752
- // :.........:
753
- //
754
- // You should use this bounding box to query your input geometry.
755
- //
756
- // For example if you build a navmesh for terrain, and want the navmesh tiles to match the terrain tile size
757
- // you will need to pass in data from neighbour terrain tiles too! In a simple case, just pass in all the 8 neighbours,
758
- // or use the bounding box below to only pass in a sliver of each of the 8 neighbours.
759
- const expandedTileBoundsMin = [...tileBoundsMin];
760
- const expandedTileBoundsMax = [...tileBoundsMax];
761
- expandedTileBoundsMin[0] -= tileConfig.borderSize * tileConfig.cs;
762
- expandedTileBoundsMin[2] -= tileConfig.borderSize * tileConfig.cs;
763
- expandedTileBoundsMax[0] += tileConfig.borderSize * tileConfig.cs;
764
- expandedTileBoundsMax[2] += tileConfig.borderSize * tileConfig.cs;
765
- tileConfig.set_bmin(0, expandedTileBoundsMin[0]);
766
- tileConfig.set_bmin(1, expandedTileBoundsMin[1]);
767
- tileConfig.set_bmin(2, expandedTileBoundsMin[2]);
768
- tileConfig.set_bmax(0, expandedTileBoundsMax[0]);
769
- tileConfig.set_bmax(1, expandedTileBoundsMax[1]);
770
- tileConfig.set_bmax(2, expandedTileBoundsMax[2]);
771
- // Reset build timer
772
- buildContext.resetTimers();
773
- // Start the build process
774
- buildContext.startTimer(Recast.RC_TIMER_TOTAL);
775
- buildContext.log(Recast.RC_LOG_PROGRESS, `Building tile ${intermediates.tileIntermediates.length} at x: ${tileX}, y: ${tileY}`);
776
- buildContext.log(Recast.RC_LOG_PROGRESS, ` - ${config.width} x ${config.height} cells`);
777
- buildContext.log(Recast.RC_LOG_PROGRESS, ` - ${numVertices / 1000}fK verts, ${numTriangles / 1000}K tris`);
778
- // Allocate voxel heightfield where we rasterize our input data to.
779
- const heightfield = allocHeightfield();
780
- tileIntermediate.heightfield = heightfield;
781
- if (!createHeightfield(buildContext, heightfield, tileConfig.width, tileConfig.height, expandedTileBoundsMin, expandedTileBoundsMax, tileConfig.cs, tileConfig.ch)) {
782
- return failTileMesh('Could not create heightfield');
783
- }
784
- // Allocate array that can hold triangle flags.
785
- // If you have multiple meshes you need to process, allocate
786
- // and array which can hold the max number of triangles you need to process.
787
- const triAreas = new TriangleAreasArray();
788
- triAreas.resize(chunkyTriMesh.maxTrisPerChunk());
789
- const tbmin = [expandedTileBoundsMin[0], expandedTileBoundsMin[2]];
790
- const tbmax = [expandedTileBoundsMax[0], expandedTileBoundsMax[2]];
791
- // TODO: Make grow when returning too many items.
792
- const maxChunkIds = 512;
793
- const chunkIdsArray = new ChunkIdsArray();
794
- chunkIdsArray.resize(maxChunkIds);
795
- const nChunksOverlapping = chunkyTriMesh.getChunksOverlappingRect(tbmin, tbmax, chunkIdsArray, maxChunkIds);
796
- if (nChunksOverlapping === 0) {
797
- return {
798
- success: true
799
- };
800
- }
801
- for (let i = 0; i < nChunksOverlapping; ++i) {
802
- const nodeId = chunkIdsArray.get(i);
803
- const node = chunkyTriMesh.nodes(nodeId);
804
- const nNodeTris = node.n;
805
- const nodeTrianglesArray = chunkyTriMesh.getNodeTris(nodeId);
806
- const triangleAreasArray = new TriangleAreasArray();
807
- triangleAreasArray.resize(nNodeTris);
808
- // Find triangles which are walkable based on their slope and rasterize them.
809
- // If your input data is multiple meshes, you can transform them here, calculate
810
- // the are type for each of the meshes and rasterize them.
811
- markWalkableTriangles(buildContext, tileConfig.walkableSlopeAngle, verticesArray, numVertices, nodeTrianglesArray, nNodeTris, triangleAreasArray);
812
- const success = rasterizeTriangles(buildContext, verticesArray, numVertices, nodeTrianglesArray, triangleAreasArray, nNodeTris, heightfield, tileConfig.walkableClimb);
813
- triangleAreasArray.destroy();
814
- if (!success) {
815
- return failTileMesh('Could not rasterize triangles');
816
- }
817
- }
818
- // Once all geometry is rasterized, we do initial pass of filtering to
819
- // remove unwanted overhangs caused by the conservative rasterization
820
- // as well as filter spans where the character cannot possibly stand.
821
- filterLowHangingWalkableObstacles(buildContext, tileConfig.walkableClimb, heightfield);
822
- filterLedgeSpans(buildContext, tileConfig.walkableHeight, tileConfig.walkableClimb, heightfield);
823
- filterWalkableLowHeightSpans(buildContext, tileConfig.walkableHeight, heightfield);
824
- // Compact the heightfield so that it is faster to handle from now on.
825
- // This will result more cache coherent data as well as the neighbours
826
- // between walkable cells will be calculated.
827
- const compactHeightfield = allocCompactHeightfield();
828
- tileIntermediate.compactHeightfield = compactHeightfield;
829
- if (!buildCompactHeightfield(buildContext, tileConfig.walkableHeight, tileConfig.walkableClimb, heightfield, compactHeightfield)) {
830
- return failTileMesh('Could not build compact heightfield');
831
- }
832
- if (!keepIntermediates) {
833
- freeHeightfield(tileIntermediate.heightfield);
834
- tileIntermediate.heightfield = undefined;
835
- }
836
- // Erode the walkable area by agent radius
837
- if (!erodeWalkableArea(buildContext, tileConfig.walkableRadius, compactHeightfield)) {
838
- return failTileMesh('Could not erode walkable area');
839
- }
840
- // (Optional) Mark areas
841
- // markConvexPolyArea(...)
842
- // Prepare for region partitioning, by calculating Distance field along the walkable surface.
843
- if (!buildDistanceField(buildContext, compactHeightfield)) {
844
- return failTileMesh('Failed to build distance field');
845
- }
846
- // Partition the walkable surface into simple regions without holes.
847
- if (!buildRegions(buildContext, compactHeightfield, tileConfig.borderSize, tileConfig.minRegionArea, tileConfig.mergeRegionArea)) {
848
- return failTileMesh('Failed to build regions');
849
- }
850
- //
851
- // Trace and simplify region contours.
852
- //
853
- const contourSet = allocContourSet();
854
- tileIntermediate.contourSet = contourSet;
855
- if (!buildContours(buildContext, compactHeightfield, tileConfig.maxSimplificationError, tileConfig.maxEdgeLen, contourSet, Recast.RC_CONTOUR_TESS_WALL_EDGES)) {
856
- return failTileMesh('Failed to create contours');
857
- }
858
- //
859
- // Build polygons mesh from contours.
860
- //
861
- const polyMesh = allocPolyMesh();
862
- tileIntermediate.polyMesh = polyMesh;
863
- if (!buildPolyMesh(buildContext, contourSet, tileConfig.maxVertsPerPoly, polyMesh)) {
864
- return failTileMesh('Failed to triangulate contours');
865
- }
866
- //
867
- // Create detail mesh which allows to access approximate height on each polygon.
868
- //
869
- const polyMeshDetail = allocPolyMeshDetail();
870
- tileIntermediate.polyMeshDetail = polyMeshDetail;
871
- if (!buildPolyMeshDetail(buildContext, polyMesh, compactHeightfield, tileConfig.detailSampleDist, tileConfig.detailSampleMaxError, polyMeshDetail)) {
872
- return failTileMesh('Failed to build detail mesh');
873
- }
874
- if (!keepIntermediates) {
875
- freeCompactHeightfield(compactHeightfield);
876
- tileIntermediate.compactHeightfield = undefined;
877
- freeContourSet(contourSet);
878
- tileIntermediate.contourSet = undefined;
879
- }
880
- // Update poly flags from areas.
881
- for (let i = 0; i < polyMesh.npolys(); i++) {
882
- if (polyMesh.areas(i) == Recast.RC_WALKABLE_AREA) {
883
- polyMesh.setAreas(i, 0);
884
- }
885
- if (polyMesh.areas(i) == 0) {
886
- polyMesh.setFlags(i, 1);
887
- }
888
- }
889
- const navMeshCreateParams = new NavMeshCreateParams();
890
- navMeshCreateParams.setPolyMeshCreateParams(polyMesh);
891
- navMeshCreateParams.setPolyMeshDetailCreateParams(polyMeshDetail);
892
- navMeshCreateParams.setWalkableHeight(tileConfig.walkableHeight * tileConfig.ch);
893
- navMeshCreateParams.setWalkableRadius(tileConfig.walkableRadius * tileConfig.cs);
894
- navMeshCreateParams.setWalkableClimb(tileConfig.walkableClimb * tileConfig.ch);
895
- navMeshCreateParams.setCellSize(tileConfig.cs);
896
- navMeshCreateParams.setCellHeight(tileConfig.ch);
897
- navMeshCreateParams.setBuildBvTree(true);
898
- if (navMeshGeneratorConfig.offMeshConnections) {
899
- navMeshCreateParams.setOffMeshConnections(navMeshGeneratorConfig.offMeshConnections);
900
- }
901
- navMeshCreateParams.setTileX(tileX);
902
- navMeshCreateParams.setTileY(tileY);
903
- const createNavMeshDataResult = createNavMeshData(navMeshCreateParams);
904
- if (!createNavMeshDataResult.success) {
905
- return failTileMesh('Failed to create Detour navmesh data');
906
- }
907
- buildContext.log(Recast.RC_LOG_PROGRESS, `>> Polymesh: ${polyMesh.nverts()} vertices ${polyMesh.npolys()} polygons`);
908
- return {
909
- success: true,
910
- data: createNavMeshDataResult.navMeshData
911
- };
912
- };
913
996
  buildContext.startTimer(Recast.RC_TIMER_TEMP);
914
997
  const lastBuiltTileBmin = [0, 0, 0];
915
998
  const lastBuiltTileBmax = [0, 0, 0];
@@ -921,12 +1004,23 @@ const generateTiledNavMesh = (positions, indices, navMeshGeneratorConfig = {}, k
921
1004
  lastBuiltTileBmax[0] = bbMin[0] + (x + 1) * tcs;
922
1005
  lastBuiltTileBmax[1] = bbMax[1];
923
1006
  lastBuiltTileBmax[2] = bbMin[2] + (y + 1) * tcs;
924
- const result = buildTileMesh(x, y, lastBuiltTileBmin, lastBuiltTileBmax);
1007
+ const tile = {
1008
+ x,
1009
+ y,
1010
+ bmin: lastBuiltTileBmin,
1011
+ bmax: lastBuiltTileBmax
1012
+ };
1013
+ const generatorOptions = {
1014
+ offMeshConnections: generatorConfig.offMeshConnections,
1015
+ buildBvTree: generatorConfig.buildBvTree
1016
+ };
1017
+ const result = generateTileNavMeshData(verticesArray, trianglesArray, rcConfig, chunkyTriMesh, tile, generatorOptions, keepIntermediates, buildContext);
1018
+ intermediates.tileIntermediates.push(result.intermediates);
925
1019
  if (result.success && result.data) {
926
1020
  navMesh.removeTile(navMesh.getTileRefAt(x, y, 0));
927
1021
  const addTileResult = navMesh.addTile(result.data, Detour.DT_TILE_FREE_DATA, 0);
928
1022
  if (statusFailed(addTileResult.status)) {
929
- buildContext.log(Recast.RC_LOG_WARNING, `Failed to add tile to nav mesh` + '\n\t' + `tx: ${x}, ty: ${y},` + `status: ${statusToReadableString(addTileResult.status)} (${addTileResult.status})`);
1023
+ buildContext.log(Recast.RC_LOG_WARNING, `Failed to add tile to nav mesh tx: ${x}, ty: ${y}, status: ${statusToReadableString(addTileResult.status)} (${addTileResult.status})`);
930
1024
  result.data.destroy();
931
1025
  }
932
1026
  }
@@ -970,4 +1064,4 @@ const mergePositionsAndIndices = meshes => {
970
1064
  return [Float32Array.from(mergedPositions), Uint32Array.from(mergedIndices)];
971
1065
  };
972
1066
 
973
- export { createDefaultTileCacheMeshProcess, dtIlog2, dtNextPow2, generateSoloNavMesh, generateTileCache, generateTiledNavMesh, getBoundingBox, mergePositionsAndIndices, soloNavMeshGeneratorConfigDefaults, tileCacheGeneratorConfigDefaults, tiledNavMeshGeneratorConfigDefaults };
1067
+ export { buildTiledNavMeshRcConfig, createDefaultTileCacheMeshProcess, dtIlog2, dtNextPow2, generateSoloNavMesh, generateSoloNavMeshData, generateTileCache, generateTileNavMeshData, generateTiledNavMesh, getBoundingBox, mergePositionsAndIndices, soloNavMeshGeneratorConfigDefaults, tileCacheGeneratorConfigDefaults, tiledNavMeshGeneratorConfigDefaults };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "email": "isaac@isaacmason.com",
7
7
  "url": "https://isaacmason.com/"
8
8
  },
9
- "version": "0.37.0",
9
+ "version": "0.39.0",
10
10
  "license": "MIT",
11
11
  "homepage": "https://github.com/isaac-mason/recast-navigation-js",
12
12
  "bugs": {
@@ -27,8 +27,8 @@
27
27
  "lint": "eslint src"
28
28
  },
29
29
  "dependencies": {
30
- "@recast-navigation/core": "0.37.0",
31
- "@recast-navigation/wasm": "0.37.0"
30
+ "@recast-navigation/core": "0.39.0",
31
+ "@recast-navigation/wasm": "0.39.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@babel/core": "^7.24.5",