Model и ModelAnimation
Загруженная GLB-модель — свой вид узла (GLB — это иерархия узлов с запечёнными клипами анимации),
отличный от Mesh (сырые примитивы, без анимации). Расширяет
Node. Каждая Model несёт аспект ModelAnimation, заранее прикреплённый на
model.anim — сам ты его не прикрепляешь.
Обзор
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))Загрузка
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 или ошибке декодирования.
отсечение по фрустуму по умолчанию выключено для моделей (скиннинг-меши двигаются за пределы
своих границ времени импорта). Передай culling: true для крупных статичных пропов, чтобы части за
кадром пропускали отрисовку.
клонирования нет — каждый Model.load создаёт один экземпляр, и загрузка одного URL N раз — текущий
паттерн для пула спавна. Загружай копии заранее (при сборке сцены, не при спавне) и переиспользуй.
Иерархия узлов
Model — корневой узел GLB; внутренние узлы файла висят под ним как обычные
Node:
model.traverse(node => { // этот узел, затем каждый потомок
if (node.name === 'Sword') node.visible = false
})
model.children // прямые детиТрансформ, visible, события, аспекты (Shape, Physics, …) работают и на корне, и на внутренних
узлах.
Анимация — model.anim
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-инструменте/экспортёре).
неизвестное имя клипа молча игнорируется — play('Runn') оставляет выбранный ранее клип и играет
его. Валидируй по clips, если имя приходит из данных.
События конца клипа
Срабатывают на узле (не на anim), с индексом клипа как аргументом:
model.addEventListener('completed', clip => { /* незацикленный клип завершился */ })
model.addEventListener('loopReached', clip => { /* зацикленный клип обернулся */ })После completed аспект переключается в playing = false; зацикленный клип идёт до stop().
Ограничения
- Нет кросс-фейда и блендинга —
play()жёстко переключается на новый клип в следующем кадре. Одновременно играет один клип; послойного/частичного микса тела нет. - Нет клонирования — см. заметку о загрузке выше; пулы грузят N копий.
Подводные камни
// ✗ 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 — добавление узлов, камера.