Files
MapLibreParticleLab/include/mbgl/shaders/gl/particle_gpu_validation.hpp
OpenAI Codex f926a3f007
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
fix: stabilize particle visibility across zoom levels
2026-09-05 11:20:33 +08:00

342 lines
15 KiB
C++

// Generated code, do not modify this file!
#pragma once
#include <mbgl/shaders/shader_source.hpp>
namespace mbgl {
namespace shaders {
template <>
struct ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::OpenGL> {
static constexpr const char* name = "ParticleGpuValidationShader";
static constexpr const char* vertex = R"(layout (location = 0) in vec2 a_pos;
out vec2 v_uv;
out vec2 v_world;
out vec2 v_particle_velocity;
out float v_particle_valid;
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_field_scale;
highp mat4 u_matrix;
};
uniform sampler2D u_state_texture;
uniform sampler2D u_previous;
uniform sampler2D u_field_texture;
uniform sampler2D u_next_field_texture;
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)));
}
void main() {
v_uv = a_pos;
v_world = a_pos;
v_particle_velocity = vec2(0.0);
v_particle_valid = 1.0;
if (u_pass.x > 4.5) {
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
} else if (u_pass.x < 1.5) {
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
} else if (u_pass.x > 3.5) {
// a_pos.x packs State texel X plus one endpoint. The formal particle
// path draws the StateRead -> StateWrite displacement in current map
// clip space; it does not synthesize a fixed world-length streak.
float endpoint = mod(a_pos.x, 2.0) * 2.0 - 1.0;
vec2 stateIndex = vec2(floor(a_pos.x * 0.5), a_pos.y);
vec2 stateUV = (stateIndex + vec2(0.5)) / u_mask.xy;
vec4 previousState = texture(u_previous, stateUV);
vec4 currentState = texture(u_state_texture, stateUV);
vec2 currentPosition = currentState.rg;
vec2 uv = fieldUv(currentPosition);
vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv);
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
u_marker.x);
bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity));
vec2 mercatorVelocity = vec2(velocity.x, -velocity.y);
float speed = length(mercatorVelocity);
vec2 segmentPosition = endpoint < 0.0 ? previousState.rg : currentPosition;
v_uv = stateUV;
v_world = currentPosition;
v_particle_velocity = mercatorVelocity;
// A zero alpha is written by State on a respawn frame. This suppresses
// oldPosition -> newRandomPosition cross-screen segments.
v_particle_valid = valid && speed > 0.00001 && currentState.a > 0.5 &&
distance(previousState.rg, currentPosition) > 0.000000001
? 1.0
: 0.0;
if (v_particle_valid < 0.5) {
gl_Position = vec4(-2.0, -2.0, 0.0, 1.0);
} else {
gl_Position = u_matrix * vec4(segmentPosition * 8192.0, 0.0, 1.0);
}
} else {
if (u_mask.z > 0.5) {
// Trail targets are viewport-sized screen-space history buffers.
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
} else {
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix
// is PaintParameters::matrixForTile() for the selected world copy.
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
}
}
}
)";
static constexpr const char* fragment = R"(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_field_scale;
highp mat4 u_matrix;
};
uniform sampler2D u_previous;
uniform sampler2D u_mask_texture;
uniform sampler2D u_field_texture;
uniform 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 weatherSpeedColor(float speed) {
// Fixed 0..30 m/s scale: no per-frame or per-dataset auto stretching.
float t = clamp(speed / 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_field_bounds.xy, u_field_bounds.zw, random);
}
bool fieldVelocityAt(vec2 position, out vec2 velocity) {
vec2 uv = fieldUv(position);
vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv);
velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
u_marker.x);
return validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity)) &&
length(velocity) > 0.00001;
}
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;
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
u_marker.x);
if (any(isnan(velocity))) discard;
fragColor = vec4(weatherSpeedColor(length(velocity)), u_visual.x);
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 velocity;
bool valid = !respawn && fieldVelocityAt(position, velocity);
if (!valid || age >= maxAge) {
respawn = true;
}
if (!respawn) {
float latitude = mercatorLatitudeRadians(position.y);
vec2 displacement = vec2(velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
-velocity.y * u_pass.y /
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
vec2 nextPosition = position + displacement;
if (fieldVelocityAt(nextPosition, velocity)) {
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 (fieldVelocityAt(candidate, velocity)) {
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 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
u_marker.x);
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5) {
position = seed;
velocity = 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.
// Web Mercator x spans Earth's circumference; y additionally scales
// with sec(latitude). V is north-positive while world y is southward.
float latitude = mercatorLatitudeRadians(position.y);
float dx = velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
float dy = -velocity.y * u_pass.y /
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
position = position + vec2(dx, dy);
if (!validFieldUv(fieldUv(position)) || any(isnan(velocity))) 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);
fragColor = vec4(previous.rgb * fadeFactor, 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);
vec3 color = u_marker.y > 0.5 ? particleSpeedColor(speed) : vec3(0.08, 0.55, 1.0);
// Trail targets are RGB accumulation buffers. Opacity changes source
// energy only; it never alters State, Field textures, or time windows.
fragColor = vec4(color * u_visual.x, 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) {
vec3 color = previous.rgb * u_visual.z;
float alpha = clamp(max(color.r, max(color.g, color.b)), 0.0, 0.96);
fragColor = vec4(color, 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
}
)";
};
} // namespace shaders
} // namespace mbgl