feat(anim): 3-������ ������ + ������������ �������� #31

Merged
min merged 6 commits from feat/jump-3-phase into main 2026-06-14 21:31:34 +00:00
2 changed files with 53 additions and 22 deletions
Showing only changes of commit 4db93592d2 - Show all commits

View File

@ -52,6 +52,7 @@ const BASE_STATES = ["idle", "walk", "run", "jump", "fall"];
// Дополнительные движения (грузятся лениво при первом setState): // Дополнительные движения (грузятся лениво при первом setState):
const EXTRA_STATES = [ const EXTRA_STATES = [
"jump_anticipate", "jump_air", "jump_land", "jump_anticipate", "jump_air", "jump_land",
"jump_fwd_anticipate", "jump_fwd_air", "jump_fwd_land",
"walk_backward", "run_backward", "run_to_stop", "run_slide", "walk_backward", "run_backward", "run_to_stop", "run_slide",
"jump_forward", "jump_backward", "jump_down", "jump_forward", "jump_backward", "jump_down",
"crouch_enter", "crouch_idle", "crouch_walk", "crouch_to_stand", "crouch_enter", "crouch_idle", "crouch_walk", "crouch_to_stand",
@ -274,7 +275,10 @@ export class MixamoAnimator {
// (только что приземлились, ноги пружинят к bind), // (только что приземлились, ноги пружинят к bind),
// середина = 0 (присед на полу), конец = выпрямление. // середина = 0 (присед на полу), конец = выпрямление.
// Для всех остальных — фильтруем (физика двигает _modelRoot). // Для всех остальных — фильтруем (физика двигает _modelRoot).
const PHASES = new Set(['jump_anticipate', 'jump_land']); const PHASES = new Set([
'jump_anticipate', 'jump_land',
'jump_fwd_anticipate', 'jump_fwd_land',
]);
if (!PHASES.has(state)) { if (!PHASES.has(state)) {
continue; continue;
} }
@ -329,6 +333,7 @@ export class MixamoAnimator {
const ONE_SHOT = new Set([ const ONE_SHOT = new Set([
"jump", "jump_forward", "jump_backward", "jump_down", "jump", "jump_forward", "jump_backward", "jump_down",
"jump_anticipate", "jump_land", "jump_anticipate", "jump_land",
"jump_fwd_anticipate", "jump_fwd_air", "jump_fwd_land",
"crouch_enter", "crouch_to_stand", "crouch_enter", "crouch_to_stand",
"hit_react", "die_forward", "die_back", "hit_react", "die_forward", "die_back",
"throw_action", "pickup", "push_button", "open_door", "throw_action", "pickup", "push_button", "open_door",
@ -374,7 +379,10 @@ export class MixamoAnimator {
// Сброс Hips.position в bind-pose при выходе из jump-фаз. // Сброс Hips.position в bind-pose при выходе из jump-фаз.
// Иначе последний keyframe анимации остаётся на Hips и idle/walk // Иначе последний keyframe анимации остаётся на Hips и idle/walk
// подхватывает смещённую позицию → персонаж проседает. // подхватывает смещённую позицию → персонаж проседает.
const JUMP_STATES = new Set(['jump_air', 'jump_land', 'jump_in_place']); const JUMP_STATES = new Set([
'jump_air', 'jump_land', 'jump_in_place', 'jump_anticipate',
'jump_fwd_anticipate', 'jump_fwd_air', 'jump_fwd_land',
]);
if (JUMP_STATES.has(this._currentState) && !JUMP_STATES.has(state) if (JUMP_STATES.has(this._currentState) && !JUMP_STATES.has(state)
&& this._restPositions) { && this._restPositions) {
const rest = this._restPositions.get('Hips'); const rest = this._restPositions.get('Hips');
@ -391,10 +399,12 @@ export class MixamoAnimator {
// Anti-flicker debounce: не даём переключать состояние чаще чем раз в 120мс, // Anti-flicker debounce: не даём переключать состояние чаще чем раз в 120мс,
// КРОМЕ переходов с/на воздушные состояния (jump/fall) — там важна скорость // КРОМЕ переходов с/на воздушные состояния (jump/fall) — там важна скорость
// и one-shot crouch_enter/crouch_to_stand (они короткие). // и one-shot crouch_enter/crouch_to_stand (они короткие).
const isVitalSwitch = state === 'jump' || state === 'fall' const JUMP_VITAL = new Set([
|| state === 'jump_air' || state === 'jump_land' 'jump', 'fall', 'jump_air', 'jump_land', 'jump_anticipate',
|| this._currentState === 'jump' || this._currentState === 'fall' 'jump_fwd_anticipate', 'jump_fwd_air', 'jump_fwd_land',
|| this._currentState === 'jump_air' || this._currentState === 'jump_land' ]);
const isVitalSwitch = JUMP_VITAL.has(state)
|| JUMP_VITAL.has(this._currentState)
|| state === 'crouch_enter' || state === 'crouch_to_stand'; || state === 'crouch_enter' || state === 'crouch_to_stand';
if (!isVitalSwitch && this._lastSwitchAt && (now - this._lastSwitchAt) < 120) { if (!isVitalSwitch && this._lastSwitchAt && (now - this._lastSwitchAt) < 120) {
// Запомним последний запрошенный state — если он не изменится за // Запомним последний запрошенный state — если он не изменится за
@ -435,12 +445,19 @@ export class MixamoAnimator {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[MixamoAnimator] setState: ${this._currentState || 'none'}${state} (loop=${loop})`); console.log(`[MixamoAnimator] setState: ${this._currentState || 'none'}${state} (loop=${loop})`);
// Per-state speedRatio: подгоняем длительность под физику.
// jump_fwd_air: Mixamo Jump полёт = 0.43с, физика = 0.73с
// → speedRatio = 0.59 (замедлить чтобы клип не зациклился).
const SPEED_RATIO = {
jump_fwd_air: 0.59,
};
const speedRatio = SPEED_RATIO[state] || 1.0;
// Запустить новую анимацию. Babylon 7 ВНИМАНИЕ: параметр loop // Запустить новую анимацию. Babylon 7 ВНИМАНИЕ: параметр loop
// в start() иногда игнорится — дублируем через loopAnimation // в start() иногда игнорится — дублируем через loopAnimation
// (выставлен в _ensureGroup). // (выставлен в _ensureGroup).
try { try {
next.reset(); next.reset();
next.start(loop, 1.0, next.from, next.to, false); next.start(loop, speedRatio, next.from, next.to, false);
} catch (e) { } catch (e) {
try { next.play(loop); } catch (_) {} try { next.play(loop); } catch (_) {}
} }

View File

@ -3092,12 +3092,21 @@ export class PlayerController {
} else } else
if (canJump && c.has('Space') && !inWater && !this._shipMode && !this._ufoMode) { if (canJump && c.has('Space') && !inWater && !this._shipMode && !this._ufoMode) {
if (!this._jumpHeld) { if (!this._jumpHeld) {
// 3-фазная модель: при первом нажатии Space запускаем // 3-фазная модель прыжка.
// jump_anticipate (присед перед прыжком) на 0.375с. // _jumpKind определяется по нажатым клавишам в момент Space:
// Физика прыжка стартует ПОСЛЕ окончания anticipate-фазы. // in_place — нет WASD (анимация Mixamo Jumping)
// forward — есть WASD (анимация Mixamo Jump = прыжок вперёд)
const cc = this._codes;
const wasdHeld = cc && (cc.has('KeyW') || cc.has('KeyS')
|| cc.has('KeyA') || cc.has('KeyD')
|| cc.has('ArrowUp') || cc.has('ArrowDown')
|| cc.has('ArrowLeft') || cc.has('ArrowRight'));
this._jumpKind = wasdHeld ? 'forward' : 'in_place';
// anticipate-фаза разной длительности.
const antDuration = this._jumpKind === 'forward' ? 170 : 375;
this._jumpHeld = true; this._jumpHeld = true;
this._coyoteLeft = 0; this._coyoteLeft = 0;
this._jumpAnticipateUntil = Date.now() + 375; this._jumpAnticipateUntil = Date.now() + antDuration;
this._jumpPendingImpulse = true; this._jumpPendingImpulse = true;
// Robot: запускаем boost-фазу на 0.45с // Robot: запускаем boost-фазу на 0.45с
if (this._robotMode) { if (this._robotMode) {
@ -3341,30 +3350,35 @@ export class PlayerController {
const now = Date.now(); const now = Date.now();
const inCrouchTransition = this._crouchTransitionUntil const inCrouchTransition = this._crouchTransitionUntil
&& now < this._crouchTransitionUntil; && now < this._crouchTransitionUntil;
// jump_anticipate — присед перед прыжком (0.375с), физика заморожена. // 3-фазная анимация прыжка. Выбираем семейство фаз по _jumpKind:
// in_place: jump_anticipate/jump_air/jump_land (Mixamo Jumping)
// forward: jump_fwd_anticipate/jump_fwd_air/jump_fwd_land (Mixamo Jump)
const isForward = this._jumpKind === 'forward';
const stAnticipate = isForward ? 'jump_fwd_anticipate' : 'jump_anticipate';
const stAir = isForward ? 'jump_fwd_air' : 'jump_air';
const stLand = isForward ? 'jump_fwd_land' : 'jump_land';
const landDuration = isForward ? 142 : 570;
const inAnticipate = this._jumpAnticipateUntil const inAnticipate = this._jumpAnticipateUntil
&& now < this._jumpAnticipateUntil && now < this._jumpAnticipateUntil
&& this._jumpPendingImpulse; && this._jumpPendingImpulse;
// jump_land — постприземление (присед→встать) 0.57с.
const inJumpLand = this._jumpLandUntil && now < this._jumpLandUntil; const inJumpLand = this._jumpLandUntil && now < this._jumpLandUntil;
if (inAnticipate) { if (inAnticipate) {
mState = 'jump_anticipate'; mState = stAnticipate;
} else if (!result.onGround) { } else if (!result.onGround) {
// В воздухе: jump_air (только в полёте, без приседа/landing). mState = stAir;
mState = 'jump_air';
this._wasAirborne = true; this._wasAirborne = true;
this._crouchEnterPending = false; this._crouchEnterPending = false;
this._crouchExitPending = false; this._crouchExitPending = false;
this._crouchTransitionUntil = 0; this._crouchTransitionUntil = 0;
// Сбрасываем anticipate-окно — мы уже в воздухе
this._jumpAnticipateUntil = 0; this._jumpAnticipateUntil = 0;
} else if (this._wasAirborne) { } else if (this._wasAirborne) {
// Только что приземлились — запустим jump_land this._jumpLandUntil = now + landDuration;
this._jumpLandUntil = now + 570;
this._wasAirborne = false; this._wasAirborne = false;
mState = 'jump_land'; mState = stLand;
} else if (inJumpLand && !isMoving) { } else if (inJumpLand) {
mState = 'jump_land'; // Для forward — доигрываем land даже при движении
// (там короткая фаза 142мс)
if (isForward || !isMoving) mState = stLand;
} else if (this._crouchEnterPending && inCrouchTransition && !isMoving) { } else if (this._crouchEnterPending && inCrouchTransition && !isMoving) {
mState = 'crouch_enter'; mState = 'crouch_enter';
} else if (this._crouchExitPending && inCrouchTransition && !isMoving) { } else if (this._crouchExitPending && inCrouchTransition && !isMoving) {