wip(assembler): M3 round 2 -- islands get real mass: deep crust, separate tapered roots, arched spans
Pinch cause: SmoothStep taper drove the underside radius to zero at one point. Version 2: depth 0.75x diameter (thin-pad control at 0.30 fails), 3-4 seeded roots, bounded contour warp + roughness, one off-route side island, 3 m rock spans. Bounds/route/W1-W8-W16 pass. Open: legacy probe metadata says 25 cm but production legacy is 12.5 cm -- round 3 must fix the import scale contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
53a1e69af7
commit
4afea7c802
15 files changed
+1007
-99
No files matched your search
@@ -0,0 +1,172 @@
|
||||
# M3 Round 2 — FloatingIslands look and standalone gates
|
||||
|
||||
**Result:** geometry version 2 gives the islands a deep, rough underside with separated tapered
|
||||
roots, variable-height placement, and broad arched connectors. Fresh matched CPU captures include
|
||||
overview, underside, edge/cliff, player-eye and route views. The measurable bounds, thickness,
|
||||
capsule-route and determinism gates pass. The native sample remains much denser than the owner
|
||||
field, so this is a deliberate routeable recipe rather than a visual-parity claim.
|
||||
|
||||
## Owner recipe and scale
|
||||
|
||||
The legacy settings do not request a fixed island count. `IslandDensity=0.5` is a per-hashed-XY-site
|
||||
existence chance. The owner parameters are spacing 112 legacy voxels, radius fields 24–48 legacy
|
||||
voxels, thickness ratio 0.6, vertical jitter/top flatten 0.6, roughness 3, blend radius 6 and boundary
|
||||
seal 4. The production `Source` constant and plugin-root `CLAUDE.md` say a legacy voxel is 12.5 cm;
|
||||
Core uses 25 cm. Round 2 therefore queries the legacy field at twice each Core coordinate and maps
|
||||
owner lengths to Core lengths at 0.5 scale:
|
||||
|
||||
| Setting | Owner | Round 2 Core | Physical intent at production scale |
|
||||
|---|---:|---:|---:|
|
||||
| Site spacing | 112 voxels | 56 voxels | 14 m |
|
||||
| Radius | 24–48 voxels | 12–24 voxels | 3–6 m radius |
|
||||
| Roughness | 3 voxels | 1.5 voxels | 0.375 m amplitude bound |
|
||||
| Blend radius | 6 voxels | 3 voxels | 0.75 m |
|
||||
| Boundary seal | 4 voxels | 2 voxels | 0.5 m |
|
||||
|
||||
There is a scale conflict to resolve before importing this recipe. The frozen owner probe metadata
|
||||
and `Standalone/Source` mirror say 25 cm, and the radius comment describes 12–24 m diameters at that
|
||||
scale. Production `Source` and `CLAUDE.md` say 12.5 cm, which makes the same radius fields 6–12 m
|
||||
diameters. The audit uses the production 12.5 cm field, with legacy anchor `(48, 16, -1904.035)`
|
||||
mapped to Core `(24, 8, -952.016)`. Round 3 must fix the snapshot producer/metadata and publish one
|
||||
import scale contract; it must not silently choose between these interpretations.
|
||||
|
||||
The native planner requires three route-connected primary islands, then uses the 0.5 density roll
|
||||
for optional off-route sites; this seed made one, for four islands and three connectors. That is a
|
||||
stated routeability constraint, not an owner fixed-count setting. Geometry version 2 adds a crust
|
||||
depth bonus of 0.15 radius and a root depth of 0.75 radius to the owner 0.6-ratio body. Total measured
|
||||
depth is 0.75 of island diameter. The deeper roots and guaranteed route account for the local density
|
||||
increase below.
|
||||
|
||||
## Visual review
|
||||
|
||||
The round-1 reading was substantially right. Its low thickness ratio and `SmoothStep` taper drove the
|
||||
underside radius to zero at the bottom plane. The vanishing cross-section made the underside gradient
|
||||
collapse at a shared point and produced the radial crease/pinch. The old profile also had no
|
||||
independent roots, and its main route lay on a nearly straight chain.
|
||||
|
||||
Version 2 gives the crust a broad, gently bowed bottom surface, then smooth-unions three or four
|
||||
seeded ellipsoid roots per island. Their lengths taper independently, so they do not converge at one
|
||||
apex. The bounded 6.3-Core-voxel contour warp breaks the silhouettes; 1.5-voxel surface roughness is
|
||||
faded to zero across a three-voxel surface band. The certified boxes include the contour warp,
|
||||
roughness and blend reach. The underside and edge captures show the additional mass and root lobes.
|
||||
|
||||
The three primary islands form a bent, vertically fitted traversal. This seed also produces one
|
||||
higher side island that is not on the route. There are still three connectors because the route has
|
||||
to cross the three primary sites; they are 3 m wide, 1 m thick at the ends, and their underside sags
|
||||
by up to 2 m at midspan. Each is marked sharp-tag-ready. The captures show a rock-span profile rather
|
||||
than the old narrow straight rods, although the route-connected backbone remains visually legible as
|
||||
a chain. The route-island tops vary by about 0.67 m; the off-route island is about 1.46 m above the
|
||||
middle route island.
|
||||
|
||||
The underside and edge captures are close views of the middle island so the larger cluster does not
|
||||
set their scale. The player-eye view is an offline perspective render from 1.8 m above the route
|
||||
floor, using the 25 cm Core voxel scale and the approximately 1.8 m player height. These are
|
||||
field-mesh review images, not Unreal camera, traversal, or collision evidence.
|
||||
|
||||
| View | Capture |
|
||||
|---|---|
|
||||
| All five matched views | [Contact sheet](results/captures/contact_sheet.jpg) |
|
||||
| Cluster overview | [overview_matched.jpg](results/captures/overview_matched.jpg) |
|
||||
| Underside and roots | [underside_matched.jpg](results/captures/underside_matched.jpg) |
|
||||
| Thick crust and edge/cliff | [edge_cliff_matched.jpg](results/captures/edge_cliff_matched.jpg) |
|
||||
| Player-eye toward next island | [player_eye_matched.jpg](results/captures/player_eye_matched.jpg) |
|
||||
| Planned route | [route_matched.jpg](results/captures/route_matched.jpg) |
|
||||
|
||||
The CPU renderer lowered its process priority. All images are 640×258 pixels individually; the
|
||||
contact sheet is 640×1,290 pixels and 71,668 bytes. Every JPEG is below 300 KB. Legacy/native pairs
|
||||
use the same target, camera and scale. The legacy side remains much smaller because it is the exact
|
||||
stochastic owner field at the same physical anchor, not a hand-expanded parity target.
|
||||
|
||||
## Standalone gates
|
||||
|
||||
The final `Target All` Core build and the audit wrapper passed. The wrapper samples the same 32,768
|
||||
physical query locations for legacy and native timing.
|
||||
|
||||
| Gate | Final result |
|
||||
|---|---:|
|
||||
| Recipe / plan / manifest hashes | `5b8f27274bad89ba` / `ac33c9460b16337a` / `0db30684eaefa75e` |
|
||||
| Bounds | 515 boxes, 13,851 point samples, 74 unknown, **0 missed**; shrunken-box control detected |
|
||||
| Thickness | 4 islands; minimum depth/diameter 0.75 vs 0.70 required; all pass |
|
||||
| Thin-pad control | Ratio 0.2999; all 4 islands fail, control detected |
|
||||
| Route | 28 segments, 254 capsule centers, 69,088 capsule samples, 1,430 support probes; 0 blocked, 0 unsupported, 0 grade failures |
|
||||
| Minimum sampled density clearance | 78 subvoxels (0.305 Core voxel) |
|
||||
| Raised-route control (+8 voxels) | Detected: 254 unsupported centers, 0 blocked |
|
||||
| Field hash at W1 / W8 / W16 | `566b6115f1aaeb71` for all three; repeated plan stable |
|
||||
| Query timing, 32,768 locations | Legacy 64.64 ms; native 38.06 ms (0.589× legacy time, about 1.70× faster) |
|
||||
| Local sign / occupancy difference | 4.437% sign mismatch; 130 legacy-solid vs 1,380 native-solid samples |
|
||||
|
||||
The sample contains 10.6× as many native-solid points as legacy (1,352 native-only and 102
|
||||
legacy-only). This is a local sparse-field sample, not a global volume estimate. It confirms the
|
||||
native density difference visible in the matched images. The extra route backbone, thick crust and
|
||||
deep roots are the stated reasons; the native field is not being presented as owner occupancy parity.
|
||||
|
||||
The audit reparsed the unchanged existing M1 round-1 manifest hash `c87ae279b0e26f53` and
|
||||
SurfaceWorld round-4 manifest hash `8933693d9560ba9f`. No existing Core world evaluator,
|
||||
SurfaceWorld recipe or mesher implementation was edited. The complete M2 mesher hash matrix was not
|
||||
replayed in this round; the M2 report records the prior fixture mesh hash `9d58e0603f1bf9c7` and
|
||||
cliff tile hash `fc7bd7a3cf997992`.
|
||||
|
||||
Full audit data is in [audit.json](results/audit.json); the successful wrapper ledger is
|
||||
[run_ledger.txt](results/run_ledger.txt). Mesh extracts used for the captures and audit are in the
|
||||
same `results` directory.
|
||||
|
||||
## Round 3 — Unreal delivery plan
|
||||
|
||||
1. Reconcile the 12.5 cm production field with the 25 cm frozen-probe metadata at the snapshot
|
||||
producer. Add one explicit owner-to-Core length scale and reject payloads whose declared scale
|
||||
conflicts with it.
|
||||
2. Add the recipe and seeded plan to the existing optional `world_assembly` import path. Parse via
|
||||
`ResolvedWorldSnapshot::FromJson`; validate schema/geometry versions, recipe/plan/manifest hashes,
|
||||
stable IDs, bounds and scale before generation. Preserve round-trip JSON and stable IDs.
|
||||
3. Prove imported generation in a null-RHI BuildHost commandlet. Compare canonical plan/field hashes
|
||||
at W1/W8/W16 and exercise malformed-hash, unknown-version and mismatched-scale controls.
|
||||
4. Build the islands and sharp-tag-ready spans through the selected mesher path. Check chunk borders,
|
||||
LOD transitions, route visibility and finite bounds in Unreal. Keep the Core field available to
|
||||
server queries without requiring render meshes.
|
||||
5. Cook collision and sweep the actual player capsule over the imported route. Check floor support,
|
||||
clearance, grade and connector landings against cooked collision, then hand the owner the imported
|
||||
build, capture set, collision evidence, resolved scale note and exact source/hash manifest.
|
||||
|
||||
Round 2 did not add Unreal import, cooked collision, BuildHost proof or owner hand-off evidence.
|
||||
|
||||
## Run ledger and scope
|
||||
|
||||
The final build used four compile workers at BelowNormal priority and completed in about 51 seconds.
|
||||
The final audit completed in 1.65 seconds and peaked at 58.8 MiB private memory; the CPU renderer
|
||||
completed in about 2 seconds. No run approached the 15-minute limit, no process crash occurred, and
|
||||
there was no `0xC0000142` retry. The build emitted the existing French `vswhere.exe`-not-found
|
||||
warning and `VoxelStrateManager.cpp` double-to-float conversion warnings before succeeding.
|
||||
|
||||
Every failed iteration before that final pass:
|
||||
|
||||
| Failed run | Result and correction |
|
||||
|---|---|
|
||||
| Core `Target All` compile | Failed on one unqualified fixed-scale `Q`; qualified it and rebuilt successfully. |
|
||||
| Audit 1 | One unsupported route center at segment 36. |
|
||||
| Audit 2 | Diagnostic repeat showed support just beyond the 2.5-voxel search depth (solid by 3); extended/adjusted support fitting. |
|
||||
| Audit 3 | Used a stale Core library after source changes and repeated the route failure; rebuilt Core before continuing. |
|
||||
| Audit 4 | Raising the connector deck fixed support but caused 17 blocked capsule samples; reduced the route margin. |
|
||||
| Audit 5 | No blocked samples, but one unsupported center at segment 49 near a connector end; tapered endpoint detail. |
|
||||
| Audit 6 | After converting owner dimensions to production physical scale, the old 0.62-radius landing produced 17 blocked and 9 unsupported samples. |
|
||||
| Audit 7 | Top-detail attenuation alone left those same counts. |
|
||||
| Audit 8 | Diagnostic repeat located the route near a root-lobe edge; moving the landing to 0.38 radius and using a 1-voxel deck lift fixed it. |
|
||||
| Renderer run | The first renderer pass stopped after four views when a raster row reached `HEIGHT`; clamped the row to `HEIGHT - 1`, then regenerated all five captures successfully. |
|
||||
|
||||
The repeated pre-final audit processes peaked at 44.6–58.8 MiB. No editor, viewer or GPU was
|
||||
launched. No owner project file, `.uasset` or `Source/` file was opened for writing or changed; no
|
||||
project build was run. No commit, push or stash was made.
|
||||
|
||||
### Source inventory
|
||||
|
||||
New source file in this round:
|
||||
|
||||
- `RenderFloatingIslandRound2.py` — CPU-only matched capture renderer.
|
||||
|
||||
Modified source files:
|
||||
|
||||
- `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`
|
||||
- `Standalone/Core/src/FloatingIslandsInternal.h`
|
||||
- `Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp` (extended for round-2 physical-scale and visual audits)
|
||||
|
||||
Documentation and dialogue changes are in `Standalone/Core/API.md`, `Standalone/DESIGN_WORLD_ASSEMBLER.md`,
|
||||
`CODEMAP.md` and `Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md`.
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small CPU-only matched views of the legacy and native FloatingIslands fields."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
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)
|
||||
WIDTH, HEIGHT = 320, 226
|
||||
BACKGROUND = (236, 240, 242)
|
||||
LEGACY_COLOR = (133, 127, 105)
|
||||
NATIVE_COLOR = (78, 135, 151)
|
||||
ROUTE_COLOR = (223, 72, 45)
|
||||
TILE_COLOR = (178, 60, 52)
|
||||
LOD_COLOR = (211, 136, 45)
|
||||
Q = 256.0
|
||||
|
||||
|
||||
def lower_priority() -> None:
|
||||
if hasattr(ctypes, "windll"):
|
||||
ctypes.windll.kernel32.SetPriorityClass(ctypes.windll.kernel32.GetCurrentProcess(), 0x00004000)
|
||||
|
||||
|
||||
def read_mesh(path: Path):
|
||||
raw = path.read_bytes()
|
||||
if len(raw) < 8 or raw[:4] != b"VFMI":
|
||||
raise RuntimeError(f"invalid field mesh: {path}")
|
||||
count = struct.unpack_from("<I", raw, 4)[0]
|
||||
stride = struct.calcsize("<12f")
|
||||
if len(raw) != 8 + count * stride:
|
||||
raise RuntimeError(f"truncated field mesh: {path}")
|
||||
triangles = [struct.unpack_from("<12f", raw, 8 + i * stride) for i in range(count)]
|
||||
vertices = [tri[0:3] for tri in triangles]
|
||||
vertices.extend(tri[3:6] for tri in triangles)
|
||||
vertices.extend(tri[6:9] for tri in triangles)
|
||||
return triangles, vertices
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
size = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / size for value in vector) if size > 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 project(points, target, view_dir, scale=None):
|
||||
direction = normalize(view_dir)
|
||||
forward = tuple(-value for value in direction)
|
||||
reference_up = (0.0, 1.0, 0.0) if abs(forward[2]) > 0.98 else (0.0, 0.0, 1.0)
|
||||
right = normalize(cross(forward, reference_up))
|
||||
up = normalize(cross(right, forward))
|
||||
camera_points = []
|
||||
for point in points:
|
||||
delta = tuple(point[i] - target[i] for i in range(3))
|
||||
camera_points.append((dot(delta, right), dot(delta, up), dot(delta, forward)))
|
||||
if scale is None:
|
||||
span_x = max((p[0] for p in camera_points), default=1.0) - min((p[0] for p in camera_points), default=0.0)
|
||||
span_y = max((p[1] for p in camera_points), default=1.0) - min((p[1] for p in camera_points), default=0.0)
|
||||
scale = 0.86 * min((WIDTH - 30) / max(span_x, 1.0e-6), (HEIGHT - 30) / max(span_y, 1.0e-6))
|
||||
pixels = [
|
||||
(WIDTH * 0.5 + point[0] * scale, 31 + (HEIGHT - 31) * 0.5 - point[1] * scale, point[2])
|
||||
for point in camera_points
|
||||
]
|
||||
return pixels, scale, right, up, forward
|
||||
|
||||
|
||||
def common_scale(mesh_pair, target, view_dir):
|
||||
vertices = mesh_pair[0][1] + mesh_pair[1][1]
|
||||
_, scale, _, _, _ = project(vertices, target, view_dir)
|
||||
return scale
|
||||
|
||||
|
||||
def projected_line(a, b, target, basis, scale):
|
||||
right, up, forward = basis
|
||||
points = []
|
||||
for point in (a, b):
|
||||
delta = tuple(point[i] - target[i] for i in range(3))
|
||||
points.append((WIDTH * 0.5 + dot(delta, right) * scale,
|
||||
31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale))
|
||||
return points
|
||||
|
||||
|
||||
def render(mesh, mesh_pair, method, target, view_dir, title, subtitle, *, route=None,
|
||||
guide=None, camera_eye=None, framing_radius=None):
|
||||
triangles, vertices = mesh
|
||||
perspective = camera_eye is not None
|
||||
if perspective:
|
||||
forward = normalize(tuple(target[i] - camera_eye[i] for i in range(3)))
|
||||
reference_up = (0.0, 1.0, 0.0) if abs(forward[2]) > 0.98 else (0.0, 0.0, 1.0)
|
||||
right = normalize(cross(forward, reference_up))
|
||||
up = normalize(cross(right, forward))
|
||||
scale = WIDTH * 0.91
|
||||
origin = camera_eye
|
||||
else:
|
||||
if framing_radius is None:
|
||||
scale = common_scale(mesh_pair, target, view_dir)
|
||||
else:
|
||||
scale = 0.86 * min((WIDTH - 30) / (2.0 * framing_radius),
|
||||
(HEIGHT - 30) / (2.0 * framing_radius))
|
||||
_, _, right, up, forward = project((), target, view_dir, scale)
|
||||
origin = target
|
||||
|
||||
def screen(point):
|
||||
delta = tuple(point[i] - origin[i] for i in range(3))
|
||||
cx, cy, depth = dot(delta, right), dot(delta, up), dot(delta, forward)
|
||||
if perspective:
|
||||
if depth <= 1.0:
|
||||
return None
|
||||
return (WIDTH * 0.5 + cx * scale / depth,
|
||||
31 + (HEIGHT - 31) * 0.5 - cy * scale / depth, depth)
|
||||
return (WIDTH * 0.5 + cx * scale,
|
||||
31 + (HEIGHT - 31) * 0.5 - cy * scale, depth)
|
||||
|
||||
background = Image.new("RGB", (WIDTH, HEIGHT + 32), BACKGROUND)
|
||||
draw = ImageDraw.Draw(background)
|
||||
draw.rectangle((0, 0, WIDTH - 1, 31), fill=(34, 46, 54))
|
||||
draw.text((8, 4), title, fill=(250, 252, 253))
|
||||
draw.text((8, 17), subtitle, fill=(190, 207, 215))
|
||||
pixels = background.load()
|
||||
zbuffer = [float("inf")] * (WIDTH * (HEIGHT - 31))
|
||||
camera_fill = forward if perspective else normalize(view_dir)
|
||||
light = normalize((-0.25 + 1.2 * camera_fill[0],
|
||||
-0.35 + 1.2 * camera_fill[1],
|
||||
0.45 + 1.2 * camera_fill[2]))
|
||||
base = LEGACY_COLOR if method == "legacy" else NATIVE_COLOR
|
||||
for tri in triangles:
|
||||
points = [screen(tri[offset:offset + 3]) for offset in (0, 3, 6)]
|
||||
if any(point is None for point in points):
|
||||
continue
|
||||
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(31, 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
|
||||
denominator = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||
if abs(denominator) < 1.0e-10:
|
||||
continue
|
||||
normal = normalize(tri[9:12])
|
||||
shade = 0.50 + 0.50 * max(0.0, dot(normal, light))
|
||||
color = tuple(max(0, min(255, int(channel * shade))) for channel in base)
|
||||
for y in range(y0, y1 + 1):
|
||||
row = (y - 31) * WIDTH
|
||||
for x in range(x0, x1 + 1):
|
||||
sx, sy = x + 0.5, y + 0.5
|
||||
u = ((by - cy) * (sx - cx) + (cx - bx) * (sy - cy)) / denominator
|
||||
v = ((cy - ay) * (sx - cx) + (ax - cx) * (sy - cy)) / denominator
|
||||
w = 1.0 - u - v
|
||||
if u < -1e-8 or v < -1e-8 or w < -1e-8:
|
||||
continue
|
||||
if perspective:
|
||||
depth = 1.0 / (u / az + v / bz + w / cz)
|
||||
else:
|
||||
depth = u * az + v * bz + w * cz
|
||||
index = row + x
|
||||
if depth < zbuffer[index]:
|
||||
zbuffer[index] = depth
|
||||
pixels[x, y] = color
|
||||
|
||||
basis = (right, up, forward)
|
||||
if guide:
|
||||
origin = guide["origin_voxels"]
|
||||
top = guide["top_voxels"]
|
||||
for x_delta, color, label in ((32.0, TILE_COLOR, "32v tile"), (64.0, LOD_COLOR, "64v LOD1")):
|
||||
line = projected_line((origin[0] + x_delta, origin[1] - 190.0, top - 65.0),
|
||||
(origin[0] + x_delta, origin[1] + 190.0, top + 14.0),
|
||||
target, basis, scale)
|
||||
draw.line(line, fill=color, width=2)
|
||||
draw.text((max(2, min(WIDTH - 55, int(line[1][0]) + 2)),
|
||||
max(33, min(HEIGHT - 11, int(line[1][1]) - 12))), label, fill=color)
|
||||
if route:
|
||||
points = [screen(point) for point in route]
|
||||
points = [point[:2] for point in points if point is not None]
|
||||
if len(points) >= 2:
|
||||
draw.line(points, fill=(250, 248, 239), width=5, joint="curve")
|
||||
draw.line(points, fill=ROUTE_COLOR, width=3, joint="curve")
|
||||
for index in (0, len(points) - 1):
|
||||
x, y = points[index]
|
||||
draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill=(250, 248, 239), outline=ROUTE_COLOR, width=2)
|
||||
return background
|
||||
|
||||
|
||||
def titled_pair(left, right, category):
|
||||
pair = Image.new("RGB", (WIDTH * 2, HEIGHT + 32), (220, 226, 229))
|
||||
pair.paste(left, (0, 0))
|
||||
pair.paste(right, (WIDTH, 0))
|
||||
draw = ImageDraw.Draw(pair)
|
||||
draw.rectangle((0, 0, WIDTH * 2 - 1, HEIGHT + 31), outline=(72, 84, 91), width=1)
|
||||
draw.text((8, HEIGHT + 14), f"{category} | same target, camera and scale", fill=(45, 55, 61))
|
||||
return pair
|
||||
|
||||
|
||||
def main():
|
||||
lower_priority()
|
||||
data = json.loads((RESULTS / "audit.json").read_text(encoding="utf-8"))
|
||||
anchor = data["comparison_anchor_core_voxels"]
|
||||
route = data["route_points_core_voxels"]
|
||||
profiles = data["island_profiles"]
|
||||
middle_island = profiles[1]
|
||||
cluster = {
|
||||
"legacy": read_mesh(RESULTS / "cluster_legacy.vfmesh"),
|
||||
"native": read_mesh(RESULTS / "cluster_native.vfmesh"),
|
||||
}
|
||||
detail = {
|
||||
"legacy": read_mesh(RESULTS / "detail_legacy.vfmesh"),
|
||||
"native": read_mesh(RESULTS / "detail_native.vfmesh"),
|
||||
}
|
||||
a = anchor
|
||||
scenes = (
|
||||
("overview", "Cluster overview", cluster, (a[0], a[1], a[2] - 28.0), (8.0, -10.0, 7.0), None, None),
|
||||
("underside", "Cluster underside and lobed roots", detail,
|
||||
(middle_island["center_voxels"][0], middle_island["center_voxels"][1],
|
||||
middle_island["bottom_voxels"] - 6.0), (0.12, -0.08, -1.0), None, None),
|
||||
("edge_cliff", "Thick crust and bounded rock detail", detail,
|
||||
(middle_island["center_voxels"][0], middle_island["center_voxels"][1],
|
||||
middle_island["top_voxels"] - middle_island["radius_voxels"] * 0.65),
|
||||
(1.0, -0.25, 0.18), None, None),
|
||||
("route", "Supported route across the cluster", cluster,
|
||||
(middle_island["center_voxels"][0], middle_island["center_voxels"][1],
|
||||
middle_island["top_voxels"] - 18.0), (8.0, -10.0, 7.0), route, None),
|
||||
)
|
||||
pairs = []
|
||||
for key, title, meshes, target, camera, path, grid in scenes:
|
||||
framing_radius = 28.0 if key in ("underside", "edge_cliff") else None
|
||||
left = render(meshes["legacy"], (meshes["legacy"], meshes["native"]), "legacy",
|
||||
target, camera, title, "Legacy bridge | exact owner field", route=None,
|
||||
guide=grid, framing_radius=framing_radius)
|
||||
route_caption = "Core native | planner route" if path else "Core native | matched field view"
|
||||
right = render(meshes["native"], (meshes["legacy"], meshes["native"]), "native",
|
||||
target, camera, title, route_caption, route=path, guide=grid,
|
||||
framing_radius=framing_radius)
|
||||
pair = titled_pair(left, right, title)
|
||||
out = CAPTURES / f"{key}_matched.jpg"
|
||||
pair.save(out, quality=82, optimize=True)
|
||||
pairs.append(pair)
|
||||
print(f"capture={out.name} bytes={out.stat().st_size} size={pair.size[0]}x{pair.size[1]}")
|
||||
|
||||
eye_index = 5
|
||||
eye_route = route[eye_index]
|
||||
eye = (eye_route[0], eye_route[1], eye_route[2] + 3.68)
|
||||
next_island = profiles[1]
|
||||
look = (next_island["center_voxels"][0], next_island["center_voxels"][1],
|
||||
next_island["top_voxels"] + 5.0)
|
||||
perspective_pair = []
|
||||
for method, meshes in (("legacy", cluster), ("native", cluster)):
|
||||
perspective_pair.append(render(meshes[method], (meshes["legacy"], meshes["native"]), method,
|
||||
look, (0.0, 0.0, -1.0), "Island-edge player view", "Eye at 1.8 m | route span to next island",
|
||||
route=None, camera_eye=eye))
|
||||
eye_pair = titled_pair(perspective_pair[0], perspective_pair[1], "Island-edge player view")
|
||||
eye_path = CAPTURES / "player_eye_matched.jpg"
|
||||
eye_pair.save(eye_path, quality=80, optimize=True)
|
||||
pairs.insert(3, eye_pair)
|
||||
print(f"capture={eye_path.name} bytes={eye_path.stat().st_size} size={eye_pair.size[0]}x{eye_pair.size[1]}")
|
||||
|
||||
sheet = Image.new("RGB", (WIDTH * 2, (HEIGHT + 32) * len(pairs)), (218, 225, 228))
|
||||
for row, pair in enumerate(pairs):
|
||||
sheet.paste(pair, (0, row * (HEIGHT + 32)))
|
||||
sheet_path = CAPTURES / "contact_sheet.jpg"
|
||||
sheet.save(sheet_path, quality=76, optimize=True)
|
||||
oversized = [(path.name, path.stat().st_size) for path in sorted(CAPTURES.glob("*.jpg"))
|
||||
if path.stat().st_size >= 300_000]
|
||||
for path in sorted(CAPTURES.glob("*.jpg")):
|
||||
print(f"capture_size={path.name} bytes={path.stat().st_size}")
|
||||
if oversized:
|
||||
raise RuntimeError(f"captures exceed the 300 KB limit: {oversized}")
|
||||
print(f"contact_sheet={sheet_path.name} bytes={sheet_path.stat().st_size} views=5 matched_pairs=5")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"recipe_hash":"5b8f27274bad89ba",
|
||||
"plan_hash":"ac33c9460b16337a",
|
||||
"manifest_hash":"0db30684eaefa75e",
|
||||
"unchanged_existing_hashes":{"m1_round1_manifest":"c87ae279b0e26f53","surfaceworld_round4_manifest":"8933693d9560ba9f"},
|
||||
"geometry_version":2,
|
||||
"owner_voxel_size_cm":12.5,
|
||||
"core_voxel_size_cm":25,
|
||||
"owner_to_core_length_scale":0.5,
|
||||
"plan_islands":4,
|
||||
"plan_connectors":3,
|
||||
"route_points":29,
|
||||
"legacy_anchor_owner_voxels":[48,16,-1904.03515625],
|
||||
"comparison_anchor_core_voxels":[24,8,-952.015625],
|
||||
"legacy_anchor_plateau_samples":25,
|
||||
"recipe_parameters":{"seed":0,"primary_island_count":3,"density_q16":32768,"spacing_voxels":56,"radius_voxels":[12,24],"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"contour_warp_voxels":6.30078125,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"roughness_voxels":1.5,"blend_voxels":3,"bridge_half_width_voxels":6,"bridge_thickness_voxels":4},
|
||||
"stable_island_ids":["e6e461e18b3a9c87","2bc1820baba047f6","adc259cbe2a395e9","7855fb022cc561c9"],
|
||||
"stable_connector_ids":["eaa5dd310e846520","034023599b466c51","4962fa885545fb26"],
|
||||
"island_profiles":[{"id":"e6e461e18b3a9c87","ordinal":0,"center_voxels":[-15.19921875,25.2734375,-957.5078125],"radius_voxels":14.08203125,"top_voxels":-954.69140625,"bottom_voxels":-975.81640625,"depth_to_diameter":0.750069348128},{"id":"2bc1820baba047f6","ordinal":1,"center_voxels":[24,-12.2265625,-954.8984375],"radius_voxels":14.42578125,"top_voxels":-952.015625,"bottom_voxels":-973.65625,"depth_to_diameter":0.75006769564},{"id":"adc259cbe2a395e9","ordinal":2,"center_voxels":[63.19921875,26.28515625,-958.57421875],"radius_voxels":20.23828125,"top_voxels":-954.52734375,"bottom_voxels":-984.88671875,"depth_to_diameter":0.750048253233},{"id":"7855fb022cc561c9","ordinal":3,"center_voxels":[-1.19921875,-68.2265625,-950.75390625],"radius_voxels":22.8125,"top_voxels":-946.19140625,"bottom_voxels":-980.41015625,"depth_to_diameter":0.75}],
|
||||
"bounds":{"passed":true,"negative_control_detected":true,"boxes":515,"samples":13851,"unknown":74,"missed":0,"island_boxes":4,"connector_boxes":3,"void_boxes_proved_air":1},
|
||||
"thickness":{"passed":true,"islands_tested":4,"islands_below_minimum":0,"minimum_depth_to_diameter_ratio":0.75,"required_depth_to_diameter_ratio":0.7,"minimum_island_id":"7855fb022cc561c9"},
|
||||
"thin_pad_negative_control":{"detected":true,"failed_islands":4,"minimum_depth_to_diameter_ratio":0.299891686975},
|
||||
"route":{"passed":true,"segments":28,"centers":254,"capsule_samples":69088,"blocked":0,"support_samples":1430,"unsupported":0,"grade_failures":0,"minimum_clearance_subvoxels":78},
|
||||
"raised_route_negative_control":{"detected":true,"passed":false,"unsupported":254,"blocked":0},
|
||||
"determinism":{"W1":"566b6115f1aaeb71","W8":"566b6115f1aaeb71","W16":"566b6115f1aaeb71","plan_repeat":true,"hash_samples":32768},
|
||||
"timing":{"same_queries":32768,"legacy_ms":64.6405,"native_ms":38.0581,"native_over_legacy":0.588765557197,"sign_mismatches":1454,"sign_mismatch_percent":4.43725585938,"legacy_solid_samples":130,"native_solid_samples":1380,"legacy_only_solid_samples":102,"native_only_solid_samples":1352},
|
||||
"mesh":[{"name":"cluster_legacy","grid_samples":36613,"triangles":1532,"bytes":73544},{"name":"cluster_native","grid_samples":36613,"triangles":8764,"bytes":420680},{"name":"detail_legacy","grid_samples":207025,"triangles":4124,"bytes":197960},{"name":"detail_native","grid_samples":207025,"triangles":27794,"bytes":1334120}],
|
||||
"route_points_core_voxels":[[-15.19921875,25.2734375,-950.4375],[-12.3984375,22.59375,-949.54296875],[-9.59765625,19.91796875,-949.453125],[-6.80078125,17.23828125,-949.3125],[-4,14.55859375,-949.23046875],[-1.19921875,11.87890625,-949.0234375],[1.6015625,9.203125,-948.734375],[4.40234375,6.5234375,-948.53515625],[7.19921875,3.84375,-948.328125],[10,1.16796875,-947.99609375],[12.80078125,-1.51171875,-947.6953125],[15.6015625,-4.19140625,-947.5390625],[18.3984375,-6.87109375,-947.33984375],[21.19921875,-9.546875,-947.93359375],[24,-12.2265625,-947.87109375],[26.80078125,-9.4765625,-946.87109375],[29.6015625,-6.7265625,-946.93359375],[32.3984375,-3.97265625,-947.31640625],[35.19921875,-1.22265625,-947.72265625],[38,1.52734375,-948.17578125],[40.80078125,4.27734375,-948.34375],[43.6015625,7.03125,-948.48046875],[46.3984375,9.78125,-948.72265625],[49.19921875,12.53125,-949.046875],[52,15.28125,-949.2109375],[54.80078125,18.03125,-949.265625],[57.59765625,20.78515625,-949.52734375],[60.3984375,23.53515625,-950.3984375],[63.19921875,26.28515625,-950.265625]]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Reference in new issue
Block a user