feat(render): fade railroad overlay near min zoom

Replace the hard zoom cutoff in RailroadPass with a linear alpha fade
controlled by a new `railFadeRange` setting. Rails (and bridge pixels)
ramp from invisible at `railMinZoom - railFadeRange` to fully opaque at
`railMinZoom`, instead of popping in. Adds a uRailFade shader uniform
and a debug slider.
This commit is contained in:
evanpelle
2026-05-27 15:42:11 -07:00
parent aa3959bffe
commit 23fbc3114a
5 changed files with 29 additions and 6 deletions
+1
View File
@@ -81,6 +81,7 @@ export interface RenderSettings {
};
railroad: {
railMinZoom: number;
railFadeRange: number;
railDetailZoom: number;
railAlpha: number;
};
+9
View File
@@ -162,6 +162,15 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] {
folder("Railroad", [
slider(s.railroad, "railMinZoom", d.railroad, 0, 10, 0.1, "Min Zoom"),
slider(
s.railroad,
"railFadeRange",
d.railroad,
0,
5,
0.1,
"Fade Range",
),
slider(
s.railroad,
"railDetailZoom",
+13 -2
View File
@@ -98,6 +98,7 @@ export class RailroadPass {
private uZoom: WebGLUniformLocation;
private uRailDetailZoom: WebGLUniformLocation;
private uRailAlpha: WebGLUniformLocation;
private uRailFade: WebGLUniformLocation;
private uGhostOwnerID: WebGLUniformLocation;
private mapW: number;
@@ -145,6 +146,7 @@ export class RailroadPass {
"uRailDetailZoom",
)!;
this.uRailAlpha = gl.getUniformLocation(this.program, "uRailAlpha")!;
this.uRailFade = gl.getUniformLocation(this.program, "uRailFade")!;
this.uGhostOwnerID = gl.getUniformLocation(this.program, "uGhostOwnerID")!;
// Texture unit bindings + ghost defaults
@@ -265,8 +267,16 @@ export class RailroadPass {
const gl = this.gl;
const rs = this.settings.railroad;
// Skip entirely when below minimum zoom
if (zoom < rs.railMinZoom) return;
// Fade out as zoom drops below railMinZoom; fully invisible at railMinZoom - railFadeRange
const fadeRange = Math.max(rs.railFadeRange, 0);
const fadeStart = rs.railMinZoom - fadeRange;
const fade =
fadeRange <= 0
? zoom >= rs.railMinZoom
? 1
: 0
: Math.min(1, Math.max(0, (zoom - fadeStart) / fadeRange));
if (fade <= 0) return;
// Flush CPU railroad state → GPU
if (this.railroadDirty) {
@@ -310,6 +320,7 @@ export class RailroadPass {
gl.uniform1f(this.uZoom, zoom);
gl.uniform1f(this.uRailDetailZoom, rs.railDetailZoom);
gl.uniform1f(this.uRailAlpha, rs.railAlpha);
gl.uniform1f(this.uRailFade, fade);
gl.uniform1f(this.uGhostOwnerID, this.ghostOwnerID);
// Bind textures: 0=railroad, 1=tile, 2=palette, 3=terrain, 4=ghostRail
@@ -79,6 +79,7 @@
},
"railroad": {
"railMinZoom": 4,
"railFadeRange": 2,
"railDetailZoom": 6,
"railAlpha": 1
},
@@ -12,6 +12,7 @@ uniform vec2 uMapSize;
uniform float uZoom;
uniform float uRailDetailZoom;
uniform float uRailAlpha;
uniform float uRailFade; // Zoom-based fade multiplier (0..1)
uniform float uGhostOwnerID; // Player smallID for ghost rail color
in vec2 vWorldPos;
@@ -139,17 +140,17 @@ void main() {
// Overlapping railroad highlight — green tint
if (highlighted) railColor = vec3(0.2, 0.85, 0.3);
if (hitBridge) {
fragColor = vec4(mix(bridgeColor, railColor, railAlpha), 1.0);
fragColor = vec4(mix(bridgeColor, railColor, railAlpha), uRailFade);
} else {
fragColor = vec4(railColor, railAlpha);
fragColor = vec4(railColor, railAlpha * uRailFade);
}
} else if (hitGhost) {
float ghostAlpha = uRailAlpha * ghostCov * 0.5;
vec3 ghostColor = uGhostOwnerID > 0.0
? texture(uPalette, vec2((uGhostOwnerID + 0.5) / float(PALETTE_SIZE), 0.75)).rgb
: vec3(0.75);
fragColor = vec4(ghostColor, ghostAlpha);
fragColor = vec4(ghostColor, ghostAlpha * uRailFade);
} else {
fragColor = vec4(bridgeColor, 1.0);
fragColor = vec4(bridgeColor, uRailFade);
}
}