- Все 189 скелетов Spine сконвертированы в нативные Godot Skeleton2D (.tscn), без Spine в рантайме - Меню + сцены: подземелье (карта комнат), просмотр спрайтов, нативные анимации - Кастомизация цвета (кожа/волосы/глаза) через шейдер - Вытащенные текстуры/UI/шрифты + нарезка спрайтов из атласа - Датасет комнат для LoRA, офлайн-инструменты конвертации в tools/ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.7 KiB
GDScript
76 lines
2.7 KiB
GDScript
extends SceneTree
|
|
|
|
# Офлайн-запекатель: прогоняет Spine-рантайм и снимает покадрово ЛОКАЛЬНЫЕ трансформы костей
|
|
# для выбранных анимаций -> tools/rig/<name>_anims.json (только числа, без Spine на выходе).
|
|
|
|
const BASE := "res://characters/elf1/spine/elf1_combat"
|
|
const OUT := "res://tools/rig/elf1_combat_anims.json"
|
|
const FPS := 30.0
|
|
const ANIMS := ["idle_0", "walk_0", "attack_0", "attack_1", "knockback_0", "stun_0"]
|
|
|
|
func _initialize():
|
|
var atlas := SpineAtlasResource.new(); atlas.load_from_atlas_file(BASE + ".atlas.txt")
|
|
var skf := SpineSkeletonFileResource.new(); skf.load_from_file(BASE + ".skel.bytes")
|
|
var data := SpineSkeletonDataResource.new()
|
|
data.set_atlas_res(atlas); data.set_skeleton_file_res(skf)
|
|
|
|
var sprite := SpineSprite.new()
|
|
sprite.set_skeleton_data_res(data)
|
|
get_root().add_child(sprite)
|
|
var st = sprite.get_animation_state()
|
|
var skel = sprite.get_skeleton()
|
|
|
|
# имена костей в порядке скелета
|
|
var bone_names := []
|
|
for bd in data.get_bones():
|
|
bone_names.append(bd.get_bone_name())
|
|
|
|
# имена слотов + индекс по имени (для draw order)
|
|
var slot_names := []
|
|
var slot_index := {}
|
|
var live_slots = skel.get_slots()
|
|
for i in live_slots.size():
|
|
var sn = live_slots[i].get_data().get_name()
|
|
slot_names.append(sn); slot_index[sn] = i
|
|
|
|
var out := { "fps": FPS, "bones": bone_names, "slots": slot_names, "animations": {} }
|
|
|
|
for anim in ANIMS:
|
|
var sa = data.find_animation(anim)
|
|
if sa == null:
|
|
print("skip (нет): ", anim); continue
|
|
var dur: float = sa.get_duration()
|
|
var nframes := int(round(dur * FPS)) + 1
|
|
st.clear_tracks()
|
|
st.set_animation(anim, false, 0)
|
|
|
|
var frames := []
|
|
var draworder := []
|
|
for f in nframes:
|
|
skel.set_bones_to_setup_pose()
|
|
skel.set_slots_to_setup_pose()
|
|
st.apply(skel)
|
|
var frame := []
|
|
for name in bone_names:
|
|
var b = skel.find_bone(name)
|
|
frame.append([
|
|
snappedf(b.get_x(), 0.001), snappedf(b.get_y(), 0.001),
|
|
snappedf(b.get_rotation(), 0.001),
|
|
snappedf(b.get_scale_x(), 0.001), snappedf(b.get_scale_y(), 0.001)
|
|
])
|
|
frames.append(frame)
|
|
# порядок отрисовки: индексы слотов сзади->вперёд
|
|
var order := []
|
|
for s in skel.get_draw_order():
|
|
order.append(slot_index[s.get_data().get_name()])
|
|
draworder.append(order)
|
|
st.update(1.0 / FPS)
|
|
out["animations"][anim] = { "duration": dur, "frames": frames, "draworder": draworder }
|
|
print("baked ", anim, " frames=", nframes, " dur=", dur)
|
|
|
|
var fp := FileAccess.open(OUT, FileAccess.WRITE)
|
|
fp.store_string(JSON.stringify(out))
|
|
fp.close()
|
|
print("SAVED ", OUT)
|
|
quit()
|