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.
108 lines
4.2 KiB
Python
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()
|