feat: add direction particle advection
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-11 08:27:28 +08:00
parent 89c72554c5
commit f3c33c6727
17 changed files with 560 additions and 57 deletions

View File

@@ -6,6 +6,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
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];
@@ -72,15 +73,24 @@ bool outsideRespawnBounds(vec2 position) {
position.y < u_respawn_bounds.y || position.y > u_respawn_bounds.w;
}
bool fieldVelocityAt(vec2 position, out vec2 velocity) {
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);
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;
vec2 mixed = 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 || 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) {
@@ -146,8 +156,8 @@ void main() {
// 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);
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.
@@ -158,11 +168,11 @@ void main() {
if (!respawn) {
float latitude = mercatorLatitudeRadians(position.y);
vec2 displacement = vec2(velocity.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
-velocity.y * u_pass.y /
vec2 displacement = vec2(advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
-advection.y * u_pass.y /
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
vec2 nextPosition = position + displacement;
if (fieldVelocityAt(nextPosition, velocity)) {
if (fieldAdvectionAt(nextPosition, advection)) {
position = nextPosition;
} else {
respawn = true;
@@ -175,7 +185,7 @@ void main() {
// 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)) {
if (fieldAdvectionAt(candidate, advection)) {
position = candidate;
found = true;
break;
@@ -201,12 +211,10 @@ void main() {
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) {
vec2 advection;
if (!fieldAdvectionAt(position, advection)) {
position = seed;
velocity = vec2(0.0);
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
@@ -214,11 +222,11 @@ void main() {
// 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 /
float dx = advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
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(velocity))) position = seed;
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