Files
gig-line/Tools/sibling_asset_audit.py
T
zwitschi 5e5e5ae9b4 refs: survey the sibling project and recalibrate the hero facade
Tools/sibling_asset_audit.py reads the sibling security-game project's prop
library straight from the GLB headers: 20 street props, 11.8k triangles, every
one with a Blender source. The 50 .blend sources are copied to
Art/props/blender_src/ with a manifest; nothing is imported into Unreal yet. Its
building meshes are deliberately not taken - they are Godot GLBs built around a
29.6 m facade that this project measured at 25.75 m.

The facade proportions were wrong against the photograph: 5.7 m of blank wall
above the capitals. Entablature is now 3.4 m, parapet 2.2 m, with architrave and
cornice bands and square pedestals, so the elevation reads as layered stone.
2026-11-07 12:20:41 +01:00

108 lines
4.2 KiB
Python

# Audit the sibling Gig Line project for assets this project can reuse.
#
# python3 Tools/sibling_asset_audit.py # summary
# python3 Tools/sibling_asset_audit.py --manifest # write Art/props/reuse-manifest.json
#
# The sibling project (…/06-games/security-game) is a Godot 4 security game by
# the same author that researched this block first. Its street props are small,
# generic and far cheaper to reuse than to remodel; its building meshes are not
# reusable because they are Godot GLBs built to a different contract.
#
# What this tool does NOT do: copy anything, decide licence, or assume a prop
# matches this project's proportions. It reports name, size, format and whether
# a Blender source exists, so the copy list can be chosen deliberately.
import argparse
import json
import os
import struct
import sys
SIBLING = "/run/media/system/DATA/Documents/06-games/security-game"
PROPS_GLB = os.path.join(SIBLING, "assets/models/blender/props")
SRC_BLEND = os.path.join(SIBLING, "assets/models/blender_src")
OUT = "Art/props/reuse-manifest.json"
def glb_stats(path):
"""Triangle count and bounds from a GLB, without any dependency."""
with open(path, "rb") as f:
data = f.read()
if data[:4] != b"glTF":
return None
json_len = struct.unpack("<I", data[12:16])[0]
doc = json.loads(data[20 : 20 + json_len].decode("utf-8"))
tris = 0
for mesh in doc.get("meshes", []):
for prim in mesh.get("primitives", []):
idx = prim.get("indices")
if idx is not None:
tris += doc["accessors"][idx]["count"] // 3
# bounds across all positions
mins = [1e9] * 3
maxs = [-1e9] * 3
for mesh in doc.get("meshes", []):
for prim in mesh.get("primitives", []):
acc = prim.get("attributes", {}).get("POSITION")
if acc is None:
continue
a = doc["accessors"][acc]
if "min" in a and "max" in a:
mins = [min(m, v) for m, v in zip(mins, a["min"])]
maxs = [max(m, v) for m, v in zip(maxs, a["max"])]
size = [round(maxs[i] - mins[i], 3) for i in range(3)] if mins[0] < 1e8 else None
return {"triangles": tris, "size_gltf": size, "materials": len(doc.get("materials", []))}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--manifest", action="store_true")
args = ap.parse_args()
if not os.path.isdir(PROPS_GLB):
sys.exit(f"missing {PROPS_GLB}")
rows = []
for name in sorted(os.listdir(PROPS_GLB)):
if not name.endswith(".glb"):
continue
stem = name[:-4]
path = os.path.join(PROPS_GLB, name)
stats = glb_stats(path) or {}
source = os.path.join(SRC_BLEND, stem + ".blend")
rows.append({
"prop": stem,
"glb": name,
"bytes": os.path.getsize(path),
"blend_source": os.path.basename(source) if os.path.exists(source) else None,
"blend_bytes": os.path.getsize(source) if os.path.exists(source) else None,
**stats,
})
for r in rows:
src = "blend" if r["blend_source"] else "glb-only"
size = r.get("size_gltf")
dims = "x".join(f"{v:.2f}" for v in size) if size else "?"
print(f"{r['prop']:18} {r['triangles']:5} tris {dims:>18} glTF m "
f"mats {r.get('materials', 0)} {src}")
print(f"\n{len(rows)} props, {sum(r['bytes'] for r in rows)//1024} KB of GLB, "
f"{sum(r['triangles'] for r in rows)} triangles total")
print("glTF is Y-up and Blender is Z-up: the exporter swaps Y/Z, so `size_gltf` "
"lists X, height, depth — not X, Y, Z as authored.")
if args.manifest:
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w") as f:
json.dump({
"source_project": SIBLING,
"scanned": "assets/models/blender/props + assets/models/blender_src",
"licence": "project-owned (same author); confirm before shipping",
"note": "audit only — nothing has been copied",
"props": rows,
}, f, indent=2)
print(f"wrote {OUT}")
if __name__ == "__main__":
main()