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:
16
docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md
Normal file
16
docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v6.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# STEP-055 Android 真实 WeatherV2 风场综合可视化 v6
|
||||
|
||||
状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**
|
||||
|
||||
Starting HEAD:`067bfdc38baa32d6e474a502d67bc0910d18e247`
|
||||
|
||||
## 低缩放可见性与 Trail 稳定性修复
|
||||
|
||||
- visual scale 使用 `effectiveScale = clamp(1200 * 2^(9 - zoom), 300, 153600)`;simulation step 再限制为最多 `2400` seconds/frame。典型 60 FPS:z2≈2400(cap)、z5≈320、z9≈20 simulation seconds/frame。
|
||||
- canonical U/V=m/s 与 m/s→Web Mercator 物理换算未改;只是 validation visual clock 依据 zoom 保持可见的屏幕位移。
|
||||
- 生命周期改为 real seconds:base=12,hash jitter 后为 9–15 real seconds;State B 使用 wall delta,不再使用 simulation delta,因此 zoom、Timeline speed、Timeline Pause 不改变寿命。
|
||||
- 普通 camera transform 首次触发 `CAMERA_INVALIDATE`,暂停 Trail segment 累积而 State 继续;连续操作不重复重建 texture;150ms 稳定后 `CAMERA_STABLE_RESUME`。仅真正 viewport size 改变才 `VIEWPORT_RESIZE` 并重新分配 Trail A/B。
|
||||
|
||||
State Float、Trail UByte、真实 previous→current segment、respawn/hash stagger、time-based fade、canonical、Timeline、Runtime Window、WeatherColorLayer 和 REAL WEATHER UI 保持不变。
|
||||
|
||||
shader generator、Linux OpenGL 精确编译、既有非 Jest style layer 精确编译和 `git diff --check` 已通过;Android 与 Jest 未执行。
|
||||
20
docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md
Normal file
20
docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v6.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收指令 v6
|
||||
|
||||
环境:Windows 11;设备 `192.168.1.146`。Android 构建、安装和设备操作由用户手工执行。
|
||||
|
||||
```powershell
|
||||
cd platform\android
|
||||
$env:RENDERER='opengl'; $env:BUILDTYPE='Debug'; make apackage --console=plain
|
||||
adb -s 192.168.1.146 install -r <APK路径>
|
||||
adb -s 192.168.1.146 logcat -c
|
||||
adb -s 192.168.1.146 logcat | Select-String 'Particle(Motion|Lifecycle|Trail|Timeline|GPU|StateTarget)'
|
||||
```
|
||||
|
||||
1. REAL WEATHER 自动 fit(z≈2–3):粒子应持续可见和运动,不应只剩 WeatherColorLayer,也不得回到固定粗橙色短棍。
|
||||
2. z≈5:粒子仍可见,`ParticleMotion` 检查 `referenceZoom=9`、有效 scale 和 simulation delta。
|
||||
3. z≈9:不应飞跃或高频越界 respawn;日志中的 simulation delta 不得超过 2400。
|
||||
4. Timeline Pause:forecast 不推进,Particle simulation 与 wall-time lifecycle 继续;`ParticleLifecycle` 应为 `lifecycleBaseRealSeconds=12`,不含 simulation lifecycle 字段。
|
||||
5. 连续 pinch zoom/pan/rotate:每段交互最多一次 `CAMERA_INVALIDATE`,停止约 150ms 后一条 `CAMERA_STABLE_RESUME`;State/Field/Timeline 不重置。只有实际 size 变化才出现 `VIEWPORT_RESIZE`。
|
||||
6. PLAY 至 T3:继续确认 OLD_T1→NEW_T0 reuse、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。
|
||||
|
||||
所有 ADB 命令必须显式使用 `adb -s 192.168.1.146 ...`。若失败,保留完整 Particle 日志;不要通过改回 world-length streak 或提高固定 speed scale 规避。
|
||||
@@ -201,7 +201,9 @@ void main() {
|
||||
float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash);
|
||||
float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47));
|
||||
bool respawn = initialSeed;
|
||||
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.y;
|
||||
// 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);
|
||||
|
||||
@@ -105,7 +105,9 @@ void main() {
|
||||
float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash);
|
||||
float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47));
|
||||
bool respawn = initialSeed;
|
||||
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.y;
|
||||
// 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);
|
||||
|
||||
@@ -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 ||
|
||||
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 (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";
|
||||
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,
|
||||
|
||||
@@ -432,3 +432,6 @@
|
||||
## STEP-055 生命周期错峰小修复
|
||||
|
||||
粒子基础生命周期调整为 14400 simulation seconds(12 real seconds),首次 seed 与 maxAge 都由稳定 hash 错峰,消除同步生命周期到期。日志改用 configuredSlots 并说明无 GPU readback;其他粒子视觉与数据链语义未改。
|
||||
## STEP-055 低缩放可见性与 Trail 稳定性修复
|
||||
|
||||
Particle visual clock 改为基于 zoom 的有界 scale 与 2400s/frame clamp;State lifecycle 改为 12s base 的 wall-time hash jitter。普通 camera 交互只 invalidate 一次并在稳定后恢复 Trail,只有 resize 重分配 Trail texture。canonical 与 Core 时间线边界未改,待 Android 验收。
|
||||
|
||||
Reference in New Issue
Block a user