fix: guard shader pow() bases so a NaN pixel can no longer black out the frame

This commit is contained in:
2026-10-12 04:23:25 +03:00
parent 50da616a46
commit 06e6a59b6f
18 changed files with 165 additions and 34 deletions

View File

@@ -56,7 +56,7 @@ export function createArrivalMeteors(resources: SceneResourceBag) {
varying float vLife;
void main() {
float across = abs(vUv.y * 2.0 - 1.0);
float taper = pow(vUv.x, 1.4);
float taper = pow(clamp(vUv.x, 0.0, 1.0), 1.4);
float core = (1.0 - smoothstep(0.06, 0.28, across)) * taper;
float glow = (1.0 - smoothstep(0.0, 1.0, across)) * taper;
float head = (1.0 - smoothstep(0.0, 1.0,

View File

@@ -88,7 +88,7 @@ export function createAtmosphere(
float drift = sin(uv.x * 5.2 + uTime * 0.025) * 0.035;
float arc = uv.y - (0.66 + 0.19 * sin(uv.x * 2.8 - 0.35) + drift);
float veil = exp(-arc * arc * 82.0);
float innerVeil = exp(-pow((arc + 0.055) * 17.0, 2.0));
float innerVeil = exp(-pow(abs(arc + 0.055) * 17.0, 2.0));
float side = smoothstep(0.15, 0.7, uv.x);
vec3 violet = vec3(0.23, 0.018, 0.50);
vec3 rose = vec3(0.50, 0.018, 0.17);

View File

@@ -422,7 +422,7 @@ export function createBillboard(resources: SceneResourceBag, options: BillboardO
float alpha = 1.0;
if (vPart < 0.5) {
color = vec3(0.03, 0.026, 0.045) + vec3(0.08, 0.07, 0.11) * key + warm * 0.05 * flood * max(0.0, n.y);
color += uAccent * 0.05 * pow(1.0 - abs(dot(n, toCamera)), 3.0);
color += uAccent * 0.05 * pow(max(1.0 - abs(dot(n, toCamera)), 0.0), 3.0);
} else if (vPart < 1.5) {
color = uAccent * (0.55 + 0.9 * flood);
// Drive-energy shimmer: one bright band runs round the frame.

View File

@@ -20,7 +20,7 @@ export const DAWN_PARS = /* glsl */ `
if (uDawnLight <= 0.0) return vec3(0.0);
vec3 sunDir = vec3(${SUN_DIRECTION.x.toFixed(4)}, ${SUN_DIRECTION.y.toFixed(4)}, ${SUN_DIRECTION.z.toFixed(4)});
float facing = abs(dot(n, sunDir));
float rim = pow(1.0 - abs(dot(n, toCamera)), 2.0);
float rim = pow(max(1.0 - abs(dot(n, toCamera)), 0.0), 2.0);
float against = max(0.0, dot(-toCamera, sunDir));
return vec3(1.0, 0.6, 0.36) * uDawnLight * (facing * 0.09 + rim * against * 0.24);
}

View File

@@ -242,7 +242,7 @@ export function createDistricts(
vec3 color = vec3(0.022, 0.018, 0.034) + vec3(0.07, 0.06, 0.1) * key + vec3(0.02, 0.015, 0.035) * fill;
// Emissive edge: the silhouette catches the chapter colour, and
// the tops of the pieces carry a thin lit band.
float rim = pow(1.0 - abs(dot(n, normalize(cameraPosition - vWorld))), 4.0);
float rim = pow(max(1.0 - abs(dot(n, normalize(cameraPosition - vWorld))), 0.0), 4.0);
float crown = smoothstep(0.92, 1.0, n.y) * smoothstep(1.0, 6.0, vHeight);
${serviced ? `
float dimmed = serviceRowDim();

View File

@@ -54,7 +54,7 @@ export function createExpressway(
vec3 cyan = mix(vec3(1.0, 0.025, 0.34), vec3(0.02, 0.78, 0.93), step(0.0, vWorld.x));
vec3 coral = vec3(1.0, 0.18, 0.095);
float facing = max(0.0, dot(normalize(vNormal), normalize(vec3(-0.5, 0.9, 0.6))));
vec3 color = ${luminous ? `mix(${coral ? 'coral' : 'cyan'}, coral, uWarmth * 0.12) * (1.3 + uEnergy * 0.5)` : 'vec3(0.028, 0.012, 0.046) + vec3(0.09, 0.055, 0.14) * pow(facing, 2.0)'};
vec3 color = ${luminous ? `mix(${coral ? 'coral' : 'cyan'}, coral, uWarmth * 0.12) * (1.3 + uEnergy * 0.5)` : 'vec3(0.028, 0.012, 0.046) + vec3(0.09, 0.055, 0.14) * pow(max(facing, 0.0), 2.0)'};
${!luminous ? 'color += cyan * pow(max(0.0, vNormal.x), 2.0) * 0.028;' : ''}
if(farFade * nearFade < 0.02) discard;
gl_FragColor = vec4(color, farFade * nearFade);

View File

@@ -28,8 +28,8 @@ export function createHorizonAir(resources: SceneResourceBag) {
varying vec2 vUv;
void main(){
float wave=sin(vUv.x*12.0+uTime*0.018)*0.055+sin(vUv.x*31.0-uTime*0.026)*0.018;
float ribbon=exp(-pow((vUv.y-0.45-wave)*5.6,2.0));
float thin=exp(-pow((vUv.y-0.51-wave)*37.0,2.0));
float ribbon=exp(-pow(abs(vUv.y-0.45-wave)*5.6,2.0));
float thin=exp(-pow(abs(vUv.y-0.51-wave)*37.0,2.0));
float horizontal=pow(max(0.0,sin(vUv.x*3.14159)),2.0);
float colorWave=0.5+0.5*sin(vUv.x*7.0+uTime*0.035);
vec3 color=mix(uColor,vec3(0.27,0.045,0.75),colorWave*0.55);

View File

@@ -275,7 +275,7 @@ export function createMaintenanceWindow(resources: SceneResourceBag) {
varying float vReturn;
void main() {
float across = abs(vUv.y);
float body = (1.0 - smoothstep(0.0, 1.0, across)) * pow(vUv.x, 1.6);
float body = (1.0 - smoothstep(0.0, 1.0, across)) * pow(clamp(vUv.x, 0.0, 1.0), 1.6);
float head = 1.0 - smoothstep(0.0, 1.0, length(vec2((vUv.x - 0.9) * 5.0, across)));
float alpha = max(body * 0.7, head) * vAlpha;
if (alpha < 0.004) discard;

View File

@@ -340,7 +340,7 @@ export function createRelayMasts(resources: SceneResourceBag, full: boolean, ton
void main() {
float across = pow(max(0.0, 1.0 - abs(vUv.x - 0.5) * 2.0), 2.0);
// The base eases in over the first 12% so the source does not read as a blown blob at the beacon.
float along = pow(1.0 - vUv.y, 1.6) * smoothstep(0.0, 0.12, vUv.y);
float along = pow(clamp(1.0 - vUv.y, 0.0, 1.0), 1.6) * smoothstep(0.0, 0.12, vUv.y);
vec3 color = mix(vec3(0.6, 0.36, 1.0), vec3(0.9, 0.92, 1.0), 0.3) * across * along * ${BEAM_INTENSITY.toFixed(2)} * vLevel;
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>

View File

@@ -171,7 +171,7 @@ export function createRoad(
float film = mix(0.36, 1.0, streak) * (0.74 + 0.26 * fine);
float grazing = smoothstep(8.0, 120.0, vViewDistance);
color += vec3(0.037, 0.022, 0.069) * film * asphaltMask * (0.35 + grazing * 0.65);
float channel = exp(-pow((roadX - 7.98) * 2.4, 2.0));
float channel = exp(-pow(abs(roadX - 7.98) * 2.4, 2.0));
color += edgeColor * channel * (0.06 + 0.10 * film);
float shoulderRibs = step(0.77, fract(travelZ / 1.4))
* smoothstep(8.28, 8.36, roadX) * (1.0 - smoothstep(8.85, 9.02, roadX));
@@ -184,19 +184,19 @@ export function createRoad(
// Rear lamp footprint: grows towards the camera from the actual rear axle.
float behindCar = smoothstep(7.0, 8.8, vWorldZ);
float tailLength = exp(-max(0.0, vWorldZ - 8.2) * 0.13);
float tailWidth = exp(-pow(vRoadX / (2.25 + max(0.0, vWorldZ-8.0)*0.035), 6.0));
float lampSegments = 0.60 + 0.40 * pow(sin(vRoadX * 8.7), 2.0);
float tailWidth = exp(-pow(abs(vRoadX / (2.25 + max(0.0, vWorldZ-8.0)*0.035)), 6.0));
float lampSegments = 0.60 + 0.40 * pow(abs(sin(vRoadX * 8.7)), 2.0);
color += vec3(1.0, 0.035, 0.055) * behindCar * tailLength * tailWidth
* film * lampSegments * (0.34 + uAcceleration * 0.14);
float cyanReflection = exp(-pow((roadX-2.7) * 6.0, 2.0));
float cyanReflection = exp(-pow(abs(roadX-2.7) * 6.0, 2.0));
color += uEdgeColor * behindCar * tailLength * cyanReflection * film * 0.4;
float sunReflection = exp(-pow(vRoadX / (0.55 + vViewDistance * 0.011), 2.0))
float sunReflection = exp(-pow(abs(vRoadX / (0.55 + vViewDistance * 0.011)), 2.0))
* smoothstep(36.0, 180.0, vViewDistance);
color += vec3(0.8, 0.15, 0.08) * sunReflection * film * 0.17;
// Wet glare: the sun lays a broken column down the asphalt.
float glare = exp(-pow(vRoadX / (1.1 + vViewDistance * 0.02), 2.0))
float glare = exp(-pow(abs(vRoadX / (1.1 + vViewDistance * 0.02)), 2.0))
* smoothstep(50.0, 220.0, vViewDistance) * (1.0 - smoothstep(360.0, 560.0, vViewDistance));
color += vec3(1.0, 0.32, 0.22) * glare * pow(film, 2.0) * asphaltMask * 0.24;
color += vec3(1.0, 0.32, 0.22) * glare * pow(max(film, 0.0), 2.0) * asphaltMask * 0.24;
// END OF SHIFT: first light grazes the wet asphalt from the low sun
// ahead, a warm sheen that strengthens toward the horizon.
if (uDawnLight > 0.0) {
@@ -214,14 +214,14 @@ export function createRoad(
float throwZ = smoothstep(-3.0, 5.0, ahead) * (1.0 - smoothstep(26.0, mix(92.0, 120.0, uBeamLift), ahead));
float spread = 0.9 + max(0.0, ahead) * 0.07;
float beamX = vRoadX - uBeamYaw * 3.4 * clamp(ahead / 26.0, 0.0, 4.6);
float cones = exp(-pow((beamX - 1.25) / spread, 2.0)) + exp(-pow((beamX + 1.25) / spread, 2.0));
float cones = exp(-pow(abs((beamX - 1.25) / spread), 2.0)) + exp(-pow(abs((beamX + 1.25) / spread), 2.0));
color += vec3(1.0, 0.84, 0.66) * min(cones, 1.4) * throwZ * (0.07 + 0.16 * film) * asphaltMask
* (1.0 + 0.3 * uBeamLift) * uThrow;
// Pylon lamps reflect as long streaks towards the camera.
float pylonGap = mod(vWorldZ + 24.0 - uPylonPhase, ${PYLON_SPACING.toFixed(3)});
float pylonStreak = exp(-pylonGap * 0.075) * smoothstep(0.0, 1.5, pylonGap)
* exp(-pow((roadX - 6.6) * 1.1, 2.0));
* exp(-pow(abs(roadX - 6.6) * 1.1, 2.0));
color += edgeColor * pylonStreak * film * 0.5 * (1.0 - smoothstep(300.0, 520.0, vViewDistance));
float horizonPickup = smoothstep(280.0, 620.0, vViewDistance)

View File

@@ -368,7 +368,7 @@ export function createSign(
uniforms: { uSweep: { value: -1 }, uOpacity: { value: 0 } },
vertexShader: `varying vec2 vUv;void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
fragmentShader: `uniform float uSweep;uniform float uOpacity;varying vec2 vUv;
void main(){float beam=exp(-pow((vUv.x+vUv.y*0.14-uSweep)*12.0,2.0));
void main(){float beam=exp(-pow(abs(vUv.x+vUv.y*0.14-uSweep)*12.0,2.0));
gl_FragColor=vec4(0.36,0.82,0.9,beam*uOpacity);}`,
transparent: true,
depthWrite: false,

View File

@@ -53,7 +53,7 @@ export function createSkyline(resources: SceneResourceBag) {
float r2 = hash(id * 1.7 + seed + 3.1);
float inset = 0.06 + r2 * 0.18;
float inside = step(inset, local) * step(local, 1.0 - inset * 0.5);
float h = (0.1 + pow(r, 2.4) * tall) * envelope;
float h = (0.1 + pow(max(r, 0.0), 2.4) * tall) * envelope;
// Stepped crowns on the tall ones.
float crown = step(0.62, r) * step(0.32, abs(local - 0.5)) * 0.06 * envelope;
return vec4(h - crown, inside, id, r);
@@ -63,9 +63,9 @@ export function createSkyline(resources: SceneResourceBag) {
float x = (vUv.x - 0.5) * ${WIDTH.toFixed(1)};
float y = vUv.y;
float xn = vUv.x - 0.5;
float core = exp(-pow(xn / 0.2, 2.0));
float outskirts = 0.28 + 0.72 * core + 0.2 * exp(-pow((xn + 0.27) / 0.06, 2.0))
+ 0.16 * exp(-pow((xn - 0.31) / 0.05, 2.0));
float core = exp(-pow(abs(xn) / 0.2, 2.0));
float outskirts = 0.28 + 0.72 * core + 0.2 * exp(-pow(abs(xn + 0.27) / 0.06, 2.0))
+ 0.16 * exp(-pow(abs(xn - 0.31) / 0.05, 2.0));
float fade = smoothstep(0.5, 0.36, abs(xn));
vec4 far = towers(x + 3.0, 15.0, 11.0, outskirts * fade, 0.74);

View File

@@ -58,10 +58,10 @@ export function createSpeedWake(resources: SceneResourceBag) {
uniform float uPower; uniform float uTime; varying vec2 vUv;
void main(){
float crossFade=pow(max(0.0,1.0-abs(vUv.x*2.0-1.0)),1.3);
float tip=pow(1.0-vUv.y,1.4)*smoothstep(0.0,0.055,vUv.y);
float tip=pow(clamp(1.0-vUv.y,0.0,1.0),1.4)*smoothstep(0.0,0.055,vUv.y);
float filament=0.76+0.24*sin(vUv.y*35.0-uTime*15.0);
vec3 color=mix(vec3(0.08,0.86,1.0),vec3(1.0,0.02,0.44),vUv.y);
color+=vec3(1.0,0.46,0.21)*pow(crossFade,5.0)*0.36;
color+=vec3(1.0,0.46,0.21)*pow(max(crossFade,0.0),5.0)*0.36;
gl_FragColor=vec4(color*1.8,crossFade*tip*filament*uPower*0.8);
#include <tonemapping_fragment>
#include <colorspace_fragment>

View File

@@ -261,7 +261,7 @@ export function createTerrain(
vec3 normal = normalize(cross(dFdx(vTerrainPosition), dFdy(vTerrainPosition)));
float light = abs(dot(normal, normalize(vec3(-0.5, 0.8, -0.3))));
vec3 color = mix(vec3(0.012, 0.003, 0.024), vGridColor * (0.065 + uSurge * 0.016), light);
color += uHighColor * pow(vHeight, 2.0) * light * 0.06;
color += uHighColor * pow(max(vHeight, 0.0), 2.0) * light * 0.06;
// HIGH BEAM: the ridges beside the road catch the headlights.
float beam = headlightBeam(vTerrainPosition);
// UNDERPASS: the deck takes the sky off the ridges beside it (at most 35%).

View File

@@ -236,14 +236,19 @@ export function createTraffic(
float core = exp(-pow(abs(c.x) * 2.3, 6.0) - pow(abs(c.y) * 2.4, 6.0));
float r = length(c * vec2(1.0, 1.6));
float glow = core * 2.4 + exp(-r * r * 22.0) * 1.2 + exp(-r * r * 5.0) * 0.3;`);
// Every pow() base is clamped: interpolation can land a fragment a hair
// outside 0..1 (a quad edge-on or clipped by the near plane), and pow() of a
// negative base is NaN. One NaN pixel in the HDR target is spread by the
// bloom blurs and the lens streak over the whole frame: a one-frame black
// flash (tests/unit/shader-pow-safety.test.ts).
// Trails: thin, brightest at the lamp, fading over their length.
const trailMaterial = additive(`
float across = exp(-pow((vUv.x - 0.5) * 5.0, 2.0));
float glow = across * pow(vUv.y, 1.6) * 0.8;`);
float across = exp(-pow(abs(vUv.x - 0.5) * 5.0, 2.0));
float glow = across * pow(clamp(vUv.y, 0.0, 1.0), 1.6) * 0.8;`);
// Wet-road pools: long and soft, brightest at the vehicle.
const poolMaterial = additive(`
float across = exp(-pow((vUv.x - 0.5) * 3.2, 2.0));
float glow = across * pow(vUv.y, 2.2) * 0.2;`);
float across = exp(-pow(abs(vUv.x - 0.5) * 3.2, 2.0));
float glow = across * pow(clamp(vUv.y, 0.0, 1.0), 2.2) * 0.2;`);
// Bodies: near-black coachwork that reads as a silhouette against the
// horizon, with the fog glow on the roof, the reader's headlights on the
@@ -321,12 +326,12 @@ export function createTraffic(
? `// Rear window: glass reflects a lavender band of sky.
float glass = rear * step(1.12, vLocal.y) * step(vLocal.z, -0.9);
color = mix(color, vec3(0.05, 0.05, 0.12) + uSheen * 0.22 * smoothstep(1.2, 1.55, vLocal.y), glass);
float lampBand = exp(-pow((vLocal.y - 0.92) / 0.18, 2.0));`
float lampBand = exp(-pow(abs(vLocal.y - 0.92) / 0.18, 2.0));`
: `// Box doors: a centre seam and two lock bars.
float seam = 1.0 - smoothstep(0.02, 0.05, abs(vLocal.x));
float bars = 1.0 - smoothstep(0.03, 0.06, abs(abs(vLocal.x) - 0.42));
color *= 1.0 - rear * max(seam * 0.6, bars * 0.35);
float lampBand = exp(-pow((vLocal.y - 1.3) / 0.2, 2.0));
float lampBand = exp(-pow(abs(vLocal.y - 1.3) / 0.2, 2.0));
// The under-run bar is a reflector strip: it answers the beam from far off.
float bar = rear * step(0.42, vLocal.y) * step(vLocal.y, 0.56);
color += vec3(1.0, 0.34, 0.22) * bar * headlightCone(vWorld) * smoothstep(18.0, 50.0, beamDistance) * (1.0 - smoothstep(180.0, 420.0, beamDistance)) * (0.2 + 1.4 * lift);`

View File

@@ -209,7 +209,7 @@ function additive(billboard: boolean) {
varying vec4 vGlow;
void main() {
float r = 1.0 - min(1.0, length(vUv - 0.5) * 2.0);
float shape = ${billboard ? 'pow(r, 3.0) * 1.3 + pow(r, 1.2) * 0.22' : 'pow(r, 1.8) * 0.7'};
float shape = ${billboard ? 'pow(max(r, 0.0), 3.0) * 1.3 + pow(max(r, 0.0), 1.2) * 0.22' : 'pow(max(r, 0.0), 1.8) * 0.7'};
gl_FragColor = vec4(vGlow.rgb * shape * vGlow.w, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>