Всё, из чего собираются спрайты, и ничего собранного. Источник истины —
assets/manifests и assets/*/catalog.yaml; PNG, .sfa и contact sheet остаются
за бортом, потому что воспроизводятся командой (docs/12-assets.md).
assets/manifests что и как рендерить: окружение, твари, агенты, предметы
assets/agents паперdoll-риг восьми направлений, каталог снаряжения
assets/monsters пять морфологий, каждая своим ригом (docs/14-monsters.md)
assets/sources исходные развёртки и импорты
assets/art_masters мастер-развёртки, по которым сверяется стиль
tools/ сборка ассетов, покрытие, генераторы, blender-скрипты
generated/ заголовки с константами SF_ASSET_*: без них игра не
соберётся, поэтому они в репозитории, хотя и производные
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1167 lines
63 KiB
Python
1167 lines
63 KiB
Python
"""Blender-сторона конвейера тварей. Запускается ВНУТРИ Blender.
|
||
|
||
blender --factory-startup -b --python tools/blender/monster_rig.py -- build --config cfg.json --out rig.blend
|
||
blender --factory-startup -b rig.blend --python tools/blender/monster_rig.py -- render --config cfg.json
|
||
|
||
Родственник tools/blender/agent_rig.py, но задача обратная. У агента слои
|
||
бумажной куклы обязаны совпасть пиксель в пиксель, поэтому там ОДИН скелет на
|
||
всё. Здесь пять видов обязаны различаться силуэтом с одного взгляда, поэтому
|
||
здесь ПЯТЬ независимых скелетов — и ровно одна общая вещь: неподвижная
|
||
ортографическая изометрическая камера и начало координат в точке опоры.
|
||
|
||
Правило выравнивания то же самое: камера СТОИТ, поворачивается модель.
|
||
Начало координат каждой твари (точка опоры на полу) всегда проецируется в
|
||
центр кадра, поэтому упаковщик на стороне Python режет все кадры одним и тем
|
||
же прямоугольником, и никакого подгона по bbox не существует в принципе.
|
||
|
||
Анатомия задана таблицами, а не кодом: кости, детали и четыре клипа на вид.
|
||
Клипы описаны в НОРМИРОВАННОМ времени (список опорных значений на 0..1) и
|
||
сэмплируются под то число кадров, которое пришло из каталога, — менять
|
||
frames в каталоге можно, не трогая этот файл.
|
||
|
||
PyYAML внутри Blender нет — конфиг приходит одним JSON-файлом.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
import bmesh
|
||
import bpy
|
||
from mathutils import Matrix, Quaternion, Vector
|
||
|
||
# Фигура смотрит в -Y (на камеру) — это направление `south`.
|
||
# Правая сторона твари — это -X, как и у агента.
|
||
X = (1.0, 0.0, 0.0) # наклон вперёд/назад
|
||
Y = (0.0, 1.0, 0.0) # завал вбок
|
||
Z = (0.0, 0.0, 1.0) # поворот вокруг вертикали
|
||
|
||
|
||
def d(degrees):
|
||
return math.radians(degrees)
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Материалы: мокрое мясо, кость, хитин и вживлённое железо
|
||
# ----------------------------------------------------------------------------
|
||
|
||
MATERIALS = {
|
||
# name: (base color RGB, roughness, metallic)
|
||
"flesh": ((0.36, 0.15, 0.14), 0.44, 0.00), # низкий roughness = мокрый блик
|
||
"muscle": ((0.47, 0.10, 0.10), 0.38, 0.00),
|
||
"gut": ((0.26, 0.07, 0.09), 0.32, 0.00),
|
||
"membrane": ((0.54, 0.27, 0.31), 0.26, 0.00),
|
||
"sinew": ((0.57, 0.44, 0.36), 0.62, 0.00),
|
||
"bone": ((0.73, 0.69, 0.58), 0.58, 0.00),
|
||
"chitin": ((0.13, 0.12, 0.14), 0.34, 0.12),
|
||
"steel": ((0.31, 0.33, 0.36), 0.28, 0.85),
|
||
"rust": ((0.29, 0.16, 0.09), 0.82, 0.25),
|
||
"tube": ((0.19, 0.17, 0.20), 0.44, 0.08),
|
||
"ocellus": ((0.86, 0.78, 0.28), 0.18, 0.00),
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Конструкторы форм — таблицы ниже читаются глазами, а не разбираются
|
||
# ----------------------------------------------------------------------------
|
||
|
||
def cap(p0, p1, r0, r1=None):
|
||
"""Капсула (усечённый конус) между двумя мировыми точками позы покоя."""
|
||
return ("capsule", (p0, p1, r0, r0 if r1 is None else r1))
|
||
|
||
|
||
def box(center, size, yaw=0.0):
|
||
return ("box", (center, size, yaw))
|
||
|
||
|
||
def ball(center, radius, scale=(1.0, 1.0, 1.0)):
|
||
return ("sphere", (center, radius, scale))
|
||
|
||
|
||
def _flip_point(p):
|
||
return (-p[0], p[1], p[2])
|
||
|
||
|
||
def _flip_shape(shape):
|
||
kind, params = shape
|
||
if kind == "capsule":
|
||
p0, p1, r0, r1 = params
|
||
return ("capsule", (_flip_point(p0), _flip_point(p1), r0, r1))
|
||
if kind == "box":
|
||
center, size, yaw = params
|
||
return ("box", (_flip_point(center), size, -yaw))
|
||
center, radius, scale = params
|
||
return ("sphere", (_flip_point(center), radius, scale))
|
||
|
||
|
||
def _flip_name(name):
|
||
if name.endswith("_r"):
|
||
return name[:-2] + "_l"
|
||
if name.endswith("_l"):
|
||
return name[:-2] + "_r"
|
||
return name
|
||
|
||
|
||
def mirror_bones(bones):
|
||
"""Правые кости -> левые. Симметрия описывается один раз, а не дважды."""
|
||
out = list(bones)
|
||
for name, head, tail, parent in bones:
|
||
if not name.endswith("_r"):
|
||
continue
|
||
out.append((_flip_name(name), _flip_point(head), _flip_point(tail),
|
||
None if parent is None else _flip_name(parent)))
|
||
return out
|
||
|
||
|
||
def mirror_parts(parts):
|
||
"""Зеркалит по имени ДЕТАЛИ, а не кости: накладка на непарной кости
|
||
(`hip_plate_r` на `hips`) тоже обязана получить пару."""
|
||
out = list(parts)
|
||
for name, bone, mat, shape in parts:
|
||
if not name.endswith("_r"):
|
||
continue
|
||
out.append((_flip_name(name), _flip_name(bone), mat, _flip_shape(shape)))
|
||
return out
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# SHAMBLER — «освежёванный»: грудная клетка вскрыта наружу, правая рука
|
||
# доросла до колена. Медленный, кривой, читается как человек, которому
|
||
# что-то сделали.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
SHAMBLER_BONES = mirror_bones([
|
||
("root", (0.00, 0.00, 0.00), (0.00, 0.00, 0.12), None),
|
||
("hips", (0.00, 0.00, 0.86), (0.00, 0.00, 0.99), "root"),
|
||
("spine", (0.00, 0.00, 0.99), (0.00, -0.03, 1.17), "hips"),
|
||
("chest", (0.00, -0.03, 1.17), (0.00, -0.09, 1.36), "spine"),
|
||
("neck", (0.00, -0.09, 1.36), (0.00, -0.14, 1.44), "chest"),
|
||
("head", (0.00, -0.14, 1.44), (0.00, -0.20, 1.56), "neck"),
|
||
("thigh_r", (-0.10, 0.00, 0.86), (-0.12, 0.02, 0.48), "hips"),
|
||
("shin_r", (-0.12, 0.02, 0.48), (-0.12, -0.01, 0.08), "thigh_r"),
|
||
("foot_r", (-0.12, -0.01, 0.08), (-0.12, -0.15, 0.02), "shin_r"),
|
||
])
|
||
# Руки НЕ зеркальны и потому объявлены вручную: правая доросла до колена,
|
||
# левая осталась человеческой. Асимметрия — половина силуэта этой твари.
|
||
SHAMBLER_BONES += [
|
||
("arm_r", (-0.19, -0.04, 1.30), (-0.24, 0.02, 0.96), "chest"),
|
||
("fore_r", (-0.24, 0.02, 0.96), (-0.27, -0.04, 0.54), "arm_r"),
|
||
("claw_r", (-0.27, -0.04, 0.54), (-0.28, -0.09, 0.38), "fore_r"),
|
||
("arm_l", (0.18, -0.04, 1.30), (0.23, 0.02, 1.02), "chest"),
|
||
("fore_l", (0.23, 0.02, 1.02), (0.26, -0.03, 0.74), "arm_l"),
|
||
("claw_l", (0.26, -0.03, 0.74), (0.27, -0.07, 0.63), "fore_l"),
|
||
]
|
||
|
||
SHAMBLER_PARTS = mirror_parts([
|
||
("rib_a_r", "chest", "bone", cap((-0.04, -0.03, 1.17), (-0.16, -0.21, 1.13), 0.020, 0.009)),
|
||
("rib_b_r", "chest", "bone", cap((-0.04, -0.05, 1.25), (-0.18, -0.23, 1.23), 0.021, 0.009)),
|
||
("rib_c_r", "chest", "bone", cap((-0.04, -0.07, 1.32), (-0.16, -0.20, 1.33), 0.019, 0.009)),
|
||
("thigh_r", "thigh_r", "flesh", cap((-0.10, 0.00, 0.88), (-0.12, 0.02, 0.48), 0.075, 0.055)),
|
||
("shin_r", "shin_r", "flesh", cap((-0.12, 0.02, 0.48), (-0.12, -0.01, 0.09), 0.052, 0.038)),
|
||
("foot_r", "foot_r", "sinew", box((-0.12, -0.08, 0.045), (0.085, 0.215, 0.085))),
|
||
])
|
||
SHAMBLER_PARTS += [
|
||
("pelvis", "hips", "flesh", cap((0.00, 0.00, 0.86), (0.00, 0.00, 1.00), 0.110, 0.100)),
|
||
("gut", "spine", "gut", ball((0.00, -0.06, 1.05), 0.115, (1.05, 0.92, 0.86))),
|
||
("thorax", "chest", "flesh", cap((0.00, -0.02, 1.11), (0.00, -0.09, 1.34), 0.112, 0.098)),
|
||
("cavity", "chest", "gut", ball((0.00, -0.11, 1.23), 0.072, (1.00, 0.75, 1.25))),
|
||
("shoulders", "chest", "flesh", cap((-0.19, -0.04, 1.31), (0.18, -0.04, 1.31), 0.058, 0.058)),
|
||
("staple_a", "spine", "steel", box((0.00, 0.06, 1.03), (0.075, 0.030, 0.014))),
|
||
("staple_b", "spine", "steel", box((0.00, 0.05, 1.11), (0.075, 0.030, 0.014))),
|
||
("staple_c", "chest", "steel", box((0.00, 0.03, 1.20), (0.070, 0.030, 0.014))),
|
||
("neck", "neck", "sinew", cap((0.00, -0.09, 1.35), (0.00, -0.14, 1.45), 0.046, 0.042)),
|
||
("skull", "head", "flesh", ball((0.00, -0.17, 1.49), 0.086, (0.86, 1.06, 1.04))),
|
||
("jaw", "head", "bone", box((0.00, -0.24, 1.425), (0.098, 0.115, 0.050))),
|
||
("crown", "head", "steel", box((0.02, -0.14, 1.552), (0.115, 0.125, 0.026))),
|
||
("bolt_r", "head", "rust", ball((-0.06, -0.20, 1.535), 0.017)),
|
||
("bolt_l", "head", "rust", ball((0.07, -0.20, 1.535), 0.017)),
|
||
# Правая рука — освежёванная и длинная: мышца без кожи, кисть-крюк.
|
||
("arm_r", "arm_r", "muscle", cap((-0.19, -0.04, 1.30), (-0.24, 0.02, 0.96), 0.052, 0.042)),
|
||
("fore_r", "fore_r", "muscle", cap((-0.24, 0.02, 0.96), (-0.27, -0.04, 0.55), 0.043, 0.028)),
|
||
("claw_r_a", "claw_r", "bone", cap((-0.27, -0.05, 0.54), (-0.30, -0.13, 0.37), 0.014, 0.000)),
|
||
("claw_r_b", "claw_r", "bone", cap((-0.27, -0.04, 0.54), (-0.25, -0.12, 0.36), 0.013, 0.000)),
|
||
("claw_r_c", "claw_r", "bone", cap((-0.27, -0.03, 0.54), (-0.28, -0.07, 0.34), 0.012, 0.000)),
|
||
("arm_l", "arm_l", "flesh", cap((0.18, -0.04, 1.30), (0.23, 0.02, 1.02), 0.050, 0.040)),
|
||
("fore_l", "fore_l", "flesh", cap((0.23, 0.02, 1.02), (0.26, -0.03, 0.75), 0.040, 0.030)),
|
||
("hand_l", "claw_l", "sinew", cap((0.26, -0.03, 0.75), (0.27, -0.07, 0.63), 0.032, 0.022)),
|
||
]
|
||
|
||
SHAMBLER_ANIM = {
|
||
"walk": {
|
||
# Хромота: правая нога шагает шире, левая волочится и почти не гнётся.
|
||
"cycle": [("thigh_r", X, d(24), 0.00, d(-4)), ("thigh_l", X, d(14), 0.50, d(3)),
|
||
("shin_r", X, -d(22), 0.22, d(-12)), ("shin_l", X, -d(8), 0.72, d(-5)),
|
||
("arm_r", X, d(9), 0.50, d(7)), ("arm_l", X, d(6), 0.00, d(4)),
|
||
("fore_r", X, d(6), 0.50, d(5)), ("spine", Y, d(6), 0.10, 0.0),
|
||
("chest", Z, d(7), 0.00, 0.0), ("head", Y, d(8), 0.30, d(7))],
|
||
"bob": (0.032, 0.0),
|
||
},
|
||
"attack": {
|
||
"keys": [("arm_r", X, [d(8), d(-92), d(-112), d(44), d(62), d(26)]),
|
||
("fore_r", X, [d(6), d(-38), d(-52), d(24), d(36), d(14)]),
|
||
("claw_r", X, [0, d(-18), d(-24), d(30), d(22), d(8)]),
|
||
("chest", Z, [0, d(26), d(32), d(-24), d(-16), d(-4)]),
|
||
("hips", Z, [0, d(11), d(14), d(-10), d(-6), 0]),
|
||
("head", X, [d(5), d(-9), d(-11), d(15), d(10), d(6)]),
|
||
("thigh_l", X, [0, d(-8), d(-10), d(12), d(8), 0])],
|
||
"root_loc": [(0, 0.03, 0), (0, 0.06, -0.02), (0, 0.07, -0.02),
|
||
(0, -0.07, -0.04), (0, -0.04, -0.02), (0, 0, 0)],
|
||
},
|
||
"hit": {
|
||
"keys": [("chest", X, [0, d(-24), d(-14), d(-4)]),
|
||
("head", X, [0, d(-28), d(-16), d(-5)]),
|
||
("arm_r", X, [0, d(26), d(15), d(4)]),
|
||
("arm_l", X, [0, d(22), d(13), d(3)]),
|
||
("thigh_r", X, [0, d(-12), d(-7), 0])],
|
||
"root_loc": [(0, 0, 0), (0, 0.06, -0.02), (0, 0.03, -0.01), (0, 0.01, 0)],
|
||
},
|
||
"death": {
|
||
# Не падение во весь рост, а осадка в кучу: труп обязан помещаться
|
||
# в клетку при ЛЮБОМ повороте, а лежащий во весь рост — не помещается.
|
||
"keys": [("hips", X, [0, d(6), d(18), d(30), d(38), d(42), d(44), d(44)]),
|
||
("spine", X, [0, d(8), d(24), d(40), d(52), d(58), d(60), d(60)]),
|
||
("chest", X, [0, d(6), d(20), d(34), d(44), d(50), d(52), d(52)]),
|
||
("head", X, [0, d(-14), d(10), d(28), d(36), d(40), d(42), d(42)]),
|
||
("thigh_r", X, [0, d(20), d(52), d(78), d(92), d(96), d(98), d(98)]),
|
||
("thigh_l", X, [0, d(16), d(46), d(72), d(88), d(94), d(96), d(96)]),
|
||
("shin_r", X, [0, d(-24), d(-62), d(-96), d(-114), d(-120), d(-122), d(-122)]),
|
||
("shin_l", X, [0, d(-20), d(-58), d(-92), d(-110), d(-118), d(-120), d(-120)]),
|
||
("arm_r", X, [0, d(-20), d(10), d(46), d(64), d(70), d(72), d(72)]),
|
||
("arm_l", X, [0, d(-16), d(8), d(40), d(58), d(64), d(66), d(66)])],
|
||
"root_rot": [("x", [0, d(-6), d(8), d(24), d(34), d(38), d(40), d(40)]),
|
||
("y", [0, d(2), d(6), d(11), d(14), d(15), d(16), d(16)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.01, -0.10), (0, 0.02, -0.26), (0, 0.02, -0.40),
|
||
(0, 0.02, -0.48), (0, 0.02, -0.52), (0, 0.02, -0.54), (0, 0.02, -0.54)],
|
||
},
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# RUSHER — четвероногий спринтер: горизонтальный хребет, пальцеходящие задние
|
||
# ноги, косы вместо предплечий, вместо головы — раскрытый цветок челюстей.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
RUSHER_BONES = mirror_bones([
|
||
("root", (0.00, 0.00, 0.00), (0.00, 0.00, 0.12), None),
|
||
("hips", (0.00, 0.30, 0.60), (0.00, 0.14, 0.63), "root"),
|
||
("spine", (0.00, 0.14, 0.63), (0.00, -0.02, 0.65), "hips"),
|
||
("chest", (0.00, -0.02, 0.65), (0.00, -0.20, 0.64), "spine"),
|
||
("neck", (0.00, -0.20, 0.64), (0.00, -0.33, 0.60), "chest"),
|
||
("head", (0.00, -0.33, 0.60), (0.00, -0.46, 0.55), "neck"),
|
||
("fore_r", (-0.13, -0.12, 0.60), (-0.16, -0.04, 0.34), "chest"),
|
||
("scythe_r", (-0.16, -0.04, 0.34), (-0.17, -0.16, 0.05), "fore_r"),
|
||
("thigh_r", (-0.12, 0.26, 0.58), (-0.15, 0.38, 0.30), "hips"),
|
||
("shin_r", (-0.15, 0.38, 0.30), (-0.15, 0.20, 0.14), "thigh_r"),
|
||
("toe_r", (-0.15, 0.20, 0.14), (-0.15, 0.06, 0.02), "shin_r"),
|
||
("tail_a", (0.00, 0.34, 0.60), (0.00, 0.56, 0.66), "hips"),
|
||
("tail_b", (0.00, 0.56, 0.66), (0.00, 0.76, 0.58), "tail_a"),
|
||
])
|
||
|
||
RUSHER_PARTS = mirror_parts([
|
||
("fore_r", "fore_r", "muscle", cap((-0.13, -0.12, 0.61), (-0.16, -0.04, 0.35), 0.042, 0.034)),
|
||
("scythe_r", "scythe_r", "bone", cap((-0.16, -0.05, 0.35), (-0.17, -0.17, 0.05), 0.030, 0.006)),
|
||
("spur_r", "scythe_r", "bone", cap((-0.16, -0.06, 0.30), (-0.19, -0.20, 0.24), 0.014, 0.000)),
|
||
("thigh_r", "thigh_r", "muscle", cap((-0.12, 0.25, 0.59), (-0.15, 0.38, 0.30), 0.070, 0.046)),
|
||
("shin_r", "shin_r", "sinew", cap((-0.15, 0.38, 0.30), (-0.15, 0.20, 0.14), 0.034, 0.026)),
|
||
("toe_r", "toe_r", "bone", cap((-0.15, 0.20, 0.14), (-0.15, 0.05, 0.02), 0.026, 0.012)),
|
||
("hip_plate_r", "hips", "chitin", box((-0.10, 0.28, 0.66), (0.090, 0.150, 0.070))),
|
||
("jaw_up_r", "head", "bone", cap((-0.03, -0.34, 0.60), (-0.17, -0.52, 0.62), 0.028, 0.005)),
|
||
("jaw_lo_r", "head", "bone", cap((-0.03, -0.34, 0.56), (-0.14, -0.50, 0.47), 0.026, 0.005)),
|
||
])
|
||
RUSHER_PARTS += [
|
||
("hips", "hips", "flesh", cap((0.00, 0.32, 0.60), (0.00, 0.14, 0.63), 0.095, 0.100)),
|
||
("belly", "spine", "gut", cap((0.00, 0.16, 0.58), (0.00, -0.06, 0.58), 0.082, 0.074)),
|
||
("spine", "spine", "flesh", cap((0.00, 0.14, 0.63), (0.00, -0.02, 0.66), 0.098, 0.094)),
|
||
("thorax", "chest", "flesh", cap((0.00, -0.02, 0.66), (0.00, -0.20, 0.64), 0.094, 0.072)),
|
||
("crest_a", "spine", "bone", box((0.00, 0.10, 0.73), (0.026, 0.090, 0.090))),
|
||
("crest_b", "spine", "bone", box((0.00, -0.02, 0.75), (0.026, 0.090, 0.110))),
|
||
("crest_c", "chest", "bone", box((0.00, -0.14, 0.73), (0.024, 0.080, 0.090))),
|
||
("port", "chest", "steel", box((0.00, -0.10, 0.58), (0.100, 0.090, 0.040))),
|
||
("neck", "neck", "sinew", cap((0.00, -0.20, 0.64), (0.00, -0.33, 0.60), 0.062, 0.050)),
|
||
("stump", "head", "muscle", ball((0.00, -0.36, 0.59), 0.058, (1.00, 0.90, 0.95))),
|
||
("jaw_up", "head", "bone", cap((0.00, -0.36, 0.63), (0.00, -0.55, 0.68), 0.028, 0.005)),
|
||
("jaw_lo", "head", "bone", cap((0.00, -0.36, 0.55), (0.00, -0.53, 0.44), 0.026, 0.005)),
|
||
("gullet", "head", "gut", ball((0.00, -0.39, 0.59), 0.034, (1.00, 1.20, 1.00))),
|
||
("tail_a", "tail_a", "flesh", cap((0.00, 0.34, 0.60), (0.00, 0.56, 0.66), 0.060, 0.038)),
|
||
("tail_b", "tail_b", "sinew", cap((0.00, 0.56, 0.66), (0.00, 0.76, 0.58), 0.036, 0.010)),
|
||
("barb", "tail_b", "bone", cap((0.00, 0.72, 0.60), (0.00, 0.84, 0.52), 0.018, 0.000)),
|
||
]
|
||
|
||
RUSHER_ANIM = {
|
||
"walk": {
|
||
# Галоп в два такта: передние вместе, задние вместе, хребет качает.
|
||
"cycle": [("fore_r", X, d(34), 0.00, d(-6)), ("fore_l", X, d(34), 0.12, d(-6)),
|
||
("scythe_r", X, d(26), 0.30, d(14)), ("scythe_l", X, d(26), 0.42, d(14)),
|
||
("thigh_r", X, d(32), 0.50, 0.0), ("thigh_l", X, d(32), 0.62, 0.0),
|
||
("shin_r", X, d(26), 0.76, 0.0), ("shin_l", X, d(26), 0.88, 0.0),
|
||
("spine", X, d(9), 0.25, 0.0), ("chest", X, d(7), 0.35, 0.0),
|
||
("neck", X, d(8), 0.55, 0.0),
|
||
("tail_a", X, d(12), 0.20, d(-6)), ("tail_b", Z, d(16), 0.45, 0.0)],
|
||
"bob": (0.055, 0.25),
|
||
},
|
||
"attack": {
|
||
# Рывок: тело выстреливает вперёд, косы сходятся, челюсти распахнуты.
|
||
"keys": [("fore_r", X, [d(-10), d(-56), d(-64), d(46), d(30), d(-4)]),
|
||
("fore_l", X, [d(-10), d(-56), d(-64), d(46), d(30), d(-4)]),
|
||
("scythe_r", X, [d(10), d(48), d(56), d(-38), d(-18), d(6)]),
|
||
("scythe_l", X, [d(10), d(48), d(56), d(-38), d(-18), d(6)]),
|
||
("neck", X, [0, d(22), d(26), d(-30), d(-14), 0]),
|
||
("head", X, [0, d(16), d(20), d(-24), d(-10), 0]),
|
||
("spine", X, [0, d(12), d(14), d(-16), d(-8), 0]),
|
||
("tail_a", X, [d(-4), d(-22), d(-26), d(18), d(10), 0])],
|
||
"root_loc": [(0, 0.04, 0), (0, 0.10, 0.04), (0, 0.12, 0.05),
|
||
(0, -0.16, 0.02), (0, -0.08, 0), (0, 0, 0)],
|
||
},
|
||
"hit": {
|
||
"keys": [("spine", X, [0, d(-20), d(-11), d(-3)]),
|
||
("neck", X, [0, d(-26), d(-15), d(-4)]),
|
||
("fore_r", X, [0, d(30), d(17), d(5)]),
|
||
("fore_l", X, [0, d(26), d(15), d(4)]),
|
||
("tail_a", X, [0, d(24), d(13), d(3)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.08, -0.03), (0, 0.04, -0.01), (0, 0.01, 0)],
|
||
},
|
||
"death": {
|
||
# Ноги подламываются, туша садится на брюхо и заваливается набок.
|
||
"keys": [("thigh_r", X, [0, d(-18), d(-46), d(-70), d(-84), d(-90), d(-92), d(-92)]),
|
||
("thigh_l", X, [0, d(-16), d(-42), d(-66), d(-80), d(-86), d(-88), d(-88)]),
|
||
("fore_r", X, [0, d(24), d(58), d(84), d(96), d(100), d(102), d(102)]),
|
||
("fore_l", X, [0, d(20), d(52), d(78), d(92), d(98), d(100), d(100)]),
|
||
("scythe_r", X, [0, d(-18), d(-44), d(-64), d(-74), d(-78), d(-80), d(-80)]),
|
||
("scythe_l", X, [0, d(-16), d(-40), d(-60), d(-70), d(-74), d(-76), d(-76)]),
|
||
("neck", X, [0, d(-12), d(6), d(24), d(34), d(38), d(40), d(40)]),
|
||
("head", Z, [0, d(10), d(24), d(36), d(42), d(44), d(46), d(46)]),
|
||
("tail_a", Z, [0, d(-14), d(-30), d(-42), d(-48), d(-50), d(-52), d(-52)]),
|
||
("tail_b", Z, [0, d(-16), d(-34), d(-48), d(-56), d(-60), d(-62), d(-62)])],
|
||
"root_rot": [("y", [0, d(6), d(18), d(32), d(42), d(48), d(50), d(50)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.01, -0.08), (0, 0.02, -0.20), (0, 0.02, -0.30),
|
||
(0, 0.02, -0.36), (0, 0.02, -0.39), (0, 0.02, -0.40), (0, 0.02, -0.40)],
|
||
},
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# SPITTER — сосуд под давлением: волочащийся железистый мешок, вживлённый в
|
||
# горло клапан и сопло вместо лица, руки атрофированы.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
SPITTER_BONES = mirror_bones([
|
||
("root", (0.00, 0.00, 0.00), (0.00, 0.00, 0.12), None),
|
||
("hips", (0.00, 0.00, 0.52), (0.00, 0.00, 0.66), "root"),
|
||
("sac", (0.00, 0.06, 0.58), (0.00, 0.34, 0.44), "hips"),
|
||
("spine", (0.00, 0.00, 0.66), (0.00, -0.03, 0.84), "hips"),
|
||
("chest", (0.00, -0.03, 0.84), (0.00, -0.05, 1.00), "spine"),
|
||
("valve", (0.00, -0.05, 1.00), (0.00, -0.10, 1.09), "chest"),
|
||
("head", (0.00, -0.10, 1.09), (0.00, -0.24, 1.14), "valve"),
|
||
("arm_r", (-0.15, -0.04, 0.94), (-0.21, 0.00, 0.76), "chest"),
|
||
("fore_r", (-0.21, 0.00, 0.76), (-0.24, -0.06, 0.62), "arm_r"),
|
||
("thigh_r", (-0.13, 0.00, 0.52), (-0.20, 0.04, 0.28), "hips"),
|
||
("shin_r", (-0.20, 0.04, 0.28), (-0.22, -0.02, 0.06), "thigh_r"),
|
||
])
|
||
|
||
SPITTER_PARTS = mirror_parts([
|
||
("arm_r", "arm_r", "flesh", cap((-0.15, -0.04, 0.94), (-0.21, 0.00, 0.77), 0.032, 0.024)),
|
||
("fore_r", "fore_r", "sinew", cap((-0.21, 0.00, 0.77), (-0.24, -0.06, 0.62), 0.023, 0.014)),
|
||
("thigh_r", "thigh_r", "flesh", cap((-0.13, 0.00, 0.53), (-0.20, 0.04, 0.28), 0.070, 0.050)),
|
||
("shin_r", "shin_r", "flesh", cap((-0.20, 0.04, 0.28), (-0.22, -0.02, 0.07), 0.048, 0.036)),
|
||
("foot_r", "shin_r", "sinew", box((-0.22, -0.07, 0.035), (0.110, 0.230, 0.070))),
|
||
("gland_r", "sac", "tube", cap((-0.06, 0.16, 0.62), (-0.05, -0.02, 0.96), 0.016, 0.013)),
|
||
])
|
||
SPITTER_PARTS += [
|
||
("pelvis", "hips", "flesh", cap((0.00, 0.00, 0.52), (0.00, 0.00, 0.68), 0.125, 0.110)),
|
||
# Мешок — главная читаемая деталь: он больше грудной клетки и волочится.
|
||
("sac", "sac", "membrane", ball((0.00, 0.24, 0.44), 0.215, (0.95, 1.25, 0.90))),
|
||
("sac_seam", "sac", "sinew", cap((0.00, 0.06, 0.56), (0.00, 0.42, 0.44), 0.030, 0.018)),
|
||
("sac_ring", "sac", "rust", box((0.00, 0.10, 0.52), (0.230, 0.040, 0.190))),
|
||
("gland_c", "sac", "tube", cap((0.00, 0.18, 0.66), (0.00, -0.02, 0.98), 0.018, 0.014)),
|
||
("spine", "spine", "flesh", cap((0.00, 0.00, 0.66), (0.00, -0.03, 0.85), 0.100, 0.096)),
|
||
("thorax", "chest", "flesh", cap((0.00, -0.03, 0.85), (0.00, -0.05, 1.00), 0.098, 0.086)),
|
||
("clamp", "chest", "steel", box((0.00, -0.04, 0.92), (0.190, 0.180, 0.030))),
|
||
# Клапан в горле: фланец, корпус, два болта. Это имплант, а не кость.
|
||
("valve", "valve", "steel", cap((0.00, -0.05, 1.00), (0.00, -0.10, 1.10), 0.050, 0.044)),
|
||
("flange", "valve", "steel", box((0.00, -0.07, 1.05), (0.140, 0.130, 0.022))),
|
||
("valve_bolt_r", "valve", "rust", ball((-0.055, -0.07, 1.05), 0.016)),
|
||
("valve_bolt_l", "valve", "rust", ball((0.055, -0.07, 1.05), 0.016)),
|
||
# Сопло вместо лица: раструб, а не голова.
|
||
("skull", "head", "flesh", ball((0.00, -0.14, 1.11), 0.070, (0.90, 1.00, 0.88))),
|
||
("nozzle", "head", "steel", cap((0.00, -0.16, 1.12), (0.00, -0.30, 1.13), 0.038, 0.062)),
|
||
("nozzle_lip", "head", "rust", cap((0.00, -0.29, 1.13), (0.00, -0.32, 1.13), 0.066, 0.060)),
|
||
("ocellus_r", "head", "ocellus", ball((-0.045, -0.19, 1.17), 0.014)),
|
||
("ocellus_l", "head", "ocellus", ball((0.045, -0.19, 1.17), 0.014)),
|
||
("ocellus_c", "head", "ocellus", ball((0.000, -0.20, 1.19), 0.012)),
|
||
]
|
||
|
||
SPITTER_ANIM = {
|
||
"walk": {
|
||
# Переваливается: мешок раскачивается противофазой к корпусу.
|
||
"cycle": [("thigh_r", X, d(19), 0.00, d(-3)), ("thigh_l", X, d(19), 0.50, d(-3)),
|
||
("shin_r", X, -d(14), 0.20, d(-8)), ("shin_l", X, -d(14), 0.70, d(-8)),
|
||
("hips", Y, d(9), 0.00, 0.0), ("sac", X, d(11), 0.50, d(-4)),
|
||
("sac", Y, d(8), 0.25, 0.0), ("spine", Y, -d(7), 0.00, 0.0),
|
||
("arm_r", X, d(10), 0.50, d(8)), ("arm_l", X, d(10), 0.00, d(8)),
|
||
("head", X, d(5), 0.15, 0.0)],
|
||
"bob": (0.028, 0.0),
|
||
},
|
||
"attack": {
|
||
# Присесть, накачать мешок, выпрямить шею — плевок на 3-4 кадре.
|
||
"keys": [("spine", X, [0, d(-18), d(-24), d(20), d(10), d(2)]),
|
||
("chest", X, [0, d(-12), d(-16), d(16), d(8), d(2)]),
|
||
("valve", X, [0, d(-14), d(-20), d(26), d(12), d(2)]),
|
||
("head", X, [0, d(-16), d(-22), d(30), d(14), d(3)]),
|
||
("sac", X, [0, d(-14), d(-20), d(16), d(8), 0]),
|
||
("arm_r", X, [d(6), d(-24), d(-30), d(18), d(10), d(6)]),
|
||
("arm_l", X, [d(6), d(-24), d(-30), d(18), d(10), d(6)]),
|
||
("thigh_r", X, [0, d(16), d(22), d(-8), d(-4), 0]),
|
||
("thigh_l", X, [0, d(16), d(22), d(-8), d(-4), 0])],
|
||
"root_loc": [(0, 0, 0), (0, 0.03, -0.06), (0, 0.04, -0.08),
|
||
(0, -0.04, 0.02), (0, -0.02, 0.01), (0, 0, 0)],
|
||
},
|
||
"hit": {
|
||
"keys": [("spine", X, [0, d(-22), d(-13), d(-4)]),
|
||
("head", X, [0, d(-26), d(-15), d(-4)]),
|
||
("sac", X, [0, d(20), d(11), d(3)]),
|
||
("arm_r", X, [0, d(24), d(14), d(4)]),
|
||
("arm_l", X, [0, d(24), d(14), d(4)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.05, -0.02), (0, 0.03, -0.01), (0, 0.01, 0)],
|
||
},
|
||
"death": {
|
||
# Мешок сдувается первым, потом ноги разъезжаются и туша оседает.
|
||
"keys": [("sac", X, [0, d(10), d(26), d(38), d(44), d(46), d(48), d(48)]),
|
||
("spine", X, [0, d(10), d(28), d(46), d(58), d(64), d(66), d(66)]),
|
||
("chest", X, [0, d(8), d(22), d(36), d(46), d(50), d(52), d(52)]),
|
||
("head", X, [0, d(-10), d(14), d(34), d(44), d(48), d(50), d(50)]),
|
||
("thigh_r", Y, [0, d(-12), d(-28), d(-40), d(-46), d(-48), d(-50), d(-50)]),
|
||
("thigh_l", Y, [0, d(12), d(28), d(40), d(46), d(48), d(50), d(50)]),
|
||
("thigh_r", X, [0, d(18), d(44), d(64), d(74), d(78), d(80), d(80)]),
|
||
("thigh_l", X, [0, d(18), d(44), d(64), d(74), d(78), d(80), d(80)]),
|
||
("shin_r", X, [0, d(-20), d(-48), d(-72), d(-84), d(-88), d(-90), d(-90)]),
|
||
("shin_l", X, [0, d(-20), d(-48), d(-72), d(-84), d(-88), d(-90), d(-90)]),
|
||
("arm_r", X, [0, d(-14), d(10), d(38), d(52), d(58), d(60), d(60)]),
|
||
("arm_l", X, [0, d(-14), d(10), d(38), d(52), d(58), d(60), d(60)])],
|
||
"root_rot": [("x", [0, d(-4), d(8), d(20), d(28), d(32), d(34), d(34)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.01, -0.06), (0, 0.02, -0.16), (0, 0.02, -0.24),
|
||
(0, 0.02, -0.29), (0, 0.02, -0.32), (0, 0.02, -0.33), (0, 0.02, -0.33)],
|
||
},
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# BRUTE — «сросшийся»: к плечу приварено второе, наполовину поглощённое тело;
|
||
# грудь забрана болтами; правая рука — молот из сросшейся кости.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
BRUTE_BONES = mirror_bones([
|
||
("root", (0.00, 0.00, 0.00), (0.00, 0.00, 0.14), None),
|
||
("hips", (0.00, 0.00, 0.98), (0.00, 0.00, 1.16), "root"),
|
||
("spine", (0.00, 0.00, 1.16), (0.00, -0.02, 1.38), "hips"),
|
||
("chest", (0.00, -0.02, 1.38), (0.00, -0.04, 1.60), "spine"),
|
||
("neck", (0.00, -0.04, 1.60), (0.00, -0.08, 1.66), "chest"),
|
||
("head", (0.00, -0.08, 1.66), (0.00, -0.12, 1.78), "neck"),
|
||
("thigh_r", (-0.17, 0.00, 0.98), (-0.19, 0.02, 0.54), "hips"),
|
||
("shin_r", (-0.19, 0.02, 0.54), (-0.19, -0.01, 0.10), "thigh_r"),
|
||
("foot_r", (-0.19, -0.01, 0.10), (-0.19, -0.16, 0.03), "shin_r"),
|
||
])
|
||
# Руки и привой не зеркальны: справа молот из сросшейся кости, слева — чужое
|
||
# тело, вросшее в плечо. Объявлены руками, иначе зеркало создало бы дубли.
|
||
BRUTE_BONES += [
|
||
("arm_r", (-0.32, -0.02, 1.54), (-0.44, 0.02, 1.18), "chest"),
|
||
("fore_r", (-0.44, 0.02, 1.18), (-0.50, -0.04, 0.82), "arm_r"),
|
||
("maul_r", (-0.50, -0.04, 0.82), (-0.52, -0.06, 0.64), "fore_r"),
|
||
("arm_l", (0.28, -0.02, 1.52), (0.38, 0.02, 1.20), "chest"),
|
||
("fore_l", (0.38, 0.02, 1.20), (0.42, -0.04, 0.90), "arm_l"),
|
||
("graft", (0.20, 0.06, 1.60), (0.32, 0.10, 1.78), "chest"),
|
||
("graft_head", (0.32, 0.10, 1.78), (0.40, 0.12, 1.90), "graft"),
|
||
("graft_arm", (0.32, 0.08, 1.72), (0.50, 0.04, 1.50), "graft"),
|
||
]
|
||
|
||
BRUTE_PARTS = mirror_parts([
|
||
("thigh_r", "thigh_r", "flesh", cap((-0.17, 0.00, 1.00), (-0.19, 0.02, 0.54), 0.115, 0.088)),
|
||
("shin_r", "shin_r", "flesh", cap((-0.19, 0.02, 0.54), (-0.19, -0.01, 0.11), 0.085, 0.062)),
|
||
("foot_r", "foot_r", "sinew", box((-0.19, -0.09, 0.055), (0.135, 0.290, 0.110))),
|
||
("knee_r", "shin_r", "bone", ball((-0.19, -0.05, 0.54), 0.062, (0.90, 0.80, 0.90))),
|
||
])
|
||
BRUTE_PARTS += [
|
||
("pelvis", "hips", "flesh", cap((0.00, 0.00, 0.98), (0.00, 0.00, 1.18), 0.180, 0.170)),
|
||
("gut", "spine", "gut", ball((0.00, -0.08, 1.24), 0.165, (1.10, 0.92, 0.86))),
|
||
("torso", "chest", "flesh", cap((0.00, -0.01, 1.28), (0.00, -0.04, 1.58), 0.215, 0.235)),
|
||
("shoulders", "chest", "muscle", cap((-0.32, -0.02, 1.54), (0.30, -0.02, 1.54), 0.115, 0.115)),
|
||
# Броня не надета, а вбита: пластины утоплены в мясо и держатся болтами.
|
||
("plate_a", "chest", "steel", box((0.00, -0.17, 1.50), (0.360, 0.130, 0.150))),
|
||
("plate_b", "chest", "steel", box((0.00, -0.16, 1.32), (0.330, 0.130, 0.140))),
|
||
("plate_back", "chest", "rust", box((0.00, 0.16, 1.42), (0.340, 0.110, 0.320))),
|
||
("bolt_a", "chest", "rust", ball((-0.15, -0.23, 1.50), 0.026)),
|
||
("bolt_b", "chest", "rust", ball((0.15, -0.23, 1.50), 0.026)),
|
||
("bolt_c", "chest", "rust", ball((-0.13, -0.22, 1.32), 0.024)),
|
||
("bolt_d", "chest", "rust", ball((0.13, -0.22, 1.32), 0.024)),
|
||
("collar", "neck", "steel", cap((-0.09, -0.05, 1.62), (0.09, -0.05, 1.62), 0.055, 0.055)),
|
||
("skull", "head", "flesh", ball((0.00, -0.10, 1.71), 0.078, (0.92, 1.00, 0.92))),
|
||
("maw", "head", "bone", box((0.00, -0.16, 1.68), (0.100, 0.090, 0.050))),
|
||
# Рука-молот: предплечье срослось в костяную колотушку с шипами.
|
||
("arm_r", "arm_r", "muscle", cap((-0.32, -0.02, 1.54), (-0.44, 0.02, 1.18), 0.115, 0.098)),
|
||
("fore_r", "fore_r", "muscle", cap((-0.44, 0.02, 1.18), (-0.50, -0.04, 0.86), 0.100, 0.086)),
|
||
("maul", "maul_r", "bone", ball((-0.51, -0.05, 0.74), 0.125, (0.95, 1.05, 1.00))),
|
||
("maul_spike_a", "maul_r", "bone", cap((-0.51, -0.05, 0.74), (-0.68, -0.10, 0.66), 0.030, 0.000)),
|
||
("maul_spike_b", "maul_r", "bone", cap((-0.51, -0.05, 0.74), (-0.50, -0.06, 0.56), 0.028, 0.000)),
|
||
("maul_spike_c", "maul_r", "bone", cap((-0.51, -0.05, 0.74), (-0.56, -0.22, 0.72), 0.026, 0.000)),
|
||
("arm_l", "arm_l", "flesh", cap((0.28, -0.02, 1.52), (0.38, 0.02, 1.20), 0.098, 0.082)),
|
||
("fore_l", "fore_l", "flesh", cap((0.38, 0.02, 1.20), (0.42, -0.04, 0.92), 0.082, 0.064)),
|
||
("fist_l", "fore_l", "sinew", ball((0.42, -0.05, 0.89), 0.070)),
|
||
# Привой: чужой торс, безвольная голова и рука, которая просто висит.
|
||
("graft", "graft", "flesh", cap((0.20, 0.06, 1.60), (0.33, 0.10, 1.78), 0.105, 0.090)),
|
||
("graft_seam", "graft", "steel", box((0.20, 0.06, 1.60), (0.130, 0.130, 0.030))),
|
||
("graft_head", "graft_head", "flesh", ball((0.38, 0.12, 1.87), 0.070, (0.92, 1.00, 0.96))),
|
||
("graft_jaw", "graft_head", "bone", box((0.40, 0.09, 1.83), (0.075, 0.070, 0.040))),
|
||
("graft_arm", "graft_arm", "sinew", cap((0.33, 0.08, 1.73), (0.50, 0.04, 1.50), 0.052, 0.034)),
|
||
("graft_hand", "graft_arm", "sinew", ball((0.51, 0.03, 1.47), 0.042)),
|
||
]
|
||
|
||
BRUTE_ANIM = {
|
||
"walk": {
|
||
# Тяжёлый шаг вразвалку: плечи катаются, привой болтается отдельно.
|
||
"cycle": [("thigh_r", X, d(20), 0.00, d(-3)), ("thigh_l", X, d(20), 0.50, d(-3)),
|
||
("shin_r", X, -d(16), 0.22, d(-9)), ("shin_l", X, -d(16), 0.72, d(-9)),
|
||
("hips", Y, d(8), 0.00, 0.0), ("chest", Y, -d(7), 0.05, 0.0),
|
||
("chest", Z, d(9), 0.25, 0.0),
|
||
("arm_r", X, d(13), 0.50, d(6)), ("arm_l", X, d(13), 0.00, d(6)),
|
||
("fore_r", X, d(7), 0.60, d(4)),
|
||
("graft", Y, d(11), 0.35, 0.0), ("graft_head", X, d(13), 0.45, d(6)),
|
||
("graft_arm", X, d(17), 0.15, 0.0),
|
||
("head", X, d(4), 0.30, d(3))],
|
||
"bob": (0.045, 0.0),
|
||
},
|
||
"attack": {
|
||
# Замах ВБОК, а не над головой: молот над головой вылезал бы из клетки.
|
||
"keys": [("arm_r", Z, [0, d(-52), d(-64), d(38), d(24), d(4)]),
|
||
("arm_r", X, [d(4), d(-34), d(-42), d(26), d(16), d(4)]),
|
||
("fore_r", X, [d(4), d(-24), d(-30), d(34), d(20), d(6)]),
|
||
("maul_r", X, [0, d(-16), d(-20), d(26), d(14), d(4)]),
|
||
("chest", Z, [0, d(30), d(36), d(-30), d(-18), d(-4)]),
|
||
("hips", Z, [0, d(14), d(17), d(-14), d(-8), 0]),
|
||
("graft", Z, [0, d(-16), d(-20), d(18), d(10), 0]),
|
||
("graft_arm", X, [0, d(-20), d(-26), d(30), d(16), 0]),
|
||
("thigh_l", X, [0, d(-10), d(-13), d(16), d(9), 0])],
|
||
"root_loc": [(0, 0.02, 0), (0, 0.05, 0.02), (0, 0.06, 0.02),
|
||
(0, -0.08, -0.05), (0, -0.05, -0.03), (0, 0, 0)],
|
||
},
|
||
"hit": {
|
||
"keys": [("chest", X, [0, d(-18), d(-10), d(-3)]),
|
||
("head", X, [0, d(-22), d(-13), d(-4)]),
|
||
("arm_r", X, [0, d(20), d(11), d(3)]),
|
||
("graft_head", X, [0, d(-26), d(-15), d(-4)]),
|
||
("graft_arm", X, [0, d(28), d(16), d(4)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.05, -0.03), (0, 0.03, -0.01), (0, 0.01, 0)],
|
||
},
|
||
"death": {
|
||
# Оседает на колени и валится вперёд на молот. Куча, а не бревно.
|
||
"keys": [("hips", X, [0, d(8), d(20), d(32), d(40), d(44), d(46), d(46)]),
|
||
("spine", X, [0, d(10), d(26), d(42), d(54), d(60), d(62), d(62)]),
|
||
("chest", X, [0, d(8), d(22), d(36), d(46), d(52), d(54), d(54)]),
|
||
("head", X, [0, d(-12), d(12), d(30), d(40), d(44), d(46), d(46)]),
|
||
("thigh_r", X, [0, d(22), d(56), d(84), d(98), d(104), d(106), d(106)]),
|
||
("thigh_l", X, [0, d(20), d(52), d(80), d(94), d(100), d(102), d(102)]),
|
||
("shin_r", X, [0, d(-26), d(-66), d(-100), d(-118), d(-124), d(-126), d(-126)]),
|
||
("shin_l", X, [0, d(-24), d(-62), d(-96), d(-114), d(-120), d(-122), d(-122)]),
|
||
("arm_r", X, [0, d(-14), d(14), d(50), d(68), d(74), d(76), d(76)]),
|
||
("arm_l", X, [0, d(-12), d(12), d(44), d(62), d(68), d(70), d(70)]),
|
||
("graft", X, [0, d(-16), d(8), d(34), d(48), d(54), d(56), d(56)]),
|
||
("graft_arm", X, [0, d(-20), d(10), d(44), d(62), d(70), d(72), d(72)])],
|
||
"root_rot": [("x", [0, d(-8), d(6), d(22), d(32), d(36), d(38), d(38)]),
|
||
("y", [0, d(-2), d(-6), d(-11), d(-14), d(-16), d(-17), d(-17)])],
|
||
# Keep the fallen mass centred around the ground pivot. Driving the
|
||
# root farther below the floor only moves the projected corpse down
|
||
# screen and clips it; the limb rotations already sell the collapse.
|
||
"root_loc": [(0, 0, 0), (0, 0.01, -0.10), (0, 0.02, -0.22), (0, 0.02, -0.32),
|
||
(0, 0.02, -0.38), (0, 0.02, -0.40), (0, 0.02, -0.40), (0, 0.02, -0.40)],
|
||
},
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# LURKER — жнец-паук: плоский зашитый панцирь подвешен под четырьмя длинными
|
||
# выгнутыми лапами, вместо головы — безглазый клин с крюками.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
LURKER_BONES = mirror_bones([
|
||
("root", (0.00, 0.00, 0.00), (0.00, 0.00, 0.10), None),
|
||
("core", (0.00, 0.10, 0.36), (0.00, -0.08, 0.38), "root"),
|
||
("head", (0.00, -0.08, 0.38), (0.00, -0.26, 0.32), "core"),
|
||
("fcoxa_r", (-0.09, -0.04, 0.40), (-0.26, -0.18, 0.66), "core"),
|
||
("ffemur_r", (-0.26, -0.18, 0.66), (-0.44, -0.30, 0.28), "fcoxa_r"),
|
||
("ftarsus_r", (-0.44, -0.30, 0.28), (-0.50, -0.36, 0.01), "ffemur_r"),
|
||
("bcoxa_r", (-0.09, 0.12, 0.40), (-0.27, 0.24, 0.64), "core"),
|
||
("bfemur_r", (-0.27, 0.24, 0.64), (-0.45, 0.36, 0.26), "bcoxa_r"),
|
||
("btarsus_r", (-0.45, 0.36, 0.26), (-0.51, 0.42, 0.01), "bfemur_r"),
|
||
("hook_r", (-0.05, -0.20, 0.34), (-0.10, -0.34, 0.24), "head"),
|
||
])
|
||
|
||
LURKER_PARTS = mirror_parts([
|
||
("fcoxa_r", "fcoxa_r", "chitin", cap((-0.09, -0.04, 0.40), (-0.26, -0.18, 0.66), 0.040, 0.028)),
|
||
("ffemur_r", "ffemur_r", "chitin", cap((-0.26, -0.18, 0.66), (-0.44, -0.30, 0.28), 0.028, 0.017)),
|
||
("ftarsus_r", "ftarsus_r", "bone", cap((-0.44, -0.30, 0.28), (-0.50, -0.36, 0.01), 0.017, 0.004)),
|
||
("fjoint_r", "ffemur_r", "sinew", ball((-0.26, -0.18, 0.66), 0.036)),
|
||
("bcoxa_r", "bcoxa_r", "chitin", cap((-0.09, 0.12, 0.40), (-0.27, 0.24, 0.64), 0.040, 0.028)),
|
||
("bfemur_r", "bfemur_r", "chitin", cap((-0.27, 0.24, 0.64), (-0.45, 0.36, 0.26), 0.028, 0.017)),
|
||
("btarsus_r", "btarsus_r", "bone", cap((-0.45, 0.36, 0.26), (-0.51, 0.42, 0.01), 0.017, 0.004)),
|
||
("bjoint_r", "bfemur_r", "sinew", ball((-0.27, 0.24, 0.64), 0.036)),
|
||
("hook_r", "hook_r", "bone", cap((-0.05, -0.20, 0.34), (-0.11, -0.36, 0.23), 0.020, 0.000)),
|
||
])
|
||
LURKER_PARTS += [
|
||
("carapace", "core", "chitin", ball((0.00, 0.02, 0.38), 0.185, (0.85, 1.15, 0.42))),
|
||
("belly", "core", "flesh", ball((0.00, 0.02, 0.31), 0.150, (0.82, 1.05, 0.30))),
|
||
# Шов: панцирь не вырос, его ЗАШИЛИ. Скобы идут по всей спине.
|
||
("suture", "core", "sinew", box((0.00, 0.02, 0.455), (0.030, 0.290, 0.020))),
|
||
("staple_a", "core", "steel", box((0.00, -0.10, 0.462), (0.110, 0.026, 0.014))),
|
||
("staple_b", "core", "steel", box((0.00, 0.02, 0.468), (0.115, 0.026, 0.014))),
|
||
("staple_c", "core", "steel", box((0.00, 0.14, 0.462), (0.110, 0.026, 0.014))),
|
||
("vent_r", "core", "rust", box((-0.13, 0.16, 0.40), (0.060, 0.070, 0.070))),
|
||
("vent_l", "core", "rust", box((0.13, 0.16, 0.40), (0.060, 0.070, 0.070))),
|
||
("wedge", "head", "chitin", cap((0.00, -0.08, 0.38), (0.00, -0.28, 0.31), 0.085, 0.030)),
|
||
("mouth", "head", "gut", ball((0.00, -0.26, 0.32), 0.036, (1.00, 0.70, 1.00))),
|
||
]
|
||
|
||
LURKER_ANIM = {
|
||
"walk": {
|
||
# Семенит: лапы работают по диагонали, корпус почти не шевелится —
|
||
# именно это и должно читаться как «крадётся», а не «топает».
|
||
"cycle": [("fcoxa_r", X, d(20), 0.00, 0.0), ("fcoxa_l", X, d(20), 0.50, 0.0),
|
||
("bcoxa_r", X, d(20), 0.50, 0.0), ("bcoxa_l", X, d(20), 0.00, 0.0),
|
||
("ffemur_r", X, d(24), 0.18, 0.0), ("ffemur_l", X, d(24), 0.68, 0.0),
|
||
("bfemur_r", X, d(24), 0.68, 0.0), ("bfemur_l", X, d(24), 0.18, 0.0),
|
||
("ftarsus_r", X, d(16), 0.34, 0.0), ("ftarsus_l", X, d(16), 0.84, 0.0),
|
||
("btarsus_r", X, d(16), 0.84, 0.0), ("btarsus_l", X, d(16), 0.34, 0.0),
|
||
("core", Y, d(5), 0.00, 0.0), ("head", X, d(6), 0.25, 0.0)],
|
||
"bob": (0.022, 0.0),
|
||
},
|
||
"attack": {
|
||
# Вздыбить перед, ударить крюками сверху вниз.
|
||
"keys": [("fcoxa_r", X, [0, d(-46), d(-56), d(34), d(18), d(2)]),
|
||
("fcoxa_l", X, [0, d(-46), d(-56), d(34), d(18), d(2)]),
|
||
("ffemur_r", X, [0, d(-30), d(-38), d(28), d(14), d(2)]),
|
||
("ffemur_l", X, [0, d(-30), d(-38), d(28), d(14), d(2)]),
|
||
("core", X, [0, d(-26), d(-32), d(22), d(10), 0]),
|
||
("head", X, [0, d(-20), d(-26), d(38), d(16), 0]),
|
||
("hook_r", X, [0, d(-18), d(-24), d(34), d(14), 0]),
|
||
("hook_l", X, [0, d(-18), d(-24), d(34), d(14), 0]),
|
||
("bfemur_r", X, [0, d(14), d(18), d(-10), d(-5), 0]),
|
||
("bfemur_l", X, [0, d(14), d(18), d(-10), d(-5), 0])],
|
||
"root_loc": [(0, 0, 0), (0, 0.02, 0.06), (0, 0.03, 0.08),
|
||
(0, -0.07, -0.02), (0, -0.03, -0.01), (0, 0, 0)],
|
||
},
|
||
"hit": {
|
||
"keys": [("core", X, [0, d(-18), d(-10), d(-3)]),
|
||
("head", X, [0, d(-24), d(-14), d(-4)]),
|
||
("fcoxa_r", X, [0, d(22), d(13), d(4)]),
|
||
("fcoxa_l", X, [0, d(22), d(13), d(4)]),
|
||
("bcoxa_r", X, [0, d(-14), d(-8), d(-2)]),
|
||
("bcoxa_l", X, [0, d(-14), d(-8), d(-2)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.06, -0.02), (0, 0.03, -0.01), (0, 0.01, 0)],
|
||
},
|
||
"death": {
|
||
# Лапы поджимаются НАВЕРХ и внутрь, панцирь падает на пол — классика.
|
||
"keys": [("fcoxa_r", X, [0, d(-16), d(-38), d(-56), d(-66), d(-70), d(-72), d(-72)]),
|
||
("fcoxa_l", X, [0, d(-16), d(-38), d(-56), d(-66), d(-70), d(-72), d(-72)]),
|
||
("bcoxa_r", X, [0, d(-14), d(-34), d(-52), d(-62), d(-66), d(-68), d(-68)]),
|
||
("bcoxa_l", X, [0, d(-14), d(-34), d(-52), d(-62), d(-66), d(-68), d(-68)]),
|
||
("ffemur_r", X, [0, d(26), d(64), d(96), d(112), d(118), d(120), d(120)]),
|
||
("ffemur_l", X, [0, d(26), d(64), d(96), d(112), d(118), d(120), d(120)]),
|
||
("bfemur_r", X, [0, d(24), d(60), d(92), d(108), d(114), d(116), d(116)]),
|
||
("bfemur_l", X, [0, d(24), d(60), d(92), d(108), d(114), d(116), d(116)]),
|
||
("ftarsus_r", X, [0, d(-20), d(-48), d(-72), d(-84), d(-88), d(-90), d(-90)]),
|
||
("ftarsus_l", X, [0, d(-20), d(-48), d(-72), d(-84), d(-88), d(-90), d(-90)]),
|
||
("btarsus_r", X, [0, d(-20), d(-48), d(-72), d(-84), d(-88), d(-90), d(-90)]),
|
||
("btarsus_l", X, [0, d(-20), d(-48), d(-72), d(-84), d(-88), d(-90), d(-90)]),
|
||
("head", X, [0, d(8), d(20), d(30), d(36), d(38), d(40), d(40)])],
|
||
"root_rot": [("y", [0, d(6), d(16), d(26), d(32), d(35), d(36), d(36)])],
|
||
"root_loc": [(0, 0, 0), (0, 0.00, -0.05), (0, 0.01, -0.12), (0, 0.01, -0.18),
|
||
(0, 0.01, -0.22), (0, 0.01, -0.24), (0, 0.01, -0.25), (0, 0.01, -0.25)],
|
||
},
|
||
}
|
||
|
||
|
||
KINDS = {
|
||
"shambler": {"bones": SHAMBLER_BONES, "parts": SHAMBLER_PARTS, "anim": SHAMBLER_ANIM},
|
||
"rusher": {"bones": RUSHER_BONES, "parts": RUSHER_PARTS, "anim": RUSHER_ANIM},
|
||
"spitter": {"bones": SPITTER_BONES, "parts": SPITTER_PARTS, "anim": SPITTER_ANIM},
|
||
"brute": {"bones": BRUTE_BONES, "parts": BRUTE_PARTS, "anim": BRUTE_ANIM},
|
||
"lurker": {"bones": LURKER_BONES, "parts": LURKER_PARTS, "anim": LURKER_ANIM},
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Мелкие утилиты (родня agent_rig.py; расходиться им нельзя по смыслу камеры)
|
||
# ----------------------------------------------------------------------------
|
||
|
||
def log(message):
|
||
print("[monster_rig] " + message)
|
||
|
||
|
||
def collection(name, parent):
|
||
existing = bpy.data.collections.get(name)
|
||
if existing is not None:
|
||
return existing
|
||
made = bpy.data.collections.new(name)
|
||
parent.children.link(made)
|
||
return made
|
||
|
||
|
||
def _set_socket(node, name, value):
|
||
socket = node.inputs.get(name)
|
||
if socket is None:
|
||
return
|
||
try:
|
||
socket.default_value = value
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
|
||
def material(name):
|
||
existing = bpy.data.materials.get(name)
|
||
if existing is not None:
|
||
return existing
|
||
color, roughness, metallic = MATERIALS[name]
|
||
mat = bpy.data.materials.new(name)
|
||
mat.use_nodes = True
|
||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
if bsdf is not None:
|
||
_set_socket(bsdf, "Base Color", (color[0], color[1], color[2], 1.0))
|
||
_set_socket(bsdf, "Roughness", roughness)
|
||
_set_socket(bsdf, "Metallic", metallic)
|
||
_set_socket(bsdf, "Specular IOR Level", 0.5)
|
||
_set_socket(bsdf, "Specular", 0.5)
|
||
mat.diffuse_color = (color[0], color[1], color[2], 1.0)
|
||
return mat
|
||
|
||
|
||
def mesh_object(name, bm, mat, coll):
|
||
mesh = bpy.data.meshes.new(name)
|
||
bm.to_mesh(mesh)
|
||
bm.free()
|
||
mesh.materials.append(mat)
|
||
for polygon in mesh.polygons:
|
||
polygon.use_smooth = False
|
||
obj = bpy.data.objects.new(name, mesh)
|
||
coll.objects.link(obj)
|
||
return obj
|
||
|
||
|
||
def build_shape(kind, params):
|
||
"""Возвращает bmesh детали в МИРОВЫХ координатах позы покоя."""
|
||
bm = bmesh.new()
|
||
if kind == "capsule":
|
||
p0, p1, r0, r1 = Vector(params[0]), Vector(params[1]), params[2], params[3]
|
||
axis = p1 - p0
|
||
length = axis.length
|
||
if length < 1e-6:
|
||
raise ValueError("degenerate capsule")
|
||
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=10,
|
||
radius1=r0, radius2=r1, depth=length)
|
||
rot = axis.to_track_quat("Z", "Y").to_matrix().to_4x4()
|
||
bmesh.ops.transform(bm, matrix=rot, verts=bm.verts)
|
||
bmesh.ops.translate(bm, vec=(p0 + p1) * 0.5, verts=bm.verts)
|
||
elif kind == "box":
|
||
center, size, yaw = Vector(params[0]), Vector(params[1]), params[2]
|
||
bmesh.ops.create_cube(bm, size=1.0)
|
||
matrix = (Matrix.Translation(center)
|
||
@ Matrix.Rotation(yaw, 4, "Z")
|
||
@ Matrix.Diagonal(size.to_4d()))
|
||
bmesh.ops.transform(bm, matrix=matrix, verts=bm.verts)
|
||
elif kind == "sphere":
|
||
center, radius, scale = Vector(params[0]), params[1], Vector(params[2])
|
||
bmesh.ops.create_uvsphere(bm, u_segments=14, v_segments=8, radius=radius)
|
||
matrix = Matrix.Translation(center) @ Matrix.Diagonal(scale.to_4d())
|
||
bmesh.ops.transform(bm, matrix=matrix, verts=bm.verts)
|
||
else:
|
||
raise ValueError("unknown shape: " + kind)
|
||
return bm
|
||
|
||
|
||
def bone_parent(obj, armature, bone_name):
|
||
"""Жёстко цепляет объект к кости, сохраняя его положение в позе покоя."""
|
||
world = obj.matrix_world.copy()
|
||
obj.parent = armature
|
||
obj.parent_type = "BONE"
|
||
obj.parent_bone = bone_name
|
||
bpy.context.view_layer.update()
|
||
obj.matrix_world = world
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Сборка сцены
|
||
# ----------------------------------------------------------------------------
|
||
|
||
def build_armature(name, bones, coll):
|
||
data = bpy.data.armatures.new(name)
|
||
armature = bpy.data.objects.new(name, data)
|
||
coll.objects.link(armature)
|
||
bpy.context.view_layer.objects.active = armature
|
||
# Режим EDIT входит для ВЫДЕЛЕННЫХ объектов: без сброса выделения второй
|
||
# риг открылся бы вместе с первым и кости уехали бы в чужой скелет.
|
||
for other in bpy.context.view_layer.objects:
|
||
other.select_set(other is armature)
|
||
bpy.ops.object.mode_set(mode="EDIT")
|
||
made = {}
|
||
for bone_name, head, tail, parent in bones:
|
||
bone = data.edit_bones.new(bone_name)
|
||
bone.head = head
|
||
bone.tail = tail
|
||
bone.roll = 0.0
|
||
bone.use_connect = False
|
||
if parent is not None:
|
||
bone.parent = made[parent]
|
||
made[bone_name] = bone
|
||
bpy.ops.object.mode_set(mode="OBJECT")
|
||
for bone in armature.pose.bones:
|
||
bone.rotation_mode = "QUATERNION"
|
||
return armature
|
||
|
||
|
||
def build_parts(parts, prefix, armature, coll):
|
||
for suffix, bone, mat, (shape, params) in parts:
|
||
obj = mesh_object("%s_%s" % (prefix, suffix), build_shape(shape, params),
|
||
material(mat), coll)
|
||
bone_parent(obj, armature, bone)
|
||
|
||
|
||
def build_camera(scene, camera_cfg, root_coll):
|
||
elevation = math.radians(float(camera_cfg["elevation_deg"]))
|
||
distance = float(camera_cfg["distance"])
|
||
data = bpy.data.cameras.new("monster_cam")
|
||
data.type = "ORTHO"
|
||
data.clip_start = 0.01
|
||
data.clip_end = distance * 4.0
|
||
data.shift_x = 0.0
|
||
data.shift_y = 0.0
|
||
camera = bpy.data.objects.new("monster_cam", data)
|
||
# Смотрит строго в начало координат: ось взгляда проходит через (0,0,0),
|
||
# поэтому в ортографии оно всегда попадает точно в центр кадра.
|
||
camera.location = (0.0, -distance * math.cos(elevation), distance * math.sin(elevation))
|
||
camera.rotation_euler = (math.pi / 2.0 - elevation, 0.0, 0.0)
|
||
root_coll.objects.link(camera)
|
||
scene.camera = camera
|
||
return camera
|
||
|
||
|
||
def build_lights(root_coll):
|
||
# Свет фиксирован в МИРЕ: поворачивается тварь, значит каждое направление
|
||
# освещено по-своему. Ключ холодный сверху-слева, мясо подсвечено снизу
|
||
# тёплым — тот самый «мокрый» контур.
|
||
key = bpy.data.lights.new("key_sun", type="SUN")
|
||
key.energy = 3.0
|
||
key.color = (0.92, 0.95, 1.0)
|
||
key.angle = math.radians(8.0)
|
||
key_obj = bpy.data.objects.new("key_sun", key)
|
||
key_obj.rotation_euler = (math.radians(50.0), 0.0, math.radians(-40.0))
|
||
root_coll.objects.link(key_obj)
|
||
|
||
fill = bpy.data.lights.new("fill_sun", type="SUN")
|
||
fill.energy = 0.7
|
||
fill.color = (1.0, 0.52, 0.44)
|
||
fill_obj = bpy.data.objects.new("fill_sun", fill)
|
||
fill_obj.rotation_euler = (math.radians(112.0), 0.0, math.radians(155.0))
|
||
root_coll.objects.link(fill_obj)
|
||
|
||
rim = bpy.data.lights.new("rim_sun", type="SUN")
|
||
rim.energy = 1.1
|
||
rim.color = (0.60, 0.78, 0.92)
|
||
rim_obj = bpy.data.objects.new("rim_sun", rim)
|
||
rim_obj.rotation_euler = (math.radians(74.0), 0.0, math.radians(196.0))
|
||
root_coll.objects.link(rim_obj)
|
||
|
||
|
||
def build_world(scene):
|
||
world = bpy.data.worlds.new("monster_world")
|
||
world.use_nodes = True
|
||
background = world.node_tree.nodes.get("Background")
|
||
if background is not None:
|
||
_set_socket(background, "Color", (0.030, 0.036, 0.046, 1.0))
|
||
_set_socket(background, "Strength", 1.0)
|
||
scene.world = world
|
||
|
||
|
||
def configure_render(scene):
|
||
for engine in ("BLENDER_EEVEE_NEXT", "BLENDER_EEVEE", "CYCLES"):
|
||
try:
|
||
scene.render.engine = engine
|
||
break
|
||
except (TypeError, ValueError):
|
||
continue
|
||
scene.render.film_transparent = True
|
||
scene.render.use_motion_blur = False
|
||
scene.render.resolution_percentage = 100
|
||
scene.render.pixel_aspect_x = 1.0
|
||
scene.render.pixel_aspect_y = 1.0
|
||
scene.render.image_settings.file_format = "PNG"
|
||
scene.render.image_settings.color_mode = "RGBA"
|
||
scene.render.image_settings.color_depth = "8"
|
||
scene.render.image_settings.compression = 0
|
||
# Standard, а не AgX: спрайт должен получить ровно тот цвет, что в материале.
|
||
try:
|
||
scene.view_settings.view_transform = "Standard"
|
||
scene.view_settings.look = "None"
|
||
except (TypeError, ValueError):
|
||
pass
|
||
eevee = getattr(scene, "eevee", None)
|
||
if eevee is not None and hasattr(eevee, "taa_render_samples"):
|
||
eevee.taa_render_samples = 48
|
||
if scene.render.engine == "CYCLES":
|
||
scene.cycles.samples = 64
|
||
scene.cycles.use_denoising = True
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Анимации: нормированное время -> кадры каталога
|
||
# ----------------------------------------------------------------------------
|
||
|
||
def sample(values, t):
|
||
"""Кусочно-линейная выборка списка опорных значений на отрезке 0..1."""
|
||
if len(values) == 1:
|
||
return float(values[0])
|
||
x = max(0.0, min(1.0, t)) * (len(values) - 1)
|
||
index = min(int(x), len(values) - 2)
|
||
frac = x - index
|
||
return float(values[index]) * (1.0 - frac) + float(values[index + 1]) * frac
|
||
|
||
|
||
def world_axis_quat(armature, bone_name, axis, angle):
|
||
"""Кватернион позы, вращающий кость вокруг МИРОВОЙ оси на angle радиан."""
|
||
rest = armature.data.bones[bone_name].matrix_local.to_3x3()
|
||
local_axis = rest.inverted() @ Vector(axis)
|
||
if local_axis.length < 1e-9:
|
||
return Quaternion((1.0, 0.0, 0.0, 0.0))
|
||
return Quaternion(local_axis.normalized(), angle)
|
||
|
||
|
||
AXIS_BY_NAME = {"x": X, "y": Y, "z": Z}
|
||
|
||
|
||
def apply_pose(armature, frame, swings, root_offset, root_rot):
|
||
"""swings: [(bone, axis, angle)] — накапливаются, если кость встретилась дважды."""
|
||
for bone in armature.pose.bones:
|
||
bone.rotation_quaternion = Quaternion((1.0, 0.0, 0.0, 0.0))
|
||
armature.pose.bones["root"].location = Vector((0.0, 0.0, 0.0))
|
||
|
||
accumulated = {}
|
||
for bone_name, axis, angle in swings:
|
||
if bone_name not in armature.pose.bones:
|
||
raise SystemExit("[monster_rig] no bone '%s' in %s" % (bone_name, armature.name))
|
||
quat = world_axis_quat(armature, bone_name, axis, angle)
|
||
accumulated[bone_name] = accumulated.get(bone_name, Quaternion((1, 0, 0, 0))) @ quat
|
||
for bone_name, quat in accumulated.items():
|
||
armature.pose.bones[bone_name].rotation_quaternion = quat
|
||
|
||
root = armature.pose.bones["root"]
|
||
rest = armature.data.bones["root"].matrix_local.to_3x3()
|
||
# root_rot приходит списком (мировая ось, угол) — в локальную систему кости
|
||
# его переводит тот же world_axis_quat, что и все остальные вращения.
|
||
if root_rot:
|
||
quat = Quaternion((1.0, 0.0, 0.0, 0.0))
|
||
for axis, angle in root_rot:
|
||
quat = quat @ world_axis_quat(armature, "root", axis, angle)
|
||
root.rotation_quaternion = quat
|
||
if root_offset is not None:
|
||
root.location = rest.inverted() @ Vector(root_offset)
|
||
|
||
for bone in armature.pose.bones:
|
||
bone.keyframe_insert(data_path="rotation_quaternion", frame=frame)
|
||
root.keyframe_insert(data_path="location", frame=frame)
|
||
|
||
|
||
def make_action(armature, name):
|
||
if armature.animation_data is None:
|
||
armature.animation_data_create()
|
||
action = bpy.data.actions.new(name)
|
||
# Blender 5 не считает слоёное действие использованным, пока в нём нет
|
||
# каналов. Без fake user клип, совпавший с позой покоя, не переживёт save.
|
||
action.use_fake_user = True
|
||
armature.animation_data.action = action
|
||
return action
|
||
|
||
|
||
def build_clip(armature, kind, animation, spec, frames):
|
||
make_action(armature, "%s_%s" % (kind, animation))
|
||
cyclic = "cycle" in spec
|
||
for frame in range(frames):
|
||
# Циклический клип замыкается: последний кадр НЕ дублирует первый.
|
||
t = frame / float(frames) if cyclic else (frame / float(max(1, frames - 1)))
|
||
swings = []
|
||
offset = None
|
||
root_rot = []
|
||
if cyclic:
|
||
for bone_name, axis, amp, phase, bias in spec["cycle"]:
|
||
swings.append((bone_name, axis,
|
||
bias + amp * math.sin(2.0 * math.pi * (t + phase))))
|
||
bob, bob_phase = spec.get("bob", (0.0, 0.0))
|
||
offset = (0.0, 0.0, -bob * abs(math.sin(2.0 * math.pi * (t + bob_phase))))
|
||
else:
|
||
for bone_name, axis, values in spec.get("keys", []):
|
||
swings.append((bone_name, axis, sample(values, t)))
|
||
loc = spec.get("root_loc")
|
||
if loc is not None:
|
||
offset = (sample([v[0] for v in loc], t),
|
||
sample([v[1] for v in loc], t),
|
||
sample([v[2] for v in loc], t))
|
||
for axis_name, values in spec.get("root_rot", []):
|
||
root_rot.append((AXIS_BY_NAME[axis_name], sample(values, t)))
|
||
apply_pose(armature, frame + 1, swings, offset, root_rot)
|
||
log("action %s_%s: %d frame(s)" % (kind, animation, frames))
|
||
|
||
|
||
def build_actions(armature, kind, anim_specs, animations):
|
||
for animation, meta in animations.items():
|
||
spec = anim_specs.get(animation)
|
||
if spec is None:
|
||
raise SystemExit("[monster_rig] %s has no '%s' clip" % (kind, animation))
|
||
build_clip(armature, kind, animation, spec, int(meta["frames"]))
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# build
|
||
# ----------------------------------------------------------------------------
|
||
|
||
def wipe_scene():
|
||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
||
|
||
def cmd_build(cfg, out_path):
|
||
wipe_scene()
|
||
scene = bpy.context.scene
|
||
scene.unit_settings.system = "METRIC"
|
||
scene.unit_settings.scale_length = 1.0
|
||
|
||
root = collection("MONSTERS", scene.collection)
|
||
|
||
# Пустышка над всеми ригами: направление задаётся её поворотом вокруг Z,
|
||
# камера при этом не двигается — иначе свет ездил бы вместе с тварью.
|
||
pivot = bpy.data.objects.new("monster_root", None)
|
||
pivot.empty_display_type = "ARROWS"
|
||
pivot.empty_display_size = 0.3
|
||
scene.collection.objects.link(pivot)
|
||
|
||
selected = cfg.get("kinds") or list(KINDS)
|
||
unknown = [kind for kind in selected if kind not in KINDS]
|
||
if unknown:
|
||
raise SystemExit("[monster_rig] unknown kind(s): " + ", ".join(unknown))
|
||
total_parts = 0
|
||
for kind in selected:
|
||
spec = KINDS[kind]
|
||
kind_coll = collection(kind.upper(), root)
|
||
armature = build_armature("rig_" + kind, spec["bones"], kind_coll)
|
||
armature.parent = pivot
|
||
build_parts(spec["parts"], kind, armature, kind_coll)
|
||
build_actions(armature, kind, spec["anim"], cfg["animations"])
|
||
total_parts += len(spec["parts"])
|
||
log("kind %s: %d bone(s), %d part(s)" % (kind, len(spec["bones"]), len(spec["parts"])))
|
||
|
||
build_camera(scene, cfg["camera"], scene.collection)
|
||
build_lights(scene.collection)
|
||
build_world(scene)
|
||
configure_render(scene)
|
||
|
||
scene.frame_start = 1
|
||
scene.frame_end = max(int(a["frames"]) for a in cfg["animations"].values())
|
||
|
||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||
bpy.ops.wm.save_as_mainfile(filepath=out_path)
|
||
log("saved %s (%d kinds, %d mesh objects)" % (out_path, len(selected), total_parts))
|
||
return 0
|
||
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# render
|
||
# ----------------------------------------------------------------------------
|
||
|
||
def assign_action(armature, name):
|
||
action = bpy.data.actions.get(name)
|
||
if action is None:
|
||
raise SystemExit("[monster_rig] no action '%s' in the .blend" % name)
|
||
if armature.animation_data is None:
|
||
armature.animation_data_create()
|
||
anim = armature.animation_data
|
||
anim.action = action
|
||
# Blender 4.4+: у Action появились слоты, и без выбранного слота она молчит.
|
||
slots = getattr(action, "slots", None)
|
||
if slots is not None and hasattr(anim, "action_slot") and anim.action_slot is None and len(slots):
|
||
anim.action_slot = slots[0]
|
||
return action
|
||
|
||
|
||
def cmd_render(cfg, kind, animation, outdir, supersample):
|
||
scene = bpy.context.scene
|
||
armature = bpy.data.objects.get("rig_" + kind)
|
||
pivot = bpy.data.objects.get("monster_root")
|
||
if armature is None or pivot is None:
|
||
raise SystemExit("[monster_rig] the .blend has no rig_%s/monster_root; "
|
||
"run `monster-init` first" % kind)
|
||
|
||
prefix = kind + "_"
|
||
shown = 0
|
||
for obj in bpy.data.objects:
|
||
if obj.type != "MESH":
|
||
continue
|
||
obj.hide_render = not obj.name.startswith(prefix)
|
||
shown += 0 if obj.hide_render else 1
|
||
if shown == 0:
|
||
raise SystemExit("[monster_rig] no objects named '%s*' in the .blend" % prefix)
|
||
|
||
configure_render(scene)
|
||
scene.render.resolution_x = int(cfg["render_width"]) * supersample
|
||
scene.render.resolution_y = int(cfg["render_height"]) * supersample
|
||
camera = scene.camera or build_camera(scene, cfg["camera"], scene.collection)
|
||
camera.data.type = "ORTHO"
|
||
camera.data.shift_x = 0.0
|
||
camera.data.shift_y = 0.0
|
||
# ortho_scale ложится на БОЛЬШУЮ сторону кадра.
|
||
longest = max(int(cfg["render_width"]), int(cfg["render_height"]))
|
||
camera.data.ortho_scale = longest / float(cfg["camera"]["pixels_per_unit"])
|
||
|
||
assign_action(armature, "%s_%s" % (kind, animation))
|
||
frames = int(cfg["frames"])
|
||
directions = cfg["directions"]
|
||
os.makedirs(outdir, exist_ok=True)
|
||
|
||
for index, name in enumerate(directions):
|
||
# Порядок направлений идёт по -45° от юга: юг, юго-запад, запад, …
|
||
pivot.rotation_euler = (0.0, 0.0, math.radians(-45.0 * index))
|
||
bpy.context.view_layer.update()
|
||
for frame in range(frames):
|
||
scene.frame_set(frame + 1)
|
||
cell = index * frames + frame
|
||
scene.render.filepath = os.path.join(outdir, "frame_%03d.png" % cell)
|
||
bpy.ops.render.render(write_still=True)
|
||
log("direction %d/%d (%s) done" % (index + 1, len(directions), name))
|
||
|
||
log("rendered %d cell(s) at %dx%d into %s"
|
||
% (len(directions) * frames, scene.render.resolution_x, scene.render.resolution_y, outdir))
|
||
return 0
|
||
|
||
|
||
def main(argv):
|
||
if "--" in argv:
|
||
argv = argv[argv.index("--") + 1:]
|
||
else:
|
||
argv = []
|
||
if not argv:
|
||
raise SystemExit("[monster_rig] usage: build|render --config <json>")
|
||
command = argv[0]
|
||
options = {}
|
||
index = 1
|
||
while index < len(argv) - 1:
|
||
if argv[index].startswith("--"):
|
||
options[argv[index][2:]] = argv[index + 1]
|
||
index += 2
|
||
else:
|
||
index += 1
|
||
with open(options["config"], "r", encoding="utf-8") as handle:
|
||
cfg = json.load(handle)
|
||
if command == "build":
|
||
return cmd_build(cfg, options["out"])
|
||
if command == "render":
|
||
return cmd_render(cfg, cfg["kind"], cfg["animation"], cfg["outdir"],
|
||
int(cfg.get("supersample", 1)))
|
||
raise SystemExit("[monster_rig] unknown command: " + command)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(list(sys.argv)))
|