#!/usr/bin/env python3 """Asset edit of Tim's "Animated Female Base Mesh" for the bundled default female (design.md, t1018u/t1020u): drop the stray animation that resets the armature rotation and the hair mesh (material ponytail01) with its material, texture, image and data, then compact the .bin. Usage: SRC_DIR DST_DIR""" import json, os, shutil, sys src, dst = sys.argv[1], sys.argv[2] g = json.load(open(os.path.join(src, "female.gltf"))) blob = open(os.path.join(src, g["buffers"][0]["uri"]), "rb").read() g["animations"] = [a for a in g["animations"] if "ponytail" not in a["name"]] hair_mat = next(i for i, m in enumerate(g["materials"]) if m.get("name") == "ponytail01") hair_meshes = {i for i, m in enumerate(g["meshes"]) if any(p.get("material") == hair_mat for p in m["primitives"])} assert len(hair_meshes) == 1 and all(p.get("material") == hair_mat for p in g["meshes"][next(iter(hair_meshes))]["primitives"]) for n in g["nodes"]: if n.get("mesh") in hair_meshes: del n["mesh"] n.pop("skin", None) def remap(items, keep): new = {old: i for i, old in enumerate(keep)} return [items[i] for i in keep], new # meshes keep = [i for i in range(len(g["meshes"])) if i not in hair_meshes] g["meshes"], m_map = remap(g["meshes"], keep) for n in g["nodes"]: if "mesh" in n: n["mesh"] = m_map[n["mesh"]] # materials / textures / images keep = [i for i in range(len(g["materials"])) if i != hair_mat] used_tex = set() def textures_of(m): out = [] def walk(o): if isinstance(o, dict): if "index" in o and set(o) & {"index"} and ("texCoord" in o or len(o) <= 3): out.append(o) for v in o.values(): walk(v) elif isinstance(o, list): for v in o: walk(v) walk(m); return out g["materials"], mat_map = remap(g["materials"], keep) for m in g["meshes"]: for p in m["primitives"]: if "material" in p: p["material"] = mat_map[p["material"]] refs = [r for m in g["materials"] for r in textures_of(m)] tkeep = sorted({r["index"] for r in refs}) g["textures"], t_map = remap(g["textures"], tkeep) for r in refs: r["index"] = t_map[r["index"]] ikeep = sorted({t["source"] for t in g["textures"]}) dropped_images = [g["images"][i]["uri"] for i in range(len(g["images"])) if i not in ikeep] g["images"], i_map = remap(g["images"], ikeep) for t in g["textures"]: t["source"] = i_map[t["source"]] # accessors still referenced used_acc = set() for m in g["meshes"]: for p in m["primitives"]: used_acc |= set(p["attributes"].values()) if "indices" in p: used_acc.add(p["indices"]) for t in p.get("targets", []): used_acc |= set(t.values()) for s in g.get("skins", []): if "inverseBindMatrices" in s: used_acc.add(s["inverseBindMatrices"]) for a in g["animations"]: for s in a["samplers"]: used_acc |= {s["input"], s["output"]} akeep = sorted(used_acc) g["accessors"], a_map = remap(g["accessors"], akeep) for m in g["meshes"]: for p in m["primitives"]: p["attributes"] = {k: a_map[v] for k, v in p["attributes"].items()} if "indices" in p: p["indices"] = a_map[p["indices"]] for s in g.get("skins", []): if "inverseBindMatrices" in s: s["inverseBindMatrices"] = a_map[s["inverseBindMatrices"]] for a in g["animations"]: for s in a["samplers"]: s["input"], s["output"] = a_map[s["input"]], a_map[s["output"]] # compact bin: every kept accessor gets its own tightly packed buffer view # (the source packs all meshes into a few shared views). comp = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4} ncomp = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16} assert not any("bufferView" in im for im in g["images"]) out = bytearray(); views = [] for a in g["accessors"]: if "bufferView" not in a: continue v = g["bufferViews"][a["bufferView"]] size = comp[a["componentType"]] * ncomp[a["type"]] stride = v.get("byteStride", size) base = v.get("byteOffset", 0) + a.get("byteOffset", 0) data = b"".join(blob[base + k * stride: base + k * stride + size] for k in range(a["count"])) while len(out) % 4: out.append(0) nv = {"buffer": 0, "byteOffset": len(out), "byteLength": len(data)} if "target" in v: nv["target"] = v["target"] views.append(nv); out += data a["bufferView"] = len(views) - 1; a.pop("byteOffset", None) g["bufferViews"] = views g["buffers"][0]["byteLength"] = len(out) g["buffers"][0]["uri"] = "female.bin" os.makedirs(os.path.join(dst, "textures"), exist_ok=True) json.dump(g, open(os.path.join(dst, "female.gltf"), "w"), indent=2) open(os.path.join(dst, "female.bin"), "wb").write(out) for im in g["images"]: shutil.copyfile(os.path.join(src, im["uri"]), os.path.join(dst, im["uri"])) for im in dropped_images: p = os.path.join(dst, im) if os.path.exists(p): os.remove(p) shutil.copyfile(os.path.join(src, "license.txt"), os.path.join(dst, "license.txt")) print("bin", len(blob), "->", len(out), "dropped images", dropped_images)