Flipbook Texture: как анимировать спрайты в Unity, Godot и VRChat
Создайте собственную текстуру-флипбук
Текстура-флипбук проигрывает спрайт-лист кадр за кадром на одной поверхности. Узнайте, как собрать такую в Unity, Godot и VRChat, и прокрутите настоящий спрайт-лист прямо в 3D.
Текстура-флипбук — это спрайт-лист, который шейдер проигрывает кадр за кадром на одной поверхности. Здесь нет скелета и нет таймлайна: лист хранит все кадры в фиксированной сетке, а материал перемещает небольшое окно по этой сетке с постоянной скоростью. Лист 8 x 8 содержит 64 кадра; при 12 кадрах в секунду цикл длится около 5,3 секунды. Меш не меняется, текстура не перезагружается, и вся анимация обходится одним обращением к текстуре.
На этой странице разбираются четыре числа, которые нужны шейдеру флипбука, и то, как индекс кадра отображается на UV-координаты; два способа управлять текстурой-флипбуком в Unity; настройка флипбука Poiyomi на аватаре VRChat; аналоги в Godot и Phaser; а также сборка собственного листа из GIF или из текстового описания.
Краткий ответ
Текстура-флипбук — это спрайт-лист, используемый как шейдерная анимация и индексируемый по строке и столбцу во времени. Лист лежит в одном слоте текстуры; при каждом вычислении шейдер получает индекс кадра из прошедшего времени и частоты кадров, сводит этот индекс к столбцу и строке и переназначает UV поверхности в эту единственную ячейку. Frame = floor(time * fps) mod frameCount, column = frame mod columns, row = floor(frame / columns). Лист 6 x 6 при 10 FPS проигрывает 36 кадров за 3,6 секунды — полностью на GPU, без покадровой загрузки и без изменения draw call.
Как работает текстура-флипбук
Анимацию определяют четыре числа. Столбцы и строки описывают раскладку листа. Число кадров обычно равно столбцам, умноженным на строки, и именно по этому значению зацикливается индекс. Частота кадров задаёт, как быстро продвигается индекс. Всё остальное — фильтрация, режим обёртки, отступы UV — существует для того, чтобы сэмплер не читал за пределами текущей ячейки.
Математика индекса коротка и стоит того, чтобы выписать её явно. Для прошедшего времени t: frame = floor(t * fps) mod frameCount, column = frame mod columns, row = floor(frame / columns). Тогда окно UV задаётся как tiling = (1 / columns, 1 / rows) и offset = (column / columns, row / rows). Именно вертикальное направление чаще всего вызывает путаницу. Большинство инструментов создания листов записывают строку 0 в верхней части изображения, тогда как начало UV в Unity и Godot находится в левом нижнем углу. Для листа с первым верхним рядом в Unity offset.y становится 1 - (row + 1) / rows, иначе ряды проигрываются в перевёрнутом порядке и первый кадр анимации оказывается последней строкой листа.
Фильтрация определяет, будет ли результат чистым. Установите режим обёртки листа в Clamp. При Repeat выборка у правого края текстуры читает левый край, поэтому столбец пикселей из кадра 0 появляется внутри последнего кадра каждого ряда. Отключите мипмапы для пиксель-арта с жёсткими краями, потому что мип-цепочка усредняет соседние ячейки и анимация превращается в кашу, стоит поверхности оказаться в нескольких метрах. Если целевой движок или шейдер принудительно включает мипмапы, оставьте прозрачный отступ в один-два пикселя вокруг каждой ячейки или сдвиньте окно UV внутрь на половину текселя со всех четырёх сторон.
| Значение | Что оно контролирует |
|---|---|
| Столбцы | Кадры бок о бок. Задаёт горизонтальный тайлинг одной ячейки: 1 / columns. |
| Строки | Ряды в листе. Задаёт вертикальный тайлинг: 1 / rows. Используйте 1 для полосы. |
| Число кадров | Общее число индексируемых кадров, обычно columns x rows. На нём зацикливается индекс. |
| FPS | Как быстро продвигается индекс. floor(time * fps) даёт текущий кадр. |
| Режим обёртки | Clamp. Repeat позволяет сэмплеру читать за краем листа. |
| Режим фильтрации | Point при отключённых мипмапах или сдвиг UV на половину текселя на ячейку. |
Текстура-флипбук в Unity
Есть два пути, и отказывают они по-разному. Путь первый — шейдер, который сам владеет индексом строки и столбца и анимируется самостоятельно. Путь второй — окно Animation, которое ключует tiling и offset материала, пока обычный шейдер Lit или Unlit выполняет выборку. Первый путь лучше для эффектов, которые должны оставаться синхронными с игровым временем; второй лучше, когда анимацию нужно вести из состояния Animator и видеть в таймлайне вместе со всем остальным в сцене.
В любом случае сначала настройте импорт спрайт-листа: Wrap Mode Clamp, Mip Maps off, Filter Mode Point и без сжатия для пиксель-арта. Эти три настройки вызывают больше багов флипбука, чем сама математика шейдера.
Путь 1: шейдер с индексом строки и столбца
В Shader Graph узел Flipbook принимает Columns, Rows и индекс кадра, управляемый временем, и выдаёт переназначенные UV. В шейдере, написанном вручную, объявите _Columns, _Rows и _Fps как свойства и вычисляйте ячейку на стадии вершин или фрагментов. Если анимация должна быть синхронной между объектами, ведите индекс кадра из float в скрипте, а не выбирайте _Time.y независимо в каждом материале, чтобы два флипбука на одном объекте не могли разойтись.
Путь 2: окно Animation управляет tiling и offset
Этот путь анимирует вектор _ST текстуры. Встроенные Standard и Unlit используют _MainTex_ST; URP Lit и Unlit используют _BaseMap_ST. Tiling остаётся фиксированным на (1, 1 / rows), меняется только offset.y. Приведённые ниже шаги относятся к этому пути.
01
Выберите материал и откройте окно Animation. Создайте клип, затем добавьте свойство-вектор _ST текстуры как дорожку — оно появится как Vector4 на материале.
02
Установите частоту дискретизации клипа равной целевому FPS. Если она ниже нужного FPS, ключевые кадры схлопнутся на одну выборку, и анимация проиграется медленнее задуманного.
03
Ключуйте offset.y один раз на кадр: значение = 1 - (row + 1) / rows для листа с первым верхним рядом, tiling = (1, 1 / rows).
04
Установите для каждого ключевого кадра интерполяцию Constant в редакторе кривых. Linear смешивает две ячейки, и вы видите оба кадра одновременно как двоение.
05
Убедитесь, что последний ключ не повторяет значение первого кадра, иначе анимация задерживается на один кадр в месте стыка цикла.
06
Если несколько рендереров используют один материал, анимируйте MaterialPropertyBlock для каждого рендерера, а не читайте renderer.material — это клонирует материал при первом обращении и оставляет клон живым на всё время существования объекта.
Флипбук Poiyomi для VRChat
Раздел Flipbook в шейдере Poiyomi прокручивает окно UV по листу на самом материале, поэтому один слот материала может показывать анимированный спрайт на меше аватара. Он предоставляет Rows, Columns и частоту кадров, а также режим, в котором вы сами управляете индексом кадра вместо того, чтобы он шёл от времени шейдера.
Используйте однорядную полосу, а не сетку. Полоса даёт шейдеру одну ось для шага, что полностью устраняет вертикальный шов. С сеткой окно UV должно в конце каждого ряда смещаться вниз на строку, и любая фильтрация, уровень мипа или ошибка округления на половину текселя на этой границе даст выборку из соседней строки — это видно как тонкая полоса другого кадра во время проигрывания. Полоса также превращает индекс кадра в простое умножение: ширина кадра в UV равна 1 / frameCount, а смещение — просто frame / frameCount. Для анимации из 8 кадров по 256 x 256 ячеек полоса будет 2048 x 256 и останется степенью двойки.
Альфа на аватаре значит больше, чем в плоской сцене. Если у спрайта жёсткие края, используйте Cutout или Opaque. Флипбуки в очереди Transparent не пишут глубину, и на аватаре они сортируются относительно тела и друг друга способами, которые меняются с углом камеры. Где можете, держите анимированную поверхность на материале, который уже есть на аватаре — каждый дополнительный слот материала бьёт по рангу производительности аватара. Если индекс кадра ведётся из состояния Animator с синхронизированным параметром, все клиенты видят один и тот же кадр; индекс, вычисленный из времени шейдера, рассчитывается локально и у разных зрителей может стоять в разных точках цикла.
01
Экспортируйте кадры одной горизонтальной полосой. Высота полосы равна высоте ячейки; ширина полосы равна числу кадров, умноженному на ширину ячейки. Не оставляйте лишнюю ячейку в конце.
02
Импортируйте текстуру с Wrap Mode Clamp, Mip Maps off и включённым sRGB. Включайте Alpha Is Transparency только если полоса действительно несёт альфу.
03
Создайте материал Poiyomi, назначьте его анимированному мешу и откройте раздел Flipbook.
04
Установите Rows в 1, а Columns — в число кадров. Задайте частоту кадров под нужную длительность — 8 кадров при 8 FPS дают секундный цикл.
05
Выберите источник кадров. Ручной режим открывает индекс кадра, которым вы управляете из состояния Animator; режим от времени зацикливается сам.
06
В разделе Rendering установите Cutout или Opaque для спрайтов с жёсткими краями. Оставляйте Transparent только для эффектов с мягкими краями и будьте готовы к особенностям сортировки по глубине.
07
Проверьте цикл: последний кадр не должен дублировать первый, иначе спрайт замирает на один кадр в каждом цикле.
08
Протестируйте на загруженном аватаре с зеркалом, вблизи и с другого конца инстанса. Именно на расстоянии проявляются проблемы с мипами и фильтрацией.
Флипбук в Godot и Phaser
В Godot 4 путь для 2D — это AnimatedSprite2D с ресурсом SpriteFrames. Импортируйте лист, откройте SpriteFrames, задайте Hframes и Vframes по числу столбцов и строк, добавьте кадры анимации, затем установите скорость анимации в кадрах в секунду и оставьте Loop включённым. SpriteFrames хранит анимацию как список индексов кадров, поэтому и сетка, и одна полоса работают без какой-либо математики UV с вашей стороны.
Если нужна всего одна анимация, обычный Sprite2D легче: задайте Hframes и Vframes один раз, затем продвигайте целочисленное свойство frame из AnimationPlayer со ступенчатой интерполяцией. Не твиньте frame кривой с обычным сглаживанием — значение является целочисленным индексом, и его интерполяция либо округляется к одному значению, либо мерцает между двумя ячейками.
Для флипбука на 3D-поверхности в Godot самый короткий путь — ShaderMaterial с той же математикой строк и столбцов, что и в версии для Unity: uv = UV * vec2(1.0 / cols, 1.0 / rows) + vec2(col / cols, row / rows), где строка вычисляется из индекса кадра. Начало UV в Godot — левый нижний угол, как и в Unity, поэтому лист с первым верхним рядом требует такого же вертикального переворота.
В Phaser 3 лист загружается один раз и нарезается при загрузке. frameWidth и frameHeight задают ячейку, кадры нумеруются слева направо и сверху вниз, а анимация определяется номерами кадров, а не координатами — не нужно писать шейдер и негде ошибиться в арифметике строк и столбцов.
| Движок | Объект | Что вы задаёте |
|---|---|---|
| Godot 4 | AnimatedSprite2D + SpriteFrames | Hframes и Vframes по раскладке листа, затем скорость анимации в FPS с включённым Loop. |
| Godot 4 | Sprite2D | Hframes и Vframes один раз, затем целочисленное свойство frame продвигается AnimationPlayer со ступенчатой интерполяцией. |
| Godot 4 | ShaderMaterial на 3D-меше | Столбцы, строки и индекс кадра в шейдере с тем же переназначением UV и вертикальным переворотом, что и в Unity. |
| Phaser 3 | this.load.spritesheet | frameWidth и frameHeight; Phaser нарезает лист слева направо, сверху вниз. |
| Phaser 3 | this.anims.create | Кадры из anims.generateFrameNumbers, а также frameRate и repeat: -1 для зацикливания. |
Прокрутите спрайт-лист прямо в 3D
Игровая сцена в этом разделе — The Cyclops' Island, работающий в 3D-конструкторе игр Neta Studio. Флипбук на экране — это та же операция преобразования индекса в UV, описанная выше, вычисляемая в шейдере Three.js на GPU; разница в том, что она выполняется на настоящем меше в настоящей сцене, с камерой под вашим управлением и движущимся светом.
Это значит больше, чем звучит. Флипбук в плоской сцене Unity обычно рассматривают строго спереди, где утечку по швам и смешивание мипов заметить трудно. Обойдите камерой анимированную поверхность под скользящим углом, и проблемы проявятся сразу: полоса соседней ячейки у края кадра, последний уровень мипа, размывающий два кадра в одно пятно, или альфа-край, который читается правильно спереди и исчезает сбоку. Браузерная сборка делает цикл между созданием листа и его просмотром на поверхности достаточно коротким, чтобы итерировать без этапа сборки.
Mini World — сцена поменьше для проверки одного листа: меньше мешей и короче загрузка. Agent Sprite Forge генерирует кадры, если листа у вас ещё нет, и обе сцены используют один и тот же путь флипбука, так что лист, который выглядит правильно в Mini World, ведёт себя так же в The Cyclops' Island.
Создайте собственный лист
Есть три пути к пригодному листу: конвертировать существующий GIF, обрезать сетку до одной полосы или сгенерировать кадры из текстового описания. Все три заканчиваются одним файлом — одним PNG, где все ячейки одного размера, выложены слева направо и сверху вниз, а ширина листа кратна ширине ячейки.
Конвертация GIF — путь с наибольшим объёмом скрытой работы, потому что GIF не является последовательностью полных кадров. Кадры часто представляют собой частичные разницы с предыдущим кадром, а задержки хранятся для каждого кадра в сотых долях секунды. Шейдер флипбука принимает одну частоту кадров, поэтому переменный тайминг приходится выравнивать дублированием кадров перед экспортом.
Обрезка сетки до полосы механическая: оставьте нужный ряд, обрежьте высоту до одной ячейки и оставьте ширину равной числу кадров, умноженному на ширину ячейки. Это правильный ход, когда лист-сетка используется как одна анимация, потому что он убирает вертикальную ось и связанный с ней шов. Генерация из описания — третий путь: Agent Sprite Forge принимает действие и число кадров и возвращает лист с фиксированным размером ячейки, что избавляет от ручной подгонки кадров, экспортированных по одному.
01
Выравнивайте каждый кадр GIF перед экспортом. Декодируйте каждый кадр на холст, который переносит предыдущий кадр вперёд, иначе вы экспортируете прямоугольники разниц вместо картинки.
02
Нормализуйте тайминг. Выберите самую короткую или медианную задержку GIF, затем продублируйте кадры там, где источник держался дольше, чтобы лист проигрывался при одном постоянном FPS.
03
Выложите кадры слева направо, сверху вниз, все ячейки одинакового размера. Сделайте размеры листа точным кратным размеру ячейки — лист шириной 9,5 ячейки будет вечно выбирать не те тексели.
04
Чтобы обрезать до полосы, обрежьте высоту до одной ячейки и оставьте ширину равной числу кадров, умноженному на ширину ячейки. Удалите пустую хвостовую ячейку, если она была в исходной сетке.
05
Добавьте прозрачный отступ в один-два пикселя вокруг каждой ячейки, если целевой движок не позволяет отключить мипмапы. Иначе сдвиньте окно UV внутрь на половину текселя со всех четырёх сторон.
06
Чтобы сгенерировать, а не конвертировать, напишите действие, число кадров и направление взгляда и позвольте Agent Sprite Forge вернуть лист с фиксированными ячейками вместо экспорта кадров по одному и их ручного выравнивания.
07
Проверьте, перебирая индекс кадра по одному в движке. Если любая ячейка показывает полоску соседа у края, неправильна раскладка листа или сдвиг UV, а не шейдер.
Войдите в мир, которому она принадлежит
Спрайт — это персонаж, которому негде стоять. Последний шаг — тот, которого не предлагает больше никто: возьмите только что собранный лист и поместите его в мир, по которому можно ходить, — небольшой остров, планету, дождливый угол улицы. Каждый мир на этом сайте был создан из одного текстового описания и опубликован как страница, в которую можно играть.
FAQ
Что такое флипбук Poiyomi? +
Это раздел Flipbook шейдера Poiyomi, который прокручивает окно UV по спрайт-листу на материале, так что один слот материала может показывать анимированный спрайт на меше аватара. Вы задаёте Rows, Columns и частоту кадров и либо позволяете индексу кадра идти от времени шейдера, либо управляете им из состояния Animator. Предпочтительна однорядная полоса, потому что она убирает вертикальный шов между рядами.
Можно ли сделать флипбук из GIF напрямую? +
Нет. GIF хранит задержки по кадрам и часто только изменившиеся пиксели, тогда как шейдер флипбука ожидает одну текстуру и одну постоянную частоту кадров. Сначала конвертируйте: выровняйте каждый кадр поверх предыдущего, нормализуйте тайминг и запишите результат в фиксированную сетку ячеек. Шейдер GIF вообще не видит.
Почему моя текстура-флипбук подтекает между кадрами? +
Сэмплер читает за пределами окна текущей ячейки. Установите режим обёртки текстуры в Clamp, отключите мипмапы для графики с жёсткими краями и сдвиньте окно UV внутрь на половину текселя с каждой стороны. Утечка усиливается с расстоянием, потому что именно там мип-цепочка начинает усреднять соседние ячейки между собой.
Какую частоту кадров использовать для флипбука? +
8–12 FPS читаются как ручная анимация и подходят для спрайтов персонажей; 24–30 подходят для огня, воды и других непрерывных эффектов. У шейдера ровно одна частота, поэтому если у источника был переменный тайминг, дублируйте кадры под неё, а не пытайтесь менять скорость посреди цикла.
Нужна ли мне сетка, или достаточно одного ряда? +
Одного ряда достаточно для всего, что проигрывается как последовательность, и он убирает одну ось ошибок по швам. Сетки нужны для листов, которые упаковывают несколько анимаций в одну текстуру, где шейдер или движок выбирает ряд для idle, бега или атаки. Если из листа-сетки получается только одна анимация, обрежьте его до полосы.
Работает ли флипбук с шейдером URP Lit в Unity? +
Из коробки нет — в URP Lit нет индекса кадра. Используйте Shader Graph с узлом Flipbook, который принимает Columns, Rows и индекс от времени, либо анимируйте tiling и offset _BaseMap_ST материала с постоянной интерполяцией ключевых кадров. Встроенные Standard и Unlit используют _MainTex_ST по тем же правилам.
Назад к 3D-конструктору игр
Все 31 готовых 3D-миров, игр и сцен собраны на одной странице — в каждый можно играть.
Открыть библиотеку спрайтовых ресурсов
Каждый ресурс в этой библиотеке — прозрачный PNG, готовый для игры, с записанным промптом — «Sprite sheet resources» и «Free sprite sheets», с тегами по действию, стилю и объекту. Бесплатно для просмотра и скачивания.