LeCodesdocs

Model и ModelAnimation

Загруженная GLB-модель — свой вид узла (GLB — это иерархия узлов с запечёнными клипами анимации), отличный от Mesh (сырые примитивы, без анимации). Расширяет Node. Каждая Model несёт аспект ModelAnimation, заранее прикреплённый на model.anim — сам ты его не прикрепляешь.

Обзор

TypeScript
const hero = await Model.load(asset('./hero.glb'))
hero.position = [0, 0, -2]
scene.add(hero)

hero.anim.play('Run', { loop: true })
hero.addEventListener('loopReached', clip => console.log('lap', clip))

Загрузка

TypeScript
Model.load(source: string | FetchResponse, options?: {
  culling?: boolean          // по умолчанию false — вся модель рисуется всегда
  onProgress?: (p: { loaded: number, total?: number }) => void
}): Promise<Model>

source — путь asset('./file.glb'), удалённый https://… URL или уже скачанный FetchResponse. Отклоняется при HTTP ≥ 400 или ошибке декодирования.

Note

отсечение по фрустуму по умолчанию выключено для моделей (скиннинг-меши двигаются за пределы своих границ времени импорта). Передай culling: true для крупных статичных пропов, чтобы части за кадром пропускали отрисовку.

Note

клонирования нет — каждый Model.load создаёт один экземпляр, и загрузка одного URL N раз — текущий паттерн для пула спавна. Загружай копии заранее (при сборке сцены, не при спавне) и переиспользуй.

Иерархия узлов

Model — корневой узел GLB; внутренние узлы файла висят под ним как обычные Node:

TypeScript
model.traverse(node => {                 // этот узел, затем каждый потомок
  if (node.name === 'Sword') node.visible = false
})
model.children                           // прямые дети

Трансформ, visible, события, аспекты (Shape, Physics, …) работают и на корне, и на внутренних узлах.

Анимация — model.anim

TypeScript
model.anim.clips                       // { name: string, duration: number }[] — запечено в glb (секунды)

model.anim.play(clip?: string | number, options?: { loop?: boolean }): this
model.anim.stop(): this

model.anim.playing = true              // get/set — продолжить/приостановить текущий клип
model.anim.loop = true                 // get/set — также задаётся через опции play()
model.anim.speed = 1.5                 // множитель скорости (1 = авторская скорость)
model.anim.time = 0.5                  // get/set — перемотка внутри текущего клипа (секунды)

play() принимает имя или индекс клипа; без аргумента (пере)проигрывает текущий клип. Проигрывание по имени работает только когда клипы GLB осмысленно названы — проверь model.anim.clips (или назови их в DCC-инструменте/экспортёре).

Note

неизвестное имя клипа молча игнорируется — play('Runn') оставляет выбранный ранее клип и играет его. Валидируй по clips, если имя приходит из данных.

События конца клипа

Срабатывают на узле (не на anim), с индексом клипа как аргументом:

TypeScript
model.addEventListener('completed', clip => { /* незацикленный клип завершился */ })
model.addEventListener('loopReached', clip => { /* зацикленный клип обернулся */ })

После completed аспект переключается в playing = false; зацикленный клип идёт до stop().

Ограничения

  • Нет кросс-фейда и блендингаplay() жёстко переключается на новый клип в следующем кадре. Одновременно играет один клип; послойного/частичного микса тела нет.
  • Нет клонирования — см. заметку о загрузке выше; пулы грузят N копий.

Подводные камни

TypeScript
// ✗ await загрузок по одной в цикле спавна — дёргается в момент спавна
const enemy = await Model.load(url)          // на каждый спавн
// ✓ предзагрузи пул один раз, затем переиспользуй
const pool = await Promise.all(Array.from({ length: 8 }, () => Model.load(url)))

// ✗ ждать плавного перехода
hero.anim.play('Idle')                       // резко — кросс-фейда не существует

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

  • Node — трансформ, иерархия, traverse, события (унаследованы).
  • Mesh — примитивы / кастомная геометрия (другой рисуемый вид).
  • Физика и формы — дать модели форму коллизии.
  • Scene — добавление узлов, камера.