fix: stabilize particle visibility across zoom levels
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:
@@ -53,8 +53,13 @@ constexpr float EarthCircumferenceMeters = 40075016.68557849f;
|
||||
// moves about 100 m (roughly 0.65 px at z9), while a 30 m/s sample remains
|
||||
// below a four-pixel segment at that zoom.
|
||||
constexpr float VisualSimulationSecondsPerRealSecond = 1200.0f;
|
||||
constexpr float ParticleLifecycleSeconds = 14400.0f;
|
||||
constexpr float VisualSimulationReferenceZoom = 9.0f;
|
||||
constexpr float MinVisualSimulationSecondsPerRealSecond = 300.0f;
|
||||
constexpr float MaxVisualSimulationSecondsPerRealSecond = 153600.0f;
|
||||
constexpr float MaxSimulationDeltaSeconds = 2400.0f;
|
||||
constexpr float ParticleLifecycleRealSeconds = 12.0f;
|
||||
constexpr float TrailFadeReferenceFramesPerSecond = 60.0f;
|
||||
constexpr auto TrailCameraStableDelay = std::chrono::milliseconds(150);
|
||||
|
||||
std::atomic_uint64_t nextGpuGeneration{0};
|
||||
std::atomic_uint64_t nextMaskGeneration{0};
|
||||
@@ -248,11 +253,13 @@ public:
|
||||
bool worldWrapLogged = false;
|
||||
bool antimeridianLogged = false;
|
||||
bool trailCameraKnown = false;
|
||||
bool trailCameraInvalidated = false;
|
||||
uint64_t lastCameraLogFrame = 0;
|
||||
std::chrono::steady_clock::time_point lastPlaybackHeartbeat{};
|
||||
std::chrono::steady_clock::time_point lastSimulationTick{};
|
||||
std::chrono::steady_clock::time_point lastMotionLog{};
|
||||
std::chrono::steady_clock::time_point lastLifecycleLog{};
|
||||
std::chrono::steady_clock::time_point lastTrailCameraChange{};
|
||||
uint64_t maskGeneration = 0;
|
||||
uint64_t fieldGeneration = 0;
|
||||
uint64_t gpuFieldGeneration = 0;
|
||||
@@ -393,6 +400,8 @@ public:
|
||||
gfx::Texture2DPtr mask_,
|
||||
gfx::Texture2DPtr stateTexture_,
|
||||
float simulationStep_,
|
||||
float wallDeltaSeconds_,
|
||||
float effectiveVisualSpeedScale_,
|
||||
int16_t worldWrap_,
|
||||
uint8_t maskMode_)
|
||||
: state(std::move(state_)),
|
||||
@@ -404,6 +413,8 @@ public:
|
||||
mask(std::move(mask_)),
|
||||
stateTexture(std::move(stateTexture_)),
|
||||
simulationStep(simulationStep_),
|
||||
wallDeltaSeconds(wallDeltaSeconds_),
|
||||
effectiveVisualSpeedScale(effectiveVisualSpeedScale_),
|
||||
worldWrap(worldWrap_),
|
||||
maskMode(maskMode_) {}
|
||||
|
||||
@@ -454,9 +465,15 @@ public:
|
||||
Log::Info(Event::Render,
|
||||
"ParticleMotion: fieldId=" + resources->field.fieldID + " sampleU=" +
|
||||
std::to_string(u) + " sampleV=" + std::to_string(v) +
|
||||
" dt=" + std::to_string(simulationStep) +
|
||||
" visualSpeedScale=" + std::to_string(VisualSimulationSecondsPerRealSecond) +
|
||||
" zoom=" + std::to_string(parameters.state.getZoom()) +
|
||||
" referenceZoom=" + std::to_string(VisualSimulationReferenceZoom) +
|
||||
" effectiveVisualSpeedScale=" + std::to_string(effectiveVisualSpeedScale) +
|
||||
" wallDeltaSeconds=" + std::to_string(wallDeltaSeconds) +
|
||||
" simulationDeltaSeconds=" + std::to_string(simulationStep) +
|
||||
" estimatedStateDeltaWorld=" + std::to_string(dx) + "," + std::to_string(dy) +
|
||||
" estimatedPixelDelta=" +
|
||||
std::to_string(std::sqrt(dx * dx + dy * dy) * 512.0f *
|
||||
std::exp2(static_cast<float>(parameters.state.getZoom()))) +
|
||||
" simulationFrame=" + std::to_string(resources->committedFrame));
|
||||
}
|
||||
}
|
||||
@@ -465,9 +482,7 @@ public:
|
||||
Log::Info(Event::Render,
|
||||
"ParticleLifecycle: configuredSlots=" + std::to_string(resources->particleCount) +
|
||||
" initialAge=HASH_STAGGERED maxAge=HASH_JITTERED_0.75_1.25 "
|
||||
"lifecycleBaseSimulationSeconds=" + std::to_string(ParticleLifecycleSeconds) +
|
||||
" lifecycleBaseRealSeconds=" +
|
||||
std::to_string(ParticleLifecycleSeconds / VisualSimulationSecondsPerRealSecond) +
|
||||
"lifecycleBaseRealSeconds=" + std::to_string(ParticleLifecycleRealSeconds) +
|
||||
" respawn=GPU_HASH_ON_INVALID_OR_AGE_EXPIRED"
|
||||
" fieldId=" + resources->field.fieldID + " simulationFrame=" +
|
||||
std::to_string(resources->committedFrame) + " readback=disabled");
|
||||
@@ -494,10 +509,13 @@ public:
|
||||
// be an explicit lifecycle signal, never inferred from sampled alpha.
|
||||
const bool seedState = pass == ValidationPass::State && !resources->stateSeeded;
|
||||
const shaders::ParticleGpuValidationUBO ubo = {
|
||||
.pass = {static_cast<float>(pass), frame, maskMode == 0 ? 0.0f : 1.0f, static_cast<float>(maskMode)},
|
||||
.pass = {static_cast<float>(pass),
|
||||
frame,
|
||||
maskMode == 0 ? 0.0f : 1.0f,
|
||||
resources->formalDisplay ? wallDeltaSeconds : static_cast<float>(maskMode)},
|
||||
.marker = {markerX,
|
||||
resources->speedColorEnabled ? 1.0f : 0.0f,
|
||||
resources->formalDisplay ? ParticleLifecycleSeconds : 0.035f,
|
||||
resources->formalDisplay ? ParticleLifecycleRealSeconds : 0.035f,
|
||||
resources->formalDisplay ? TrailFadeReferenceFramesPerSecond : 0.965f},
|
||||
.mask = {static_cast<float>(resources->stateSize.width),
|
||||
static_cast<float>(resources->stateSize.height),
|
||||
@@ -633,6 +651,8 @@ private:
|
||||
gfx::Texture2DPtr mask;
|
||||
gfx::Texture2DPtr stateTexture;
|
||||
float simulationStep;
|
||||
float wallDeltaSeconds;
|
||||
float effectiveVisualSpeedScale;
|
||||
int16_t worldWrap;
|
||||
uint8_t maskMode;
|
||||
};
|
||||
@@ -693,7 +713,9 @@ bool buildQuad(gfx::Context& context,
|
||||
const uint8_t writeSlot,
|
||||
const int16_t worldWrap,
|
||||
const uint8_t maskMode,
|
||||
const bool clearGroup) {
|
||||
const bool clearGroup,
|
||||
const float wallDeltaSeconds = 0.0f,
|
||||
const float effectiveVisualSpeedScale = VisualSimulationSecondsPerRealSecond) {
|
||||
if (clearGroup) {
|
||||
group.clearDrawables();
|
||||
}
|
||||
@@ -734,7 +756,19 @@ bool buildQuad(gfx::Context& context,
|
||||
builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
|
||||
}
|
||||
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(
|
||||
resources, pass, readSlot, writeSlot, source, target, mask, stateTexture, simulationStep, worldWrap, maskMode));
|
||||
resources,
|
||||
pass,
|
||||
readSlot,
|
||||
writeSlot,
|
||||
source,
|
||||
target,
|
||||
mask,
|
||||
stateTexture,
|
||||
simulationStep,
|
||||
wallDeltaSeconds,
|
||||
effectiveVisualSpeedScale,
|
||||
worldWrap,
|
||||
maskMode));
|
||||
|
||||
// DrawableBuilder owns the current drawable until flush finalizes it.
|
||||
builder->flush(context);
|
||||
@@ -793,6 +827,8 @@ bool buildParticlePoints(gfx::Context& context,
|
||||
resources->maskTexture,
|
||||
currentStateTexture,
|
||||
0.0f,
|
||||
0.0f,
|
||||
VisualSimulationSecondsPerRealSecond,
|
||||
0,
|
||||
0));
|
||||
builder->flush(context);
|
||||
@@ -1355,26 +1391,45 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
|
||||
const auto viewport = validViewportSize(transformState.getSize());
|
||||
const auto trailCamera = transformState.getLatLng(LatLng::Unwrapped);
|
||||
const auto cameraChanged = resources->formalDisplay && resources->trailCameraKnown &&
|
||||
(resources->trailSize != viewport ||
|
||||
std::abs(resources->trailCameraLatitude - trailCamera.latitude()) > 0.00001 ||
|
||||
std::abs(resources->trailCameraLongitude - trailCamera.longitude()) > 0.00001 ||
|
||||
std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ||
|
||||
std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ||
|
||||
std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001);
|
||||
if (cameraChanged) {
|
||||
const auto reason = resources->trailSize != viewport ? "resize"
|
||||
: std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ? "zoom"
|
||||
: std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ? "rotate"
|
||||
: std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001 ? "pitch"
|
||||
: "pan";
|
||||
const auto cameraNow = std::chrono::steady_clock::now();
|
||||
const bool viewportChanged = resources->formalDisplay && resources->trailTargets[0] && resources->trailTargets[1] &&
|
||||
resources->trailSize != viewport;
|
||||
const bool transformChanged = resources->formalDisplay && resources->trailCameraKnown &&
|
||||
(std::abs(resources->trailCameraLatitude - trailCamera.latitude()) > 0.00001 ||
|
||||
std::abs(resources->trailCameraLongitude - trailCamera.longitude()) > 0.00001 ||
|
||||
std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ||
|
||||
std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ||
|
||||
std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001);
|
||||
if (viewportChanged) {
|
||||
const auto previousSize = resources->trailSize;
|
||||
resources->trailTargets = {};
|
||||
resources->trailSize = {};
|
||||
resources->trailRead = 0;
|
||||
resources->trailSeeded = false;
|
||||
resources->trailCameraInvalidated = true;
|
||||
resources->lastTrailCameraChange = cameraNow;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleTrail: event=CAMERA_RESET reason=" + std::string(reason) + " statePreserved=true "
|
||||
"fieldResubmit=false timelineReset=false");
|
||||
"ParticleTrail: event=VIEWPORT_RESIZE old=" + std::to_string(previousSize.width) + "x" +
|
||||
std::to_string(previousSize.height) + " new=" + std::to_string(viewport.width) + "x" +
|
||||
std::to_string(viewport.height) + " statePreserved=true fieldResubmit=false timelineReset=false");
|
||||
} else if (transformChanged) {
|
||||
if (!resources->trailCameraInvalidated) {
|
||||
const auto reason = std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ? "zoom"
|
||||
: std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ? "rotate"
|
||||
: std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001 ? "pitch"
|
||||
: "pan";
|
||||
resources->trailCameraInvalidated = true;
|
||||
resources->trailSeeded = false;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleTrail: event=CAMERA_INVALIDATE reason=" + std::string(reason) +
|
||||
" statePreserved=true fieldResubmit=false timelineReset=false");
|
||||
}
|
||||
resources->lastTrailCameraChange = cameraNow;
|
||||
} else if (resources->trailCameraInvalidated &&
|
||||
cameraNow - resources->lastTrailCameraChange >= TrailCameraStableDelay) {
|
||||
resources->trailCameraInvalidated = false;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleTrail: event=CAMERA_STABLE_RESUME statePreserved=true fieldResubmit=false timelineReset=false");
|
||||
}
|
||||
resources->trailCameraKnown = true;
|
||||
resources->trailCameraLatitude = trailCamera.latitude();
|
||||
@@ -1463,7 +1518,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
0,
|
||||
maskMode,
|
||||
true);
|
||||
const bool particlesBuilt = !resources->formalDisplay ||
|
||||
const bool particlesBuilt = !resources->formalDisplay || resources->trailCameraInvalidated ||
|
||||
buildParticlePoints(context,
|
||||
resources,
|
||||
trailGroup,
|
||||
@@ -1562,8 +1617,14 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
}
|
||||
resources->lastSimulationTick = now;
|
||||
const float trailWallDeltaSeconds = std::clamp(wallDeltaSeconds, 0.0f, 0.250f);
|
||||
const float visualSimulationDeltaSeconds =
|
||||
std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * VisualSimulationSecondsPerRealSecond;
|
||||
const float zoom = static_cast<float>(transformState.getZoom());
|
||||
const float effectiveVisualSpeedScale = std::clamp(
|
||||
VisualSimulationSecondsPerRealSecond * std::exp2(VisualSimulationReferenceZoom - zoom),
|
||||
MinVisualSimulationSecondsPerRealSecond,
|
||||
MaxVisualSimulationSecondsPerRealSecond);
|
||||
const float visualSimulationDeltaSeconds = std::min(
|
||||
std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * effectiveVisualSpeedScale,
|
||||
MaxSimulationDeltaSeconds);
|
||||
|
||||
const bool stateBuilt = buildQuad(context,
|
||||
resources,
|
||||
@@ -1579,7 +1640,9 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
stateWrite,
|
||||
0,
|
||||
maskMode,
|
||||
true);
|
||||
true,
|
||||
trailWallDeltaSeconds,
|
||||
effectiveVisualSpeedScale);
|
||||
const bool trailBuilt = buildQuad(context,
|
||||
resources,
|
||||
trailGroup,
|
||||
@@ -1594,8 +1657,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
trailWrite,
|
||||
0,
|
||||
maskMode,
|
||||
true);
|
||||
const bool particlesBuilt = !resources->formalDisplay ||
|
||||
true,
|
||||
trailWallDeltaSeconds,
|
||||
effectiveVisualSpeedScale);
|
||||
const bool particlesBuilt = !resources->formalDisplay || resources->trailCameraInvalidated ||
|
||||
buildParticlePoints(context,
|
||||
resources,
|
||||
trailGroup,
|
||||
|
||||
Reference in New Issue
Block a user