fix(client): bottom-wall scan pierces 2-thick walls + corner diagonal; whole-box quantized cuts (T-1088)
Live-session findings, three in one: (1) Gauntlet walls are 2 tiles thick — the outer row's camera-far neighbor is the inner WALL, so its flag stayed 0 and it stood full-height behind the collapsed inner stub; the flag now scans up to 3 tiles through consecutive KNOWN walls (unknown stops the scan — no assumptions past the info boundary), and any newly-learned tile refreshes the wall chain behind it. (2) Corner pillars at the screen-bottom junction never collapsed — their interior floor sits DIAGONALLY behind, which no straight scan line reaches; the combined diagonal joins the scan set. (3) Per-fragment smoothstep falloff carved organic notches (the 'sphere' read) — the cut now computes per-instance in the vertex shader from the box's tile center and quantizes at 0.5: every wall box is either full or stub, X2-crisp. Verified via locomotion_cutaway autopilot capture: south runs read as clean stub rows, corners collapse, camera-far walls stand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,10 @@ var _free_slots: Array = [[], []]
|
||||
var _state_colors: Array[Color] = []
|
||||
var _last_tick: int = -1
|
||||
var _capacity_warned := false
|
||||
## Bottom-wall scan depth (tiles) along each camera-far direction — covers the
|
||||
## Gauntlet's 2-thick walls plus door trim.
|
||||
const BOTTOM_SCAN_DEPTH := 3
|
||||
|
||||
## Cutaway mode: 0 = sightline corridor, 1 = bottom walls low, 2 = both.
|
||||
var cutaway_mode: int = 0
|
||||
## Camera-far sim-neighbor deltas for the bottom-wall rule (see
|
||||
@@ -284,10 +288,13 @@ func _paint_new(key: Vector3i) -> void:
|
||||
_slots[key] = Vector2i(kind, slot)
|
||||
if kind == Store.Kind.WALL:
|
||||
_refresh_bottom_flag(key)
|
||||
else:
|
||||
# A newly-learned floor can promote camera-ward walls to bottom walls.
|
||||
for d in _behind_dirs:
|
||||
_refresh_bottom_flag(key - d)
|
||||
# Any newly-learned tile can promote camera-ward walls up the scan chain:
|
||||
# a floor is the promotion itself; a wall can EXTEND a chain that previously
|
||||
# stopped at unknown (Gauntlet walls are 2 tiles thick — the outer row sees
|
||||
# floor only THROUGH the inner row).
|
||||
for d in _behind_dirs:
|
||||
for step in range(1, BOTTOM_SCAN_DEPTH + 1):
|
||||
_refresh_bottom_flag(key - d * step)
|
||||
|
||||
|
||||
## Cutaway mode (design amendment, live session 2026-07-06): 0 = sightline
|
||||
@@ -322,6 +329,11 @@ func _compute_behind_dirs() -> void:
|
||||
# hides lies OPPOSITE that face -> behind_dir = -d for camera-ward d.
|
||||
if world_dir.z > 0.01:
|
||||
_behind_dirs.append(-d)
|
||||
# Corner pillars: at a wall-run junction the interior floor sits DIAGONALLY
|
||||
# behind (neither straight scan line reaches it) — include the combined
|
||||
# diagonal in the scan set.
|
||||
if _behind_dirs.size() == 2:
|
||||
_behind_dirs.append(_behind_dirs[0] + _behind_dirs[1])
|
||||
# Repaint flags for walls painted before the deferred computation ran.
|
||||
for key: Vector3i in _slots:
|
||||
if (_slots[key] as Vector2i).x == Store.Kind.WALL:
|
||||
@@ -332,11 +344,20 @@ func _refresh_bottom_flag(key: Vector3i) -> void:
|
||||
var slot: Vector2i = _slots.get(key, Vector2i(-1, -1))
|
||||
if slot.y < 0 or slot.x != Store.Kind.WALL:
|
||||
return
|
||||
# Scan up to BOTTOM_SCAN_DEPTH tiles along each camera-far direction,
|
||||
# walking THROUGH consecutive known walls (Gauntlet walls are 2 thick):
|
||||
# a known non-wall tile behind the wall run marks the whole run "bottom".
|
||||
# Unknown tiles stop the scan — no assumptions past the info boundary.
|
||||
var flag := 0.0
|
||||
for d in _behind_dirs:
|
||||
var n: Vector3i = key + d
|
||||
if store.has_tile(n) and store.kind_of(n) != Store.Kind.WALL:
|
||||
flag = 1.0
|
||||
for step in range(1, BOTTOM_SCAN_DEPTH + 1):
|
||||
var n: Vector3i = key + d * step
|
||||
if not store.has_tile(n):
|
||||
break
|
||||
if store.kind_of(n) != Store.Kind.WALL:
|
||||
flag = 1.0
|
||||
break
|
||||
if flag > 0.0:
|
||||
break
|
||||
_wall_mm.multimesh.set_instance_custom_data(slot.y, Color(flag, 0.0, 0.0, 0.0))
|
||||
|
||||
|
||||
@@ -38,19 +38,17 @@ uniform int u_cutaway_mode = 0;
|
||||
uniform vec3 u_base_albedo : source_color = vec3(0.66, 0.64, 0.60);
|
||||
|
||||
varying vec3 v_world_pos;
|
||||
// Bottom-wall flag (INSTANCE_CUSTOM.r, set by GreyboxWorld): 1.0 when a KNOWN
|
||||
// non-wall tile sits on this wall's camera-far side — the wall would occlude
|
||||
// interior the player should see, X2-style.
|
||||
varying flat float v_bottom;
|
||||
// Whole-box cut decision, computed per-INSTANCE in vertex() from the box's tile
|
||||
// center — every wall box is either full height or the stub, no per-fragment
|
||||
// falloff curves carving organic notches (the "sphere" read). X2-crisp tile
|
||||
// cuts; the smoothstep bands only shape where the 0.5 threshold falls.
|
||||
varying flat float v_cut;
|
||||
|
||||
void vertex() {
|
||||
// MODEL_MATRIX folds in the per-instance MultiMesh transform.
|
||||
v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
v_bottom = INSTANCE_CUSTOM.r;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 to_wall = v_world_pos.xz - u_char_pos_xz;
|
||||
// MODEL_MATRIX folds in the per-instance MultiMesh transform; its origin
|
||||
// is the box center — .xz is the wall tile's center in world space.
|
||||
vec2 inst_xz = (MODEL_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xz;
|
||||
vec2 to_wall = inst_xz - u_char_pos_xz;
|
||||
// Lateral gate: full cut inside (half_w - band), fading to none at half_w.
|
||||
float lateral = 1.0 - smoothstep(u_corridor_half_w - u_cut_band, u_corridor_half_w, abs(to_wall.x));
|
||||
// Near gate: soft start just behind the character (no hard seam at their feet).
|
||||
@@ -58,13 +56,20 @@ void fragment() {
|
||||
// Far gate: corridor ends where a WALL_H wall can no longer occlude.
|
||||
float far_f = 1.0 - smoothstep(u_cut_reach, u_cut_reach + u_cut_band, to_wall.y);
|
||||
float corridor_f = lateral * near_f * far_f;
|
||||
// Bottom-wall flag (INSTANCE_CUSTOM.r, GreyboxWorld): KNOWN non-wall tile
|
||||
// behind this wall run — it would occlude interior the player should see.
|
||||
float cut_f = corridor_f;
|
||||
if (u_cutaway_mode == 1) {
|
||||
cut_f = v_bottom;
|
||||
cut_f = INSTANCE_CUSTOM.r;
|
||||
} else if (u_cutaway_mode == 2) {
|
||||
cut_f = max(corridor_f, v_bottom);
|
||||
cut_f = max(corridor_f, INSTANCE_CUSTOM.r);
|
||||
}
|
||||
float allowed = mix(u_wall_h, u_cut_height, cut_f);
|
||||
v_cut = cut_f > 0.5 ? 1.0 : 0.0;
|
||||
v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float allowed = mix(u_wall_h, u_cut_height, v_cut);
|
||||
if (v_world_pos.y > allowed) {
|
||||
discard;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user