fix: stabilize particle lifecycle and view-space trails
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -50,6 +50,27 @@ vec3 weatherSpeedColor(float speed) {
|
||||
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;
|
||||
|
||||
@@ -75,6 +96,55 @@ void main() {
|
||||
// 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 respawn = u_mask.w > 0.5;
|
||||
float age = respawn ? 0.0 : previous.b + u_pass.y;
|
||||
vec2 position = previous.rg;
|
||||
vec2 velocity;
|
||||
bool valid = !respawn && fieldVelocityAt(position, velocity);
|
||||
if (!valid || age >= u_marker.z) {
|
||||
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);
|
||||
}
|
||||
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
|
||||
@@ -105,7 +175,10 @@ void main() {
|
||||
// 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) {
|
||||
fragColor = vec4(previous.rgb * u_visual.w, 1.0);
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
};
|
||||
|
||||
uniform sampler2D u_state_texture;
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
|
||||
@@ -38,15 +39,16 @@ void main() {
|
||||
} 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) {
|
||||
// 40-05: a_pos.x packs State texel X plus a static line endpoint.
|
||||
// State position, interpolated Field velocity, speed color and streak
|
||||
// orientation are all evaluated on GPU; the CPU never supplies live
|
||||
// particle position, direction, or color data.
|
||||
// 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;
|
||||
vec2 position = texture(u_state_texture, stateUV).rg;
|
||||
vec2 uv = fieldUv(position);
|
||||
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,
|
||||
@@ -55,20 +57,29 @@ void main() {
|
||||
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);
|
||||
v_uv = position;
|
||||
v_world = position;
|
||||
vec2 segmentPosition = endpoint < 0.0 ? previousState.rg : currentPosition;
|
||||
v_uv = stateUV;
|
||||
v_world = currentPosition;
|
||||
v_particle_velocity = mercatorVelocity;
|
||||
v_particle_valid = valid && speed > 0.00001 ? 1.0 : 0.0;
|
||||
// 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 {
|
||||
vec2 direction = mercatorVelocity / speed;
|
||||
vec2 streakPosition = position + direction * endpoint * u_visual.y * 0.5;
|
||||
gl_Position = vec4(streakPosition * 2.0 - 1.0, 0.0, 1.0);
|
||||
gl_Position = u_matrix * vec4(segmentPosition * 8192.0, 0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix is
|
||||
// PaintParameters::matrixForTile() for the selected MapLibre world copy.
|
||||
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user