captures: readable player-view captures with a coverage gate (L69); R36 native-generator survey
- Capture exports now cover a 90-degree player frustum (radius 3, LOD 0-8, overlap patches); a 17x9 ray coverage probe aborts before rendering if tiles are missing (truncated-mesh control: 480 uncovered samples, rejected). Clear colour is sky blue, so un-meshed space can't pass as dark rock. 16 views of the owner's four-strate plan. - Explore commandlet: finite XYZ spawn override for traces (Source/, Linux-compiled). - In-engine four-strate trace NOT obtained: the engine-generated plan fails passage validation at a solid centerline voxel (~(-43,-34.5,-448.5) m) while the headless owner_world.json plan passes. Engine and headless plans diverge. - R36 (read-only): order TN -> VS -> FI for native migration; native FI is reachable from the adapter but not selected in the owner run, cause unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -17982,6 +17982,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
FVector StartupTraceSpawnCm = FVector::ZeroVector;
|
||||||
|
FString StartupTraceSpawnText;
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="),
|
||||||
|
StartupTraceSpawnText, false))
|
||||||
|
{
|
||||||
|
TArray<FString> Components;
|
||||||
|
StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true);
|
||||||
|
if (Components.Num() != 3
|
||||||
|
|| !LexTryParseString(StartupTraceSpawnCm.X, *Components[0])
|
||||||
|
|| !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1])
|
||||||
|
|| !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2])
|
||||||
|
|| !FMath::IsFinite(StartupTraceSpawnCm.X)
|
||||||
|
|| !FMath::IsFinite(StartupTraceSpawnCm.Y)
|
||||||
|
|| !FMath::IsFinite(StartupTraceSpawnCm.Z))
|
||||||
|
{
|
||||||
|
OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
||||||
if (PawnRoot == nullptr)
|
if (PawnRoot == nullptr)
|
||||||
{
|
{
|
||||||
@@ -17990,7 +18009,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
}
|
}
|
||||||
PlayerPawn->SetRootComponent(PawnRoot);
|
PlayerPawn->SetRootComponent(PawnRoot);
|
||||||
PawnRoot->RegisterComponent();
|
PawnRoot->RegisterComponent();
|
||||||
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
|
PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
|
||||||
PlayerController->Possess(PlayerPawn);
|
PlayerController->Possess(PlayerPawn);
|
||||||
if (TestWorld->GetFirstPlayerController() != PlayerController
|
if (TestWorld->GetFirstPlayerController() != PlayerController
|
||||||
|| PlayerController->GetPawn() != PlayerPawn)
|
|| PlayerController->GetPawn() != PlayerPawn)
|
||||||
@@ -18029,6 +18048,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Actor->SetActorTickEnabled(true);
|
Actor->SetActorTickEnabled(true);
|
||||||
|
VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf(
|
||||||
|
TEXT("\"location_cm\":[%s,%s,%s]"),
|
||||||
|
*FString::SanitizeFloat(StartupTraceSpawnCm.X),
|
||||||
|
*FString::SanitizeFloat(StartupTraceSpawnCm.Y),
|
||||||
|
*FString::SanitizeFloat(StartupTraceSpawnCm.Z)));
|
||||||
|
|
||||||
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,725 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render arrival, departure, and mid-tunnel views for the four-strate owner plan."""
|
||||||
|
import json
|
||||||
|
import importlib.util
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l69"
|
||||||
|
BIN = OUT / "bin"
|
||||||
|
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||||
|
HEADLESS = OUT / "bin/VoxelForgeCoreHeadless-l69"
|
||||||
|
RENDERER = BIN / "VoxelForgeOffscreenCapture-l69"
|
||||||
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
RECIPE_INPUTS = (
|
||||||
|
("passage-0-1", ROOT / "Standalone/data/owner_world.json",
|
||||||
|
"surface-world", "SurfaceWorld", "upper"),
|
||||||
|
("passage-1-2", ROOT / "Standalone/data/owner_world.json",
|
||||||
|
"surface-world", "FloatingIslands", "upper"),
|
||||||
|
("passage-2-3", ROOT / "Standalone/data/owner_world.json",
|
||||||
|
"surface-world", "TunnelNetwork", "upper"),
|
||||||
|
)
|
||||||
|
SUBVOXELS_PER_VOXEL = 256.0
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
|
ring_profile = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(ring_profile)
|
||||||
|
|
||||||
|
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
ENV = os.environ.copy()
|
||||||
|
ENV["LP_NUM_THREADS"] = "12"
|
||||||
|
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||||
|
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||||
|
PARSER.add_argument("--view", choices=("arrival-mouth", "departure-mouth", "mid-tunnel"))
|
||||||
|
OPTIONS = PARSER.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
log = OUT / f"capture-{name}.log"
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1740, check=False)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[L69CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||||
|
flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
def add(a, b):
|
||||||
|
return tuple(x + y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def sub(a, b):
|
||||||
|
return tuple(x - y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def mul(a, scale):
|
||||||
|
return tuple(x * scale for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def norm(a):
|
||||||
|
length = math.sqrt(sum(x * x for x in a))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
raise ValueError("passage has a degenerate camera tangent")
|
||||||
|
return tuple(x / length for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_route(points, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(points, points[1:]):
|
||||||
|
delta = sub(b[0], a[0])
|
||||||
|
length = math.sqrt(sum(x * x for x in delta))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||||
|
radius = a[1] + (b[1] - a[1]) * t
|
||||||
|
return pos, radius
|
||||||
|
remaining -= length
|
||||||
|
return points[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def route_frame(points, distance):
|
||||||
|
center, radius = interpolate_route(points, distance)
|
||||||
|
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate_route(points, distance + 0.25)
|
||||||
|
tangent = norm(sub(after, before))
|
||||||
|
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||||
|
side = (-horizontal[1], horizontal[0], 0.0)
|
||||||
|
return center, radius, tangent, side
|
||||||
|
|
||||||
|
|
||||||
|
def camera_angles(camera, target):
|
||||||
|
delta = sub(target, camera)
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||||
|
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||||
|
return yaw, pitch
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values):
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def load_reference_cameras():
|
||||||
|
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||||
|
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||||
|
values = {}
|
||||||
|
for name, camera, target, clip in pattern.findall(
|
||||||
|
log.read_text(encoding="utf-8", errors="replace")):
|
||||||
|
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||||
|
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_CAMERAS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def terrain_surface_near(mesh, x, y, expected_height):
|
||||||
|
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||||
|
_, triangles, _ = ring_profile.read_scene(
|
||||||
|
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||||
|
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||||
|
hits = []
|
||||||
|
for dy in range(-4, 5):
|
||||||
|
for dx in range(-4, 5):
|
||||||
|
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||||
|
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||||
|
preferred_height=expected_height)
|
||||||
|
if hit is not None:
|
||||||
|
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||||
|
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
distance, _, sample_x, sample_y, height = min(hits)
|
||||||
|
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||||
|
return None
|
||||||
|
return sample_x, sample_y, height
|
||||||
|
|
||||||
|
|
||||||
|
def load_layout(name, source, recipe):
|
||||||
|
path = OUT / f"layout-{name}.json"
|
||||||
|
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||||
|
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||||
|
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def q_to_meters(layout):
|
||||||
|
return layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||||
|
|
||||||
|
|
||||||
|
def q_position(value, scale):
|
||||||
|
return tuple(float(component) * scale for component in value)
|
||||||
|
|
||||||
|
|
||||||
|
def q_route(points, scale):
|
||||||
|
return [(q_position(point["position_q"], scale), point["radius_q"] * scale)
|
||||||
|
for point in points]
|
||||||
|
|
||||||
|
|
||||||
|
def vector_length(value):
|
||||||
|
return math.sqrt(sum(component * component for component in value))
|
||||||
|
|
||||||
|
|
||||||
|
def unit(value):
|
||||||
|
length = vector_length(value)
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
raise RuntimeError("capture camera direction is degenerate")
|
||||||
|
return tuple(component / length for component in value)
|
||||||
|
|
||||||
|
|
||||||
|
def add_scaled(point, direction, scale):
|
||||||
|
return tuple(point[axis] + direction[axis] * scale for axis in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def make_player_views(layout):
|
||||||
|
scale = q_to_meters(layout)
|
||||||
|
passages = {(int(item["upper_strate_index"]), int(item["lower_strate_index"])): item
|
||||||
|
for item in layout["passages"]}
|
||||||
|
p01, p12, p23 = passages[(0, 1)], passages[(1, 2)], passages[(2, 3)]
|
||||||
|
strate0 = next(item for item in layout["strates"] if int(item["strate_index"]) == 0)
|
||||||
|
surface_anchors = [item["values"] for item in strate0.get("anchors", [])
|
||||||
|
if item.get("kind") == "route_point"]
|
||||||
|
if len(surface_anchors) < 2:
|
||||||
|
raise RuntimeError("strate 0 has no resolved surface arrival route")
|
||||||
|
surface_route = [(q_position(value[:3], scale), value[3] * scale)
|
||||||
|
for value in surface_anchors]
|
||||||
|
|
||||||
|
route_specs = [
|
||||||
|
(0, surface_route, True, surface_route[-1][0], p01, "upper", False),
|
||||||
|
(1, q_route(p01["control_points"], scale), True,
|
||||||
|
q_position(p01["lower_mouth_q"], scale), p12, "upper", True),
|
||||||
|
(2, q_route(p12["control_points"], scale), True,
|
||||||
|
q_position(p12["lower_mouth_q"], scale), p23, "upper", False),
|
||||||
|
(3, q_route(p23["control_points"], scale), True,
|
||||||
|
q_position(p23["lower_mouth_q"], scale), p23, "lower", True),
|
||||||
|
]
|
||||||
|
views = {}
|
||||||
|
for strate, source_route, reverse_route, spawn, departure, departure_end, reverse_departure in route_specs:
|
||||||
|
route = list(reversed(source_route)) if reverse_route else list(source_route)
|
||||||
|
if vector_length(sub(route[0][0], spawn)) > 5.0:
|
||||||
|
raise RuntimeError(f"strate {strate} spawn is {vector_length(sub(route[0][0], spawn)):.2f} m from its arrival route")
|
||||||
|
path_length = sum(vector_length(sub(b[0], a[0])) for a, b in zip(route, route[1:]))
|
||||||
|
arrival_target_pos, arrival_target_radius = interpolate_route(route, min(20.0, path_length))
|
||||||
|
arrival_camera = (spawn[0], spawn[1], spawn[2] + 1.6)
|
||||||
|
arrival_target = (arrival_target_pos[0], arrival_target_pos[1],
|
||||||
|
arrival_target_pos[2] - arrival_target_radius + 1.8)
|
||||||
|
ayaw, apitch = camera_angles(arrival_camera, arrival_target)
|
||||||
|
views[(strate, "arrival")] = {
|
||||||
|
"camera": arrival_camera, "target": arrival_target, "yaw": ayaw,
|
||||||
|
"pitch": apitch, "fov_deg": 90.0,
|
||||||
|
"description": "player eye at the strate arrival, facing into its route"}
|
||||||
|
|
||||||
|
route_distance = min(10.0, path_length * 0.2)
|
||||||
|
route_center, route_radius, _, _ = route_frame(route, route_distance)
|
||||||
|
route_camera = (route_center[0], route_center[1],
|
||||||
|
route_center[2] - route_radius + 1.6)
|
||||||
|
route_target_pos, route_target_radius = interpolate_route(
|
||||||
|
route, min(path_length, route_distance + 28.0))
|
||||||
|
route_target = (route_target_pos[0], route_target_pos[1],
|
||||||
|
route_target_pos[2] - route_target_radius + 1.8)
|
||||||
|
ryaw, rpitch = camera_angles(route_camera, route_target)
|
||||||
|
views[(strate, "route")] = {
|
||||||
|
"camera": route_camera, "target": route_target, "yaw": ryaw,
|
||||||
|
"pitch": rpitch, "fov_deg": 90.0,
|
||||||
|
"description": "player-height view along the passage or surface route"}
|
||||||
|
|
||||||
|
mouth_path = q_route(departure["control_points"], scale)
|
||||||
|
if departure_end == "lower":
|
||||||
|
mouth_path.reverse()
|
||||||
|
mouth, mouth_radius, inward, _ = route_frame(mouth_path, 0.0)
|
||||||
|
departure_distance = min(7.4, sum(vector_length(sub(b[0], a[0]))
|
||||||
|
for a, b in zip(mouth_path, mouth_path[1:])))
|
||||||
|
inside, inside_radius, _, _ = route_frame(mouth_path, departure_distance)
|
||||||
|
departure_camera = (inside[0], inside[1], inside[2] - inside_radius + 1.6)
|
||||||
|
outward = mul(inward, -1.0)
|
||||||
|
departure_target = (mouth[0] + outward[0] * 12.0,
|
||||||
|
mouth[1] + outward[1] * 12.0,
|
||||||
|
mouth[2] - mouth_radius + 1.8)
|
||||||
|
dyaw, dpitch = camera_angles(departure_camera, departure_target)
|
||||||
|
views[(strate, "departure-mouth")] = {
|
||||||
|
"camera": departure_camera, "target": departure_target,
|
||||||
|
"yaw": dyaw, "pitch": dpitch, "fov_deg": 90.0,
|
||||||
|
"description": f"inside the {departure_end} mouth looking out"}
|
||||||
|
|
||||||
|
overview_camera = (spawn[0] - 12.0, spawn[1] - 16.0, spawn[2] + 70.0)
|
||||||
|
overview_target = (spawn[0], spawn[1], spawn[2] - 4.0)
|
||||||
|
oyaw, opitch = camera_angles(overview_camera, overview_target)
|
||||||
|
views[(strate, "overview")] = {
|
||||||
|
"camera": overview_camera, "target": overview_target,
|
||||||
|
"yaw": oyaw, "pitch": opitch, "fov_deg": 90.0,
|
||||||
|
"description": "overview from 70 m above the arrival"}
|
||||||
|
return views
|
||||||
|
|
||||||
|
|
||||||
|
def scene_records(path, include_offsets=False):
|
||||||
|
records = []
|
||||||
|
keys = set()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
header = stream.read(60)
|
||||||
|
if len(header) != 60 or header[:7] != b"VFMSH00":
|
||||||
|
raise RuntimeError(f"{path} has an invalid mesh header")
|
||||||
|
version, voxel_um, count = struct.unpack_from("<III", header, 8)
|
||||||
|
if version not in (1, 2) or voxel_um == 0:
|
||||||
|
raise RuntimeError(f"{path} has an unsupported mesh header")
|
||||||
|
for _ in range(count):
|
||||||
|
start = stream.tell()
|
||||||
|
key_data = stream.read(16)
|
||||||
|
count_data = stream.read(20)
|
||||||
|
if len(key_data) != 16 or len(count_data) != 20:
|
||||||
|
raise RuntimeError(f"{path} has a truncated tile header")
|
||||||
|
key = struct.unpack("<iiii", key_data)
|
||||||
|
_, _, vertices, indices, masks = struct.unpack("<IIIII", count_data)
|
||||||
|
payload_bytes = vertices * 24 + indices * 4 + masks
|
||||||
|
if version == 2:
|
||||||
|
payload_bytes += vertices * 10
|
||||||
|
stream.seek(payload_bytes, os.SEEK_CUR)
|
||||||
|
end = stream.tell()
|
||||||
|
if end > path.stat().st_size:
|
||||||
|
raise RuntimeError(f"{path} ends inside a tile record")
|
||||||
|
keys.add(key)
|
||||||
|
if include_offsets:
|
||||||
|
records.append((key, start, end))
|
||||||
|
if stream.read(1):
|
||||||
|
raise RuntimeError(f"{path} has trailing data after {count} tiles")
|
||||||
|
return header, voxel_um, keys, records
|
||||||
|
|
||||||
|
|
||||||
|
def tile_keys_at(point, keys, voxel_um):
|
||||||
|
voxel_m = voxel_um / 1_000_000.0
|
||||||
|
found = set()
|
||||||
|
for level in range(9):
|
||||||
|
extent = 32.0 * (1 << level) * voxel_m
|
||||||
|
key = (math.floor(point[0] / extent), math.floor(point[1] / extent),
|
||||||
|
math.floor(point[2] / extent), level)
|
||||||
|
if key in keys:
|
||||||
|
found.add(key)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def frame_basis(yaw, pitch):
|
||||||
|
yaw, pitch = math.radians(yaw), math.radians(pitch)
|
||||||
|
forward = (math.sin(yaw) * math.cos(pitch),
|
||||||
|
math.cos(yaw) * math.cos(pitch), math.sin(pitch))
|
||||||
|
right = (math.cos(yaw), -math.sin(yaw), 0.0)
|
||||||
|
up = (-math.sin(yaw) * math.sin(pitch),
|
||||||
|
-math.cos(yaw) * math.sin(pitch), math.cos(pitch))
|
||||||
|
return unit(forward), unit(right), unit(up)
|
||||||
|
|
||||||
|
|
||||||
|
def coverage_audit(camera, yaw, pitch, fov, keys, voxel_um):
|
||||||
|
forward, right, up = frame_basis(yaw, pitch)
|
||||||
|
tan_h = math.tan(math.radians(fov) * 0.5)
|
||||||
|
tan_v = tan_h * 900.0 / 1600.0
|
||||||
|
uncovered = []
|
||||||
|
missing_points = []
|
||||||
|
rays = 0
|
||||||
|
distance_samples = tuple(range(1, 200, 4))
|
||||||
|
for row in range(9):
|
||||||
|
sy = (row / 8.0) * 2.0 - 1.0
|
||||||
|
for column in range(17):
|
||||||
|
sx = (column / 16.0) * 2.0 - 1.0
|
||||||
|
ray = unit(tuple(forward[axis] + right[axis] * sx * tan_h
|
||||||
|
+ up[axis] * sy * tan_v for axis in range(3)))
|
||||||
|
rays += 1
|
||||||
|
for distance in distance_samples:
|
||||||
|
point = add_scaled(camera, ray, distance)
|
||||||
|
if not tile_keys_at(point, keys, voxel_um):
|
||||||
|
uncovered.append((row, column, distance))
|
||||||
|
missing_points.append(point)
|
||||||
|
return {"rays": rays, "samples_per_ray": len(distance_samples),
|
||||||
|
"missing_samples": len(uncovered), "first_missing": uncovered[:8],
|
||||||
|
"missing_points": missing_points}
|
||||||
|
|
||||||
|
|
||||||
|
def merge_scene_exports(destination, sources):
|
||||||
|
unique = {}
|
||||||
|
reference_header = None
|
||||||
|
reference_version = None
|
||||||
|
reference_voxel_um = None
|
||||||
|
for source in sources:
|
||||||
|
header, voxel_um, _, records = scene_records(source, include_offsets=True)
|
||||||
|
version = struct.unpack_from("<I", header, 8)[0]
|
||||||
|
if reference_header is None:
|
||||||
|
reference_header, reference_version, reference_voxel_um = header, version, voxel_um
|
||||||
|
elif version != reference_version or voxel_um != reference_voxel_um:
|
||||||
|
raise RuntimeError("mesh export fragments use incompatible voxel formats")
|
||||||
|
for record in records:
|
||||||
|
unique.setdefault(record[0], (source, record[1], record[2]))
|
||||||
|
|
||||||
|
merged_header = bytearray(reference_header)
|
||||||
|
struct.pack_into("<I", merged_header, 16, len(unique))
|
||||||
|
temporary = destination.with_suffix(destination.suffix + ".merge-tmp")
|
||||||
|
with temporary.open("wb") as output_stream:
|
||||||
|
output_stream.write(merged_header)
|
||||||
|
for source, start, end in unique.values():
|
||||||
|
with source.open("rb") as input_stream:
|
||||||
|
input_stream.seek(start)
|
||||||
|
remaining = end - start
|
||||||
|
while remaining:
|
||||||
|
block = input_stream.read(min(1024 * 1024, remaining))
|
||||||
|
if not block:
|
||||||
|
raise RuntimeError(f"{source} changed while merging mesh exports")
|
||||||
|
output_stream.write(block)
|
||||||
|
remaining -= len(block)
|
||||||
|
os.replace(temporary, destination)
|
||||||
|
return len(unique)
|
||||||
|
|
||||||
|
|
||||||
|
def write_truncated_scene(source, destination, header, keys, records, remove_keys):
|
||||||
|
retained = [record for record in records if record[0] not in remove_keys]
|
||||||
|
new_header = bytearray(header)
|
||||||
|
struct.pack_into("<I", new_header, 16, len(retained))
|
||||||
|
with source.open("rb") as input_stream, destination.open("wb") as output_stream:
|
||||||
|
output_stream.write(new_header)
|
||||||
|
for _, start, end in retained:
|
||||||
|
input_stream.seek(start)
|
||||||
|
remaining = end - start
|
||||||
|
while remaining:
|
||||||
|
block = input_stream.read(min(1024 * 1024, remaining))
|
||||||
|
if not block:
|
||||||
|
raise RuntimeError("source mesh changed while writing the negative control")
|
||||||
|
output_stream.write(block)
|
||||||
|
remaining -= len(block)
|
||||||
|
return {key for key in keys if key not in remove_keys}
|
||||||
|
|
||||||
|
|
||||||
|
def run_negative_control(mesh, stem, view, header, voxel_um, keys, records):
|
||||||
|
camera = view["camera"]
|
||||||
|
forward, right, up = frame_basis(view["yaw"], view["pitch"])
|
||||||
|
remove = set()
|
||||||
|
for distance in range(89, 110, 4):
|
||||||
|
center = add_scaled(camera, forward, distance)
|
||||||
|
for side in (-8.0, 0.0, 8.0):
|
||||||
|
for vertical in (-8.0, 0.0, 8.0):
|
||||||
|
point = add_scaled(add_scaled(center, right, side), up, vertical)
|
||||||
|
remove.update(tile_keys_at(point, keys, voxel_um))
|
||||||
|
if not remove:
|
||||||
|
raise RuntimeError("negative-control ray did not intersect any exported tiles")
|
||||||
|
truncated = OUT / "controls" / f"{stem}.truncated.vfmesh"
|
||||||
|
truncated.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
remaining = write_truncated_scene(mesh, truncated, header, keys, records, remove)
|
||||||
|
audit = coverage_audit(camera, view["yaw"], view["pitch"], view["fov_deg"],
|
||||||
|
remaining, voxel_um)
|
||||||
|
if audit["missing_samples"] == 0:
|
||||||
|
raise RuntimeError("negative control unexpectedly passed frustum coverage")
|
||||||
|
print(f"[L69NegativeControl] file={truncated.relative_to(ROOT)} removed_tiles={len(remove)} "
|
||||||
|
f"missing_samples={audit['missing_samples']} flagged_before_render=1 status=PASS",
|
||||||
|
flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def export_capture(strate, label, view, negative_control=False):
|
||||||
|
stem = f"strate{strate}-{label}"
|
||||||
|
camera = view["camera"]
|
||||||
|
yaw, pitch = view["yaw"], view["pitch"]
|
||||||
|
mesh = OUT / "meshes" / f"{stem}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{stem}.png"
|
||||||
|
jpg = CAPTURES / f"{stem}.jpg"
|
||||||
|
mesh.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
source = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
parts = [mesh]
|
||||||
|
|
||||||
|
def export_part(part_name, export_camera):
|
||||||
|
part = mesh if part_name.endswith("-base") else OUT / "mesh-patches" / f"{part_name}.vfmesh"
|
||||||
|
part.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
run(f"{part_name}-export", [HEADLESS, source, "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||||
|
"--render-distance-chunks=0", "--mesh-coverage", "--capture-skip-walkability",
|
||||||
|
"--recipe=surface-world",
|
||||||
|
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={part}"])
|
||||||
|
return part
|
||||||
|
|
||||||
|
export_part(f"{stem}-base", camera)
|
||||||
|
header, voxel_um, keys, records = scene_records(mesh, include_offsets=negative_control)
|
||||||
|
audit = coverage_audit(camera, yaw, pitch, view["fov_deg"], keys, voxel_um)
|
||||||
|
patch_index = 0
|
||||||
|
while audit["missing_samples"] and patch_index < 3:
|
||||||
|
patch_index += 1
|
||||||
|
points = audit["missing_points"]
|
||||||
|
patch_camera = tuple(sum(point[axis] for point in points) / len(points)
|
||||||
|
for axis in range(3))
|
||||||
|
patch_name = f"strate{strate}-{label}-patch{patch_index}"
|
||||||
|
part = export_part(patch_name, patch_camera)
|
||||||
|
parts.append(part)
|
||||||
|
merged_tiles = merge_scene_exports(mesh, parts)
|
||||||
|
print(f"[L69CoveragePatch] view={stem} patch={patch_index} "
|
||||||
|
f"camera_m=({fmt(patch_camera)}) merged_tiles={merged_tiles}", flush=True)
|
||||||
|
header, voxel_um, keys, records = scene_records(mesh, include_offsets=negative_control)
|
||||||
|
audit = coverage_audit(camera, yaw, pitch, view["fov_deg"], keys, voxel_um)
|
||||||
|
print(f"[L69Coverage] view={stem} tiles={len(keys)} rays={audit['rays']} "
|
||||||
|
f"samples_per_ray={audit['samples_per_ray']} distance_m=200 "
|
||||||
|
f"missing_samples={audit['missing_samples']} "
|
||||||
|
f"status={'PASS' if audit['missing_samples'] == 0 else 'FAIL'}", flush=True)
|
||||||
|
if audit["missing_samples"]:
|
||||||
|
raise SystemExit(f"{stem} lacks tile coverage within the 200 m camera frustum: "
|
||||||
|
f"{audit['first_missing']}")
|
||||||
|
if negative_control:
|
||||||
|
run_negative_control(mesh, stem, view, header, voxel_um, keys, records)
|
||||||
|
print(f"[L69CaptureCamera] view={stem} camera_m=({fmt(camera)}) "
|
||||||
|
f"target_m=({fmt(view['target'])}) yaw={yaw:.3f} pitch={pitch:.3f} "
|
||||||
|
f"fov_deg={view['fov_deg']:.1f} description={view['description']}", flush=True)
|
||||||
|
render = [RENDERER, mesh, png, "--size=1600x900",
|
||||||
|
f"--fov-deg={view['fov_deg']:.1f}", f"--camera-m={fmt(camera)}",
|
||||||
|
f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||||
|
"--tile-skirts=off", "--shade=readable", "--headlamp=on",
|
||||||
|
"--height-tint=on", "--tile-edges=off"]
|
||||||
|
run(f"{stem}-render", render)
|
||||||
|
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
|
"-y", "-i", png, "-frames:v", "1", "-q:v", "3", jpg])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
if not 0 < size <= 300_000:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB (bytes={size})")
|
||||||
|
print(f"[L69CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} status=PASS",
|
||||||
|
flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not HEADLESS.is_file():
|
||||||
|
raise SystemExit(f"missing current L68 headless binary: {HEADLESS}")
|
||||||
|
if not RENDERER.is_file():
|
||||||
|
raise SystemExit(f"missing L69 offscreen renderer: {RENDERER}")
|
||||||
|
layout = load_layout("owner-four-strate", ROOT / "Standalone/data/owner_world.json",
|
||||||
|
"surface-world")
|
||||||
|
views = make_player_views(layout)
|
||||||
|
print(f"[L69CapturePlan] strates=4 views={len(views)} frustum_m=200 "
|
||||||
|
f"clip_radius=3 max_clip_level=8 max_tile_level=8 patches=up-to-3", flush=True)
|
||||||
|
first = True
|
||||||
|
for strate in range(4):
|
||||||
|
for label in ("arrival", "route", "departure-mouth", "overview"):
|
||||||
|
export_capture(strate, label, views[(strate, label)], negative_control=first)
|
||||||
|
first = False
|
||||||
|
print(f"[L69CaptureSet] files={len(list(CAPTURES.glob('*.jpg')))} "
|
||||||
|
f"directory={CAPTURES.relative_to(ROOT)} status=PASS", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, mouth_generator, preferred_side):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]
|
||||||
|
sides = []
|
||||||
|
if upper["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
elif sides:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def make_views(layout, passage, reverse):
|
||||||
|
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||||
|
controls = [
|
||||||
|
(tuple(value * q_to_m for value in point["position_q"]),
|
||||||
|
point["radius_q"] * q_to_m)
|
||||||
|
for point in passage["control_points"]
|
||||||
|
]
|
||||||
|
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||||
|
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if len(path) < 2:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||||
|
for a, b in zip(path, path[1:])]
|
||||||
|
total = sum(lengths)
|
||||||
|
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||||
|
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||||
|
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||||
|
if (int(passage["upper_strate_index"],) == 1
|
||||||
|
and int(passage["lower_strate_index"]) == 2 and not reverse):
|
||||||
|
# The old 7 m authored-radius beat sat near the middle of this audit window.
|
||||||
|
middle_distance = min(total - 12.0, 90.8)
|
||||||
|
entry_distance = min(6.0, total * 0.25)
|
||||||
|
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||||
|
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||||
|
|
||||||
|
def exterior_view(prefix, center, radius, outward, target):
|
||||||
|
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||||
|
eye_height = center[2] - radius + 1.6
|
||||||
|
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||||
|
center[1] + horizontal_outward[1] * 5.5,
|
||||||
|
eye_height)
|
||||||
|
yaw, pitch = camera_angles(eye, target)
|
||||||
|
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||||
|
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||||
|
|
||||||
|
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||||
|
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||||
|
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||||
|
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||||
|
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||||
|
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||||
|
ground["focus"] = entrance
|
||||||
|
ground["outside_camera"] = True
|
||||||
|
|
||||||
|
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||||
|
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||||
|
center[2] - radius + 1.6)
|
||||||
|
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||||
|
target = (target_pos[0], target_pos[1],
|
||||||
|
target_pos[2] - target_radius + 1.6)
|
||||||
|
yaw, pitch = camera_angles(camera, target)
|
||||||
|
plane = (side[0], side[1], 0.0,
|
||||||
|
-(side[0] * center[0] + side[1] * center[1]))
|
||||||
|
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||||
|
"focus": center, "mouth": center}
|
||||||
|
|
||||||
|
def inside_mouth_view(prefix, at_start):
|
||||||
|
endpoint_distance = 0.0 if at_start else total
|
||||||
|
mouth, _, tangent, mouth_side = route_frame(path, endpoint_distance)
|
||||||
|
camera_distance = min(total, 7.4) if at_start else max(0.0, total - 5.5)
|
||||||
|
center, _, _, side = route_frame(path, camera_distance)
|
||||||
|
camera = (center[0] + side[0] * 2.5,
|
||||||
|
center[1] + side[1] * 2.5, center[2])
|
||||||
|
outward = mul(tangent, -1.0) if at_start else tangent
|
||||||
|
target = (mouth[0] + outward[0] * 8.0 + mouth_side[0] * 2.5,
|
||||||
|
mouth[1] + outward[1] * 8.0 + mouth_side[1] * 2.5,
|
||||||
|
mouth[2])
|
||||||
|
yaw, pitch = camera_angles(camera, target)
|
||||||
|
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": None, "camera_xy": camera[:2],
|
||||||
|
"focus": mouth, "mouth": mouth, "outside_camera": False,
|
||||||
|
"camera_location": "passage-air", "focus_radius": 30.0,
|
||||||
|
"eye_height_z": camera[2], "fov_deg": 80.0}
|
||||||
|
|
||||||
|
# Place each interface view at its true route endpoint. The endpoint camera
|
||||||
|
# is a side cutaway so the solid terrain in front of an underground mouth
|
||||||
|
# cannot hide the floor-to-tunnel connection.
|
||||||
|
arrival_distance = 0.0
|
||||||
|
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||||
|
path, arrival_distance)
|
||||||
|
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||||
|
arrival_tangent, arrival_side, arrival_distance,
|
||||||
|
min(total, arrival_distance + 12.0))
|
||||||
|
arrival["camera_location"] = "passage-air"
|
||||||
|
arrival["focus_radius"] = 20.0
|
||||||
|
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||||
|
arrival["camera"], arrival["target"])
|
||||||
|
|
||||||
|
entry_camera_distance = min(8.0, total * 0.1)
|
||||||
|
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||||
|
path, entry_camera_distance)
|
||||||
|
# Player-height view from just inside the passage, aimed through its actual
|
||||||
|
# interface mouth so the floor, bore, and opening share one frame.
|
||||||
|
entry_camera = (entry_center[0], entry_center[1],
|
||||||
|
entry_center[2] - entry_radius + 1.6)
|
||||||
|
entry_target_pos, entry_target_radius = interpolate_route(
|
||||||
|
path, min(total, entry_distance + 14.0))
|
||||||
|
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||||
|
entry_camera[2] - 2.5)
|
||||||
|
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||||
|
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||||
|
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||||
|
"clip": None, "camera_xy": entry_camera[:2],
|
||||||
|
"focus": entry_center, "mouth": entrance, "outside_camera": False,
|
||||||
|
"camera_location": "passage-air", "focus_radius": 20.0,
|
||||||
|
"eye_height_z": entry_camera[2]}
|
||||||
|
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||||
|
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||||
|
ground["focus_radius"] = 20.0
|
||||||
|
middle_view["focus_radius"] = 20.0
|
||||||
|
middle_camera_center, _ = interpolate_route(path, middle_distance)
|
||||||
|
middle_view["camera"] = (middle_camera_center[0] + middle_side[0] * 0.75,
|
||||||
|
middle_camera_center[1] + middle_side[1] * 0.75,
|
||||||
|
middle[2] - middle_radius + 1.6)
|
||||||
|
middle_target_pos, middle_target_radius = interpolate_route(
|
||||||
|
path, min(total, middle_distance + 14.0))
|
||||||
|
middle_view["target"] = (middle_target_pos[0] + middle_side[0] * 2.35,
|
||||||
|
middle_target_pos[1] + middle_side[1] * 2.35,
|
||||||
|
middle_target_pos[2] - middle_target_radius + 1.8)
|
||||||
|
# Keep the camera inside the bore, close to one wall, and look down the
|
||||||
|
# route so the old beat location is visible at player height.
|
||||||
|
middle_view["clip"] = None
|
||||||
|
middle_view["camera_location"] = "passage-air"
|
||||||
|
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||||
|
middle_view["camera"], middle_view["target"])
|
||||||
|
return (ground, entry_view, middle_view, arrival,
|
||||||
|
inside_mouth_view("inside-mouth-start", True),
|
||||||
|
inside_mouth_view("inside-mouth-end", False)), total, lower_index
|
||||||
|
|
||||||
|
|
||||||
|
def export_capture(scene_name, source, recipe, passage, endpoint, view):
|
||||||
|
stem = f"{passage['stable_id'].replace('/', '-')}-{endpoint}"
|
||||||
|
view["name"] = stem
|
||||||
|
mesh = OUT / f"{stem}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{stem}.png"
|
||||||
|
jpg = CAPTURES / f"{stem}.jpg"
|
||||||
|
yaw, pitch = view["yaw"], view["pitch"]
|
||||||
|
camera = view["camera"]
|
||||||
|
export_camera = view.get("focus", camera)
|
||||||
|
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||||
|
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||||
|
render_clip = view["clip"]
|
||||||
|
surface = None
|
||||||
|
if view.get("outside_camera", False):
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
if surface is None:
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1], view["eye_height_z"])
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
elif render_clip is not None:
|
||||||
|
camera = view["camera"]
|
||||||
|
yaw, pitch = camera_angles(camera, view["target"])
|
||||||
|
fov_deg = view.get("fov_deg", 80.0)
|
||||||
|
print(f"[L68CaptureCamera] view={stem} camera_m=({fmt(camera)}) "
|
||||||
|
f"target_m=({fmt(view['target'])}) "
|
||||||
|
f"clip={fmt(render_clip) if render_clip is not None else 'none'} fov_deg={fov_deg:.1f}",
|
||||||
|
flush=True)
|
||||||
|
render = [RENDERER, mesh, png, "--size=1600x900", f"--fov-deg={fov_deg:.1f}",
|
||||||
|
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||||
|
"--tile-skirts=off", "--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||||
|
f"--height-center-m={view['target'][2]:.6f}", "--height-range-m=8", "--tile-edges=off"]
|
||||||
|
if view.get("focus_radius", 24.0) is not None:
|
||||||
|
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||||
|
if render_clip is not None:
|
||||||
|
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||||
|
run(f"{stem}-render", render)
|
||||||
|
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
|
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
print(f"[L68CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
|
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||||
|
if not 0 < size <= 300_000:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Linux69 — owner four-strata captures and spawn trace
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
The L68 exports were limited to a small clip radius and LOD0 (max-clip-level=0, max-level=0). The camera frustum extended beyond those tiles, while the renderer cleared empty pixels to RGB (0.035, 0.055, 0.075). Unmeshed space therefore looked nearly black.
|
||||||
|
|
||||||
|
L69 exports use a 90° player-view frustum, radius 3 and LODs through 8. A coverage probe samples a 17×9 ray grid at 50 points per ray, every 4 m from 1 m through 197 m. Nine views needed one overlapping mesh patch. All 16 ended with zero uncovered probe samples. The renderer clear color is now sky blue (RGB (0.30, 0.48, 0.62)). A tile coverage failure aborts before rendering, so a missing export cannot silently become a black capture. This is a sampled coverage check, not a per-pixel proof.
|
||||||
|
|
||||||
|
The negative control removes 8 tiles from the strate 0 arrival mesh. It reports 480 uncovered samples and fails before rendering. Control mesh: Saved/Linux/l69/controls/strate0-arrival.truncated.vfmesh.
|
||||||
|
|
||||||
|
The coverage probe only answers whether mesh tiles cover its sampled frustum points. No real hole was confirmed in the captures. The engine trace blocker below is a solid passage sample. Blue pixels indicate an unoccluded ray through covered tiles; this round did not add a separate classifier for open sky versus a terrain breach.
|
||||||
|
|
||||||
|
All JPEGs are below 300 KB; combined size is 447,288 bytes.
|
||||||
|
|
||||||
|
| Strate | View | File | Bytes | What it shows |
|
||||||
|
|---:|---|---|---:|---|
|
||||||
|
| 0 | Arrival | [strate0-arrival.jpg](captures/strate0-arrival.jpg) | 40,064 | Player-eye view at the surface arrival, facing into the route. |
|
||||||
|
| 0 | Route | [strate0-route.jpg](captures/strate0-route.jpg) | 38,567 | Player-height view along the surface route toward the passage. |
|
||||||
|
| 0 | Departure mouth | [strate0-departure-mouth.jpg](captures/strate0-departure-mouth.jpg) | 35,055 | Inside the upper mouth looking outward. |
|
||||||
|
| 0 | Overview | [strate0-overview.jpg](captures/strate0-overview.jpg) | 23,395 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||||
|
| 1 | Arrival | [strate1-arrival.jpg](captures/strate1-arrival.jpg) | 24,524 | Player-eye view at the strate 1 arrival, facing into its route. |
|
||||||
|
| 1 | Route | [strate1-route.jpg](captures/strate1-route.jpg) | 29,602 | Player-height view along the passage. |
|
||||||
|
| 1 | Departure mouth | [strate1-departure-mouth.jpg](captures/strate1-departure-mouth.jpg) | 26,090 | Inside the upper mouth looking outward. |
|
||||||
|
| 1 | Overview | [strate1-overview.jpg](captures/strate1-overview.jpg) | 25,960 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||||
|
| 2 | Arrival | [strate2-arrival.jpg](captures/strate2-arrival.jpg) | 20,339 | Player-eye view at the strate 2 arrival, facing into its route. |
|
||||||
|
| 2 | Route | [strate2-route.jpg](captures/strate2-route.jpg) | 20,244 | Player-height view along the passage. |
|
||||||
|
| 2 | Departure mouth | [strate2-departure-mouth.jpg](captures/strate2-departure-mouth.jpg) | 33,125 | Inside the upper mouth looking outward. |
|
||||||
|
| 2 | Overview | [strate2-overview.jpg](captures/strate2-overview.jpg) | 25,668 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||||
|
| 3 | Arrival | [strate3-arrival.jpg](captures/strate3-arrival.jpg) | 24,307 | Player-eye view at the strate 3 arrival, facing into its route. |
|
||||||
|
| 3 | Route | [strate3-route.jpg](captures/strate3-route.jpg) | 23,966 | Player-height view along the passage. |
|
||||||
|
| 3 | Departure mouth | [strate3-departure-mouth.jpg](captures/strate3-departure-mouth.jpg) | 35,034 | Inside the lower mouth looking outward. |
|
||||||
|
| 3 | Overview | [strate3-overview.jpg](captures/strate3-overview.jpg) | 21,348 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||||
|
|
||||||
|
## In-engine spawn trace
|
||||||
|
|
||||||
|
The host rebuilt successfully on Linux. The trace harness was prepared for 3 rotated/interleaved trials per strate, spawning at each arrival. No strate 0 attempt produced a valid Core trace; the other strata were not run. Engine metrics are therefore unavailable, including walkable ring, LOD0 ring, full set, worker-seconds, LOD0 p50/p95, and generateTile / mesh-copy / RMC timings.
|
||||||
|
|
||||||
|
| Strate | Arrival spawn (m) | Headless whole-view median | Valid engine trials | Walkable ring | LOD0 ring | Full set | Worker-s | LOD0 p50 / p95 | generateTile / mesh copy / RMC |
|
||||||
|
|---:|---|---:|---:|---:|---:|---:|---:|---|---|
|
||||||
|
| 0 | (0, 0, -374.939) | 1.074 s | 0/3 (attempts blocked) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||||
|
| 1 | (25.837, 3.199, -737.000) | 1.503 s | 0/3 (not run) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||||
|
| 2 | (19.166, -14.200, -753.374) | 0.577 s | 0/3 (not run) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||||
|
| 3 | (-20.063, -5.068, -900.354) | 0.210 s | 0/3 (not run) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||||
|
|
||||||
|
No engine/headless percentage comparison is available, so no >30% gap can be assessed. The default engine-generated four-strate plan failed passage validation at the solid Core voxel (-172, -138, -1794) (25 cm per voxel; approximately (-43.0, -34.5, -448.5) m). This is a blocked centerline point, not a hole. The saved owner manifest lacked layout_chunk_width_q; regenerating and combining its current SurfaceWorld plan with the saved FloatingIslands plan got past parsing, but UE rejected the island plan bounds. The imported slot is strate 1 chunks [-79, -16], while the live slot resolves to [-93, -64]. A floating-islands-only headless export also failed because it could not resolve a SurfaceWorld strate. I stopped without bypassing the production checks.
|
||||||
|
|
||||||
|
## Failed runs and build notes
|
||||||
|
|
||||||
|
- Radius 4 exports, with max clip level 8 and then 7, exceeded the 160-million-node shape safety bound. Radius 3 plus overlapping patches covered the views.
|
||||||
|
- The UE host toolchain failed under the 6 GiB address-space limit during CoreCLR startup, so host build and UE runs used the requested 6 GiB RSS limit. The host build passed after using the local VFHost/PluginDeps plugin mirror; the final incremental build compiled the spawn-coordinate parser.
|
||||||
|
- The first UE trace launch exposed comma parsing in the new spawn argument. The parser was corrected and the host rebuilt. One RSS-capped launch exited when AndroidFileServer failed to map; disabling that unrelated plugin let the commandlet run. The generated-settings plan hit the solid-centerline failure above. Manifest attempts then encountered the stale schema and stale island bounds; none reached valid spawn tracing.
|
||||||
|
- No attempted run exceeded 30 minutes. Logs are under Saved/Linux/l69.
|
||||||
|
|
||||||
|
No world recipe, geometry, or peer-determinism inputs changed. The only Source/ edit is Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp, adding a finite XYZ spawn override for the trace; it compiled in the Linux host build. Standalone/Source/StreamBenchmarkHeadless.cpp also writes the canonical manifest after plan assembly for trace setup; its Linux rebuild passed. Other capture-only edits are in Standalone/Source/LinuxOffscreenCapture.cpp and Standalone/Source/StreamBenchmarkPipeline.h.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run three interleaved UE startup traces at each owner-plan arrival."""
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
HOST = Path("/home/jahni/VFHost")
|
||||||
|
PLUGIN_SAVED = ROOT / "Saved/Linux/l69"
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l69"
|
||||||
|
OWNER_WORLD = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
OWNER_MANIFEST = HOST_PLUGIN_SAVED / "owner_world_manifest.json"
|
||||||
|
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||||
|
TRACES = PLUGIN_SAVED / "traces"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l69"
|
||||||
|
SPAWNS_M = {
|
||||||
|
0: (0.0, 0.0, -374.939453),
|
||||||
|
1: (25.836914, 3.199219, -737.0),
|
||||||
|
2: (19.166016, -14.200195, -753.374023),
|
||||||
|
3: (-20.0625, -5.068359, -900.353516),
|
||||||
|
}
|
||||||
|
ORDER = ((0, 1), (1, 1), (2, 1), (3, 1),
|
||||||
|
(1, 2), (2, 2), (3, 2), (0, 2),
|
||||||
|
(2, 3), (3, 3), (0, 3), (1, 3))
|
||||||
|
|
||||||
|
|
||||||
|
def run(strate, trial):
|
||||||
|
label = f"strate{strate}-trial{trial}"
|
||||||
|
trace = TRACES / f"{label}.trace.jsonl"
|
||||||
|
abslog = TRACES / f"{label}.log"
|
||||||
|
stdout = TRACES / f"{label}.stdout.log"
|
||||||
|
output = HOST_PLUGIN_SAVED / "output" / label
|
||||||
|
userdir = HOST_SAVED / label / "User"
|
||||||
|
zendata = HOST_SAVED / label / "Zen"
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
for directory in (output, userdir, zendata, ddc, TRACES):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWNS_M[strate])
|
||||||
|
command = [
|
||||||
|
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||||
|
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||||
|
"-notraceserver", "-nocrashreports", "-ddc=NoZenLocalFallback",
|
||||||
|
f"-userdir={userdir}", f"-ZenDataPath={zendata}", f"-LocalDataCachePath={ddc}",
|
||||||
|
"-run=VoxelForgeExplore", "-seed=0", "-archetype=TunnelNetwork", "-slot=0",
|
||||||
|
"-modes=export", "-gameconfig=1", "-coreworldtest=1", "-ownerfourstrateplan=1",
|
||||||
|
"-corevoxelsizecm=25", "-coreislandtest=0", "-corefallbacktest=0",
|
||||||
|
"-coreofftest=0", "-coreplantest=0",
|
||||||
|
"-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0",
|
||||||
|
f"-coreworldassembly={OWNER_MANIFEST}",
|
||||||
|
f"-out={output}",
|
||||||
|
"-voxel.StreamingCoarseFirst=0", "-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0", "-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||||
|
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||||
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||||
|
"-coreworldstreamtrace=1", "-voxel.TestMove=0",
|
||||||
|
"-voxel.TestMoveSpeedCmPerSecond=0", f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||||
|
f"-voxel.StartupTraceFile={trace}", "-voxel.StartupTraceUntilEndPlay=1",
|
||||||
|
"-voxel.StartupTraceMaxTiles=16384", "-voxel.CollisionGate=0",
|
||||||
|
f"-abslog={abslog}",
|
||||||
|
]
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--rss=6442450944", "--",
|
||||||
|
"timeout", "1740s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1740, check=False)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[L69EngineTrace] label={label} spawn_cm={spawn_cm} elapsed_s={elapsed:.2f} "
|
||||||
|
f"exit={result.returncode} trace={trace} stdout={stdout}", flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{label} failed; inspect {stdout} and {abslog}")
|
||||||
|
if not trace.is_file() or trace.stat().st_size == 0:
|
||||||
|
raise SystemExit(f"{label} exited successfully but produced no startup trace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not EDITOR.is_file():
|
||||||
|
raise SystemExit(f"missing UE commandlet executable: {EDITOR}")
|
||||||
|
HOST_PLUGIN_SAVED.mkdir(parents=True, exist_ok=True)
|
||||||
|
owner = json.loads(OWNER_WORLD.read_text(encoding="utf-8"))
|
||||||
|
assembly = owner.get("settings_asset", {}).get("WorldAssemblyManifestJson")
|
||||||
|
if not isinstance(assembly, str) or not assembly:
|
||||||
|
raise SystemExit(f"{OWNER_WORLD} has no serialized world-assembly manifest")
|
||||||
|
OWNER_MANIFEST.write_text(assembly + "\n", encoding="utf-8")
|
||||||
|
print(f"[L69EngineTracePlan] source={OWNER_WORLD} manifest={OWNER_MANIFEST} "
|
||||||
|
f"bytes={OWNER_MANIFEST.stat().st_size}", flush=True)
|
||||||
|
for strate, trial in ORDER:
|
||||||
|
run(strate, trial)
|
||||||
|
print("[L69EngineTraceSet] strata=4 trials_each=3 total=12 status=PASS", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,77 @@
|
|||||||
|
## Ranked recommendation
|
||||||
|
|
||||||
|
**Keep the order TN → VS → FI.** The order is unchanged; I’ve corrected the explanation for FI and tightened the first-round gate.
|
||||||
|
|
||||||
|
| Rank | Generator | Recommendation |
|
||||||
|
|---|---|---|
|
||||||
|
| **1. TN** | Best first native-family step for caves and liminal variety. **[CODE]** A native cave graph, field evaluator and adapter recipe exist. **[ASSET]** The owner’s TN asset does not select that recipe or provide its graph. **[INFERENCE]** The parameter-to-graph work is a substantial migration, and a speed gain is unproven: the owner’s legacy TN is 0.577 s; the native cave fixture was 0.706 s. |
|
||||||
|
| **2. VS** | Strong visual payoff, but the existing native lattice is not yet a convincing shaft generator. **[CODE]** The adapter accepts `NativeLattice` for `VerticalShafts` and evaluates explicit room/tunnel graphs. **[REPORT]** The fixture looked like a corkscrew tunnel and called for an open shaft core; it took 1.694 s. This makes VS the highest geometry and gameplay risk. |
|
||||||
|
| **3. FI** | Keep last in a performance-led sequence, **not** because native FI is fixture-only. **[CODE]** Its native field, adapter planner and runtime dispatch path exist. **[REPORT]** The current owner run still reports compiled analytic FI at 1.503 s, and other FI fixtures are slower. **[INFERENCE]** It is the most mature native implementation, but its speed payoff is unclear. |
|
||||||
|
|
||||||
|
**What changed in this re-audit:** the FI adapter can auto-plan for an owner world without an imported assembly; an imported assembly must contain a matching plan for every FI stratum or validation fails. I cannot identify from the inspected code why the reported owner run still dispatches compiled analytic FI, so I withdraw the earlier suggestion that an imported override explains it. I also cannot attribute the owner’s FI time to the native mouth filter: that code runs only on the native path, while L67 reports the owner path as compiled analytic. Finally, changing TN’s field could disturb a passage landing; the first-round brief now makes the L68 route audits a promotion gate.
|
||||||
|
|
||||||
|
Labels below distinguish **[CODE]** source inspection, **[ASSET]** checked-in owner export, **[REPORT]** recorded run evidence, and **[INFERENCE]** an estimate or conclusion rather than a verified implementation fact.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
### The native paths are reachable from the runtime adapter
|
||||||
|
|
||||||
|
**[CODE] They are not reachable only through a fixture flag.** The production route in source is:
|
||||||
|
|
||||||
|
1. `AVoxelWorld::InitializeVoxelForgeCoreWorld` calls `SerializeResolvedWorld`, parses the returned snapshot and constructs the Core `World`. [VoxelWorld.cpp:7591–7623](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7591) [VoxelWorld.cpp:7661–7664](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7661)
|
||||||
|
2. The Unreal adapter attaches a validated assembly. On the generated-manifest branch it calls `BuildFloatingIslandStratumPlan` for FI slots; it also writes resolved layout entries, configures each `CoreGeometry` recipe, plans passages and resolves authored room/tunnel records. [VoxelForgeCoreAdapter.cpp:1552–1664](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1552) [VoxelForgeCoreAdapter.cpp:1836–1850](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1836) [VoxelForgeCoreAdapter.cpp:1980–2000](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1980)
|
||||||
|
3. Core’s regular tile-region build passes the manifest and resolved layout to `FStandaloneShapeWorld::Build`; that build sets `bUseNativeManifestFields=true` and binds the resolved fields. This is the normal build path, separate from `bNativeOnly`, the audit mode. [VoxelForgeCore.cpp:1667–1673](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1667) [ShapeWorldInternal.h:2332–2352](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2332)
|
||||||
|
4. `DensityAt` tries native FI, then the bound native cave/surface field; `DensityDispatchPathAt` probes those same evaluators and reports the selected path. Native cave composition applies the resolved graph and shared composition. [ShapeWorldInternal.h:3068–3099](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3068) [ShapeWorldInternal.h:3123–3140](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3123) [ShapeWorldInternal.h:1892–1920](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1892)
|
||||||
|
|
||||||
|
**[REPORT] Reachable does not mean active in the owner run.** L67 says the owner’s FI, TN and VS paths use the compiled analytic base evaluator. [L67 report:5](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux67/REPORT.md:5) **[ASSET]** The checked-in export selects `CoreGeometry.GeneratorRecipe=None`, so TN and VS are not opted into their graph recipes. [owner_world.json:1](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/data/owner_world.json:1) FI is different: its planner is automatic on one adapter branch, but the L67 dispatch evidence says the current run did not select it. The precise runtime reason remains unresolved.
|
||||||
|
|
||||||
|
There is one boundary to this finding: **[CODE]** the adapter has a CoreGeometry-only layout path when there are no SurfaceWorld or FI strata, but native density evaluation requires a `WorldAssembly` as well as a resolved layout. That may leave a caves-only world unable to use this path without an assembly. The owner’s four-strata world includes SurfaceWorld, so this caveat does not by itself explain its present dispatch. [VoxelForgeCoreAdapter.cpp:1602–1606](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1602) [ShapeWorldInternal.h:6570–6583](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6570)
|
||||||
|
|
||||||
|
### Exists / missing, checked against code
|
||||||
|
|
||||||
|
| Generator | Exists: what I verified | Missing: what I verified or inferred |
|
||||||
|
|---|---|---|
|
||||||
|
| **FI** | **[CODE]** `FloatingIslandFieldV1` evaluates island plans and provides feature bounds and intervals. The Unreal adapter maps island settings and creates plans; `ShapeWorldInternal.h` binds and dispatches the field when its plan is present. [FloatingIslandsInternal.h:2286–2315](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:2286) [FloatingIslandsInternal.h:2492–2540](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:2492) [VoxelForgeCoreAdapter.cpp:857–950](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:857) | **[CODE + REPORT]** No island-field algorithm port is missing. What is missing from the current owner run is successful native selection; the cause is not established. **[INFERENCE]** Likely work is path instrumentation and, only if it exposes a defect, a small adapter/runtime or verification change—not a new field family. |
|
||||||
|
| **TN** | **[CODE]** `NativeCaves` selects `Core.Caves.PrimitiveField` v1. The Unreal adapter converts authored rooms and tunnels; the Core resolver and shape evaluator resolve and compose that graph. [VoxelForgeCoreAdapter.cpp:145–158](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:145) [VoxelForgeCoreAdapter.cpp:1288–1414](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1288) [ShapeWorldInternal.h:7381–7430](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:7381) | **[CODE]** The inspected adapter resolves authored Core records; it does not translate the owner’s 82-field `FStrateGenerationParams` into them. **[ASSET]** The owner recipe is None and its graph arrays are empty. A parameter compiler and explicit opt-in are therefore missing. **[INFERENCE]** Likely touch points are `VoxelStrateTypes.h`, `VoxelForgeCoreAdapter.cpp`, Core graph/schema or resolver code if needed, `ShapeWorldInternal.h` if the field needs more semantics, plus owner verification. |
|
||||||
|
| **VS** | **[CODE]** `NativeLattice` is accepted for `VerticalShafts`, and uses resolved rooms and explicit tunnel paths. **[REPORT]** A native lattice fixture proves reachability and interval checks. [VoxelForgeCoreAdapter.cpp:159–171](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:159) [L58 report:9–13](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md:9) | **[CODE SURVEY + REPORT]** The inspected recipe/planner is generic lattice graph support, not a shaft-specific planner for `FVerticalShaftParams`; the fixture report says its result is not an open vertical shaft. A shaft plan/open core and safe landings remain. The claim that there is no other dedicated planner is limited to the inspected adapter/Core route; the report independently confirms the visual gap. |
|
||||||
|
|
||||||
|
**[INFERENCE] File counts from my prior answer were estimates, not counted changes.** For FI, the relevant implementation is already in `FloatingIslandsInternal.h`, `ShapeWorldInternal.h` and `VoxelForgeCoreAdapter.cpp`; first locate the selection mismatch before estimating edits. For TN, those same adapter/Core layers plus `VoxelStrateTypes.h` are the likely work area. For VS, expect those layers plus shaft-specific geometry and proof. Exact touched-file counts depend on whether existing records can represent the required parameter semantics.
|
||||||
|
|
||||||
|
### FI cost: traced paths and falsifier
|
||||||
|
|
||||||
|
There are three paths to keep separate:
|
||||||
|
|
||||||
|
- **[REPORT] Current owner path:** L67 reports `compiled-analytic/FloatingIslands`, not native FI. In `ShapeWorldInternal.h`, that means `DensityAt` falls through the native probes to `AnalyticDensityAt`; its general field path uses `EffectiveRockDistance`, the local detail pass, and shared composition. [ShapeWorldInternal.h:3068–3099](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3068) [ShapeWorldInternal.h:5343–5422](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5343) **I have not proved that the L55/L60 FI timings isolate this exact evaluator.**
|
||||||
|
- **[CODE] Unreal legacy generator path:** `BuildFloatingIslandStack` starts from void, adds `FIslandBlobSource`, roughness, fill and structural posts. `FIslandBlobSource` evaluates nearby hash-placed islands and contour warp. [VoxelGenerator.cpp:2630–2636](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2630) [VoxelDensityOpStack.cpp:2871–2935](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:2871) [VoxelDensityOpStack.cpp:9504–9533](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:9504)
|
||||||
|
- **[CODE] Native FI path:** when selected, Core samples `FloatingIslandFieldV1`; near a mouth it may add a 27-tap smoothing filter and six neighbor probes. [ShapeWorldInternal.h:6592–6608](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6592) [ShapeWorldInternal.h:6737–6800](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6737)
|
||||||
|
|
||||||
|
**[REPORT]** L55 attributes substantial worker work to grid and skirts: 36.414 worker-seconds of grid work and 2.787 of skirts across its 2,370-tile FI drain. These are worker-seconds for that fixture, not the current owner’s 1.503 s wall time. [L55 report:15–30](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux55/REPORT.md:15) R32’s optimistic calculation says removing all measured FI grid work from the L60 setup still leaves about 1.59 s; again, that is not the current owner path. [R32 report:41–45](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research32/REPORT.md:41)
|
||||||
|
|
||||||
|
So the defensible conclusion is **native FI is not yet demonstrated to be a speed fix**. I would revise that if a paired owner benchmark held camera, tile counts and clip settings fixed, logged `DensityDispatchPathAt`, and attributed time to field sampling, grid classification, meshing/normals and skirts. If native dispatch cut the same owner cold view to about 1 s without changing passage audits, that would disprove the current caution.
|
||||||
|
|
||||||
|
### Passage and landing risk
|
||||||
|
|
||||||
|
**Yes, a TN field migration could break a contract that L66–L68 just made pass.** The shared composer consumes the generator field before applying the boundary bands and passage carves; native graph caves also skip the legacy floor-cap stage. Changing the underlying field can therefore change support near a landing even if the passage plan and composer code stay untouched. [ShapeWorldInternal.h:1892–1920](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1892)
|
||||||
|
|
||||||
|
L68’s three interfaces had 0 blocked, unsupported and grade failures, with wall ripple ≤3.5 cm and floor ripple ≤2.5 cm. [L68 report:11–21](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux68/REPORT.md:11) Keeping the passage configuration fixed is necessary, but it does not prove the changed generator field preserves those results.
|
||||||
|
|
||||||
|
## First-round brief for the implementer
|
||||||
|
|
||||||
|
**Goal:** Add an explicitly opt-in native TN path for the owner’s current TN stratum. Compile its existing generation settings into a versioned native cave plan; keep legacy TN as the default until the owner plan passes all gates below. Do not change the shared passage planner or composer in this round.
|
||||||
|
|
||||||
|
**Invariants:**
|
||||||
|
|
||||||
|
- Account for every non-default owner TN setting in a field-by-field mapping. If the native graph cannot represent one, extend the recipe or reject native opt-in and leave legacy active. Do not silently drop it.
|
||||||
|
- Hold the owner’s seed, strate bounds, resolved passage configuration, passage route and shared composition code fixed.
|
||||||
|
- Keep FI and VS dispatch unchanged.
|
||||||
|
- Do not promote TN if its changed field alters a landing or route contract.
|
||||||
|
|
||||||
|
**Evidence required before enabling it on the owner asset:**
|
||||||
|
|
||||||
|
- `DensityDispatchPathAt` reports `Caves-native` throughout the TN stratum; record the resolved manifest/layout identity and graph plan used.
|
||||||
|
- Repeat the **full L68 passage audit across all three interfaces**, not just TN→VS: zero blocked/unsupported/grade failures and the same wall/floor ripple limits. Compare route and landing geometry with the fixed L68 plan.
|
||||||
|
- Recheck reachability, headroom and grade, plus interval boxes against brute-force samples; false all-air/all-solid controls must still be rejected.
|
||||||
|
- Verify deterministic hashes at W1/W8/W12 on both compilers.
|
||||||
|
- Run five interleaved cold owner whole-view repetitions with stable tile counts; target a median at or below 1.0 s and report moving p50/p95 against the legacy baseline.
|
||||||
|
|
||||||
|
Until those checks pass, native TN should remain an opt-in canary and the current owner recipe should remain on legacy dispatch.
|
||||||
@@ -794,7 +794,9 @@ void main()
|
|||||||
glEnableVertexAttribArray(5);glVertexAttribPointer(5,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
glEnableVertexAttribArray(5);glVertexAttribPointer(5,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||||
reinterpret_cast<void*>(offsetof(FVertex,Material)));
|
reinterpret_cast<void*>(offsetof(FVertex,Material)));
|
||||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||||
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
|
// A covered empty ray represents open air. Keep it visibly blue so it cannot
|
||||||
|
// be mistaken for the old near-black result from a scene that lacked tiles.
|
||||||
|
glDepthFunc(GL_LEQUAL);glClearColor(0.30f,0.48f,0.62f,1.0f);
|
||||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||||
glUseProgram(Program);
|
glUseProgram(Program);
|
||||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast<float>(TanHalfHorizontal));
|
glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast<float>(TanHalfHorizontal));
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ namespace
|
|||||||
bool HasCamera=false;
|
bool HasCamera=false;
|
||||||
bool HasCameraStep=false;
|
bool HasCameraStep=false;
|
||||||
bool AutoSurfaceCamera=false;
|
bool AutoSurfaceCamera=false;
|
||||||
|
bool CaptureSkipWalkability=false;
|
||||||
FVector CameraMeters=FVector::ZeroVector;
|
FVector CameraMeters=FVector::ZeroVector;
|
||||||
FVector CameraStepMeters=FVector::ZeroVector;
|
FVector CameraStepMeters=FVector::ZeroVector;
|
||||||
float CameraYawDegrees=0.0f;
|
float CameraYawDegrees=0.0f;
|
||||||
@@ -1410,6 +1411,7 @@ namespace
|
|||||||
" --region-crossing (compile entering Core regions at the camera step)\n"
|
" --region-crossing (compile entering Core regions at the camera step)\n"
|
||||||
" --column-cache=shared|thread-local (default: shared)\n"
|
" --column-cache=shared|thread-local (default: shared)\n"
|
||||||
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
||||||
|
" --capture-skip-walkability (mesh export only; defer the owner-wide passage gate)\n"
|
||||||
" --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n"
|
" --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n"
|
||||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||||
" --seed=N --recipe=surface-world|floating-islands|caves|lattice\n"
|
" --seed=N --recipe=surface-world|floating-islands|caves|lattice\n"
|
||||||
@@ -1589,6 +1591,7 @@ namespace
|
|||||||
if(options.MeshExportPath.empty())
|
if(options.MeshExportPath.empty())
|
||||||
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
||||||
}
|
}
|
||||||
|
else if(arg=="--capture-skip-walkability")options.CaptureSkipWalkability=true;
|
||||||
else if(arg.starts_with("--workers="))
|
else if(arg.starts_with("--workers="))
|
||||||
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
||||||
else if(arg.starts_with("--clip-radius="))
|
else if(arg.starts_with("--clip-radius="))
|
||||||
@@ -1626,6 +1629,8 @@ namespace
|
|||||||
{std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;}
|
{std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;}
|
||||||
if(options.LatticeRecipe&&options.LatticeStrateIndex<0)
|
if(options.LatticeRecipe&&options.LatticeStrateIndex<0)
|
||||||
{std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return false;}
|
{std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return false;}
|
||||||
|
if(options.CaptureSkipWalkability&&options.MeshExportPath.empty())
|
||||||
|
{std::fprintf(stderr,"--capture-skip-walkability requires --mesh-export=FILE\n");return false;}
|
||||||
if((options.AuditNativeLattice||options.LatticeOrphanControl)&&!options.LatticeRecipe)
|
if((options.AuditNativeLattice||options.LatticeOrphanControl)&&!options.LatticeRecipe)
|
||||||
{std::fprintf(stderr,"native lattice audits require --recipe=lattice\n");return false;}
|
{std::fprintf(stderr,"native lattice audits require --recipe=lattice\n");return false;}
|
||||||
if(options.TlsProbeRequested)return true;
|
if(options.TlsProbeRequested)return true;
|
||||||
@@ -1716,12 +1721,31 @@ int main(int argc,char** argv)
|
|||||||
const auto planStart=Clock::now();
|
const auto planStart=Clock::now();
|
||||||
VoxelForgeCore::WorldAssemblyManifest worldAssembly;
|
VoxelForgeCore::WorldAssemblyManifest worldAssembly;
|
||||||
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||||
*ownerWorld,ownerJsonRoot,error,options.WorldAssemblyManifestOutputPath.string(),&worldAssembly))
|
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
||||||
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
||||||
if(options.FloatingIslandsRecipe
|
if(options.FloatingIslandsRecipe
|
||||||
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||||
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
||||||
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
if(!options.WorldAssemblyManifestOutputPath.empty())
|
||||||
|
{
|
||||||
|
const std::string manifestText=worldAssembly.ToJson();
|
||||||
|
const auto& outputPath=options.WorldAssemblyManifestOutputPath;
|
||||||
|
std::error_code directoryError;
|
||||||
|
if(!outputPath.parent_path().empty())
|
||||||
|
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
||||||
|
if(directoryError)
|
||||||
|
{std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;}
|
||||||
|
std::ofstream output(outputPath,std::ios::binary);
|
||||||
|
if(!output)
|
||||||
|
{std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;}
|
||||||
|
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
||||||
|
if(!output)
|
||||||
|
{std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;}
|
||||||
|
std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n",
|
||||||
|
manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(),
|
||||||
|
outputPath.string().c_str());
|
||||||
|
}
|
||||||
if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly))
|
if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly))
|
||||||
{
|
{
|
||||||
std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n");
|
std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n");
|
||||||
@@ -2159,5 +2183,6 @@ int main(int argc,char** argv)
|
|||||||
options.ExactMidpointDensityMemo,options.ActiveLaneBatching,
|
options.ExactMidpointDensityMemo,options.ActiveLaneBatching,
|
||||||
options.ProofBlockCells,options.HasSignEdgeRepairOverride
|
options.ProofBlockCells,options.HasSignEdgeRepairOverride
|
||||||
?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe,
|
?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe,
|
||||||
options.AuditNativeLattice,options.LatticeOrphanControl);
|
options.AuditNativeLattice,options.LatticeOrphanControl,
|
||||||
|
options.CaptureSkipWalkability);
|
||||||
}
|
}
|
||||||
@@ -1872,7 +1872,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
bool bExactMidpointDensityMemo=false,
|
bool bExactMidpointDensityMemo=false,
|
||||||
bool bActiveLaneBatching=false,int32 ProofBlockCells=8,
|
bool bActiveLaneBatching=false,int32 ProofBlockCells=8,
|
||||||
bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false,
|
bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false,
|
||||||
bool bLatticeOrphanControl=false)
|
bool bLatticeOrphanControl=false,bool bCaptureSkipWalkability=false)
|
||||||
{
|
{
|
||||||
using Clock=std::chrono::steady_clock;
|
using Clock=std::chrono::steady_clock;
|
||||||
FStandaloneOwnerWorld& World=*OwnerWorld;
|
FStandaloneOwnerWorld& World=*OwnerWorld;
|
||||||
@@ -2029,6 +2029,9 @@ namespace StandaloneStreamBenchmark
|
|||||||
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
|
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
|
||||||
ShapeRequest.roundTunnels=bRoundTunnels;
|
ShapeRequest.roundTunnels=bRoundTunnels;
|
||||||
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
||||||
|
ShapeRequest.validatePassageRoutes=!bCaptureSkipWalkability;
|
||||||
|
if(bCaptureSkipWalkability)
|
||||||
|
std::printf("[CaptureWalkabilityGate] validation=deferred geometry_input=unchanged mode=mesh-export\n");
|
||||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||||||
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);
|
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);
|
||||||
|
|||||||