extends SceneTree # Пакетный конвертер: по всем tools/rig/geom/__.json запекает анимации из Spine # и собирает нативные characters//_native.tscn (Skeleton2D + Polygon2D + AnimationPlayer). # Запуск: godot ... --headless --script res://tools/convert_all.gd const GEOM_DIR := "res://tools/rig/geom" const FPS := 30.0 const VP := Vector2(1280, 720) func _initialize(): var files := [] var d := DirAccess.open(GEOM_DIR) d.list_dir_begin() var n := d.get_next() while n != "": if n.ends_with(".json"): files.append(n) n = d.get_next() files.sort() var ok := 0; var fail := 0; var i := 0 for fn in files: i += 1 var stem_full: String = fn.get_basename() # char__stem var sep := stem_full.find("__") var char := stem_full.substr(0, sep) var stem := stem_full.substr(sep + 2) var base := "res://characters/%s/spine/%s" % [char, stem] if not FileAccess.file_exists(base + ".skel.bytes"): fail += 1; continue var out := "res://characters/%s/%s_native.tscn" % [char, stem] if _convert(base, GEOM_DIR + "/" + fn, out): ok += 1 else: fail += 1 if i % 10 == 0: print("... %d/%d (ok=%d)" % [i, files.size(), ok]) await process_frame print("CONVERT ALL done: ok=%d fail=%d из %d" % [ok, fail, files.size()]) quit() func _convert(base: String, geom_path: String, out_path: String) -> bool: var gf := FileAccess.open(geom_path, FileAccess.READ) if gf == null: return false var geom: Dictionary = JSON.parse_string(gf.get_as_text()); gf.close() if geom == null or not geom.has("bones"): return false var anims = _bake(base) if anims == null: return false var root = _build(geom, anims, base.get_base_dir()) if root == null: return false _set_owner(root, root) var packed := PackedScene.new() if packed.pack(root) != OK: root.free(); return false var err := ResourceSaver.save(packed, out_path) root.free() return err == OK # ---------- запекание анимаций ---------- func _bake(base: String): var atlas := SpineAtlasResource.new() if atlas.load_from_atlas_file(base + ".atlas.txt") != OK: return null 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) if not data.is_skeleton_data_loaded(): return null 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()) 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 sa in data.get_animations(): var anim_name = sa.get_name() var dur: float = sa.get_duration() var nframes := int(round(dur * FPS)) + 1 st.clear_tracks(); st.set_animation(anim_name, 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 nm in bone_names: var b = skel.find_bone(nm) 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_name] = { "duration": dur, "frames": frames, "draworder": draworder } get_root().remove_child(sprite); sprite.free() return out # ---------- сборка нативного рига ---------- func _build(geom: Dictionary, anims: Dictionary, spine_dir: String): # текстуры страниц атласа var pagetex := {} for pg in geom.get("pages", []): pagetex[pg] = load(spine_dir + "/" + pg) var root := Node2D.new(); root.name = "Native" var spine := Node2D.new(); spine.name = "SpineRoot"; root.add_child(spine) var skel := Skeleton2D.new(); skel.name = "Skeleton2D"; spine.add_child(skel) var by_name := {}; var bones := [] for bd in geom["bones"]: var b := Bone2D.new(); b.name = bd["name"] b.set_autocalculate_length_and_angle(false); b.set_length(float(bd["length"])); b.set_bone_angle(0.0) var t := Transform2D(deg_to_rad(float(bd["rotation"])), Vector2(bd["scaleX"], bd["scaleY"]), 0.0, Vector2(bd["x"], bd["y"])) var parent = by_name.get(bd["parent"], skel) parent.add_child(b); b.transform = t; b.rest = t bones.append(b); by_name[bd["name"]] = b var poly_by_slot := {}; var base_rank := {} var minv := Vector2(1e9,1e9); var maxv := Vector2(-1e9,-1e9); var rank := 0 for att in geom["attachments"]: var world: Array = att["world"]; var uv: Array = att["uv"]; var tris: Array = att["triangles"]; var weights: Array = att["weights"] var vc := world.size() / 2 if vc < 3: continue var tex = pagetex.get(att["page"], null) if tex == null: continue var ts: Vector2 = tex.get_size() var poly := Polygon2D.new(); poly.name = String(att["slot"]); poly.texture = tex var pts := PackedVector2Array(); var uvs := PackedVector2Array(); pts.resize(vc); uvs.resize(vc) for i in vc: var p := Vector2(world[i*2], world[i*2+1]); pts[i] = p uvs[i] = Vector2(uv[i*2] * ts.x, uv[i*2+1] * ts.y) minv = minv.min(p); maxv = maxv.max(p) poly.polygon = pts; poly.uv = uvs var polys := [] for t in range(0, tris.size(), 3): polys.append(PackedInt32Array([tris[t], tris[t+1], tris[t+2]])) poly.polygons = polys var c: Array = att["color"]; poly.modulate = Color(c[0],c[1],c[2],c[3]) poly.z_index = rank skel.add_child(poly); poly.skeleton = poly.get_path_to(skel) poly_by_slot[String(att["slot"])] = poly; base_rank[String(att["slot"])] = rank; rank += 1 var bw := {} for i in vc: for pair in weights[i]: var bi := int(pair[0]) if not bw.has(bi): var a := PackedFloat32Array(); a.resize(vc); bw[bi] = a bw[bi][i] = float(pair[1]) for bi in bw: poly.add_bone(skel.get_path_to(bones[bi]), bw[bi]) if maxv.x < minv.x: root.free(); return null # нет привязок var size := maxv - minv; var center := (minv + maxv) * 0.5 var fit: float = min(VP.x * 0.55 / max(size.x,1), VP.y * 0.8 / max(size.y,1)) spine.scale = Vector2(fit, -fit) spine.position = Vector2(VP.x*0.5 - center.x*fit, VP.y*0.5 + center.y*fit) # AnimationPlayer var player := AnimationPlayer.new(); player.name = "AnimationPlayer"; root.add_child(player) player.root_node = player.get_path_to(skel) var lib := AnimationLibrary.new() var bone_names: Array = anims["bones"]; var slot_names: Array = anims.get("slots", []) for anim_name in anims["animations"]: var a: Dictionary = anims["animations"][anim_name] var frames: Array = a["frames"]; var nf := frames.size() var anim := Animation.new(); anim.length = max(float(a["duration"]), 0.001); anim.loop_mode = Animation.LOOP_LINEAR for bi in bone_names.size(): var bn: String = bone_names[bi] if not by_name.has(bn): continue var rel := String(skel.get_path_to(by_name[bn])) var f0: Array = frames[0][bi]; var pv := false; var rv := false; var sv := false for fr in range(1, nf): var v: Array = frames[fr][bi] if not pv and (not is_equal_approx(v[0],f0[0]) or not is_equal_approx(v[1],f0[1])): pv = true if not rv and not is_equal_approx(v[2],f0[2]): rv = true if not sv and (not is_equal_approx(v[3],f0[3]) or not is_equal_approx(v[4],f0[4])): sv = true _track(anim, rel+":position", frames, bi, nf, 0, pv) _track(anim, rel+":rotation", frames, bi, nf, 1, rv) _track(anim, rel+":scale", frames, bi, nf, 2, sv) var draworder: Array = a.get("draworder", []) if not draworder.is_empty() and not slot_names.is_empty(): var ranks := {} for nm in poly_by_slot: ranks[nm] = [] for fr in nf: var r := 0 for si in draworder[fr]: var sn: String = slot_names[si] if poly_by_slot.has(sn): ranks[sn].append(r); r += 1 for sn in ranks: var seq: Array = ranks[sn]; var vary := false for v in seq: if v != base_rank[sn]: vary = true; break if not vary or seq.size() != nf: continue var rel := String(skel.get_path_to(poly_by_slot[sn])) var ti := anim.add_track(Animation.TYPE_VALUE) anim.track_set_path(ti, rel + ":z_index") anim.value_track_set_update_mode(ti, Animation.UPDATE_DISCRETE) anim.track_set_interpolation_type(ti, Animation.INTERPOLATION_NEAREST) for fr in nf: anim.track_insert_key(ti, fr / FPS, seq[fr]) lib.add_animation(anim_name, anim) player.add_animation_library("", lib) return root func _track(anim: Animation, path: String, frames: Array, bi: int, nf: int, kind: int, vary: bool): if not vary: return var ti := anim.add_track(Animation.TYPE_VALUE) anim.track_set_path(ti, path); anim.track_set_interpolation_type(ti, Animation.INTERPOLATION_LINEAR) for fr in nf: var v: Array = frames[fr][bi]; var val match kind: 0: val = Vector2(v[0], v[1]) 1: val = deg_to_rad(float(v[2])) 2: val = Vector2(v[3], v[4]) anim.track_insert_key(ti, fr / FPS, val) func _set_owner(node: Node, owner: Node): for ch in node.get_children(): ch.owner = owner; _set_owner(ch, owner)