UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

214 lines (150 loc) • 6.75 kB
import { SurfacePoint3 } from "../../../core/geom/3d/SurfacePoint3.js"; import Quaternion from "../../../core/geom/Quaternion.js"; import { v3_displace_in_direction } from "../../../core/geom/vec3/v3_displace_in_direction.js"; import { v3_dot } from "../../../core/geom/vec3/v3_dot.js"; import { v3_length } from "../../../core/geom/vec3/v3_length.js"; import Vector3 from "../../../core/geom/Vector3.js"; import { clamp } from "../../../core/math/clamp.js"; import { clamp01 } from "../../../core/math/clamp01.js"; import { inverseLerp } from "../../../core/math/inverseLerp.js"; import { findSkeletonBoneByType } from "../../graphics/ecs/mesh/SkeletonUtils.js"; import { IKSolver } from "./IKSolver.js"; const boneWorldPosition = new Vector3(); const boneWorldRotation = new Quaternion(); const boneWorldScale = new Vector3(); const parentWorldPosition = new Vector3(); const contact = new SurfacePoint3(); const targetPosition = new Vector3(); const v3 = new Vector3(); const ONE_OVER_SQRT_3 = 0.57735026919; const r = new Quaternion(); const up = new Vector3(); const r_i = new Quaternion(); const axis = new Vector3(); const x = new Quaternion(); const target = new Quaternion(); /** * Align a single bone onto terrain surface */ export class OneBoneSurfaceAlignmentSolver extends IKSolver { solve(problem) { const { constraint, terrain, skeleton } = problem; //obtain the bone const bone = findSkeletonBoneByType(skeleton, constraint.effector); if (bone === null) { throw new Error('Bone not found'); } //get bone parent, this is needed to figure out the "down" direction to ray casting const boneParent = bone.parent; if (boneParent === undefined || boneParent === null) { throw new Error('Bone has no parent'); } //Update matrix root bone, this will update all bones in the chain. Matrix update is necessary to ensure we read actual transform values boneParent.updateMatrixWorld(true); //obtain bone world position and rotation const matrixWorld = bone.matrixWorld; matrixWorld.decompose(boneWorldPosition, boneWorldRotation, boneWorldScale); //obtain parent bone world position const parentBoneWM = boneParent.matrixWorld.elements; parentWorldPosition.x = parentBoneWM[12]; parentWorldPosition.y = parentBoneWM[13]; parentWorldPosition.z = parentBoneWM[14]; //compute direction for ray casting const directionX = boneWorldPosition.x - parentWorldPosition.x; const directionY = boneWorldPosition.y - parentWorldPosition.y; const directionZ = boneWorldPosition.z - parentWorldPosition.z; let contactExists = terrain.raycastFirstSync( contact, parentWorldPosition.x, parentWorldPosition.y, parentWorldPosition.z, directionX, directionY, directionZ ); if (!contactExists) { //no contact return; } const contactPosition = contact.position; const contactNormal = contact.normal; //perform secondary cast from the bone, back along the surface normal contactExists = terrain.raycastFirstSync( contact, boneWorldPosition.x + contactNormal.x, boneWorldPosition.y + contactNormal.y, boneWorldPosition.z + contactNormal.z, -contactNormal.x, -contactNormal.y, -contactNormal.z ); if (!contactExists) { //no contact return; } const bone_scale = boneWorldScale.length() * ONE_OVER_SQRT_3; const targetOffsetDistance = constraint.offset * bone_scale; v3_displace_in_direction( targetPosition, targetOffsetDistance, contactPosition.x, contactPosition.y, contactPosition.z, contactNormal.x, contactNormal.y, contactNormal.z ); const delta_contact_c_x = targetPosition.x - boneWorldPosition.x; const delta_contact_c_y = targetPosition.y - boneWorldPosition.y; const delta_contact_c_z = targetPosition.z - boneWorldPosition.z; //check if current effector position is above the contact point const dot_contact_side = v3_dot(directionX, directionY, directionZ, delta_contact_c_x, delta_contact_c_y, delta_contact_c_z); const delta_contact_c_length = v3_length(delta_contact_c_x, delta_contact_c_y, delta_contact_c_z); let influence; if (dot_contact_side < 0) { //penetration detected influence = 1; } else { //no penetration, hovering case, use hover distance for influence const normalized_effector_distance = inverseLerp(0, bone_scale, delta_contact_c_length); influence = 1 - clamp01(constraint.distance.normalizeValue(normalized_effector_distance)); } if (influence <= 0) { //no influence return; } r.copy(bone.quaternion); //we want to make bone align orthogonally to surface contact normal //convert bone UP vector to world space up.copy(Vector3.forward); up.applyQuaternion(boneWorldRotation); up.normalize(); r_i.copyInverse(boneWorldRotation); v3.copy(contactNormal); v3.applyQuaternion(r_i); // axis.crossVectors(v3, Vector3.forward); const betaSine = axis.length(); //compute angle between normal and up vector of the bone const beta = Math.asin(clamp(betaSine, -1, 1)); // axis.normalize(); x.fromAxisAngle(axis, beta); target.copy(r); target.multiply(x); //compute angle between quaternions const a = target.angleTo(boneParent.quaternion); const a_abs = Math.abs(a); if (a_abs >= constraint.limit) { const b = target.angleTo(r); const b_abs = Math.abs(b); if (b_abs < constraint.limit) { const l = inverseLerp(a_abs, b_abs, constraint.limit); influence *= l; } else { return; } } r.lerp(target, constraint.strength * influence); bone.setRotationFromQuaternion(r); bone.updateMatrix(); bone.updateMatrixWorld(); bone.updateWorldMatrix(false, true); } }