UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

196 lines (148 loc) • 7.99 kB
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); } }