@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
355 lines (268 loc) • 12.9 kB
JavaScript
import { assert } from "../../../core/assert.js";
import { array_copy } from "../../../core/collection/array/array_copy.js";
import { array_swap } from "../../../core/collection/array/array_swap.js";
import { v3_compute_triangle_normal } from "../../../core/geom/3d/triangle/v3_compute_triangle_normal.js";
import { v3_dot } from "../../../core/geom/vec3/v3_dot.js";
import { v3_dot_array_array } from "../../../core/geom/vec3/v3_dot_array_array.js";
import { v3_negate_array } from "../../../core/geom/vec3/v3_negate_array.js";
const EPA_TOLERANCE = 0.0001;
const EPA_MAX_NUM_FACES = 64;
const EPA_MAX_NUM_LOOSE_EDGES = 32;
const EPA_MAX_NUM_ITERATIONS = 64;
const FACE_ELEMENT_COUNT = 3 * 4;
const EDGE_ELEMENT_COUNT = 2 * 3;
//Array of faces, each with 3 verts and a normal
const faces = new Float32Array(EPA_MAX_NUM_FACES * FACE_ELEMENT_COUNT);
/**
* keep track of edges we need to fix after removing faces
* @type {Float32Array}
*/
const loose_edges = new Float32Array(EPA_MAX_NUM_LOOSE_EDGES * EDGE_ELEMENT_COUNT);
const scratch_v3 = new Float32Array(3);
/**
*
* @param {number[]} target
* @param {number} target_index
* @param {number[]} v
*/
function write_v3(target, target_index, v) {
array_copy(v, 0, faces, target_index * 3, 3);
}
/**
*
* @param {number[]|Float32Array} faces
* @param {number} index
* @param {number[]} a
* @param {number[]} b
* @param {number[]} c
*/
function write_face(faces, index, a, b, c) {
const index4 = index * 4;
write_v3(faces, index4 + 0, a);
write_v3(faces, index4 + 1, b);
write_v3(faces, index4 + 2, c);
const normal_offset = (index4 + 3) * 3;
v3_compute_triangle_normal(
faces, normal_offset,
a[0], a[1], a[2],
b[0], b[1], b[2],
c[0], c[1], c[2]
);
}
/**
* TODO: needs to be tested thoroughly, intended to be working with GJK
* @param {number[]} result
* @param {number} result_offset
* @param {number[]} a
* @param {number[]} b
* @param {number[]} c
* @param {number[]} d
* @param {AbstractShape3D} coll1
* @param {AbstractShape3D} coll2
*/
export function expanding_polytope_algorithm(
result, result_offset,
a, b, c, d,
coll1, coll2
) {
/*
Adapted from https://github.com/kevinmoran/GJK/blob/master/GJK.h
"GJK + Expanding Polytope Algorithm - Implementation and Visualization" https://www.youtube.com/watch?v=6rgiPrzqt9w
*/
//Init with final simplex from GJK
write_face(faces, 0, a, b, c);
write_face(faces, 1, a, c, d);
write_face(faces, 2, a, d, b);
write_face(faces, 3, b, d, c);
let num_faces = 4;
let closest_face;
for (let iterations = 0; iterations < EPA_MAX_NUM_ITERATIONS; iterations++) {
//Find face that's closest to origin
let min_dist = v3_dot_array_array(faces, 0, faces, 3 * 3);
closest_face = 0;
for (let i = 1; i < num_faces; i++) {
const dist = v3_dot_array_array(faces, i * FACE_ELEMENT_COUNT, faces, i * FACE_ELEMENT_COUNT + 3 * 3);
if (dist < min_dist) {
min_dist = dist;
closest_face = i;
}
}
//search normal to face that's closest to origin
const closest_face_normal_offset = closest_face * FACE_ELEMENT_COUNT + 3 * 3;
const search_dir_x = faces[closest_face_normal_offset];
const search_dir_y = faces[closest_face_normal_offset + 1];
const search_dir_z = faces[closest_face_normal_offset + 2];
if (search_dir_x === 0 && search_dir_y === 0 && search_dir_z === 0) {
debugger;
}
// build new support point to expand polytope
coll2.support(scratch_v3, 0, search_dir_x, search_dir_y, search_dir_z);
const support2_x = scratch_v3[0];
const support2_y = scratch_v3[1];
const support2_z = scratch_v3[2];
coll1.support(scratch_v3, 0, -search_dir_x, -search_dir_y, -search_dir_z);
const support1_x = scratch_v3[0];
const support1_y = scratch_v3[1];
const support1_z = scratch_v3[2];
const p_x = support2_x - support1_x;
const p_y = support2_y - support1_y;
const p_z = support2_z - support1_z;
const dot_p_search_dir = v3_dot(p_x, p_y, p_z, search_dir_x, search_dir_y, search_dir_z);
if (dot_p_search_dir - min_dist < EPA_TOLERANCE) {
//Convergence (new point is not significantly further from origin)
result[result_offset] = search_dir_x * dot_p_search_dir;
result[result_offset + 1] = search_dir_y * dot_p_search_dir;
result[result_offset + 2] = search_dir_z * dot_p_search_dir;
return;
}
let num_loose_edges = 0;
//Find all triangles that are facing p
for (let i = 0; i < num_faces; i++) {
const face_offset = i * FACE_ELEMENT_COUNT;
const face_normal_address = face_offset + 3 * 3;
const face_a_address = face_offset;
const face_normal_x = faces[face_normal_address];
const face_normal_y = faces[face_normal_address + 1];
const face_normal_z = faces[face_normal_address + 2];
const face_a_x = faces[face_a_address];
const face_a_y = faces[face_a_address + 1];
const face_a_z = faces[face_a_address + 2];
const pa_x = p_x - face_a_x;
const pa_y = p_y - face_a_y;
const pa_z = p_z - face_a_z;
if (v3_dot(face_normal_x, face_normal_y, face_normal_z, pa_x, pa_y, pa_z) <= 0) {
continue;
}
// triangle i faces p, remove it
for (let j = 0; j < 3; j++) //Three edges per face
{
const a_offset = j * 3;
const current_edge_ax = faces[face_offset + a_offset];
const current_edge_ay = faces[face_offset + a_offset + 1];
const current_edge_az = faces[face_offset + a_offset + 2];
const b_offset = ((j + 1) % 3) * 3;
const current_edge_bx = faces[face_offset + b_offset];
const current_edge_by = faces[face_offset + b_offset + 1];
const current_edge_bz = faces[face_offset + b_offset + 2];
// vec3 current_edge[2] = { faces[i][j], faces[i][(j + 1) % 3] };
let found_edge = false;
//Check if current edge is already in list
for (let k = 0; k < num_loose_edges; k++) {
const loose_edge_offset = k * EDGE_ELEMENT_COUNT;
const loose_edge_ax = loose_edges[loose_edge_offset];
const loose_edge_ay = loose_edges[loose_edge_offset + 1];
const loose_edge_az = loose_edges[loose_edge_offset + 2];
const loose_edge_bx = loose_edges[loose_edge_offset + 3];
const loose_edge_by = loose_edges[loose_edge_offset + 4];
const loose_edge_bz = loose_edges[loose_edge_offset + 5];
// if (loose_edges[k][1] === current_edge[0] && loose_edges[k][0] === current_edge[1]) {
if (
(
loose_edge_ax === current_edge_bx
&& loose_edge_ay === current_edge_by
&& loose_edge_az === current_edge_bz
)
&& (
loose_edge_bx === current_edge_ax
&& loose_edge_by === current_edge_ay
&& loose_edge_bz === current_edge_az
)
) {
//Edge is already in the list, remove it
//THIS ASSUMES EDGE CAN ONLY BE SHARED BY 2 TRIANGLES (which should be true)
//THIS ALSO ASSUMES SHARED EDGE WILL BE REVERSED IN THE TRIANGLES (which
//should be true provided every triangle is wound CCW)
// Overwrite current edge with last edge in list
loose_edges.copyWithin(loose_edge_offset, (num_loose_edges - 1) * EDGE_ELEMENT_COUNT, (num_loose_edges) * EDGE_ELEMENT_COUNT);
num_loose_edges--;
found_edge = true;
//exit loop because edge can only be shared once
break;
}
}
if (!found_edge) { //add current edge to list
// assert(num_loose_edges<EPA_MAX_NUM_LOOSE_EDGES);
assert.lessThan(num_loose_edges, EPA_MAX_NUM_LOOSE_EDGES);
if (num_loose_edges >= EPA_MAX_NUM_LOOSE_EDGES) {
// this should not happen, but if it does it is best to be "kind of right" than to fail completely
break;
}
const last_edge_offset = num_loose_edges * EDGE_ELEMENT_COUNT;
loose_edges[last_edge_offset] = current_edge_ax;
loose_edges[last_edge_offset + 1] = current_edge_ay;
loose_edges[last_edge_offset + 2] = current_edge_az;
loose_edges[last_edge_offset + 3] = current_edge_bx;
loose_edges[last_edge_offset + 4] = current_edge_by;
loose_edges[last_edge_offset + 5] = current_edge_bz;
num_loose_edges++;
}
}
// move last face to fill the removed element
faces.copyWithin(i * FACE_ELEMENT_COUNT, (num_faces - 1) * FACE_ELEMENT_COUNT, num_faces * FACE_ELEMENT_COUNT);
num_faces--;
i--;
}
//Reconstruct polytope with p added
for (let i = 0; i < num_loose_edges; i++) {
// assert(num_faces<EPA_MAX_NUM_FACES);
assert.lessThan(num_faces, EPA_MAX_NUM_FACES);
if (num_faces >= EPA_MAX_NUM_FACES) {
// should never happen
break;
}
const face_offset = num_faces * FACE_ELEMENT_COUNT;
const loose_edge_offset = i * EDGE_ELEMENT_COUNT;
array_copy(
loose_edges, loose_edge_offset,
faces, face_offset,
EDGE_ELEMENT_COUNT
);
faces[face_offset + 3 * 2] = p_x;
faces[face_offset + 3 * 2 + 1] = p_y;
faces[face_offset + 3 * 2 + 2] = p_z;
// construct normal
const face_normal_offset = face_offset + 3 * 3;
construct_normal(face_normal_offset, i, p_x, p_y, p_z);
//Check for wrong normal to maintain CCW winding
const bias = 0.000001; //in case dot result is only slightly < 0 (because origin is on face)
const dot = v3_dot_array_array(faces, face_offset, faces, face_normal_offset)
if (dot + bias < 0) {
array_swap(faces, face_offset, faces, face_offset + 3, 3);
// invert normal
v3_negate_array(faces, face_normal_offset, faces, face_normal_offset);
}
num_faces++;
}
}
console.warn("EPA did not converge");
//Return most recent closest point
const closest_face_offset = closest_face * FACE_ELEMENT_COUNT;
const closest_face_normal_offset = closest_face_offset + 3 * 3;
const dot = v3_dot_array_array(faces, closest_face_offset, faces, closest_face_normal_offset);
result[result_offset] = faces[closest_face_normal_offset + 0] * dot;
result[result_offset + 1] = faces[closest_face_normal_offset + 1] * dot;
result[result_offset + 2] = faces[closest_face_normal_offset + 2] * dot;
}
/**
*
* @param {number} result_offset
* @param {number} edge_index
* @param {number} p_x
* @param {number} p_y
* @param {number} p_z
*/
function construct_normal(result_offset, edge_index, p_x, p_y, p_z) {
const edge_offset = edge_index * EDGE_ELEMENT_COUNT;
const edge_ax = loose_edges[edge_offset];
const edge_ay = loose_edges[edge_offset + 1];
const edge_az = loose_edges[edge_offset + 2];
const edge_bx = loose_edges[edge_offset + 3];
const edge_by = loose_edges[edge_offset + 4];
const edge_bz = loose_edges[edge_offset + 5];
v3_compute_triangle_normal(
faces, result_offset,
edge_ax, edge_ay, edge_az,
edge_bx, edge_by, edge_bz,
p_x, p_y, p_z
);
}