import * as THREE from 'three'; import { V3, assert } from '../core/math.js'; // [name, parent, local offset] — rest local rotations are all identity, so the // rest pose is an A-pose and every rest world position falls out of the offsets. export const BONEDEF = [ ['root', null, [0, 0, 0]], ['pelvis', 'root', [0, 1.0, 0]], ['spine1', 'pelvis', [0, 0.09, 0.004]], ['spine2', 'spine1', [0, 0.12, 0.005]], ['spine3', 'spine2', [0, 0.13, 0.005]], ['neck', 'spine3', [0, 0.1, 0.012]], ['head', 'neck', [0, 0.075, 0.008]], ['clavicleL', 'spine3', [0.075, 0.048, 0]], ['upperArmL', 'clavicleL', [0.135, -0.022, 0]], ['forearmL', 'upperArmL', [0.15, -0.252, 0.01]], ['handL', 'forearmL', [0.105, -0.227, 0.016]], ['clavicleR', 'spine3', [-0.075, 0.048, 0]], ['upperArmR', 'clavicleR', [-0.135, -0.022, 0]], ['forearmR', 'upperArmR', [-0.15, -0.252, 0.01]], ['handR', 'forearmR', [-0.105, -0.227, 0.016]], ['thighL', 'pelvis', [0.105, -0.05, 0.005]], ['shinL', 'thighL', [0.02, -0.44, 0.006]], ['footL', 'shinL', [0.005, -0.437, -0.012]], ['toeL', 'footL', [-0.004, -0.055, 0.112]], ['thighR', 'pelvis', [-0.105, -0.05, 0.005]], ['shinR', 'thighR', [-0.02, -0.44, 0.006]], ['footR', 'shinR', [-0.005, -0.437, -0.012]], ['toeR', 'footR', [0.004, -0.055, 0.112]], ]; /** Child bone that defines each bone's capsule segment axis. */ export const SEG_CHILD = { pelvis: 'spine1', spine1: 'spine2', spine2: 'spine3', spine3: 'neck', neck: 'head', clavicleL: 'upperArmL', upperArmL: 'forearmL', forearmL: 'handL', clavicleR: 'upperArmR', upperArmR: 'forearmR', forearmR: 'handR', thighL: 'shinL', shinL: 'footL', footL: 'toeL', thighR: 'shinR', shinR: 'footR', footR: 'toeR', root: null, head: null, handL: null, handR: null, toeL: null, toeR: null, }; /** Per-bone skin influence radius for the capsule falloff. */ export const BONE_RADIUS = { root: 0.2, pelvis: 0.175, spine1: 0.165, spine2: 0.17, spine3: 0.175, neck: 0.08, head: 0.125, clavicleL: 0.07, upperArmL: 0.078, forearmL: 0.068, handL: 0.06, clavicleR: 0.07, upperArmR: 0.078, forearmR: 0.068, handR: 0.06, thighL: 0.125, shinL: 0.098, footL: 0.075, toeL: 0.055, thighR: 0.125, shinR: 0.098, footR: 0.075, toeR: 0.055, }; /** * Body regions from GDD 5.3. Every bone belongs to exactly one region, and * damage, armor coverage and ragdoll limb-disable all key off these. */ export const REGION = { HEAD: 'head', TORSO: 'torso', UPPER_ARM_L: 'upperArmL', LOWER_ARM_L: 'lowerArmL', UPPER_ARM_R: 'upperArmR', LOWER_ARM_R: 'lowerArmR', UPPER_LEG_L: 'upperLegL', LOWER_LEG_L: 'lowerLegL', UPPER_LEG_R: 'upperLegR', LOWER_LEG_R: 'lowerLegR', }; export const BONE_REGION = { head: REGION.HEAD, neck: REGION.HEAD, pelvis: REGION.TORSO, spine1: REGION.TORSO, spine2: REGION.TORSO, spine3: REGION.TORSO, clavicleL: REGION.TORSO, clavicleR: REGION.TORSO, upperArmL: REGION.UPPER_ARM_L, forearmL: REGION.LOWER_ARM_L, handL: REGION.LOWER_ARM_L, upperArmR: REGION.UPPER_ARM_R, forearmR: REGION.LOWER_ARM_R, handR: REGION.LOWER_ARM_R, thighL: REGION.UPPER_LEG_L, shinL: REGION.LOWER_LEG_L, footL: REGION.LOWER_LEG_L, toeL: REGION.LOWER_LEG_L, thighR: REGION.UPPER_LEG_R, shinR: REGION.LOWER_LEG_R, footR: REGION.LOWER_LEG_R, toeR: REGION.LOWER_LEG_R, }; export function buildSkeleton() { const bones = {}; const list = []; for (const [name, parentName, off] of BONEDEF) { const b = new THREE.Bone(); b.name = name; b.position.set(off[0], off[1], off[2]); if (parentName) bones[parentName].add(b); bones[name] = b; list.push(b); } const root = bones.root; root.updateMatrixWorld(true); const restWorld = {}; for (const b of list) restWorld[b.name] = b.getWorldPosition(new THREE.Vector3()); const skeleton = new THREE.Skeleton(list); const index = {}; list.forEach((b, i) => { index[b.name] = i; }); return { bones, list, index, skeleton, restWorld, rootBone: root }; } /** The capsule segment a bone deforms, in rest world space. */ export function boneSegment(name, restWorld) { const a = restWorld[name]; const child = SEG_CHILD[name]; let b; if (child) b = restWorld[child]; else if (name === 'head') b = a.clone().add(V3(0, 0.15, 0.012)); else if (name.startsWith('hand')) { const s = name.endsWith('L') ? 1 : -1; b = a.clone().add(V3(s * 0.045, -0.095, 0.008)); } else b = a.clone().add(V3(0, -0.012, 0.085)); // toes return { a, b, r: BONE_RADIUS[name] }; } export function assertNoNaNBones(skelData) { for (const b of skelData.list) { const e = b.matrixWorld.elements; for (let i = 0; i < 16; i++) assert(Number.isFinite(e[i]), 'NaN in bone matrix ' + b.name); } }