| 1 | #!/usr/bin/env python3 |
| 2 | """Asset edit of Tim's "Animated Female Base Mesh" for the bundled default |
| 3 | female (design.md, t1018u/t1020u): drop the stray animation that resets the |
| 4 | armature rotation and the hair mesh (material ponytail01) with its material, |
| 5 | texture, image and data, then compact the .bin. Usage: SRC_DIR DST_DIR""" |
| 6 | import json, os, shutil, sys |
| 7 | |
| 8 | src, dst = sys.argv[1], sys.argv[2] |
| 9 | g = json.load(open(os.path.join(src, "female.gltf"))) |
| 10 | blob = open(os.path.join(src, g["buffers"][0]["uri"]), "rb").read() |
| 11 | |
| 12 | g["animations"] = [a for a in g["animations"] if "ponytail" not in a["name"]] |
| 13 | hair_mat = next(i for i, m in enumerate(g["materials"]) if m.get("name") == "ponytail01") |
| 14 | hair_meshes = {i for i, m in enumerate(g["meshes"]) if any(p.get("material") == hair_mat for p in m["primitives"])} |
| 15 | assert len(hair_meshes) == 1 and all(p.get("material") == hair_mat for p in g["meshes"][next(iter(hair_meshes))]["primitives"]) |
| 16 | for n in g["nodes"]: |
| 17 | if n.get("mesh") in hair_meshes: |
| 18 | del n["mesh"] |
| 19 | n.pop("skin", None) |
| 20 | |
| 21 | def 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 |
| 26 | keep = [i for i in range(len(g["meshes"])) if i not in hair_meshes] |
| 27 | g["meshes"], m_map = remap(g["meshes"], keep) |
| 28 | for n in g["nodes"]: |
| 29 | if "mesh" in n: n["mesh"] = m_map[n["mesh"]] |
| 30 | # materials / textures / images |
| 31 | keep = [i for i in range(len(g["materials"])) if i != hair_mat] |
| 32 | used_tex = set() |
| 33 | def 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 |
| 42 | g["materials"], mat_map = remap(g["materials"], keep) |
| 43 | for m in g["meshes"]: |
| 44 | for p in m["primitives"]: |
| 45 | if "material" in p: p["material"] = mat_map[p["material"]] |
| 46 | refs = [r for m in g["materials"] for r in textures_of(m)] |
| 47 | tkeep = sorted({r["index"] for r in refs}) |
| 48 | g["textures"], t_map = remap(g["textures"], tkeep) |
| 49 | for r in refs: r["index"] = t_map[r["index"]] |
| 50 | ikeep = sorted({t["source"] for t in g["textures"]}) |
| 51 | dropped_images = [g["images"][i]["uri"] for i in range(len(g["images"])) if i not in ikeep] |
| 52 | g["images"], i_map = remap(g["images"], ikeep) |
| 53 | for t in g["textures"]: t["source"] = i_map[t["source"]] |
| 54 | # accessors still referenced |
| 55 | used_acc = set() |
| 56 | for 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()) |
| 61 | for s in g.get("skins", []): |
| 62 | if "inverseBindMatrices" in s: used_acc.add(s["inverseBindMatrices"]) |
| 63 | for a in g["animations"]: |
| 64 | for s in a["samplers"]: used_acc |= {s["input"], s["output"]} |
| 65 | akeep = sorted(used_acc) |
| 66 | g["accessors"], a_map = remap(g["accessors"], akeep) |
| 67 | for 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"]] |
| 71 | for s in g.get("skins", []): |
| 72 | if "inverseBindMatrices" in s: s["inverseBindMatrices"] = a_map[s["inverseBindMatrices"]] |
| 73 | for 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). |
| 77 | comp = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4} |
| 78 | ncomp = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16} |
| 79 | assert not any("bufferView" in im for im in g["images"]) |
| 80 | out = bytearray(); views = [] |
| 81 | for 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) |
| 93 | g["bufferViews"] = views |
| 94 | g["buffers"][0]["byteLength"] = len(out) |
| 95 | g["buffers"][0]["uri"] = "female.bin" |
| 96 | os.makedirs(os.path.join(dst, "textures"), exist_ok=True) |
| 97 | json.dump(g, open(os.path.join(dst, "female.gltf"), "w"), indent=2) |
| 98 | open(os.path.join(dst, "female.bin"), "wb").write(out) |
| 99 | for im in g["images"]: |
| 100 | shutil.copyfile(os.path.join(src, im["uri"]), os.path.join(dst, im["uri"])) |
| 101 | for im in dropped_images: |
| 102 | p = os.path.join(dst, im) |
| 103 | if os.path.exists(p): os.remove(p) |
| 104 | shutil.copyfile(os.path.join(src, "license.txt"), os.path.join(dst, "license.txt")) |
| 105 | print("bin", len(blob), "->", len(out), "dropped images", dropped_images) |