Files
CosmicClash/Game/shaders/arena_deck.gdshader
T
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

139 lines
5.9 KiB
Plaintext

// Opaque hull for the arena: the deck, the fillets easing it into the walls,
// and the bulkheads surrounding each goal mouth — the lit half of
// ArenaBoundary's visual shell.
//
// Everything is drawn procedurally from the vertex's position in the
// boundary's local space, so field markings are resolution-independent and
// need no decals, UVs or texture authoring. All dimensions arrive as uniforms
// set from ArenaBoundary's constants (see _make_deck_material), so the
// markings cannot drift away from the collision geometry they describe.
shader_type spatial;
render_mode cull_back, diffuse_burley, specular_schlick_ggx;
uniform vec3 deck_color : source_color = vec3(0.085, 0.095, 0.122);
// Kept close to deck_color on purpose: per-plate variation is meant to break
// up a 24x36 m plane, not to reintroduce visible tiling.
uniform vec3 panel_color : source_color = vec3(0.104, 0.116, 0.146);
// Stand-in for bounce light. The arena is lit by one dim directional plus low
// ambient, so hull facing away from it — the goal bulkheads, the fillet caps
// either side of each mouth — otherwise crushed to pure black and the whole
// end zone read as a void with a goal floating in it.
uniform float hull_fill : hint_range(0.0, 2.0) = 0.55;
uniform float panel_variation : hint_range(0.0, 1.0) = 0.4;
uniform vec3 line_color : source_color = vec3(0.62, 0.78, 1.0);
uniform vec3 team0_color : source_color = vec3(0.15, 0.45, 1.0);
uniform vec3 team1_color : source_color = vec3(1.0, 0.35, 0.25);
uniform vec3 seam_color : source_color = vec3(0.35, 0.75, 1.0);
// Play-volume dimensions, from ArenaBoundary's constants.
uniform float half_x = 12.0;
uniform float half_z = 18.0;
uniform float goal_line_z = 18.0;
uniform float base_radius = 2.0;
uniform float centre_circle_radius = 4.0;
uniform float goal_area_depth = 6.0;
uniform float goal_area_half_width = 6.0;
uniform float line_width = 0.14;
// Each arena's Environment runs glow at hdr_threshold ~1.0 with hdr_scale 2.0,
// so emission much above 1 blooms into a white smear. These stay under it.
uniform float line_emission : hint_range(0.0, 8.0) = 0.65;
uniform float seam_emission : hint_range(0.0, 8.0) = 0.8;
uniform float seam_width = 0.22;
uniform float end_zone_strength : hint_range(0.0, 1.0) = 0.12;
uniform float panel_size = 4.0;
uniform float panel_line_width : hint_range(0.0, 0.5) = 0.05;
// Deliberately not a high metallic value: this arena is lit by one dim
// directional plus low ambient, and a near-metal deck has almost no diffuse
// response, which crushed the plating to black.
uniform float deck_metallic : hint_range(0.0, 1.0) = 0.3;
uniform float deck_roughness : hint_range(0.0, 1.0) = 0.34;
varying vec3 v_local;
varying vec3 v_local_normal;
void vertex() {
v_local = VERTEX;
v_local_normal = NORMAL;
}
float hash21(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
// 1 inside a band of width `w` centred on the zero of `d`, screen-space
// antialiased so lines stay crisp at grazing angles and don't shimmer.
float band(float d, float w) {
float aa = fwidth(d) + 0.001;
return 1.0 - smoothstep(w * 0.5, w * 0.5 + aa, abs(d));
}
// Antialiased outline of the axis-aligned box |p| <= half_size.
float rect_outline(vec2 p, vec2 half_size, float w) {
vec2 d = abs(p) - half_size;
return band(max(d.x, d.y), w);
}
// Plate seam mask for one projection plane.
float plate_seam(vec2 p) {
vec2 d = abs(fract(p / panel_size) - 0.5) * panel_size;
float m = min(d.x, d.y);
return 1.0 - smoothstep(panel_line_width, panel_line_width + fwidth(m) + 0.01, m);
}
void fragment() {
vec2 xz = v_local.xz;
vec3 n = normalize(v_local_normal);
float up_facing = clamp(n.y, 0.0, 1.0);
// --- hull plating ----------------------------------------------------
// Projected onto whichever plane each facet most faces, blended by
// |normal|, so plating doesn't smear up the fillets or stripe the vertical
// goal bulkheads the way a flat XZ projection did.
vec3 w = abs(n);
w /= max(w.x + w.y + w.z, 0.001);
float seams = w.x * plate_seam(v_local.zy)
+ w.y * plate_seam(v_local.xz)
+ w.z * plate_seam(v_local.xy);
vec3 albedo = mix(deck_color, panel_color, hash21(floor(xz / panel_size)) * panel_variation);
albedo = mix(albedo, deck_color * 0.55, seams);
float rough = deck_roughness + seams * 0.25;
// --- team-tinted end zones -------------------------------------------
// Team 0 defends +z (see the arenas' GoalTeam0 transforms), team 1 -z.
float t0 = smoothstep(0.0, half_z, xz.y);
float t1 = smoothstep(0.0, half_z, -xz.y);
vec3 zone = team0_color * t0 + team1_color * t1;
// Markings belong on the flat deck only: fade them out as the fillet lifts
// away from the floor, and drop them entirely on anything not facing up so
// they never wrap onto a goal bulkhead.
float flat_deck = (1.0 - smoothstep(0.0, base_radius * 0.4, v_local.y)) * up_facing;
albedo = mix(albedo, albedo + zone * 0.18, end_zone_strength * flat_deck);
// --- field markings ---------------------------------------------------
float marks = band(length(xz) - centre_circle_radius, line_width);
marks = max(marks, band(xz.y, line_width));
for (int i = 0; i < 2; i++) {
float s = i == 0 ? 1.0 : -1.0;
vec2 c = vec2(0.0, s * (goal_line_z - goal_area_depth * 0.5));
marks = max(marks, rect_outline(
xz - c, vec2(goal_area_half_width, goal_area_depth * 0.5), line_width));
}
marks *= flat_deck;
// --- deck/field handover ---------------------------------------------
// A band, not a step: it has to light the top edge of the fillets and the
// matching edge of the goal bulkheads without setting the whole of an
// elevated bulkhead (which sits far above base_radius) glowing.
float seam = 1.0 - smoothstep(0.0, seam_width, abs(v_local.y - base_radius));
ALBEDO = mix(albedo, line_color, marks);
METALLIC = deck_metallic * (1.0 - marks);
ROUGHNESS = clamp(mix(rough, 0.5, marks), 0.05, 1.0);
EMISSION = albedo * hull_fill
+ line_color * marks * line_emission
+ seam_color * seam * seam_emission
+ zone * 0.015 * flat_deck;
}