BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / scripts / edit_default_female.py

edit_default_female.py

edit_default_female.py 4.9 KiB Download Raw
1#!/usr/bin/env python3
2"""Asset edit of Tim's "Animated Female Base Mesh" for the bundled default
3female (design.md, t1018u/t1020u): drop the stray animation that resets the
4armature rotation and the hair mesh (material ponytail01) with its material,
5texture, image and data, then compact the .bin. Usage: SRC_DIR DST_DIR"""
6import json, os, shutil, sys
7
8src, dst = sys.argv[1], sys.argv[2]
9g = json.load(open(os.path.join(src, "female.gltf")))
10blob = open(os.path.join(src, g["buffers"][0]["uri"]), "rb").read()
11
12g["animations"] = [a for a in g["animations"] if "ponytail" not in a["name"]]
13hair_mat = next(i for i, m in enumerate(g["materials"]) if m.get("name") == "ponytail01")
14hair_meshes = {i for i, m in enumerate(g["meshes"]) if any(p.get("material") == hair_mat for p in m["primitives"])}
15assert len(hair_meshes) == 1 and all(p.get("material") == hair_mat for p in g["meshes"][next(iter(hair_meshes))]["primitives"])
16for n in g["nodes"]:
17 if n.get("mesh") in hair_meshes:
18 del n["mesh"]
19 n.pop("skin", None)
20
21def remap(items, keep):
22 new = {old: i for i, old in enumerate(keep)}
23 return [items[i] for i in keep], new
24
25# meshes
26keep = [i for i in range(len(g["meshes"])) if i not in hair_meshes]
27g["meshes"], m_map = remap(g["meshes"], keep)
28for n in g["nodes"]:
29 if "mesh" in n: n["mesh"] = m_map[n["mesh"]]
30# materials / textures / images
31keep = [i for i in range(len(g["materials"])) if i != hair_mat]
32used_tex = set()
33def textures_of(m):
34 out = []
35 def walk(o):
36 if isinstance(o, dict):
37 if "index" in o and set(o) & {"index"} and ("texCoord" in o or len(o) <= 3): out.append(o)
38 for v in o.values(): walk(v)
39 elif isinstance(o, list):
40 for v in o: walk(v)
41 walk(m); return out
42g["materials"], mat_map = remap(g["materials"], keep)
43for m in g["meshes"]:
44 for p in m["primitives"]:
45 if "material" in p: p["material"] = mat_map[p["material"]]
46refs = [r for m in g["materials"] for r in textures_of(m)]
47tkeep = sorted({r["index"] for r in refs})
48g["textures"], t_map = remap(g["textures"], tkeep)
49for r in refs: r["index"] = t_map[r["index"]]
50ikeep = sorted({t["source"] for t in g["textures"]})
51dropped_images = [g["images"][i]["uri"] for i in range(len(g["images"])) if i not in ikeep]
52g["images"], i_map = remap(g["images"], ikeep)
53for t in g["textures"]: t["source"] = i_map[t["source"]]
54# accessors still referenced
55used_acc = set()
56for m in g["meshes"]:
57 for p in m["primitives"]:
58 used_acc |= set(p["attributes"].values())
59 if "indices" in p: used_acc.add(p["indices"])
60 for t in p.get("targets", []): used_acc |= set(t.values())
61for s in g.get("skins", []):
62 if "inverseBindMatrices" in s: used_acc.add(s["inverseBindMatrices"])
63for a in g["animations"]:
64 for s in a["samplers"]: used_acc |= {s["input"], s["output"]}
65akeep = sorted(used_acc)
66g["accessors"], a_map = remap(g["accessors"], akeep)
67for m in g["meshes"]:
68 for p in m["primitives"]:
69 p["attributes"] = {k: a_map[v] for k, v in p["attributes"].items()}
70 if "indices" in p: p["indices"] = a_map[p["indices"]]
71for s in g.get("skins", []):
72 if "inverseBindMatrices" in s: s["inverseBindMatrices"] = a_map[s["inverseBindMatrices"]]
73for a in g["animations"]:
74 for s in a["samplers"]: s["input"], s["output"] = a_map[s["input"]], a_map[s["output"]]
75# compact bin: every kept accessor gets its own tightly packed buffer view
76# (the source packs all meshes into a few shared views).
77comp = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4}
78ncomp = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}
79assert not any("bufferView" in im for im in g["images"])
80out = bytearray(); views = []
81for a in g["accessors"]:
82 if "bufferView" not in a: continue
83 v = g["bufferViews"][a["bufferView"]]
84 size = comp[a["componentType"]] * ncomp[a["type"]]
85 stride = v.get("byteStride", size)
86 base = v.get("byteOffset", 0) + a.get("byteOffset", 0)
87 data = b"".join(blob[base + k * stride: base + k * stride + size] for k in range(a["count"]))
88 while len(out) % 4: out.append(0)
89 nv = {"buffer": 0, "byteOffset": len(out), "byteLength": len(data)}
90 if "target" in v: nv["target"] = v["target"]
91 views.append(nv); out += data
92 a["bufferView"] = len(views) - 1; a.pop("byteOffset", None)
93g["bufferViews"] = views
94g["buffers"][0]["byteLength"] = len(out)
95g["buffers"][0]["uri"] = "female.bin"
96os.makedirs(os.path.join(dst, "textures"), exist_ok=True)
97json.dump(g, open(os.path.join(dst, "female.gltf"), "w"), indent=2)
98open(os.path.join(dst, "female.bin"), "wb").write(out)
99for im in g["images"]:
100 shutil.copyfile(os.path.join(src, im["uri"]), os.path.join(dst, im["uri"]))
101for im in dropped_images:
102 p = os.path.join(dst, im)
103 if os.path.exists(p): os.remove(p)
104shutil.copyfile(os.path.join(src, "license.txt"), os.path.join(dst, "license.txt"))
105print("bin", len(blob), "->", len(out), "dropped images", dropped_images)