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 для крупных статичных пропов, чтобы части за кадром пропускали отрисовку.

Клонирование

TypeScript
const enemy = template.clone(): Model

clone() делает глубокую копию GLB — меши, скелет и запечённые клипы анимации — переиспользуя уже декодированный ассет (без повторного скачивания и парсинга), поэтому он сильно дешевле второго Model.load. Клон прикрепляется к родителю источника и попадает в его сцену, стартуя с текущего трансформа источника. У него своё независимое состояние анимации, доступное через clone.anim. Отсечение по умолчанию выключено, как у Model.load.

TypeScript
const template = await Model.load(asset('./enemy.glb'))   // декодируем один раз
const wave = Array.from({ length: 8 }, () => template.clone())  // дешёвые копии, без await
wave.forEach((e, i) => { e.x = i * 2; scene.add(e); e.anim.play('Walk', { loop: true }) })

Для пулов спавна предпочитай clone() повторной загрузке того же 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() жёстко переключается на новый клип в следующем кадре. Одновременно играет один клип; послойного/частичного микса тела нет.

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

TypeScript
// ✗ await свежей загрузки на каждый спавн — повторное скачивание + декод, дёргается в момент спавна
const enemy = await Model.load(url)          // на каждый спавн
// ✓ загрузи один раз, затем clone() — без await, без повторного декода
const template = await Model.load(url)
const enemy2 = template.clone()

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

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

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