Files
tilt/src/character/skeleton.js
T
2026-08-03 10:28:11 -05:00

115 lines
4.6 KiB
JavaScript

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);
}
}