IK и стопы
Поправки, которые движок вносит в анимированную позу. IK-цепи — аспекты на костях: IK.TwoBone
ставит конец конечности на цель (руку на рукоять, стопу на ступеньку), IK.LookAt поворачивает кость
к цели (голову, турель). Стопы — стадия каждого Animator, anim.feet: какие
кости — стопы, фиксация стопы, IK по земле и события шагов. В JS ничего не работает покадрово — аспекты
описывают, движок решает.
Где они работают в кадре: updateBefore() → физика → аниматоры (клипы, root motion,
варп, стопы) → update() → IK-цепи →
динамические кости → скиннинг. Цель, которую игра двигает в update(),
достигается в том же кадре; кость цепи, прочитанная в update(), всё ещё показывает позу до решения
(читай её в updateBefore(), который видит финальную позу предыдущего кадра).
Обзор
const main = async () => {
const scene = new Scene()
const hero = await Model.load(asset('./hero.glb'))
scene.add(hero)
scene.open()
hero.anim.playLoop('Idle') // IK поправляет позу: что-то должно играть
const grip = new Node() // узел: движок читает его каждый кадр
grip.position = [0.3, 1.2, 0.4]
scene.add(grip)
const hand = hero.bone('LeftHand')!.aspect(IK.TwoBone, { target: grip, pole: [0.6, 1.2, -1] }) // LeftArm ▸ LeftForeArm ▸ LeftHand
hero.bone('Head')!.aspect(IK.LookAt, { target: scene.camera, axis: [0, 0, 1], limit: 60 })
hand.ik.weight = 0.5 // плавно вмешать / вымешать
console.log(`hand off its target: ${hand.ik.error.toFixed(3)} m`)
hero.anim.feet.set({ lock: true, ik: true }) // поставленные стопы стоят на месте; стопы на земле
hero.anim.feet.onStep((side, at) => console.log(`${side} foot down at ${at.x.toFixed(2)}, ${at.z.toFixed(2)}`))
}
main()на загруженной Model, чей Animator ещё не использовал ни одного клипа (ничего не играло и не
измерялось), прикрепление цепи схлопывает весь скелет в начало координат модели — даже при
weight: 0. Как только клип привязан (сыгран — остановлен с тех пор или нет — или прочитан через
anim.time() / anim.clipInfo()), цепи работают. Сначала запусти цикл. Голое дерево узлов не
затронуто.
Двухкостный IK — IK.TwoBone
bone.aspect(IK.TwoBone, { target?, pole?, rotation?, rotationWeight?, weight?, enabled?, anchor?, anchorTime?, anchorPin? })
bone.ik // аспект — каждая опция это живое свойство
bone.ik.error // после последнего решения: метры, на которые конечная кость всё ещё не дотянулась до цели (0 = достигнута)| Опция | По умолчанию | Значение |
|---|---|---|
target |
— | Куда идёт начало конечной кости: Node (движок читает его каждый кадр) или мировая точка |
pole |
анимированный изгиб | Подсказка изгиба — средний сустав тянется к ней: Node или мировая точка |
rotation |
анимированный | Мировой поворот, который принимает конечная кость: Node (его мировой поворот) или Quat — рука на рукояти, стопа плашмя на склоне |
rotationWeight |
1 |
Доля rotation, поверх weight |
weight |
1 |
0–1 смешивание всего решения |
enabled |
true |
false = нет решения |
anchor, anchorTime, anchorPin |
'', 0.05, 0 |
Ехать на сокете вместо цели — ниже |
Прикрепляй к КОНЕЧНОЙ кости: цепь — это она, её родитель и её прародитель (плечо ▸ предплечье ▸ кисть).
Прародитель и родитель поворачиваются так, чтобы начало конечной кости достигло target; цель вне
досягаемости вытягивает конечность к ней, а error говорит, насколько она не дотянулась. Цепь
принадлежит ригу кости — ближайшему предку, несущему Animator (каждая Model); голое дерево узлов
получает Animator на своём верхнем узле. Отсоединение аспекта убирает цепь.
Look-at — IK.LookAt
bone.aspect(IK.LookAt, { target?, axis?, limit?, weight?, enabled? })
bone.lookAt // аспект (weight / enabled / error, как выше)| Опция | По умолчанию | Значение |
|---|---|---|
target |
— | Node (читается каждый кадр) или мировая точка |
axis |
[0, 0, -1] |
ЛОКАЛЬНОЕ направление кости, которое должно смотреть на цель — риги различаются (многие головы Mixamo смотрят вдоль +Z) |
limit |
80 |
Максимальное отклонение от анимированного направления, градусы |
weight |
1 |
0–1 смешивание |
enabled |
true |
false = нет решения |
Прикрепляй к самой кости. Цель за пределами limit поворачивает кость на limit градусов к ней и не
дальше.
аксессор — lookAt, так что на этой кости он заменяет собственный метод узла lookAt().
Сценарный аспект LookAt (аксессор facing) — другая вещь:
он поворачивает целый узел, в JS.
Сокеты и привязанные цепи
anim.sockets(bone: Node | string, { [name]: Node | { position?, quaternion? } }, { set? }): Animator
anim.removeSocket(name, set?)
hand.aspect(IK.TwoBone, { anchor: 'grip_l', anchorTime?, anchorPin? })
clip.anchors(bone: string, { donor?, spans?: [from, to, socket, rotation?, pin?][] }): AnimationClip // секунды
clip.clearAnchors(bone)
hand.ik.anchorDelta // { distance (м), angle (рад) } — насколько живой сокет сдвинул руку относительно дубляДля рук на оружии. Сокет — именованный фрейм на кости рига: Node (пустышка на оружии — читается
каждый кадр, так что сокет на движущейся части следует за ней) или фиксированный { position, quaternion }
в пространстве кости. Набор по умолчанию — ЖИВОЙ (оружие в руках); { set: 'tr15' } именует набор
ДОНОРА — те же сокеты на оружии, на котором были созданы дубли. Привязанная цепь
(anchor: 'grip_l' — target, pole и rotation тогда игнорируются) сохраняет позу анимированной
руки относительно её сокета на доноре и воспроизводит её относительно живого: сдвинутую и повёрнутую
на разницу двух сокетов, никогда не переделанную. rotationWeight — доля поворота сокета, которую она
берёт.
Якорные отрезки клипа перемещают руку между сокетами, пока он играет: [from, to, socket] в
секундах клипа ('' = свободна: рука едет на своём собственном суставе), rotation 0–1 (по умолчанию
1), pin 0–1 (по умолчанию 0 — рука на пути к магазину сохраняет собственное движение дубля). Между
отрезками рука на собственном сокете цепи, удерживаемая там через anchorPin (0–1: 1 = точно на
сокете, так что ничто в позе ниже не может её сдвинуть). Рука движется между сокетами на пружине с
полупериодом anchorTime секунд. Клип без якорей не участвует; AnimationClip.from переносит
отрезки, перетаймленные (без их pin, который откатывается к 0), а зеркальный клип сохраняет их на
той же кости.
arms.anim.sockets('ik_hand_gun', { grip_r: gun.gripR, grip_l: gun.gripL, mag: gun.mag })
arms.anim.sockets('ik_hand_gun', { grip_l: { position: [0.03, -0.12, -0.05] } }, { set: 'tr15' })
arms.bone('hand_l')!.aspect(IK.TwoBone, { anchor: 'grip_l' })
arms.anim.clip('Reload')!.anchors('hand_l', { donor: 'tr15', spans: [[0.9, 1.4, 'mag'], [1.4, 2.0, ''], [2.0, 2.4, 'mag']] })Стопы — anim.feet
anim.feet.set({ lock?, ik?, left?, right?, pelvis?, … }) // меняются только переданные ключи — чейнится
anim.feet.lock = true / anim.feet.ik = true / … // каждая опция это ещё и свойство
anim.feet.options // что было задано
anim.feet.state('left') // FootState | undefined — { locked, weight, anchor: Vec3, target: Vec3 }, мировые
anim.feet.kneeAxis('left') // откалиброванная ось шарнира колена (в локале бедра) + её разброс (рад); undefined = нет цепи ноги
anim.feet.onStep((side, position) => …) / anim.feet.offStep(cb) // стопа поставлена: 'left' | 'right', мировой Vec3| Опция | По умолчанию | Значение |
|---|---|---|
lock |
false |
ФИКСАЦИЯ СТОПЫ: стопа, которую показываемый клип называет поставленной, остаётся там, где приземлилась — от пятки к носку, перекатываясь, как перекатывает клип — нога пересчитывается, пока тело движется дальше |
ik |
false |
IK ПО ЗЕМЛЕ: каждая стопа на земле, прощупанной под ней (лестница, склон, бордюр), выровнена по её нормали, таз опущен, чтобы нога дотянулась. Щупает физический мир; без него земля — собственная плоскость узла |
left, right |
классифицируются | Кости контакта с каждой стороны — 'LeftFoot' или ['LeftFoot', 'LeftToeBase']. По умолчанию: находятся по имени (Mixamo / Unity / Blender / UE). Их задание перезапекает контакты каждого клипа |
pelvis |
0.35 |
Метры, на которые таз может опуститься ради земли |
unlockDistance |
0.10 |
Метры разницы, которые зафиксированная стопа поглощает максимум — дальше якорь следует за анимацией вместо борьбы с ней |
lockIn, lockOut |
0.08, 0.12 |
Плавность входа / выхода фиксации, секунды |
align |
1 |
0–1: насколько стопа наклоняется на нормаль земли |
probe |
0.6 |
Дальность луча-щупа выше и ниже лодыжки, метры |
plantSpeed |
0.2 |
Фиксация ставит стопу, только когда анимированная лодыжка медленнее этого, м/с |
onStep срабатывает для того, что показывает БАЗОВЫЙ слой, на каждом контакте клипа — метках
контакта, которые пайплайн ассетов записывает в GLB (lecodes assets marks), или контактах,
обнаруженных по движению, когда их нет; клип без контактов ничего не вызывает. state() — undefined,
пока ничего не играло, и на хостах без стадии стоп. Locomotion задаёт стопы
сам (фиксация включена по умолчанию).
Смотрите также
- Animator — клипы, циклы, one-shot'ы, слои
- Locomotion — движение персонажа, варп шага и ориентации
- DynamicBone — вторичное движение, решается после цепей
- Аспекты —
update/updateBefore, порядок