Files
CosmicClash/Game/shaders/energy_field.gdshader
Josh Creek 02df09e40d feat(arena): draw the play volume as one non-overlapping surface shell
Every visible surface of the enclosure shared one alpha-blended material and
was drawn as several overlapping layers: floor, ceiling and four walls as
solid BoxMeshes, plus corner curves and fillets generated on top of the box
faces they eased into. Alpha-blended surfaces don't write depth and sort per
object, so the perimeter composited that tint twice and the corners three
times, giving hard-edged trapezoidal patches that re-sorted as the camera
moved. The floor box also overhung the walls by 1 m and showed through them.

Replace all of it with a single generated mesh covering the inner surface
exactly once, every piece cut to meet its neighbours edge-on and only
inward-facing triangles emitted. It carries two surfaces: an opaque hull
(deck, base fillets, goal bulkheads) and the translucent containment field
(walls, corners, ceiling fillets, ceiling).

The field shader is additive and unshaded rather than alpha-blended: additive
cannot double-darken and composites order-independently, so overlap is
structurally invisible, and being unshaded it no longer picks up per-arena
light and GI gradients across a 28x40 m panel. Fresnel replaces the old
StandardMaterial3D rim, which was a lit effect and the wrong tool. The deck
shader draws plating and field markings procedurally from the boundary's own
constants, so markings cannot drift from the collision geometry.

Per-face MeshInstance3D visibility toggling is gone; the field shader fades
facets the camera has crossed outside of per-pixel from one uniform, which is
what allows a single merged mesh.

Also adds ELEVATED goal mode and the ceiling fillets, and skips the mesh build
entirely under --headless, where training spawns instances that never render.

Collision is untouched: 166 collider shapes in FLOOR mode and 158 in ELEVATED,
verified byte-identical to before, so trained policies in Game/bots/ are
unaffected.
2026-08-04 13:35:55 +01:00

84 lines
3.5 KiB
Plaintext

// Containment-field surface for the arena walls, ceiling, corner curves and
// ceiling fillets — the translucent half of ArenaBoundary's visual shell.
//
// Additive and unshaded on purpose. The StandardMaterial3D this replaces used
// alpha blending plus rim lighting, which had two problems:
// - alpha blending darkens whatever is behind it, so wherever two boundary
// surfaces overlapped the tint composited twice and the overlap read as a
// hard-edged patch that re-sorted as the camera moved;
// - rim is a *lit* effect, so each arena's two DirectionalLight3Ds and SDFGI
// drove visible gradients across a single 28x40 m panel.
// Additive cannot double-darken and composites order-independently, so the
// field reads uniformly regardless of how the geometry happens to sort.
shader_type spatial;
render_mode blend_add, depth_draw_never, cull_back, unshaded;
uniform vec3 field_color : source_color = vec3(0.45, 0.65, 1.0);
uniform float field_intensity : hint_range(0.0, 1.0) = 0.09;
// Face-on brightness floor, before fresnel and cell detail.
uniform float base_level : hint_range(0.0, 2.0) = 0.5;
// Grazing-angle glow — the "glass" read the old rim param was reaching for.
// Kept gentle: a steep exponent makes a head-on end wall read as a hard-edged
// dark rectangle against the corner curves flanking it.
uniform float fresnel_power : hint_range(0.5, 8.0) = 2.2;
uniform float fresnel_boost : hint_range(0.0, 8.0) = 2.6;
uniform float cell_size = 1.6;
uniform float cell_width : hint_range(0.0, 0.5) = 0.06;
uniform float cell_strength : hint_range(0.0, 4.0) = 1.1;
// Camera position in ArenaBoundary's local space, pushed each frame by
// arena_boundary.gd. Facets the camera has crossed to the outside of fade out
// rather than popping, so looking into the arena from outside stays clear.
// Per-pixel, which is what lets one merged shell replace the old per-face
// MeshInstance3D visibility toggling.
uniform vec3 camera_local_pos = vec3(0.0, 6.0, 0.0);
uniform float cull_fade_distance : hint_range(0.01, 8.0) = 1.25;
varying vec3 v_local_pos;
varying vec3 v_local_normal;
void vertex() {
v_local_pos = VERTEX;
v_local_normal = NORMAL;
}
// Offset-grid nearest-cell offset, used to build a hex lattice.
vec2 hex_cell(vec2 p) {
vec2 r = vec2(1.0, 1.7320508);
vec2 h = r * 0.5;
vec2 a = mod(p, r) - h;
vec2 b = mod(p - h, r) - h;
return dot(a, a) < dot(b, b) ? a : b;
}
float hex_edge(vec2 p) {
vec2 g = abs(hex_cell(p / cell_size));
float d = max(dot(g, normalize(vec2(1.0, 1.7320508))), g.x);
return smoothstep(0.5 - cell_width, 0.5, d);
}
void fragment() {
vec3 n = normalize(v_local_normal);
// Project the hex lattice onto whichever plane the facet most faces,
// blended by |normal| so the curved corner and fillet surfaces cross
// between projections without a seam.
vec3 w = abs(n);
w /= max(w.x + w.y + w.z, 0.001);
float cells =
w.x * hex_edge(v_local_pos.zy) +
w.y * hex_edge(v_local_pos.xz) +
w.z * hex_edge(v_local_pos.xy);
float facing = clamp(dot(normalize(NORMAL), normalize(VIEW)), 0.0, 1.0);
float fresnel = pow(1.0 - facing, fresnel_power);
float level = base_level + fresnel * fresnel_boost + cells * cell_strength;
ALBEDO = field_color * level * field_intensity;
// Signed distance from the camera to this facet's plane along its inward
// normal: positive while the camera is still inside the enclosure, so the
// panel fades out just before back-face culling would drop it.
float side = dot(camera_local_pos - v_local_pos, n);
ALPHA = smoothstep(0.0, cull_fade_distance, side);
}