UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

379 lines (320 loc) 12.1 kB
import { v3_dot } from "../../../core/geom/vec3/v3_dot.js"; import { v3_length } from "../../../core/geom/vec3/v3_length.js"; const GJK_MAX_ITERATIONS = 64; /** * Module-level scratch buffers to avoid per-call allocations. */ const scratch_support_a = new Float32Array(3); const scratch_support_b = new Float32Array(3); const scratch_dir = new Float32Array(3); /** * Compute the Minkowski difference support point for two shapes. * support(A-B, d) = support(A, d) - support(B, -d) * * The direction is normalized before being passed to the shape support * functions, since {@link AbstractShape3D#support} assumes a unit vector. * * @param {number[]|Float32Array} result * @param {number} result_offset * @param {AbstractShape3D} shape_a * @param {AbstractShape3D} shape_b * @param {number} dir_x * @param {number} dir_y * @param {number} dir_z */ function minkowski_support( result, result_offset, shape_a, shape_b, dir_x, dir_y, dir_z ) { // Normalize direction — support() contract requires a unit vector const len = v3_length(dir_x, dir_y, dir_z); if (len > 0) { const inv = 1 / len; dir_x *= inv; dir_y *= inv; dir_z *= inv; } shape_a.support(scratch_support_a, 0, dir_x, dir_y, dir_z); shape_b.support(scratch_support_b, 0, -dir_x, -dir_y, -dir_z); result[result_offset] = scratch_support_a[0] - scratch_support_b[0]; result[result_offset + 1] = scratch_support_a[1] - scratch_support_b[1]; result[result_offset + 2] = scratch_support_a[2] - scratch_support_b[2]; } /** * GJK intersection test for two convex shapes. * * Determines whether two convex shapes overlap by iteratively building a simplex * in the Minkowski difference space. If the origin is enclosed by the simplex, * the shapes intersect. * * Adapted from https://github.com/kevinmoran/GJK/blob/master/GJK.h * * @param {number[]|Float32Array} simplex Working buffer for simplex vertices (4 vec3s). Must have length >= 12. * @param {AbstractShape3D} shape_a * @param {AbstractShape3D} shape_b * @returns {boolean} true if the shapes intersect */ export function gjk(simplex, shape_a, shape_b) { const dir = scratch_dir; // Initial search direction — arbitrary, (1,0,0) // Get initial support point (simplex point C) minkowski_support( simplex, 6, shape_a, shape_b, 1, 0, 0 ); // Search toward the origin from C dir[0] = -simplex[6]; dir[1] = -simplex[7]; dir[2] = -simplex[8]; if (dir[0] === 0 && dir[1] === 0 && dir[2] === 0) { // Origin is exactly on the first support point — intersection return true; } // Get second support point (simplex point B) minkowski_support(simplex, 3, shape_a, shape_b, dir[0], dir[1], dir[2]); // B didn't pass the origin — no intersection possible if (v3_dot(simplex[3], simplex[4], simplex[5], dir[0], dir[1], dir[2]) < 0) { return false; } // Line case: compute direction perpendicular to line segment BC toward origin // CB = C - B const cb_x = simplex[6] - simplex[3]; const cb_y = simplex[7] - simplex[4]; const cb_z = simplex[8] - simplex[5]; // BO = -B (origin - B) const bo_x = -simplex[3]; const bo_y = -simplex[4]; const bo_z = -simplex[5]; // dir = CB × BO × CB (triple cross product) triple_cross_product(dir, cb_x, cb_y, cb_z, bo_x, bo_y, bo_z); // Handle degenerate case where origin is on the line segment if (dir[0] === 0 && dir[1] === 0 && dir[2] === 0) { // Origin is on the line segment — pick any perpendicular direction // Use the cross product of CB with an arbitrary axis // CB × (1,0,0) dir[0] = cb_y; dir[1] = -cb_x; dir[2] = 0; if (dir[0] === 0 && dir[1] === 0 && dir[2] === 0) { // CB is parallel to (1,0,0), use (0,1,0) dir[0] = -cb_z; dir[1] = 0; dir[2] = cb_x; } } let simplex_size = 2; // we have B and C for (let iterations = 0; iterations < GJK_MAX_ITERATIONS; iterations++) { // Get new support point A minkowski_support(simplex, 0, shape_a, shape_b, dir[0], dir[1], dir[2]); const a_x = simplex[0]; const a_y = simplex[1]; const a_z = simplex[2]; // Check if A passed the origin if (v3_dot(a_x, a_y, a_z, dir[0], dir[1], dir[2]) < 0) { return false; // no intersection } simplex_size++; if (simplex_size === 3) { // Triangle case update_simplex_triangle(dir, simplex); } else { // Tetrahedron case if (update_simplex_tetrahedron(dir, simplex)) { // Origin is inside the tetrahedron — intersection! return true; } // The simplex was reduced to a triangle, so keep size at 3 simplex_size = 3; } } // Did not converge — assume no intersection return false; } /** * Compute the triple cross product: (a × b) × a * * @param {number[]|Float32Array} result output vec3 * @param {number} ax * @param {number} ay * @param {number} az * @param {number} bx * @param {number} by * @param {number} bz */ function triple_cross_product(result, ax, ay, az, bx, by, bz) { // First: t = a × b const tx = ay * bz - az * by; const ty = az * bx - ax * bz; const tz = ax * by - ay * bx; // Then: t × a result[0] = ty * az - tz * ay; result[1] = tz * ax - tx * az; result[2] = tx * ay - ty * ax; } /** * Update simplex for the triangle case (3 points: A, B, C). * * Determines the Voronoi region of the origin relative to triangle ABC * and updates the search direction accordingly. * * simplex layout: [A(0-2), B(3-5), C(6-8)] * * @param {number[]|Float32Array} search_dir output: next search direction * @param {Float32Array} simplex */ function update_simplex_triangle(search_dir, simplex) { const a_x = simplex[0], a_y = simplex[1], a_z = simplex[2]; const b_x = simplex[3], b_y = simplex[4], b_z = simplex[5]; const c_x = simplex[6], c_y = simplex[7], c_z = simplex[8]; // AB = B - A const ab_x = b_x - a_x; const ab_y = b_y - a_y; const ab_z = b_z - a_z; // AC = C - A const ac_x = c_x - a_x; const ac_y = c_y - a_y; const ac_z = c_z - a_z; // AO = -A (origin - A) const ao_x = -a_x; const ao_y = -a_y; const ao_z = -a_z; // Normal of triangle ABC const abc_x = ab_y * ac_z - ab_z * ac_y; const abc_y = ab_z * ac_x - ab_x * ac_z; const abc_z = ab_x * ac_y - ab_y * ac_x; // Test edge AC // ABC × AC const abc_cross_ac_x = abc_y * ac_z - abc_z * ac_y; const abc_cross_ac_y = abc_z * ac_x - abc_x * ac_z; const abc_cross_ac_z = abc_x * ac_y - abc_y * ac_x; if (v3_dot(abc_cross_ac_x, abc_cross_ac_y, abc_cross_ac_z, ao_x, ao_y, ao_z) > 0) { // Origin is on the AC side // Reduce simplex to line AC (A stays as A, C moves to B position) simplex[3] = c_x; simplex[4] = c_y; simplex[5] = c_z; // New direction: AC × AO × AC triple_cross_product(search_dir, ac_x, ac_y, ac_z, ao_x, ao_y, ao_z); return; } // Test edge AB // AB × ABC const ab_cross_abc_x = ab_y * abc_z - ab_z * abc_y; const ab_cross_abc_y = ab_z * abc_x - ab_x * abc_z; const ab_cross_abc_z = ab_x * abc_y - ab_y * abc_x; if (v3_dot(ab_cross_abc_x, ab_cross_abc_y, ab_cross_abc_z, ao_x, ao_y, ao_z) > 0) { // Origin is on the AB side // Keep simplex as line AB (C slot is cleared by not using it) simplex[6] = simplex[3]; simplex[7] = simplex[4]; simplex[8] = simplex[5]; // New direction: AB × AO × AB triple_cross_product(search_dir, ab_x, ab_y, ab_z, ao_x, ao_y, ao_z); return; } // Origin is above or below the triangle if (v3_dot(abc_x, abc_y, abc_z, ao_x, ao_y, ao_z) > 0) { // Origin is above — search in normal direction // Simplex stays as A, B, C search_dir[0] = abc_x; search_dir[1] = abc_y; search_dir[2] = abc_z; } else { // Origin is below — swap B and C, search in -normal const tmp_x = simplex[3], tmp_y = simplex[4], tmp_z = simplex[5]; simplex[3] = simplex[6]; simplex[4] = simplex[7]; simplex[5] = simplex[8]; simplex[6] = tmp_x; simplex[7] = tmp_y; simplex[8] = tmp_z; search_dir[0] = -abc_x; search_dir[1] = -abc_y; search_dir[2] = -abc_z; } } /** * Update simplex for the tetrahedron case (4 points: A, B, C, D). * * Tests which face of the tetrahedron is closest to the origin and * reduces the simplex accordingly. Returns true if the origin is * inside the tetrahedron. * * simplex layout: [A(0-2), B(3-5), C(6-8), D(9-11)] * * @param {number[]|Float32Array} search_dir output: next search direction (only written when returning false) * @param {Float32Array} simplex * @returns {boolean} true if origin is inside the tetrahedron */ function update_simplex_tetrahedron(search_dir, simplex) { const a_x = simplex[0], a_y = simplex[1], a_z = simplex[2]; const b_x = simplex[3], b_y = simplex[4], b_z = simplex[5]; const c_x = simplex[6], c_y = simplex[7], c_z = simplex[8]; const d_x = simplex[9], d_y = simplex[10], d_z = simplex[11]; // AB, AC, AD vectors const ab_x = b_x - a_x, ab_y = b_y - a_y, ab_z = b_z - a_z; const ac_x = c_x - a_x, ac_y = c_y - a_y, ac_z = c_z - a_z; const ad_x = d_x - a_x, ad_y = d_y - a_y, ad_z = d_z - a_z; // AO = origin - A = -A const ao_x = -a_x, ao_y = -a_y, ao_z = -a_z; // Face normals (outward-facing from A's perspective) // ABC normal const abc_x = ab_y * ac_z - ab_z * ac_y; const abc_y = ab_z * ac_x - ab_x * ac_z; const abc_z = ab_x * ac_y - ab_y * ac_x; // ACD normal const acd_x = ac_y * ad_z - ac_z * ad_y; const acd_y = ac_z * ad_x - ac_x * ad_z; const acd_z = ac_x * ad_y - ac_y * ad_x; // ADB normal const adb_x = ad_y * ab_z - ad_z * ab_y; const adb_y = ad_z * ab_x - ad_x * ab_z; const adb_z = ad_x * ab_y - ad_y * ab_x; // Test each face to see if the origin is on the outside // Check face ABC (opposite to D) const abc_test = v3_dot(abc_x, abc_y, abc_z, ao_x, ao_y, ao_z); // Make sure normal points away from D const abc_d_side = v3_dot(abc_x, abc_y, abc_z, ad_x, ad_y, ad_z); if (abc_test * abc_d_side < 0) { // Origin is on the opposite side of ABC from D // Reduce to triangle ABC: D is dropped // simplex = [A, B, C] — A(0), B(3), C(6) already in place update_simplex_triangle(search_dir, simplex); return false; } // Check face ACD (opposite to B) const acd_test = v3_dot(acd_x, acd_y, acd_z, ao_x, ao_y, ao_z); const acd_b_side = v3_dot(acd_x, acd_y, acd_z, ab_x, ab_y, ab_z); if (acd_test * acd_b_side < 0) { // Origin is outside face ACD // Reduce to triangle ACD: replace B with D simplex[3] = c_x; simplex[4] = c_y; simplex[5] = c_z; simplex[6] = d_x; simplex[7] = d_y; simplex[8] = d_z; update_simplex_triangle(search_dir, simplex); return false; } // Check face ADB (opposite to C) const adb_test = v3_dot(adb_x, adb_y, adb_z, ao_x, ao_y, ao_z); const adb_c_side = v3_dot(adb_x, adb_y, adb_z, ac_x, ac_y, ac_z); if (adb_test * adb_c_side < 0) { // Origin is outside face ADB // Reduce to triangle ADB: replace C with B, B with D simplex[6] = b_x; simplex[7] = b_y; simplex[8] = b_z; simplex[3] = d_x; simplex[4] = d_y; simplex[5] = d_z; update_simplex_triangle(search_dir, simplex); return false; } // Origin is inside the tetrahedron return true; }