|
@@ -0,0 +1,380 @@
|
|
|
+import { INavigationEnginePlugin, ICrowd, IAgentParameters, INavMeshParameters } from "../../Navigation/INavigationEngine";
|
|
|
+import { Logger } from "../../Misc/logger";
|
|
|
+import { VertexData } from "../../Meshes/mesh.vertexData";
|
|
|
+import { Mesh } from "../../Meshes/mesh";
|
|
|
+import { Scene } from "../../scene";
|
|
|
+import { Vector3 } from '../../Maths/math';
|
|
|
+import { TransformNode } from "../../Meshes/transformNode";
|
|
|
+import { Observer } from "../../Misc/observable";
|
|
|
+import { Nullable } from "../../types";
|
|
|
+import { VertexBuffer } from "../../Meshes/buffer";
|
|
|
+
|
|
|
+declare var Recast: any;
|
|
|
+
|
|
|
+/**
|
|
|
+ * RecastJS navigation plugin
|
|
|
+ */
|
|
|
+export class RecastJSPlugin implements INavigationEnginePlugin {
|
|
|
+ /**
|
|
|
+ * Reference to the Recast library
|
|
|
+ */
|
|
|
+ public bjsRECAST: any = {};
|
|
|
+
|
|
|
+ /**
|
|
|
+ * plugin name
|
|
|
+ */
|
|
|
+ public name: string = "RecastJSPlugin";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * the first navmesh created. We might extend this to support multiple navmeshes
|
|
|
+ */
|
|
|
+ public navMesh: any;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Initializes the recastJS plugin
|
|
|
+ * @param recastInjection can be used to inject your own recast reference
|
|
|
+ */
|
|
|
+ public constructor(recastInjection: any = Recast) {
|
|
|
+ if (typeof recastInjection === "function") {
|
|
|
+ recastInjection(this.bjsRECAST);
|
|
|
+ } else {
|
|
|
+ this.bjsRECAST = recastInjection;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.isSupported()) {
|
|
|
+ Logger.Error("RecastJS is not available. Please make sure you included the js file.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a navigation mesh
|
|
|
+ * @param meshes array of all the geometry used to compute the navigatio mesh
|
|
|
+ * @param parameters bunch of parameters used to filter geometry
|
|
|
+ */
|
|
|
+ createMavMesh(meshes: Array<Mesh>, parameters: INavMeshParameters): void {
|
|
|
+ const rc = new this.bjsRECAST.rcConfig();
|
|
|
+ rc.cs = parameters.cs;
|
|
|
+ rc.ch = parameters.ch;
|
|
|
+ rc.borderSize = 0;
|
|
|
+ rc.tileSize = 0;
|
|
|
+ rc.walkableSlopeAngle = parameters.walkableSlopeAngle;
|
|
|
+ rc.walkableHeight = parameters.walkableHeight;
|
|
|
+ rc.walkableClimb = parameters.walkableClimb;
|
|
|
+ rc.walkableRadius = parameters.walkableRadius;
|
|
|
+ rc.maxEdgeLen = parameters.maxEdgeLen;
|
|
|
+ rc.maxSimplificationError = parameters.maxSimplificationError;
|
|
|
+ rc.minRegionArea = parameters.minRegionArea;
|
|
|
+ rc.mergeRegionArea = parameters.mergeRegionArea;
|
|
|
+ rc.maxVertsPerPoly = parameters.maxVertsPerPoly;
|
|
|
+ rc.detailSampleDist = parameters.detailSampleDist;
|
|
|
+ rc.detailSampleMaxError = parameters.detailSampleMaxError;
|
|
|
+
|
|
|
+ this.navMesh = new this.bjsRECAST.NavMesh();
|
|
|
+
|
|
|
+ var index: number;
|
|
|
+ var tri: number;
|
|
|
+ var pt: number;
|
|
|
+
|
|
|
+ var indices = [];
|
|
|
+ var positions = [];
|
|
|
+ var offset = 0;
|
|
|
+ for (index = 0; index < meshes.length; index++) {
|
|
|
+ if (meshes[index]) {
|
|
|
+ var mesh = meshes[index];
|
|
|
+
|
|
|
+ const meshIndices = mesh.getIndices();
|
|
|
+ if (!meshIndices) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const meshPositions = mesh.getVerticesData(VertexBuffer.PositionKind, false, false);
|
|
|
+ if (!meshPositions) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const wm = mesh.computeWorldMatrix(false);
|
|
|
+
|
|
|
+ for (tri = 0; tri < meshIndices.length; tri++) {
|
|
|
+ indices.push(meshIndices[tri] + offset);
|
|
|
+ }
|
|
|
+
|
|
|
+ var transformed = Vector3.Zero();
|
|
|
+ var position = Vector3.Zero();
|
|
|
+ for (pt = 0; pt < meshPositions.length; pt += 3) {
|
|
|
+ Vector3.FromArrayToRef(meshPositions, pt, position);
|
|
|
+ Vector3.TransformCoordinatesToRef(position, wm, transformed);
|
|
|
+ positions.push(transformed.x, transformed.y, transformed.z);
|
|
|
+ }
|
|
|
+
|
|
|
+ offset += meshPositions.length / 3;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.navMesh.build(positions, offset, indices, indices.length, rc);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a navigation mesh debug mesh
|
|
|
+ * @param scene is where the mesh will be added
|
|
|
+ * @returns debug display mesh
|
|
|
+ */
|
|
|
+ createDebugNavMesh(scene: Scene): Mesh {
|
|
|
+ var tri: number;
|
|
|
+ var pt: number;
|
|
|
+ var debugNavMesh = this.navMesh.getDebugNavMesh();
|
|
|
+ let triangleCount = debugNavMesh.getTriangleCount();
|
|
|
+
|
|
|
+ var indices = [];
|
|
|
+ var positions = [];
|
|
|
+ for (tri = 0; tri < triangleCount * 3; tri++)
|
|
|
+ {
|
|
|
+ indices.push(tri);
|
|
|
+ }
|
|
|
+ for (tri = 0; tri < triangleCount; tri++)
|
|
|
+ {
|
|
|
+ for (pt = 0; pt < 3 ; pt++)
|
|
|
+ {
|
|
|
+ let point = debugNavMesh.getTriangle(tri).getPoint(pt);
|
|
|
+ positions.push(point.x, point.y, point.z);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var mesh = new Mesh("NavMeshDebug", scene);
|
|
|
+ var vertexData = new VertexData();
|
|
|
+
|
|
|
+ vertexData.indices = indices;
|
|
|
+ vertexData.positions = positions;
|
|
|
+ vertexData.applyToMesh(mesh, false);
|
|
|
+ return mesh;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get a navigation mesh constrained position, closest to the parameter position
|
|
|
+ * @param position world position
|
|
|
+ * @returns the closest point to position constrained by the navigation mesh
|
|
|
+ */
|
|
|
+ getClosestPoint(position: Vector3) : Vector3
|
|
|
+ {
|
|
|
+ var p = new this.bjsRECAST.Vec3(position.x, position.y, position.z);
|
|
|
+ var ret = this.navMesh.getClosestPoint(p);
|
|
|
+ var pr = new Vector3(ret.x, ret.y, ret.z);
|
|
|
+ return pr;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get a navigation mesh constrained position, within a particular radius
|
|
|
+ * @param position world position
|
|
|
+ * @param maxRadius the maximum distance to the constrained world position
|
|
|
+ * @returns the closest point to position constrained by the navigation mesh
|
|
|
+ */
|
|
|
+ getRandomPointAround(position: Vector3, maxRadius: number): Vector3 {
|
|
|
+ var p = new this.bjsRECAST.Vec3(position.x, position.y, position.z);
|
|
|
+ var ret = this.navMesh.getRandomPointAround(p, maxRadius);
|
|
|
+ var pr = new Vector3(ret.x, ret.y, ret.z);
|
|
|
+ return pr;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Compute a navigation path from start to end. Returns an empty array if no path can be computed
|
|
|
+ * @param start world position
|
|
|
+ * @param end world position
|
|
|
+ * @returns array containing world position composing the path
|
|
|
+ */
|
|
|
+ computePath(start: Vector3, end: Vector3): Vector3[]
|
|
|
+ {
|
|
|
+ var pt: number;
|
|
|
+ let startPos = new this.bjsRECAST.Vec3(start.x, start.y, start.z);
|
|
|
+ let endPos = new this.bjsRECAST.Vec3(end.x, end.y, end.z);
|
|
|
+ let navPath = this.navMesh.computePath(startPos, endPos);
|
|
|
+ let pointCount = navPath.getPointCount();
|
|
|
+ var positions = [];
|
|
|
+ for (pt = 0; pt < pointCount; pt++)
|
|
|
+ {
|
|
|
+ let p = navPath.getPoint(pt);
|
|
|
+ positions.push(new Vector3(p.x, p.y, p.z));
|
|
|
+ }
|
|
|
+ return positions;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a new Crowd so you can add agents
|
|
|
+ * @param maxAgents the maximum agent count in the crowd
|
|
|
+ * @param maxAgentRadius the maximum radius an agent can have
|
|
|
+ * @param scene to attach the crowd to
|
|
|
+ * @returns the crowd you can add agents to
|
|
|
+ */
|
|
|
+ createCrowd(maxAgents: number, maxAgentRadius: number, scene: Scene) : ICrowd
|
|
|
+ {
|
|
|
+ var crowd = new RecastJSCrowd(this, maxAgents, maxAgentRadius, scene);
|
|
|
+ return crowd;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Disposes
|
|
|
+ */
|
|
|
+ public dispose() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If this plugin is supported
|
|
|
+ * @returns true if plugin is supported
|
|
|
+ */
|
|
|
+ public isSupported(): boolean {
|
|
|
+ return this.bjsRECAST !== undefined;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Recast detour crowd implementation
|
|
|
+ */
|
|
|
+export class RecastJSCrowd implements ICrowd {
|
|
|
+ /**
|
|
|
+ * Recast/detour plugin
|
|
|
+ */
|
|
|
+ public bjsRECASTPlugin: RecastJSPlugin;
|
|
|
+ /**
|
|
|
+ * Link to the detour crowd
|
|
|
+ */
|
|
|
+ public recastCrowd: any = {};
|
|
|
+ /**
|
|
|
+ * One transform per agent
|
|
|
+ */
|
|
|
+ public transforms: TransformNode[] = new Array<TransformNode>();
|
|
|
+ /**
|
|
|
+ * All agents created
|
|
|
+ */
|
|
|
+ public agents: number[] = new Array<number>();
|
|
|
+ /**
|
|
|
+ * Link to the scene is kept to unregister the crowd from the scene
|
|
|
+ */
|
|
|
+ private _scene: Scene;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Observer for crowd updates
|
|
|
+ */
|
|
|
+ private _onBeforeAnimationsObserver: Nullable<Observer<Scene>> = null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructor
|
|
|
+ * @param plugin recastJS plugin
|
|
|
+ * @param maxAgents the maximum agent count in the crowd
|
|
|
+ * @param maxAgentRadius the maximum radius an agent can have
|
|
|
+ * @param scene to attach the crowd to
|
|
|
+ * @returns the crowd you can add agents to
|
|
|
+ */
|
|
|
+ public constructor(plugin: RecastJSPlugin, maxAgents: number, maxAgentRadius: number, scene: Scene) {
|
|
|
+ this.bjsRECASTPlugin = plugin;
|
|
|
+ this.recastCrowd = new this.bjsRECASTPlugin.bjsRECAST.Crowd(maxAgents, maxAgentRadius, this.bjsRECASTPlugin.navMesh.getNavMesh());
|
|
|
+ this._scene = scene;
|
|
|
+
|
|
|
+ this._onBeforeAnimationsObserver = scene.onBeforeAnimationsObservable.add(() => {
|
|
|
+ this.update(scene.getEngine().getDeltaTime() * 0.001);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add a new agent to the crowd with the specified parameter a corresponding transformNode.
|
|
|
+ * You can attach anything to that node. The node position is updated in the scene update tick.
|
|
|
+ * @param pos world position that will be constrained by the navigation mesh
|
|
|
+ * @param parameters agent parameters
|
|
|
+ * @param transform hooked to the agent that will be update by the scene
|
|
|
+ * @returns agent index
|
|
|
+ */
|
|
|
+ addAgent(pos: Vector3, parameters: IAgentParameters, transform: TransformNode): number
|
|
|
+ {
|
|
|
+ var agentParams = new this.bjsRECASTPlugin.bjsRECAST.dtCrowdAgentParams();
|
|
|
+ agentParams.radius = parameters.radius;
|
|
|
+ agentParams.height = parameters.height;
|
|
|
+ agentParams.maxAcceleration = parameters.maxAcceleration;
|
|
|
+ agentParams.maxSpeed = parameters.maxSpeed;
|
|
|
+ agentParams.collisionQueryRange = parameters.collisionQueryRange;
|
|
|
+ agentParams.pathOptimizationRange = parameters.pathOptimizationRange;
|
|
|
+ agentParams.separationWeight = parameters.separationWeight;
|
|
|
+ agentParams.updateFlags = 7;
|
|
|
+ agentParams.obstacleAvoidanceType = 0;
|
|
|
+ agentParams.queryFilterType = 0;
|
|
|
+ agentParams.userData = 0;
|
|
|
+
|
|
|
+ var agentIndex = this.recastCrowd.addAgent(new this.bjsRECASTPlugin.bjsRECAST.Vec3(pos.x, pos.y, pos.z), agentParams);
|
|
|
+ this.transforms.push(transform);
|
|
|
+ this.agents.push(agentIndex);
|
|
|
+ return agentIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the agent position in world space
|
|
|
+ * @param index agent index returned by addAgent
|
|
|
+ * @returns world space position
|
|
|
+ */
|
|
|
+ getAgentPosition(index: number): Vector3 {
|
|
|
+ var agentPos = this.recastCrowd.getAgentPosition(index);
|
|
|
+ return new Vector3(agentPos.x, agentPos.y, agentPos.z);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the agent velocity in world space
|
|
|
+ * @param index agent index returned by addAgent
|
|
|
+ * @returns world space velocity
|
|
|
+ */
|
|
|
+ getAgentVelocity(index: number): Vector3 {
|
|
|
+ var agentVel = this.recastCrowd.getAgentVelocity(index);
|
|
|
+ return new Vector3(agentVel.x, agentVel.y, agentVel.z);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Asks a particular agent to go to a destination. That destination is constrained by the navigation mesh
|
|
|
+ * @param index agent index returned by addAgent
|
|
|
+ * @param destination targeted world position
|
|
|
+ */
|
|
|
+ agentGoto(index: number, destination: Vector3): void {
|
|
|
+ this.recastCrowd.agentGoto(index, new this.bjsRECASTPlugin.bjsRECAST.Vec3(destination.x, destination.y, destination.z));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * remove a particular agent previously created
|
|
|
+ * @param index agent index returned by addAgent
|
|
|
+ */
|
|
|
+ removeAgent(index: number): void {
|
|
|
+ this.recastCrowd.removeAgent(index);
|
|
|
+
|
|
|
+ var item = this.agents.indexOf(index);
|
|
|
+ if (item > -1) {
|
|
|
+ this.agents.splice(item, 1);
|
|
|
+ this.transforms.splice(item, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * get the list of all agents attached to this crowd
|
|
|
+ * @returns list of agent indices
|
|
|
+ */
|
|
|
+ getAgents(): number[] {
|
|
|
+ return this.agents;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Tick update done by the Scene. Agent position/velocity/acceleration is updated by this function
|
|
|
+ * @param deltaTime in seconds
|
|
|
+ */
|
|
|
+ update(deltaTime: number): void {
|
|
|
+ // update crowd
|
|
|
+ this.recastCrowd.update(deltaTime);
|
|
|
+
|
|
|
+ // update transforms
|
|
|
+ for (let index = 0; index < this.agents.length; index++)
|
|
|
+ {
|
|
|
+ this.transforms[index].position = this.getAgentPosition(this.agents[index]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Release all resources
|
|
|
+ */
|
|
|
+ dispose() : void
|
|
|
+ {
|
|
|
+ this.recastCrowd.destroy();
|
|
|
+ this._scene.onBeforeAnimationsObservable.remove(this._onBeforeAnimationsObserver);
|
|
|
+ this._onBeforeAnimationsObserver = null;
|
|
|
+ }
|
|
|
+}
|