@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
196 lines (148 loc) • 7.99 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
import { BinaryTopology } from "../../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
import {
bt_mesh_cleanup_faceless_references
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_cleanup_faceless_references.js";
import {
bt_mesh_compute_face_normals
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_compute_face_normals.js";
import {
bt_mesh_face_decouple_islands
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_face_decouple_islands.js";
import {
bt_mesh_face_island_erode
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js";
import {
bt_mesh_from_unindexed_geometry
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_from_unindexed_geometry.js";
import { bt_mesh_triangulate } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_triangulate.js";
import {
bt_mesh_fuse_duplicate_edges
} from "../../../../core/geom/3d/topology/struct/binary/io/edge/bt_mesh_fuse_duplicate_edges.js";
import {
bt_merge_verts_by_distance
} from "../../../../core/geom/3d/topology/struct/binary/io/vertex/bt_merge_verts_by_distance.js";
import { bt_face_is_degenerate } from "../../../../core/geom/3d/topology/struct/binary/query/bt_face_is_degenerate.js";
import {
bt_mesh_compute_face_islands
} from "../../../../core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js";
import { v3_angle_between } from "../../../../core/geom/vec3/v3_angle_between.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { seededRandom } from "../../../../core/math/random/seededRandom.js";
import { bvh_build_from_bt_mesh } from "../bvh_build_from_bt_mesh.js";
import { bvh_build_from_unindexed_triangles } from "./bvh_build_from_unindexed_triangles.js";
import { enforce_agent_height_clearance } from "./enforce_agent_height_clearance.js";
/**
* Build from given scene geometry
* @param {BinaryTopology} destination
* @param {BinaryTopology} source Must be triangulated. Use {@link bt_mesh_triangulate} if needed.
* @param {number} [agent_radius]
* @param {number} [agent_height]
* @param {number} [agent_max_step_height] agent can bridge vertical gaps in topology, such as stepping up a stair
* @param {number} [agent_max_step_distance] agent can bridge lateral gaps in topology, such as stepping over a hole in the floor
* @param {number} [agent_max_climb_angle] In radians, how steep of an angle can the agent go up by
* @param {Vector3} [up] Defines world's "UP" direction, this is what the agent will respect for climbing constraint
*/
export function navmesh_build_topology({
destination,
source,
agent_radius = 0,
agent_height = 2,
agent_max_step_height = 0,
agent_max_step_distance = 0,
agent_max_climb_angle = Math.PI / 4,
up = Vector3.up,
}) {
assert.defined(destination, 'destination');
assert.defined(source, 'source');
assert.greaterThanOrEqual(agent_radius, 0, 'agent_radius');
assert.greaterThanOrEqual(agent_height, 0, 'agent_height');
assert.greaterThanOrEqual(agent_max_climb_angle, 0, 'agent_max_climb_angle');
assert.greaterThanOrEqual(agent_max_step_height, 0, 'agent_max_step_height');
assert.greaterThanOrEqual(agent_max_step_distance, 0, 'agent_max_step_distance');
const random = seededRandom();
const source_bvh = new BVH();
// prepare a BVH, we'll need it for height queries later on
bvh_build_from_bt_mesh(source_bvh, source);
const scratch_normal = new Float32Array(3);
// unpack topology into triangle soup
const source_face_count = source.faces.size;
// TODO add cleanup phase
// - fuse vertices of tiny triangles eliminating them
// - identify flat areas and re-triangulate them to remove unnecessary triangles
/**
*
* @type {number[]}
*/
const raw_triangles = [];
let triangle_count = 0;
for (let face_id = 0; face_id < source_face_count; face_id++) {
source.face_read_normal(scratch_normal, 0, face_id);
const angle = v3_angle_between(up.x, up.y, up.z, scratch_normal[0], scratch_normal[1], scratch_normal[2]);
if (angle > agent_max_climb_angle) {
// discard
continue;
}
if (bt_face_is_degenerate(source, face_id)) {
// discard
continue;
}
let loop = source.face_read_loop(face_id);
const loop_start = loop;
let face_vertex_count = 0;
do {
assert.lessThan(face_vertex_count, 3, 'not a triangle');
const vertex = source.loop_read_vertex(loop);
const triangle_offset = triangle_count * 9;
source.vertex_read_coordinate(raw_triangles, triangle_offset + face_vertex_count * 3, vertex);
face_vertex_count++;
loop = source.loop_read_next(loop);
} while (loop !== loop_start)
triangle_count++;
}
// knock out holes in triangles based on agent's height
if (agent_height > 0) {
triangle_count = enforce_agent_height_clearance({
bvh: source_bvh,
agent_height: agent_height,
agent_radius: agent_radius,
triangle_count: triangle_count,
triangles: raw_triangles,
random: random
});
}
{
const bvh = new BVH();
// first dump all triangles into a BVH for speed of access
bvh_build_from_unindexed_triangles(bvh, raw_triangles, triangle_count);
// find possible triangle edge connections, that is - where edges of 2 triangles touch, but don't match exactly.
// the touching edges will need to be cut and affected triangles split accordingly
const mesh = new BinaryTopology();
// raw_triangles may have stale tail data from filtering steps above (e.g. height clearance);
// bt_mesh_from_unindexed_geometry reads the whole array, so trim to the live range first.
raw_triangles.length = triangle_count * 9;
bt_mesh_from_unindexed_geometry(mesh, raw_triangles);
// fuse coincident positions so neighbouring triangles share a vertex id...
bt_merge_verts_by_distance(mesh, 1e-6);
// ...then fuse the resulting duplicate edges so they share an edge id too,
// which is what the loop-based neighbour queries rely on
bt_mesh_fuse_duplicate_edges(mesh);
const mesh_bounds = new Float32Array(6);
bvh.node_get_aabb(bvh.root, mesh_bounds);
const islands = bt_mesh_compute_face_islands(mesh);
// enabled us to modify islands independently
bt_mesh_face_decouple_islands(mesh, islands);
// shrink islands to agent_radius
for (const island of islands) {
bt_mesh_face_island_erode(mesh, island, agent_radius);
}
// TODO attempt to reduce mesh complexity by re-triangulating flat areas or running a very constrained decimation
// remove dangling references
bt_mesh_cleanup_faceless_references(mesh);
// face normals are consumed by navigation queries (string-pulling portal normals), populate them now
bt_mesh_compute_face_normals(mesh);
// write out to destination
destination.copy(mesh);
}
}