Files
VoxelForge/Standalone/Research/WorldAssemblerM2/RenderSharpMesherCaptures.py
Fr0zkaandClaude Opus 5.5 a3b7d2015e wip(assembler): M2 round 2 -- same-LOD borders watertight for MC/hybrid/DC; LOD0/1 transitions still open
Shared halo + deterministic border-quad ownership + T-junction conformance: 625/625 same-LOD for all three.
LOD 2:1 prototype (collar zipper) fails MC 5 / hybrid 38 / DC 40 cases (corners, overhangs).
Hybrid is bit-identical to MC on a sampled native SurfaceWorld cliff; DC rougher. Standalone only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-10-10 03:56:28 +02:00

222 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""Render the standalone spike's exported meshes from matched orthographic views."""
from __future__ import annotations
import math
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent
RESULTS = ROOT / "results"
CAPTURES = RESULTS / "captures"
CAPTURES.mkdir(parents=True, exist_ok=True)
METHODS = ("MC", "Hybrid", "DC")
PIECES = (
("corner90", "90 degree corner"),
("doorway", "doorway"),
("riser1m", "one metre riser"),
("cliff_overhang", "cliff ledge / overhang"),
("organic_cave_chamber", "organic cave chamber wall"),
("native_surface_cliff", "sampled SurfaceWorld cliff"),
)
WIDTH, HEIGHT = 288, 216
BG = (239, 242, 244)
INK = (36, 44, 51)
SEAM_FINE = (73, 133, 174)
SEAM_COARSE = (188, 112, 68)
def normalize(v):
n = math.sqrt(sum(c * c for c in v))
return tuple(c / n for c in v) if n > 1.0e-12 else (0.0, 0.0, 1.0)
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def cross(a, b):
return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
def read_obj(path: Path):
vertices, normals, faces = [], [], []
for line in path.read_text(encoding="ascii").splitlines():
if line.startswith("v "):
vertices.append(tuple(map(float, line.split()[1:4])))
elif line.startswith("vn "):
normals.append(tuple(map(float, line.split()[1:4])))
elif line.startswith("f "):
face = []
for item in line.split()[1:4]:
fields = item.split("//")
face.append((int(fields[0]) - 1, int(fields[1]) - 1 if len(fields) > 1 else int(fields[0]) - 1))
faces.append(tuple(face))
return vertices, normals, faces
def project_mesh(vertices, faces, seam=False, target=None):
# One view is reused for every candidate in a row: azimuth 38°, elevation 27°.
if target is None:
if vertices:
bounds_min = tuple(min(v[i] for v in vertices) for i in range(3))
bounds_max = tuple(max(v[i] for v in vertices) for i in range(3))
target = tuple((bounds_min[i] + bounds_max[i]) * 0.5 for i in range(3))
else:
target = (0.0, 0.0, 0.0)
view_dir = normalize((8.0, -10.0, 7.0))
forward = tuple(-c for c in view_dir)
right = normalize(cross(forward, (0.0, 0.0, 1.0)))
up = normalize(cross(right, forward))
projected_bounds = []
for point in vertices:
rel = tuple(point[i] - target[i] for i in range(3))
projected_bounds.append((dot(rel, right), dot(rel, up)))
span_x = max((point[0] for point in projected_bounds), default=0.0) - min((point[0] for point in projected_bounds), default=0.0)
span_y = max((point[1] for point in projected_bounds), default=0.0) - min((point[1] for point in projected_bounds), default=0.0)
scale = min(20.2, 0.84 * min((WIDTH - 24) / max(span_x, 1.0e-6), (HEIGHT - 24) / max(span_y, 1.0e-6)))
projected = []
for p in vertices:
rel = tuple(p[i] - target[i] for i in range(3))
projected.append((WIDTH * 0.5 + dot(rel, right) * scale,
HEIGHT * 0.51 - dot(rel, up) * scale,
dot(rel, forward)))
return projected
def raster(path: Path, color_by_x=False):
vertices, normals, faces = read_obj(path)
if color_by_x:
clipped_vertices, clipped_normals, clipped_faces = [], [], []
for face in faces:
center_x = sum(vertices[i][0] for i, _ in face) / 3.0
left = center_x < 0.0
polygon = [(vertices[vi], normals[ni] if ni < len(normals) else (0.0, 0.0, 1.0)) for vi, ni in face]
clipped = []
for i, (pa, na) in enumerate(polygon):
pb, nb = polygon[(i + 1) % len(polygon)]
inside_a = pa[0] <= 0.0 if left else pa[0] >= 0.0
inside_b = pb[0] <= 0.0 if left else pb[0] >= 0.0
if inside_a:
clipped.append((pa, na))
if inside_a != inside_b:
t = (0.0 - pa[0]) / (pb[0] - pa[0])
point = tuple(pa[k] + t * (pb[k] - pa[k]) for k in range(3))
normal = normalize(tuple(na[k] + t * (nb[k] - na[k]) for k in range(3)))
clipped.append((point, normal))
if len(clipped) < 3:
continue
first = len(clipped_vertices)
for point, normal in clipped:
clipped_vertices.append(point)
clipped_normals.append(normal)
for i in range(1, len(clipped) - 1):
clipped_faces.append(((first, first), (first + i, first + i), (first + i + 1, first + i + 1)))
vertices, normals, faces = clipped_vertices, clipped_normals, clipped_faces
bounds_min = tuple(min(v[i] for v in vertices) for i in range(3))
bounds_max = tuple(max(v[i] for v in vertices) for i in range(3))
target = tuple((bounds_min[i] + bounds_max[i]) * 0.5 for i in range(3))
projected = project_mesh(vertices, faces, color_by_x, target)
pixels = Image.new("RGB", (WIDTH, HEIGHT), BG)
zbuf = [float("inf")] * (WIDTH * HEIGHT)
px = pixels.load()
light = normalize((-0.45, -0.55, 0.75))
for face in faces:
ids = [v for v, _ in face]
points = [projected[i] for i in ids]
x0 = max(0, int(math.floor(min(p[0] for p in points))))
x1 = min(WIDTH - 1, int(math.ceil(max(p[0] for p in points))))
y0 = max(0, int(math.floor(min(p[1] for p in points))))
y1 = min(HEIGHT - 1, int(math.ceil(max(p[1] for p in points))))
if x0 > x1 or y0 > y1:
continue
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = points
denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
if abs(denom) < 1.0e-9:
continue
face_normals = [normals[ni] if ni < len(normals) else (0.0, 0.0, 1.0) for _, ni in face]
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
sx, sy = x + 0.5, y + 0.5
u = ((by - cy) * (sx - cx) + (cx - bx) * (sy - cy)) / denom
v = ((cy - ay) * (sx - cx) + (ax - cx) * (sy - cy)) / denom
w = 1.0 - u - v
if u < -1.0e-8 or v < -1.0e-8 or w < -1.0e-8:
continue
depth = u * az + v * bz + w * cz
idx = y * WIDTH + x
if depth >= zbuf[idx]:
continue
zbuf[idx] = depth
n = normalize(tuple(u * face_normals[0][k] + v * face_normals[1][k] + w * face_normals[2][k] for k in range(3)))
shade = 0.36 + 0.64 * max(0.0, dot(n, light))
if color_by_x:
center_x = sum(vertices[i][0] for i in ids) / 3.0
base = SEAM_FINE if center_x < 0.0 else SEAM_COARSE
else:
base = (130, 145, 151)
px[x, y] = tuple(max(0, min(255, int(c * shade))) for c in base)
if color_by_x:
draw = ImageDraw.Draw(pixels)
# Project a visible segment on the LOD boundary plane, x=0.
seam_world = [(-0.0, y, z) for y, z in ((-4, -4), (4, -4), (4, 4), (-4, 4), (-4, -4))]
seam_points = project_mesh(seam_world, [], True, target)
draw.line([(p[0], p[1]) for p in seam_points], fill=(181, 55, 49), width=2)
return pixels
def capture_title(image: Image.Image, title: str, subtitle: str):
canvas = Image.new("RGB", (WIDTH, HEIGHT + 30), BG)
canvas.paste(image, (0, 30))
draw = ImageDraw.Draw(canvas)
draw.rectangle((0, 0, WIDTH, 29), fill=(31, 42, 50))
draw.text((8, 4), title, fill=(255, 255, 255))
draw.text((8, 16), subtitle, fill=(196, 210, 218))
return canvas
def main():
captures = []
for key, label in PIECES:
row = []
for method in METHODS:
obj = RESULTS / "meshes" / f"{key}_{method}.obj"
image = capture_title(raster(obj), label, f"{method} | matched orthographic view")
path = CAPTURES / f"{key}_{method}.jpg"
image.save(path, quality=86, optimize=True)
row.append(image)
captures.append(row)
seam_row = []
for method in METHODS:
obj = RESULTS / "meshes" / f"lodseam_cliff_crossing_{method}.obj"
image = capture_title(raster(obj, color_by_x=True), "Sharp cliff across LOD border", f"{method} | LOD0 left, LOD1 right, x=0")
path = CAPTURES / f"lodseam_cliff_crossing_{method}.jpg"
image.save(path, quality=86, optimize=True)
seam_row.append(image)
captures.append(seam_row)
sheet = Image.new("RGB", (WIDTH * 3, (HEIGHT + 30) * len(captures)), (221, 227, 230))
draw = ImageDraw.Draw(sheet)
for row_index, row in enumerate(captures):
for col_index, image in enumerate(row):
x, y = col_index * WIDTH, row_index * (HEIGHT + 30)
sheet.paste(image, (x, y))
draw.rectangle((x, y, x + WIDTH - 1, y + HEIGHT + 29), outline=(73, 83, 89), width=1)
sheet_path = CAPTURES / "contact_sheet.jpg"
sheet.save(sheet_path, quality=86, optimize=True)
oversized = []
for path in sorted(CAPTURES.glob("*.jpg")):
size = path.stat().st_size
print(f"capture file={path.name} bytes={size}")
if size >= 300_000:
oversized.append((path.name, size))
if oversized:
raise RuntimeError(f"captures exceed 300 KB: {oversized}")
print(f"contact_sheet={sheet_path} bytes={sheet_path.stat().st_size} image_count={len(list(CAPTURES.glob('*.jpg')))}")
if __name__ == "__main__":
main()