Арматура, скиннинг и анимация: как ассет начинает двигаться
Разбор файла из главы «Экспорт в GLB» печатал две строки, на которые мы тогда не смотрели:
клипы: []
кости: 0
Пока модель стоит на полке, это нормально. Как только она попадает в игру, возникает вопрос: чем она шевелится. И тут же — второй, который задают реже, а стоит задавать первым: должна ли она вообще шевелиться средствами файла.
Три способа заставить объект двигаться
| Способ | Что лежит в файле | Что делает рантайм | Цена |
|---|---|---|---|
| Трансформации в коде | ничего | сам двигает, вращает, масштабирует объект | нулевая; движется объект целиком |
| Скелетная анимация | кости, веса, клипы | считает позу и деформирует поверхность | вес файла, расчёт скиннинга на кадр |
| Морфы (ключи формы) | несколько состояний геометрии | смешивает их коэффициентами | вес растёт с числом состояний |
Правило выбора простое и почти всегда игнорируемое: скелет нужен тогда, когда деформируется сама поверхность. Если объект движется как целое — едет, вращается, подпрыгивает, — это работа кода рантайма, и она стоит ноль байт.
В игре из главы «Живой кейс» фигура перемещается по клеткам доски прыжком. Соблазн — записать прыжок клипом в файле. Решение — считать прыжок кодом: интерполяция позиции по дуге. Причин три, и все практические.
- Прыжок обязан совпадать с логикой хода: длина зависит от того, через сколько фигур перескакивают. Клип фиксированной длительности этого не умеет.
- Отменённый ход должен откатываться мгновенно. Анимацию, доигрывающую в никуда, придётся гасить руками.
- Ноль байт против клипа, костей и весов на каждой из пятидесяти фигур.
Скелет понадобился бы, если бы у робота гнулись руки или менялось выражение «лица». Он не гнётся — значит, платить не за что.
Обобщение: прежде чем добавлять механизм в артефакт, спросите, не решает ли задачу то, что уже есть на другой стороне границы. Данные, которые можно вычислить, дешевле данных, которые надо хранить, версионировать и грузить.
Дальше — про случай, когда скелет всё-таки нужен.
Арматура кодом
Кости создаются в режиме правки арматуры и только там:
import bpy, math
arm_data = bpy.data.armatures.new("RobotRig")
arm_obj = bpy.data.objects.new("RobotRig", arm_data)
bpy.context.collection.objects.link(arm_obj)
bpy.context.view_layer.objects.active = arm_obj
bpy.ops.object.mode_set(mode='EDIT')
root = arm_data.edit_bones.new("root")
root.head, root.tail = (0, 0, 0), (0, 0, 0.2)
body = arm_data.edit_bones.new("body")
body.head, body.tail = (0, 0, 0.2), (0, 0, 1.1)
body.parent, body.use_connect = root, True
head = arm_data.edit_bones.new("head")
head.head, head.tail = (0, 0, 1.1), (0, 0, 1.6)
head.parent, head.use_connect = body, True
bpy.ops.object.mode_set(mode='OBJECT')
head и tail — начало и конец кости, use_connect приклеивает начало дочерней
к концу родительской. Иерархия костей и есть скелет: поворот body тащит за
собой head, обратное неверно.
Имена костей — часть контракта, ровно как имена мешей и материалов из главы
про экспорт. Рантайм ищет кость по имени, когда надо прицепить к ней предмет или
камеру; переименование head в Head ломает игру, не меняя ни одной вершины.
Отсюда полезный приём: заводите кости-держатели — короткие кости без
геометрии в местах крепления (socket_hand_r, socket_back). Они ничего не
деформируют и стоят почти ноль, зато у рантайма появляется официальная точка
подвеса вместо магических координат в коде.
Скиннинг: почему автоматика здесь хуже правила
Каноническая привязка меша к скелету — автоматические веса:
bpy.ops.object.select_all(action='DESELECT')
mesh_obj.select_set(True)
bpy.context.view_layer.objects.active = arm_obj
bpy.ops.object.parent_set(type='ARMATURE_AUTO')
Blender считает влияние костей растеканием по поверхности: чем ближе вершина к кости, тем сильнее та на неё влияет, и влияния соседних костей плавно смешиваются. Для органики это то, что нужно: кожа у локтя обязана тянуться непрерывно.
Для робота из твёрдых деталей это ровно то, что не нужно. Кожух головы не должен «немного» слушаться кости тела — иначе на повороте он поедет и растянется, а пластик, который тянется, выглядит как ошибка, потому что ошибка и есть.
Здесь агент выигрывает у мыши тем же, чем во всём этом треке: правило выражается кодом.
GROUPS = {
"head": lambda v: v.co.z > 1.10,
"body": lambda v: 0.20 <= v.co.z <= 1.10,
"root": lambda v: v.co.z < 0.20,
}
mesh_obj.parent = arm_obj
mod = mesh_obj.modifiers.new("Armature", 'ARMATURE')
mod.object = arm_obj
for bone_name, belongs in GROUPS.items():
vg = mesh_obj.vertex_groups.new(name=bone_name) # имя = имя кости
vg.add([v.index for v in mesh_obj.data.vertices if belongs(v)], 1.0, 'REPLACE')
Вес 1,0 и никакого смешивания: каждая вершина принадлежит ровно одной кости. Такой скиннинг называют жёстким, и для техники он не компромисс, а правильный выбор — деталь движется как деталь.
Две ловушки, обе тихие.
Имя группы вершин обязано совпадать с именем кости. Не совпало — группа просто не участвует, ошибки нет, модель молча не двигается. Проверяется одной строкой:
assert {vg.name for vg in mesh_obj.vertex_groups} <= {b.name for b in arm_data.bones}
Вершина, не попавшая ни в одну группу, не двигается вообще. При разбиении по координате легко оставить щель на границе — и кусок меша останется висеть в воздухе, когда всё остальное поехало. Проверка та же по духу: сумма размеров групп должна равняться числу вершин.
Клипы
Поза выставляется на костях в режиме позы, ключи ставятся на свойствах кости:
import mathutils
bpy.context.view_layer.objects.active = arm_obj
bpy.ops.object.mode_set(mode='POSE')
pb = arm_obj.pose.bones["head"]
pb.rotation_mode = 'QUATERNION'
for frame, angle in ((1, 0), (12, 12), (24, 0)):
pb.rotation_quaternion = mathutils.Quaternion((0, 0, 1), math.radians(angle))
pb.keyframe_insert("rotation_quaternion", frame=frame)
action = arm_obj.animation_data.action
action.name = "idle"
action.use_fake_user = True # иначе действие без пользователя пропадёт при перезагрузке
bpy.ops.object.mode_set(mode='OBJECT')
rotation_mode = 'QUATERNION' — не вкусовщина. glTF хранит повороты
кватернионами, и углы Эйлера всё равно будут в них переведены; кроме того, у
Эйлера есть шарнирный замок, из-за которого поворот на середине интерполяции
уезжает не туда, где вы его оставили.
use_fake_user = True — из разряда «узнаёшь один раз». Действие, на которое
никто не ссылается, Blender считает мусором и не сохраняет. Клип, который вы
сделали и отложили, исчезает при следующей загрузке файла — без единого
сообщения.
Имя действия становится именем клипа в GLB, а имя клипа — это контракт с
рантаймом. Переименование idle — ломающее изменение, даже если ни одна вершина
не поменялась. Об этом же — конец главы
«Экспорт в GLB».
Ограничители не экспортируются
В главе «Материалы, свет и рендер» камера
наводилась ограничителем TRACK_TO — и это было правильное решение для съёмки в
Blender. Для экспорта оно не годится: glTF не знает ограничителей и обратной
кинематики. В файле лежат только ключи трансформаций.
Значит, всё, что двигается ограничителями, надо перед экспортом запечь в обычные ключи:
bpy.ops.nla.bake(frame_start=1, frame_end=24, only_selected=False,
visual_keying=True, clear_constraints=True, bake_types={'POSE'})
visual_keying=True записывает итоговое положение — то, которое получилось
после работы ограничителей, а не то, что записано в свойствах кости.
clear_constraints=True снимает ограничители после запекания: они уже сделали
своё дело, а оставленные — источник расхождения между тем, что видно в Blender,
и тем, что уехало в файл.
Симптом пропущенного запекания узнаваемый: в Blender анимация играет, в движке
модель стоит столбом или дёргается по прямой. Никакой ошибки при этом никто не
показывает — привет из главы
«Как агент смотрит на свою работу», где
success уже один раз ничего не значил.
Вес анимации и откуда он берётся
bpy.ops.export_scene.gltf(
filepath="/абсолютный/путь/robot.glb",
export_format='GLB',
export_apply=True,
export_animations=True,
export_animation_mode='ACTIONS', # каждое действие — отдельный клип
)
Проверяем тем же разбором файла, что в главе про экспорт:
меши: ['Robot']
материалы: ['Robot_Shell', 'Robot_Visor', 'Robot_Gold', …]
клипы: ['idle', 'wave']
кости: 9
треугольники: 8596
Теперь строки клипы и кости наконец не пустые — и это ровно та проверка,
которой не хватало генератору моделей по картинке из главы про кейс: он отдавал
геометрию, у которой обе строки нулевые навсегда.
Про вес стоит знать заранее. glTF не умеет кривых Безье: в формате есть только ступенчатая, линейная и кубическая интерполяция. Поэтому экспортёр пересчитывает анимацию покадрово — три поставленных вами ключа превращаются в двадцать четыре записанных. На одном коротком клипе это незаметно, на десяти длинных — это заметная часть файла.
Проверять надо тем же способом, что и всё остальное в этом треке: разобрать файл и посмотреть на размер данных анимации, а не рассуждать о нём. Рычаги — уменьшить частоту записи, укоротить клип, включить оптимизацию размера анимации в настройках экспорта — и каждый из них снова стоит своей цены: анимация становится грубее.
Что унести из главы
- Сначала спросите, нужен ли скелет. Движение объекта как целого дешевле считать кодом.
- Правило вместо автоматики там, где правило известно. Жёсткий скиннинг по геометрическому признаку надёжнее весов «на глаз» — и он проверяем.
- Имена костей и клипов — контракт, наравне с именами мешей и материалов.
- Всё, что держится на ограничителях, перед экспортом запекается. Формат их не знает и не скажет об этом.
- Проверяйте файл, а не сцену. Строки
клипыикостив разборе GLB — единственное доказательство, что анимация доехала.
Что дальше
Пока речь шла об одном ассете. Настоящая экономия от процедурного подхода начинается там, где ассетов много: во вводной главе трека сто ящиков с разными пропорциями заявлены как то, в чём агент силён, но мы этого ещё не показывали.
Дальше — «Массовые вариации»: почему просить агента сделать сто моделей — плохая идея, чем её заменить, и как получить воспроизводимый результат вместо ста разных случайностей.