295 lines
14 KiB
GLSL
295 lines
14 KiB
GLSL
// The shared GL prelude defaults fragment float to mediump. Byte reconstruction
|
|
// from RGBA8 UNORM and Mercator state require highp on ES 3.x.
|
|
precision highp float;
|
|
|
|
layout (std140) uniform ParticleGpuValidationUBO {
|
|
highp vec4 u_pass;
|
|
highp vec4 u_marker;
|
|
highp vec4 u_mask;
|
|
highp vec4 u_visual;
|
|
highp vec4 u_field_bounds;
|
|
highp vec4 u_respawn_bounds;
|
|
highp vec4 u_field_scale;
|
|
highp vec4 u_advection;
|
|
highp vec4 u_particle_color;
|
|
highp vec4 u_trail_color;
|
|
highp vec4 u_weather_stops[8];
|
|
highp vec4 u_weather_colors[8];
|
|
highp vec4 u_weather_ramp;
|
|
highp mat4 u_matrix;
|
|
};
|
|
|
|
uniform sampler2D u_previous;
|
|
uniform sampler2D u_mask_texture;
|
|
uniform highp sampler2D u_field_texture;
|
|
uniform highp sampler2D u_next_field_texture;
|
|
uniform sampler2D u_state_texture;
|
|
in vec2 v_uv;
|
|
in vec2 v_world;
|
|
in vec2 v_particle_velocity;
|
|
in float v_particle_valid;
|
|
|
|
vec3 particleSpeedColor(float speed) {
|
|
float t = clamp(speed * 0.75, 0.0, 1.0);
|
|
vec3 slow = vec3(0.10, 0.45, 1.00);
|
|
vec3 medium = vec3(0.10, 0.95, 0.72);
|
|
vec3 fast = vec3(1.00, 0.48, 0.12);
|
|
return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t));
|
|
}
|
|
|
|
vec2 fieldUv(vec2 world) {
|
|
return vec2((world.x - u_field_bounds.x) / (u_field_bounds.z - u_field_bounds.x),
|
|
(world.y - u_field_bounds.y) / (u_field_bounds.w - u_field_bounds.y));
|
|
}
|
|
|
|
bool validFieldUv(vec2 uv) {
|
|
return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
|
|
}
|
|
|
|
const float EARTH_CIRCUMFERENCE_METERS = 40075016.68557849;
|
|
|
|
float mercatorLatitudeRadians(float worldY) {
|
|
return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY)));
|
|
}
|
|
|
|
vec3 weatherDefaultValueColor(float value) {
|
|
// Historical default ramp endpoints are 0 and 30 generic field-value
|
|
// units. Hosts provide explicit stops whenever another scale is needed.
|
|
float t = clamp(value / 30.0, 0.0, 1.0);
|
|
vec3 slow = vec3(0.08, 0.18, 0.65);
|
|
vec3 medium = vec3(0.08, 0.80, 0.78);
|
|
vec3 fast = vec3(0.98, 0.22, 0.10);
|
|
return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t));
|
|
}
|
|
|
|
float particleHash(vec2 value) {
|
|
return fract(sin(dot(value, vec2(127.1, 311.7))) * 43758.5453123);
|
|
}
|
|
|
|
vec2 respawnSeed(vec2 particleUV, vec2 entropy, float attempt) {
|
|
vec2 salt = entropy * vec2(13.17, 19.91) + particleUV * vec2(97.23, 53.71) + attempt;
|
|
vec2 random = vec2(particleHash(salt), particleHash(salt.yx + 41.29));
|
|
return mix(u_respawn_bounds.xy, u_respawn_bounds.zw, random);
|
|
}
|
|
|
|
bool outsideRespawnBounds(vec2 position) {
|
|
return position.x < u_respawn_bounds.x || position.x > u_respawn_bounds.z ||
|
|
position.y < u_respawn_bounds.y || position.y > u_respawn_bounds.w;
|
|
}
|
|
|
|
bool fieldAdvectionAt(vec2 position, out vec2 advection) {
|
|
vec2 uv = fieldUv(position);
|
|
vec4 field0 = texture(u_field_texture, uv);
|
|
vec4 field1 = texture(u_next_field_texture, uv);
|
|
vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
|
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
|
u_marker.x);
|
|
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5 || any(isnan(mixed)) ||
|
|
any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
|
if (u_advection.x > 0.5) {
|
|
float directionLength = length(mixed);
|
|
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
|
|
advection = mixed / directionLength * u_advection.y;
|
|
return true;
|
|
}
|
|
// VELOCITY keeps its previous component-wise interpolation and magnitude.
|
|
advection = mixed;
|
|
return length(advection) > 0.00001;
|
|
}
|
|
|
|
vec4 weatherRampColor(float value) {
|
|
int count = int(u_weather_ramp.x);
|
|
if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0);
|
|
vec4 color = u_weather_colors[0];
|
|
for (int i = 1; i < 8; ++i) {
|
|
if (i >= count) break;
|
|
float lo = u_weather_stops[i - 1].x;
|
|
float hi = u_weather_stops[i].x;
|
|
if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0));
|
|
color = u_weather_colors[i];
|
|
}
|
|
return color;
|
|
}
|
|
|
|
void main() {
|
|
float mode = u_pass.x;
|
|
|
|
if (mode > 4.5) {
|
|
vec2 uv = fieldUv(v_world);
|
|
if (!validFieldUv(uv)) discard;
|
|
vec4 field0 = texture(u_field_texture, uv);
|
|
vec4 field1 = texture(u_next_field_texture, uv);
|
|
if (field0.a < 0.5 || field1.a < 0.5) discard;
|
|
float fieldValue;
|
|
if (u_weather_ramp.y > 0.5) {
|
|
vec2 vectorValue = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
|
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
|
u_marker.x);
|
|
if (any(isnan(vectorValue))) discard;
|
|
fieldValue = length(vectorValue);
|
|
} else {
|
|
fieldValue = mix(((floor(field0.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
|
((floor(field1.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
|
u_marker.x);
|
|
if (isnan(fieldValue)) discard;
|
|
}
|
|
vec4 color = weatherRampColor(fieldValue);
|
|
float alpha = color.a * u_visual.x;
|
|
fragColor = vec4(color.rgb * alpha, alpha);
|
|
return;
|
|
}
|
|
|
|
vec4 previous = texture(u_previous, v_uv);
|
|
|
|
if (mode < 0.5) {
|
|
// State A/B samples the uploaded canonical Particle Field.
|
|
// RG stores Web Mercator world position, B stores age, and the Field
|
|
// texture encodes canonical U-east/V-north at that geographic sample.
|
|
// u_mask.w is the explicit State lifecycle flag: new RenderTargets are
|
|
// cleared opaque by MapLibre, so sampled alpha cannot identify a seed.
|
|
if (u_mask.z > 0.5) {
|
|
// Formal particles use GPU-resident lifecycle state. Each slot keeps
|
|
// its age in B; A=0 marks this frame as a respawn so Trail does not
|
|
// connect the retired position to the new randomized one.
|
|
bool initialSeed = u_mask.w > 0.5;
|
|
float lifecycleHash = particleHash(v_uv + vec2(17.31, 83.17));
|
|
float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash);
|
|
float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47));
|
|
bool respawn = initialSeed;
|
|
// Lifecycle is wall-clock based: zoom-aware visual advection and
|
|
// forecast playback must not change respawn cadence.
|
|
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.w;
|
|
vec2 position = previous.rg;
|
|
vec2 advection;
|
|
bool valid = !respawn && fieldAdvectionAt(position, advection);
|
|
// The field bounds remain the only sampling domain. Once the
|
|
// camera is stable, slots already outside the expanded viewport
|
|
// may recycle into its separate preferred respawn domain.
|
|
if (!valid || age >= maxAge ||
|
|
(u_field_scale.z > 0.5 && !initialSeed && outsideRespawnBounds(position))) {
|
|
respawn = true;
|
|
}
|
|
|
|
if (!respawn) {
|
|
float latitude = mercatorLatitudeRadians(position.y);
|
|
vec2 displacement = vec2(advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)),
|
|
-advection.y * u_pass.y /
|
|
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
|
|
vec2 nextPosition = position + displacement;
|
|
if (fieldAdvectionAt(nextPosition, advection)) {
|
|
position = nextPosition;
|
|
} else {
|
|
respawn = true;
|
|
}
|
|
}
|
|
|
|
if (respawn) {
|
|
bool found = false;
|
|
// Fixed, bounded retries keep the State pass deterministic and
|
|
// avoid a CPU upload or an unbounded shader loop.
|
|
for (int attempt = 0; attempt < 3; ++attempt) {
|
|
vec2 candidate = respawnSeed(v_uv, previous.rg, float(attempt));
|
|
if (fieldAdvectionAt(candidate, advection)) {
|
|
position = candidate;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
// Keep a deterministic in-bounds candidate and retry next
|
|
// frame; alpha still prevents an invalid visible segment.
|
|
position = respawnSeed(v_uv, previous.rg, 3.0);
|
|
}
|
|
if (!initialSeed) {
|
|
age = 0.0;
|
|
}
|
|
}
|
|
fragColor = vec4(position, age, respawn ? 0.0 : 1.0);
|
|
return;
|
|
}
|
|
|
|
vec2 seed = mix(u_field_bounds.xy, u_field_bounds.zw, v_uv);
|
|
vec2 position = u_mask.w > 0.5 ? seed : previous.rg;
|
|
// 40-02: decode each independent Field texture, then interpolate the
|
|
// velocity on GPU. u_marker.x is the clamped Layer timeMix property.
|
|
vec2 uv = fieldUv(position);
|
|
vec4 field0 = texture(u_field_texture, uv);
|
|
vec4 field1 = texture(u_next_field_texture, uv);
|
|
vec2 advection;
|
|
if (!fieldAdvectionAt(position, advection)) {
|
|
position = seed;
|
|
advection = vec2(0.0);
|
|
}
|
|
// U/V remain canonical metres per second. u_pass.y is the explicit
|
|
// visual simulation delta in seconds; the validation clock may run
|
|
// faster than wall time, but it never changes the field contract.
|
|
// Both east and north metres scale with sec(latitude) in Web Mercator.
|
|
// V is north-positive while world y is southward.
|
|
float latitude = mercatorLatitudeRadians(position.y);
|
|
float dx = advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
|
|
float dy = -advection.y * u_pass.y /
|
|
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
|
|
position = position + vec2(dx, dy);
|
|
if (!validFieldUv(fieldUv(position)) || any(isnan(advection))) position = seed;
|
|
fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0);
|
|
} else if (mode < 1.5) {
|
|
// Formal Trail first fades the previous target. Its full-particle point
|
|
// draw follows in the same target and consumes every State texel.
|
|
if (u_mask.z > 0.5) {
|
|
// trailFade remains the UI's retention-at-60Hz value. Raising it
|
|
// to elapsed wall time makes visual persistence FPS-independent.
|
|
float fadeFactor = pow(clamp(u_visual.w, 0.0001, 1.0), u_pass.y * u_marker.w);
|
|
// R is history coverage, G marks the current segment, B its speed.
|
|
// Apply independent Host colours only at composite time.
|
|
fragColor = vec4(previous.r * fadeFactor, 0.0, 0.0, 1.0);
|
|
return;
|
|
}
|
|
|
|
// LEGACY VALIDATION DISPLAY: retain the original four deterministic
|
|
// samples for the historical ParticleValidationLayer only.
|
|
float marker = 0.0;
|
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.125, 0.125)).rg)));
|
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.375, 0.375)).rg)));
|
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.625, 0.625)).rg)));
|
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.875, 0.875)).rg)));
|
|
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
|
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
|
} else if (mode > 3.5) {
|
|
if (v_particle_valid < 0.5) discard;
|
|
float speed = length(v_particle_velocity);
|
|
fragColor = vec4(1.0, 1.0, clamp(speed / 30.0, 0.0, 1.0), 1.0);
|
|
} else {
|
|
// The formal RasterParticle layer displays the Trail texture written by
|
|
// the current State -> Trail pass. Legacy ParticleValidation keeps its
|
|
// independent 30-03 stripe/anchor diagnostic display below.
|
|
float intensity = max(previous.r, max(previous.g, previous.b));
|
|
float ocean = texture(u_mask_texture, v_world).a;
|
|
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
|
if (u_mask.z > 0.5) {
|
|
bool current = previous.g > 0.5;
|
|
vec4 base = current ? u_particle_color : u_trail_color;
|
|
if (current && u_marker.y > 0.5) base.rgb = particleSpeedColor(previous.b * 30.0);
|
|
float alpha = clamp(previous.r * base.a * (current ? u_visual.x : u_visual.z), 0.0, 1.0);
|
|
fragColor = vec4(base.rgb * alpha, alpha);
|
|
return;
|
|
}
|
|
|
|
// LEGACY VALIDATION DISPLAY: preserve the frozen 30-03
|
|
// stripe/anchor/world-wrap diagnostic for ParticleValidationLayer.
|
|
float stripe = 0.55 + 0.45 * sin((v_world.x + v_world.y) * 100.0 + u_pass.y * 0.07);
|
|
// A=(0°,0°), B=(-90°,approximately 0°), C/D straddle ±180°.
|
|
float anchors = 0.0;
|
|
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.50, 0.50))));
|
|
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.25, 0.50))));
|
|
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.02, 0.50))));
|
|
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.98, 0.50))));
|
|
vec3 color = u_pass.w < 1.5 ? vec3(0.10, 0.70, 1.00) : vec3(0.35, 1.00, 0.45);
|
|
fragColor = vec4(mix(color * stripe, vec3(1.00, 0.82, 0.15), anchors),
|
|
0.28 + min(intensity, 0.35) * 0.35 + anchors * 0.35);
|
|
}
|
|
|
|
#ifdef OVERDRAW_INSPECTOR
|
|
fragColor = vec4(1.0);
|
|
#endif
|
|
}
|