Initial commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { E } from '../../core/math.js';
|
||||
import { clamp, lerp } from '../../../shared/scalar.js';
|
||||
|
||||
/**
|
||||
* Upper-body authoring for skating.
|
||||
*
|
||||
* Split out from the animator for the same reason Ludus splits its stance
|
||||
* poses: the runtime concerns (foot path, IK, blending) are fiddly and stable,
|
||||
* while these numbers are pure feel and get tuned constantly.
|
||||
*
|
||||
* Everything keys off four scalars the sim already produces:
|
||||
* gait 0..1 how much of a stride is being thrown (from effort + speed)
|
||||
* speed m/s planar
|
||||
* bank rad lean into the current turn, signed (+ = turning right)
|
||||
* phase 0..1 stride cycle position
|
||||
*/
|
||||
|
||||
export const SKATE_POSE = {
|
||||
/** Knee bend at a standstill and at a full stride, in metres of root drop. */
|
||||
crouchIdle: 0.1,
|
||||
crouchStride: 0.26,
|
||||
/**
|
||||
* Forward pitch, radians, at a standstill and at speed.
|
||||
*
|
||||
* This is the *root* pitch; the spine adds roughly another half of it on top
|
||||
* as it stacks up the chain, so the finished torso angle is around 1.5x these
|
||||
* numbers. Authoring the final angle here instead would mean re-tuning every
|
||||
* time a spine joint changed.
|
||||
*/
|
||||
leanIdle: 0.08,
|
||||
leanFast: 0.3,
|
||||
/** How much of the bank the torso takes; the rest is absorbed by the legs. */
|
||||
bankTorso: 0.7,
|
||||
/** Head stays closer to level than the body — a skater looks up the ice. */
|
||||
bankHeadCounter: 0.55,
|
||||
/** Arm swing amplitude, radians, at a full stride. */
|
||||
armSwing: 0.72,
|
||||
/** Elbow bend: skaters carry their hands, they don't run with straight arms. */
|
||||
elbow: -0.62,
|
||||
/** Roll that pulls the arms in from the skeleton's rest A-pose. */
|
||||
armTuck: 0.34,
|
||||
/** Hip / shoulder counter-rotation with the stride. */
|
||||
hipTwist: 0.2,
|
||||
shoulderTwist: 0.26,
|
||||
};
|
||||
|
||||
/**
|
||||
* The moving pose: crouched, pitched forward, twisting against the stride.
|
||||
*
|
||||
* `P` is the animator's pose buffer — quaternions per bone plus a root offset.
|
||||
* Foot targets are not written here; they are world-space and belong to the
|
||||
* stepper.
|
||||
*/
|
||||
export function poseSkate(P, { gait, speed, bank, phase, t }) {
|
||||
const K = SKATE_POSE;
|
||||
const fast = clamp(speed / 7, 0, 1);
|
||||
const s1 = Math.sin(phase * Math.PI * 2);
|
||||
const s2 = Math.sin(phase * Math.PI * 4);
|
||||
// Idle breathing, so a stopped skater is not a statue.
|
||||
const idle = (1 - gait) * Math.sin(t * 1.6) * 0.02;
|
||||
|
||||
const crouch = lerp(K.crouchIdle, K.crouchStride, gait) + idle;
|
||||
const pitch = lerp(K.leanIdle, K.leanFast, fast);
|
||||
const twist = K.hipTwist * gait;
|
||||
|
||||
// Pelvis rocks with the push — the hip on the pushing side drops and rotates
|
||||
// open, which is most of what makes a stride read as a stride and not a run.
|
||||
E(P.q.pelvis, pitch * 0.18, twist * s1, -bank * 0.25 + gait * 0.05 * s1);
|
||||
E(P.q.spine1, pitch * 0.3, -twist * 0.35 * s1, -bank * K.bankTorso * 0.3);
|
||||
E(P.q.spine2, pitch * 0.3, -twist * 0.45 * s1, -bank * K.bankTorso * 0.35);
|
||||
E(P.q.spine3, pitch * 0.22 + idle, -K.shoulderTwist * gait * s1, -bank * K.bankTorso * 0.25);
|
||||
// Neck and head pull back up: the torso is folded forward, the eyes are not.
|
||||
E(P.q.neck, -pitch * 0.5, 0, bank * K.bankHeadCounter * 0.4);
|
||||
E(P.q.head, -pitch * 0.42, K.shoulderTwist * 0.3 * gait * s1, bank * K.bankHeadCounter * 0.6);
|
||||
|
||||
// Arms swing opposite the legs and slightly across the chest. Amplitude is
|
||||
// pure gait: a gliding skater's hands barely move.
|
||||
// The rest skeleton is an A-pose, so the arms already sit ~30° off the body.
|
||||
// Roll about local Z is what brings them in, and its sign is mirrored: the
|
||||
// left arm tucks on negative Z, the right on positive. Getting that backwards
|
||||
// is what turns a skater into a scarecrow, so it is written as `-m` once here
|
||||
// rather than as a per-side constant.
|
||||
const swing = K.armSwing * gait;
|
||||
for (const side of ['L', 'R']) {
|
||||
const m = side === 'L' ? 1 : -1;
|
||||
const armPhase = side === 'L' ? s1 : -s1;
|
||||
E(P.q[`clavicle${side}`], 0.04, 0, -m * (0.04 + 0.05 * gait));
|
||||
E(
|
||||
P.q[`upperArm${side}`],
|
||||
// Shoulders sit forward of the ribs at speed, hands ahead of the chest.
|
||||
-0.45 - 0.35 * fast + swing * armPhase,
|
||||
m * (0.1 + 0.12 * gait),
|
||||
-m * K.armTuck,
|
||||
);
|
||||
E(P.q[`forearm${side}`], K.elbow - 0.25 * gait - Math.abs(armPhase) * 0.12 * gait, 0, -m * 0.1);
|
||||
E(P.q[`hand${side}`], -0.1, 0, -m * 0.06);
|
||||
}
|
||||
|
||||
// Vertical bob is small and at twice the stride rate: the body rises over
|
||||
// each push, not once per cycle.
|
||||
P.rootOffset.set(-bank * 0.06, -crouch + gait * 0.018 * s2, gait * 0.02);
|
||||
E(P.rootQuat, pitch, 0, -bank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hockey stop: both blades thrown across the direction of travel, weight
|
||||
* dropped hard onto them, shoulders squared back up the ice.
|
||||
*
|
||||
* `dir` is +1 or -1 for which shoulder leads, so a stop has a side to it.
|
||||
*/
|
||||
export function poseStop(P, { speed, dir, t }) {
|
||||
const bite = clamp(speed / 6, 0.25, 1);
|
||||
const shake = Math.sin(t * 22) * 0.012 * bite;
|
||||
|
||||
E(P.q.pelvis, 0.12, dir * 0.55 * bite, dir * 0.18 * bite);
|
||||
E(P.q.spine1, 0.16 + shake, -dir * 0.18 * bite, -dir * 0.12 * bite);
|
||||
E(P.q.spine2, 0.16 + shake, -dir * 0.2 * bite, -dir * 0.14 * bite);
|
||||
E(P.q.spine3, 0.1, -dir * 0.16 * bite, -dir * 0.1 * bite);
|
||||
E(P.q.neck, -0.24, -dir * 0.2 * bite, 0);
|
||||
E(P.q.head, -0.18, -dir * 0.24 * bite, 0);
|
||||
|
||||
for (const side of ['L', 'R']) {
|
||||
const m = side === 'L' ? 1 : -1;
|
||||
// Hands come out for balance against the deceleration — the one pose where
|
||||
// the arms should leave the body, so the tuck roll relaxes toward zero.
|
||||
E(P.q[`clavicle${side}`], 0, 0, -m * 0.04);
|
||||
E(P.q[`upperArm${side}`], -0.72 * bite, m * 0.16, -m * (0.3 - 0.28 * bite));
|
||||
E(P.q[`forearm${side}`], -0.5 - 0.3 * bite, 0, -m * 0.12);
|
||||
E(P.q[`hand${side}`], -0.12, 0, 0);
|
||||
}
|
||||
|
||||
// Deep sit into the stop, hips back over the heels.
|
||||
P.rootOffset.set(dir * 0.05 * bite, -(0.2 + 0.12 * bite), -0.06 * bite);
|
||||
E(P.rootQuat, 0.12, 0, dir * 0.28 * bite);
|
||||
}
|
||||
Reference in New Issue
Block a user