DynamicBone

Вторичное движение для хвостов, ушей, волос, верёвок и плащей, симулируемое в движке. DynamicBone ставится на КОРНЕВУЮ кость цепочки: её поддерево становится цепочками частиц, которые качаются, отстают и успокаиваются под действием гравитации, ветра и движения персонажа, притягиваются обратно к позе, которую даёт им анимация, не входят в тело благодаря капсулам DynamicBoneCollider и держатся над полом. Стадия работает после поздней фазы и IK-цепей, до скиннинга — Animator, активный ragdoll и записи в кости в поздней фазе — её вход. Подходит любое дерево узлов, не только скиннутая модель (верёвка из простых узлов, с bones, именующим её суставы).

Обзор

TypeScript
const main = async () => {
  const scene = new Scene()
  const fox = await Model.load(asset('./fox.glb'))
  scene.add(fox)
  scene.open()
  fox.anim.playLoop('walk')

  const tail = fox.bone('tail')!.aspect(DynamicBone, {
    stiffness: [0.5, 0.15],        // жёсткий у основания, свободный на кончике
    damping: 0.1,
    gravity: 2,                    // м/с²
    angleLimit: [30, 45],          // градусы от анимированного направления
    radius: 0.03,                  // метры, для коллизий
  })
  for (const ear of ['earend', 'R_earend']) fox.bone(ear)!.aspect(DynamicBone, { stiffness: 0.6, damping: 0.2, angleLimit: 30, radius: 0.01 })
  fox.bone('Hips')!.aspect(DynamicBoneCollider, { radius: 0.11, to: 'chest' })   // капсула, в которую хвост не входит

  tail.dynamicBone.floor = 0       // и над плоскостью земли y = 0
  console.log(`tail chain: ${tail.dynamicBone.chain.map((b) => b.name).join(' > ')}`)
}
main()

DynamicBone

TypeScript
bone.aspect(DynamicBone, { … })
bone.dynamicBone                 // аспект — каждое поле живое (отправляется при изменении)
bone.dynamicBone.chain           // Node[] — кости цепочки, корень первым
bone.dynamicBone.particles       // Vec3[] — симулированные мировые позиции: кости, затем кончик на каждый лист
bone.dynamicBone.reset()         // прыгнуть на анимированную позу в следующем кадре (телепорт, смена кадра камеры)
bone.dynamicBone.guide([{ bone, at, weight }])   // мировые точки, к которым кости тянутся на каждом подшаге; заменяет, [] очищает

Покостные значения (DynamicBoneCurve) принимают одно число для всей цепочки или пару [root, tip], интерполируемую вниз по ней по глубине — хвост, жёсткий у основания и свободный на кончике.

Поле По умолчанию Значение
stiffness 0.3 0–1: как быстро кость возвращается к анимированной форме, за 1/60 с. 0 = верёвка, 1 = жёстко (точно анимация)
damping 0.1 0–1: скорость, теряемая за 1/60 с — 0.02 качается как верёвка, 0.1 успокаивается как хвост, 0.3 — мёд
gravity 0 м/с², вниз. Висящая поза покоя её не чувствует; торчащая поза свисает настолько, насколько позволяет stiffness
angleLimit 0 Максимальное отклонение от анимированного направления, градусы на кость (0 = свободно)
radius 0.02 Радиус частицы для коллизий, метры
mass 1 Относительная масса на кость: [3, 1] заставляет основание хвоста нести его кончик, 1 делит поровну как верёвка
weight 1 0–1 смешивание от анимации к симуляции. При 0 ничего не симулируется, и цепочка перевзводится на анимированной позе, когда возвращается
follow 0 0–1 перемещения персонажа, которое цепочка берёт жёстко (0 = полный хлыст при рывке)
wind [0, 0, 0] Мировой ветер, м/с²
floor 'none' 'probe' (луч вниз от корня цепочки против физического мира, каждый кадр), высота (плоскость на этом мировом y) или мировая точка (плоскость через неё, floorNormal вверх)
floorNormal [0, 1, 0] Нормаль плоскости пола, когда floor — точка
floorFriction 0.5 Коэффициент Кулона о пол (0 = лёд, 1 = резиновая подошва)
colliders 'auto' 'auto' = каждый DynamicBoneCollider на той же модели; 'humanoid' = они плюс капсулы, сгенерированные на стандартных костях гуманоида (бёдра, позвоночник, голова, конечности — раскладка Ragdoll); 'none'; или узлы, несущие коллайдеры, которые использовать
bones всё поддерево Только эти имена костей входят в цепочку (корень всегда входит; пропущенная кость забирает с собой своё поддерево)
depth 0 Предел глубины под корнем (0 = нет)
pinned, pinGive —, 0 Кости, которые едут на анимации, пока их поддеревья симулируются — полосы плаща под одним якорем, перечисленные по кольцу; pinGive = метры, на которые коллайдер может сдвинуть закреплённую кость с её места
link 0 0–1: связи между костями одной глубины соседних столбцов (плащ; соседи в порядке pinned)
side 'none' Наружная сторона ткани, для коллайдеров с пометкой oneSided: 'auto' = от оси корневой кости, или направление во фрейме корневой кости
spin, spinInertia 0, 0 ПОВОРОТ тела: выше 0 цепочка живёт во вращающемся фрейме родительской кости, так что демпфированный плащ едет с поворотом; его инерция возвращается как силы фрейма × spin (1 = физично). spinInertia = постоянная времени, секунды, с которой вращение ткани следует за вращением тела (0 = приклеено к повороту)
rate 60 Частота подшагов, Гц (не больше 4 подшагов на кадр)
iterations 4 Проходов ограничений на подшаг, 1–8 (длинная верёвка может хотеть 8)
teleport 1 Прыжок корня длиннее стольких метров за один кадр сбрасывает цепочку вместо хлыста
guideHold 0 0–1: насколько крепко цели guide() держатся сквозь проходы ограничений
edges true Сегменты костей и связи тоже сталкиваются с капсулами, не только частицы

Дочерняя кость, несущая DynamicBone или DynamicBoneCollider в момент прикрепления цепочки, остаётся вне её вместе со своим поддеревом (это другая цепочка или тело).

Note

кость цепочки, прочитанная в update(), всё ещё показывает анимированную позу — стадия работает после поздней фазы. particles держат симуляцию (по состоянию на последний кадр), а updateBefore() читает финальную позу предыдущего кадра.

DynamicBoneCollider

TypeScript
bone.aspect(DynamicBoneCollider, { radius?, offset?, end?, to?, oneSided? })
Поле По умолчанию Значение
radius 0.1 Метры, в мире — масштаб кости его не трогает
offset [0, 0, 0] Начало капсулы, в локальном пространстве кости
end — Конец капсулы, в локальном пространстве кости
to — Кость (по имени), чьё начало — конец капсулы, вместо end
oneSided false Ткань должна быть на стороне side цепочки от него: пока капсула движется прочь от той стороны, она переносит кость, в которую врезалась, на ту сторону, вместо того чтобы тащить её (рука, махнувшая вперёд под накидкой)

Ни end, ни to = сфера в offset. Коллайдер подхватывается каждым DynamicBone той же модели с colliders: 'auto' или 'humanoid', либо используется там, где перечислен.

Note

зависит от хоста. Хост без динамических костей (headless-рендерер: lecodes render / lecodes test без --sim / --desktop) предупреждает один раз и показывает анимацию как есть; chain и particles там остаются пустыми.

Смотрите также

  • Animator — поза, за которой следуют цепочки
  • IK и стопы — решаются прямо перед этой стадией
  • Model — model.bone(name)