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.
This commit is contained in:
zwitschi committed 2026-11-07 12:20:41 +01:00
1 parent 52a736bc9f
commit 5e5e5ae9b4
67 files changed
+591 -27

No files matched your search

+15 -4
View File
@@ -43,11 +43,15 @@ COLUMN_BAY = 4.96
FRONTAGE_Y = 0.0 # front plane sits at the building line of the block frame
# --- assumptions -------------------------------------------------------------
# Calibrated against the 2009 façade photograph: the entablature starts a little
# over half the total height, and the parapet carries the motto at about three
# quarters. The first pass had the enframed wall carrying 5.7 m of blank stone
# above the capitals, which read as a warehouse rather than a temple.
PEDESTAL_H = 1.45 # column pedestal top; entrance platform sits on it
COLUMN_D = 0.95 # Ionic shaft diameter at the base
CAPITAL_H = 0.95
ENTABLATURE_H = 2.20
PARAPET_H = 1.60
ENTABLATURE_H = 3.40 # band between the capitals and the motto panel
PARAPET_H = 2.20 # motto panel and cornice above the entablature
PORTAL_W = 3.10 # each of the three door openings
PORTAL_H = 3.40
STEP_TREAD = 0.34
@@ -127,8 +131,8 @@ def build():
for i in range(COLUMN_COUNT):
x = -span / 2.0 + i * COLUMN_BAY
col_xs.append(x)
cylinder(f"Hero_Pedestal_{i + 1}", x, -0.55, PEDESTAL_H, PEDESTAL_H + 0.55,
0.62, stone, vertices=16)
cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27,
1.24, 1.24, 0.54, stone)
cylinder(f"Hero_Column_{i + 1}", x, -0.55, PEDESTAL_H + 0.55,
PEDESTAL_H + 0.55 + (HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H - 0.55 - CAPITAL_H),
COLUMN_D / 2.0, stone)
@@ -140,6 +144,13 @@ def build():
# --- entablature and parapet ------------------------------------------
ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H
# Lower entablature band (architrave) standing proud of the wall, so the
# elevation reads as layered stone rather than one flat plane.
cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone)
note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands")
# Cornice under the parapet.
cube("Hero_Cornice", 0.0, -1.6, HEIGHT - PARAPET_H - 0.18, FRONTAGE_W, 1.75, 0.36, stone)
note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line")
cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0,
FRONTAGE_W, 1.5, ENTABLATURE_H, stone)
note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth")
+107
View File
@@ -0,0 +1,107 @@
# 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()