UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

277 lines (222 loc) • 8.01 kB
import { v3_array_normalize } from "../../../../core/geom/vec3/v3_array_normalize.js"; import { v3_displace_in_direction_array } from "../../../../core/geom/vec3/v3_displace_in_direction_array.js"; import { v3_distance } from "../../../../core/geom/vec3/v3_distance.js"; import { v3_distance_sqr } from "../../../../core/geom/vec3/v3_distance_sqr.js"; import { v3_length_sqr } from "../../../../core/geom/vec3/v3_length_sqr.js"; const scratch_direction = new Float32Array(3); /** * Implementation of FABRIK algorithm, "Forward And Backward Reaching Inverse Kinematics" * This implementation deals with single chain at a time, without support for multiple effectors * see "FABRIK: a fast, iterative solver for inverse kinematics" * @param {number} size number of points in the chain * @param {Float32Array|number[]} positions * @param {Float32Array|number[]} lengths * @param {number} origin_x * @param {number} origin_y * @param {number} origin_z * @param {number} target_x * @param {number} target_y * @param {number} target_z * @param {number} [max_iterations] More steps will lead to higher accuracy, but at the cost of computation. Generally solution will be reached in just a few iteration so this is just a ceiling * @param {number} [distance_tolerance] Minimum squared distance to be achieved to the target, used to terminate earlier before maximum number of iterations is reached */ export function fabrik3d_solve_primitive( size, positions, lengths, origin_x, origin_y, origin_z, target_x, target_y, target_z, max_iterations = 4, distance_tolerance = 1e-7 ) { if (size === 0) { // nothing to solve, chain has insufficient number of joints return; } if ( !is_reachable( size, lengths, origin_x, origin_y, origin_z, target_x, target_y, target_z) ) { // target is not reachable, so stretch out the chain towards it instead reach_out( size, positions, lengths, origin_x, origin_y, origin_z, target_x, target_y, target_z ); return; } for (let i = 0; i < max_iterations; i++) { solve_backward(size, positions, lengths, target_x, target_y, target_z); solve_forward(size, positions, lengths, origin_x, origin_y, origin_z); const last_join_address = (size - 1) * 3; const last_joint_x = positions[last_join_address]; const last_joint_y = positions[last_join_address + 1]; const last_joint_z = positions[last_join_address + 2]; if (v3_distance_sqr(last_joint_x, last_joint_y, last_joint_z, target_x, target_y, target_z) <= distance_tolerance) { // close enough break; } } } /** * @param {number} size number of points in the chain * @param {Float32Array|number[]} positions * @param {Float32Array|number[]} lengths * @param {number} origin_x * @param {number} origin_y * @param {number} origin_z * @param {number} target_x * @param {number} target_y * @param {number} target_z */ function reach_out( size, positions, lengths, origin_x, origin_y, origin_z, target_x, target_y, target_z, ) { positions[0] = origin_x; positions[1] = origin_y; positions[2] = origin_z; scratch_direction[0] = target_x - origin_x; scratch_direction[1] = target_y - origin_y; scratch_direction[2] = target_z - origin_z; v3_array_normalize(scratch_direction, 0, scratch_direction, 0); for (let i = 1; i < size; i++) { const current_offset = i * 3; const previous_offset = current_offset - 3; const previous_x = positions[previous_offset]; const previous_y = positions[previous_offset + 1]; const previous_z = positions[previous_offset + 2]; const length = lengths[i - 1]; positions[current_offset] = previous_x + scratch_direction[0] * length; positions[current_offset + 1] = previous_y + scratch_direction[1] * length; positions[current_offset + 2] = previous_z + scratch_direction[2] * length; } } /** * * @param {number} size * @param {number[]} lengths * @param {number} origin_x * @param {number} origin_y * @param {number} origin_z * @param {number} target_x * @param {number} target_y * @param {number} target_z */ function is_reachable( size, lengths, origin_x, origin_y, origin_z, target_x, target_y, target_z ) { let total_length = 0; const limit = size - 1; for (let i = 0; i < limit; i++) { const x = lengths[i]; total_length += x; } const distance_to_target = v3_distance(origin_x, origin_y, origin_z, target_x, target_y, target_z); return distance_to_target <= total_length; } /** * Produced vector normalize(B-A) * @param result * @param result_offset * @param positions * @param offsetA * @param offsetB */ function compute_direction( result, result_offset, positions, offsetA, offsetB ) { const ax = positions[offsetA]; const ay = positions[offsetA + 1]; const az = positions[offsetA + 2]; const bx = positions[offsetB]; const by = positions[offsetB + 1]; const bz = positions[offsetB + 2]; const dx = bx - ax; const dy = by - ay; const dz = bz - az; const length_sqr = v3_length_sqr(dx, dy, dz); if (length_sqr === 0) { // TODO use random point on a sphere to prevent pathology // points are on top of each other, set arbitrary direction result[result_offset + 0] = 0; result[result_offset + 1] = 0; result[result_offset + 2] = 1; return; } const m = 1 / Math.sqrt(length_sqr); result[result_offset + 0] = dx * m; result[result_offset + 1] = dy * m; result[result_offset + 2] = dz * m; } /** * * @param {number} size * @param {Float32Array} positions * @param {Float32Array|number[]} lengths * @param {number} origin_x * @param {number} origin_y * @param {number} origin_z */ function solve_forward(size, positions, lengths, origin_x, origin_y, origin_z) { // move first point to origin positions[0] = origin_x; positions[1] = origin_y; positions[2] = origin_z; for (let i = 1; i < size; i++) { const current_address = i * 3; const previous_address = current_address - 3; compute_direction( scratch_direction, 0, positions, previous_address, current_address ); const length = lengths[i - 1]; v3_displace_in_direction_array( positions, current_address, positions, previous_address, scratch_direction, 0, length ); } } /** * * @param {number} size * @param {Float32Array} positions * @param {Float32Array|number[]} lengths * @param {number} target_x * @param {number} target_y * @param {number} target_z */ function solve_backward(size, positions, lengths, target_x, target_y, target_z) { const last_index = size - 1; // move last point to target const last_address = last_index * 3; positions[last_address] = target_x; positions[last_address + 1] = target_y; positions[last_address + 2] = target_z; for (let i = last_index - 1; i >= 0; i--) { const current_address = i * 3; const next_address = current_address + 3; compute_direction( scratch_direction, 0, positions, next_address, current_address ); v3_displace_in_direction_array( positions, current_address, positions, next_address, scratch_direction, 0, lengths[i] ) } }