mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-14 01:22:17 +00:00
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.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bogr8ydj6q8rp
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqe75ud45htst
|
||||
Reference in New Issue
Block a user