diff --git a/CMakeLists.txt b/CMakeLists.txt index de2d01689..bb6e8f2d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,6 +254,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/background_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/particle_validation_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/raster_particle_layer_factory.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/weather_color_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/circle_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/custom_layer_factory.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/fill_extrusion_layer_factory.hpp @@ -367,6 +368,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/background_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/particle_validation_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/raster_particle_layer.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/weather_color_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/raster_array_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/circle_layer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/custom_layer_host.hpp @@ -514,6 +516,7 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/background_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/particle_validation_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/raster_particle_layer_factory.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/weather_color_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/circle_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_extrusion_layer_factory.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_layer_factory.cpp @@ -594,6 +597,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_particle_validation_layer.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_particle_validation_layer.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_raster_particle_layer.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_weather_color_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_weather_color_layer.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_circle_layer.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_circle_layer.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_fill_extrusion_layer.cpp @@ -768,6 +773,8 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/particle_render_layer_properties.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/raster_particle_layer.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/raster_particle_layer_impl.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/weather_color_layer.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/weather_color_layer_impl.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_impl.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_impl.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_properties.cpp diff --git a/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md new file mode 100644 index 000000000..32dae8020 --- /dev/null +++ b/docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md @@ -0,0 +1,34 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 + +状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION + +Starting HEAD:`da71c8784ec20ed830f5d23880718647d666cf2f` + +## 目标与数据边界 + +本阶段只复用 STEP-054 已生成的真实 WeatherV2 canonical 资产:`integration/particle-weatherv2/ecmwf-hres-wind-20260807/` 下的 2026080700、0706、0712、0718。数据保持原始 513×769 网格、`interleaved-uv/float32-le`、U=eastward、V=northward、单位 m/s 和原始 validTime;未重新生成、重采样、裁剪或伪造数据。 + +## 实现 + +- 新增独立 `weather-color` Style/Render Layer。其仅从现有 `RasterArraySource` 的 canonical T0/T1 snapshot 读字段,拥有独立的颜色 GPU texture/draw pass;不合并到 `RasterParticleLayer`。 +- 颜色 shader 对同一位置的 T0/T1 先还原 canonical U/V、按 Core mix 插值,再计算 `hypot(U,V)`;色带固定为 0–30 m/s,不做自动拉伸。 +- 字段 texture 以每个 snapshot 的显式 signed velocity scale 编码/还原,避免真实 m/s 数据被旧 `[-1,1]` 编码截断。 +- 粒子 GPU 字段采样改为 metadata bounds 的 Web Mercator UV。字段区域外保持 fail-closed;State 初始 seed 和越界恢复均使用字段 bounds,因此真实日本区域不再被当作全球 UV。 +- Android MORE 新增 `REAL WEATHER`:一次 style 初始化后,经既有 STEP-053 Host Submission Contract 依次提交真实 T0–T3;样式层级为 OSM 无 Key raster basemap → `weather-color` → `raster-particle` → Android HUD,并按 metadata bounds 自动 fit camera。 +- `REAL WEATHER` 状态随 Activity 保存/恢复。重建后自动再次以相同 metadata、payload 与 validTime 提交,不要求用户按按钮恢复;粒子 Trail FBO 仍由既有 viewport resize 路径重建。 + +## 所有权与冻结边界 + +Core 仍拥有 Timeline、Playback 和 Runtime Field Window。颜色层不 tick Timeline,`RasterParticleLayer` 仍是唯一的 playback tick 消费者。未修改 Adapter contract、Snapshot、Window A/B→B/C→C/D、Duplicate NOOP、Invalid/Gap fail-closed、Renderer provider/network 边界或 WeatherColorLayer/RasterParticleLayer 分离。 + +正常连续边界继续依赖既有 `OLD_T1 → NEW_T0 reuse` 与 `only NEW_T1 upload`;颜色层相同地复用其旧 T1 texture,仅为新的 T1 建立新 texture。正常 playback 的粒子 `simulationRebuild=false`、`trailCleared=false` 语义不变。 + +## 可观察性 + +新增低频日志:`REAL_WEATHER_DATASET_LOAD`、`REAL_WEATHER_INITIAL_WINDOW_READY`、`WEATHER_COLOR_FIELD_READY`、`REAL_WEATHER_COMPOSITE_READY`。现有 Host submit、Timeline、Window Boundary、FIELD_REUSE、FIELD_UPLOAD 和 ParticleRender 日志继续可用。没有新增每帧 Android JNI 或 UI timer。 + +## 验证与待验收 + +Codex 未执行 Android Gradle、APK build/install、ADB、App 启动、设备/模拟器测试或 Jest。Linux/OpenGL 受影响 translation unit 精确编译与相关定向测试结果见完成报告。 + +Android 仍需用户一次性手工综合验收,步骤见 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md`。已知 Validation UI 的 MIX 文本滞后以及 WAITING/INVALID 可见性仍为 NON-BLOCKING,不在本阶段单独修复。 diff --git a/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md new file mode 100644 index 000000000..f2bfa0386 --- /dev/null +++ b/docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md @@ -0,0 +1,76 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化人工验收 + +状态:等待用户手工 Android 验收。Codex 不执行本文件中的 Gradle、APK 或 ADB 操作。 + +## Windows 11 构建与安装 + +在 Windows 11 项目目录执行既有 Android OpenGL Debug 构建命令,并保留 plain 控制台输出,例如: + +```powershell +./gradlew :MapLibreAndroidTestApp:assembleOpenGLDebug --console=plain +``` + +安装、启动和日志均显式选择设备: + +```powershell +adb -s 192.168.1.146 install -r +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 logcat | findstr /I "REAL_WEATHER WEATHER_COLOR HOST_FIELD ParticleTimeline FIELD_REUSE FIELD_UPLOAD ParticleRender simulationRebuild trailCleared" +``` + +## 入口 + +进入 `Particle Core Layer Validation`: + +1. `MORE → REAL WEATHER`。 +2. 等待地图自动 fit 到 112.5–157.5E、0–55.776573N;应看到 OSM 底图、半透明风速色场及其上的运动粒子。 +3. `TIME → PLAY / RESTART`。真实 validTime 保持 6 小时步长;没有额外 Android 加速机制,可使用现有 `SPEED 2x`,或用 `STEP NEXT` 检查边界。 + +## V1–V8 视觉与行为矩阵 + +| 项 | 操作 | 期望 | +| --- | --- | --- | +| V1 | `REAL WEATHER` | 出现真实日本/西北太平洋覆盖范围、OSM 底图、风速色场和粒子;无黑屏/crash。 | +| V2 | 比较色场与粒子 | 色场强弱分布与粒子移动方向同源;U=eastward、V=northward,无明显横纵交换/符号反转。 | +| V3 | PLAY | Timeline READY 后 PLAY 成功;MIX 自动推进,颜色与粒子同时连续变化。 | +| V4 | 两次自动边界或 STEP NEXT | T0/T1→T1/T2→T2/T3;日志有 `OLD_T1_TO_NEW_T0` / `FIELD_REUSE`,只出现一个 `NEW_CONTENT` upload。 | +| V5 | 连续播放观察 | 正常 boundary 为 `simulationRebuild=false`、`trailCleared=false`;不得 style reload/layer rebuild。 | +| V6 | Pan/zoom/rotate/viewport resize | 底图、色场和粒子均保持地理对齐;旋转/恢复后自动可见,不按任何恢复按钮。 | +| V7 | `MORE → HOST DUPLICATE` | 对当前 Host window 得到 Duplicate NOOP,field upload/reset count 无增量。 | +| V8 | `MORE → HOST INVALID` | Host canonical invalid 被拒,最后有效 field、色场/Renderer 持续,无黑屏/crash。 | + +## 必要日志 + +正常加载必须可见: + +```text +REAL_WEATHER_DATASET_LOAD +HOST_FIELD_SUBMIT_REQUEST / HOST_FIELD_SUBMIT_ACCEPTED +HOST_FIELD_INITIAL_WINDOW_READY +ParticleTimeline: event=READY +REAL_WEATHER_INITIAL_WINDOW_READY +WEATHER_COLOR_FIELD_READY +REAL_WEATHER_COMPOSITE_READY +``` + +播放/边界检查: + +```text +PLAYBACK_PLAY +TIME_MIX_UPDATE or PLAYBACK_TICK +WINDOW_BOUNDARY +FIELD_REUSE from=OLD_T1 to=NEW_T0 +FIELD_UPLOAD reason=NEW_CONTENT +simulationRebuild=false +trailCleared=false +``` + +负向检查: + +```text +HOST_FIELD_SUBMIT_REJECTED +ADAPTER_REJECTED +result=NOOP +``` + +请保存上述日志、V1–V8 结果,以及任何色场与粒子错位、黑屏、旋转后不可恢复或 crash 的时间点。MIX 文本显示可能滞后属于既知 NON-BLOCKING observability,不应替代 Core log 判断。 diff --git a/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md new file mode 100644 index 000000000..acbe2a980 --- /dev/null +++ b/docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md @@ -0,0 +1,55 @@ +# STEP-055 Android 真实 WeatherV2 风场综合可视化 Codex 完成报告 + +## 基线与范围 + +- Starting HEAD:`da71c8784ec20ed830f5d23880718647d666cf2f` +- 分支:`particle/main` +- 状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION +- 输入:STEP-054 已提交的真实 WeatherV2 ECMWF HRES wind canonical T0–T3(513×769,原始 validTime,U=eastward、V=northward、m/s)。本阶段没有重新生成、重采样或修改这些数据。 + +本任务实现 Android Validation 的真实场综合可视化,不引入 Weather Service、HTTP、Provider、下载、缓存、React Native 或 Jest。 + +## 实际修改 + +- 新增 provider-neutral、独立的 `weather-color` Style Layer、Factory 和 OpenGL RenderLayer。它从现有 `RasterArraySource` 的 canonical snapshot 读 T0/T1;不含 provider/network/business logic,也不与 `RasterParticleLayer` 合并。 +- `WeatherColorLayer` 颜色逻辑为:还原 canonical U/V → Core mix 插值 → `hypot(U,V)`;采用固定 0–30 m/s 色标和固定 opacity。 +- 更新粒子 GPU 字段采样:真实 metadata bounds 转为 Web Mercator UV;字段区域外 fail-closed,粒子 seed/恢复受相同 bounds 约束。为真实 m/s 引入 texture decode scale,避免旧 `[-1,1]` 编码饱和。 +- Android `MORE → REAL WEATHER` 一键加载 STEP-054 的 T0–T3,通过既有 Host Submission Contract submit;使用既有 OSM 无 Key raster basemap,自动 fit real bounds,并以 basemap → color → particle → HUD 排序。 +- 保存 REAL WEATHER / Host mode 状态;Activity 重建后自动以相同 metadata/payload/validTime 恢复可见内容。现有 RasterParticle viewport trail FBO resize 路径保持负责其 GPU target 重建。 +- 新增 `WeatherColorUsesIndependentFixedOpacity` Core style test;新增 Android V1–V8 人工验收指令。 + +## 架构保护 + +未修改 `ParticleFieldDescriptor`、`ParticleFieldSnapshot`、`ParticleFieldDataAdapter`、Host submission contract、Timeline state machine、Playback ownership、Runtime Field Window、Duplicate NOOP、Gap/Invalid fail-closed 或 Renderer provider/network 边界。 + +`RasterParticleLayer` 仍是唯一 tick Timeline 的 render consumer。WeatherColorLayer 只读取同一 Source-owned frame,故不建立第二时钟/Timeline。正常 Boundary 的 `OLD_T1 → NEW_T0 reuse` 和 `only NEW_T1 upload` 继续适用;颜色层也复用旧 T1 GPU texture。 + +## 验证 + +- CMake OpenGL 配置:PASS。 +- 受影响 C++ 精确编译:WeatherColor Layer、Factory、RenderLayer、RasterParticle shader-side renderer、默认 LayerManager:PASS(同一 CMake OpenGL 编译参数)。 +- 使用同一 CMake OpenGL 配置的 `mbgl-core` 全量目标已启动并确认既有依赖/前序 Core 可编译;按任务“不默认跑无关全量测试”的边界,在进入与 STEP-055 无关的大量 Core translation units 后停止。它不构成本阶段的验收条件。 +- 新增 `test/style/style_layer.test.cpp` 和受影响 `test/style/source.test.cpp`:以同一 CMake 参数精确编译 PASS;未构建/执行无关的完整 test runner。 +- `git diff --check`:待最终文档完成后执行。 +- Android Gradle / APK build / install / ADB / App launch / device test:未执行(用户手工)。 +- Jest:未执行,未修改。 + +## Android 待验收 + +按 `docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md` 执行一次构建/安装,重点确认:真实 T0–T3、色场/粒子地理对齐、PLAY/MIX、两次 Boundary、reuse/only NEW_T1、Duplicate/Invalid、旋转恢复以及无黑屏/crash。 + +真实 validTime 保持 6 小时间隔;当前没有另加 Android 时间压缩。可用既有 2x 或 STEP NEXT 观察边界。 + +## 非阻塞项 + +Validation UI MIX 文本滞后及 WAITING/INVALID 的视觉不够明显仍为 NON-BLOCKING observability,不被表述为 Core、Timeline 或 Renderer 故障。 + +## Git 收尾 + +- Commit hash:待最终验证后填写 +- Commit message:待最终验证后填写 +- Push result:待最终验证后填写 +- Remote branch:`origin/particle/main` +- Final HEAD:待最终验证后填写 + +无关 untracked 文件(历史 diff、任务书、日志等)将保持未触碰、未 stage。 diff --git a/include/mbgl/layermanager/weather_color_layer_factory.hpp b/include/mbgl/layermanager/weather_color_layer_factory.hpp new file mode 100644 index 000000000..9d79e8b02 --- /dev/null +++ b/include/mbgl/layermanager/weather_color_layer_factory.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +namespace mbgl { +class WeatherColorLayerFactory final : public LayerFactory { +public: + const style::LayerTypeInfo* getTypeInfo() const noexcept override; + std::unique_ptr createLayer(const std::string&, const style::conversion::Convertible&) noexcept override; + std::unique_ptr createRenderLayer(Immutable) noexcept override; +}; +} // namespace mbgl diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 111d2bcfb..773c5024b 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -20,6 +20,8 @@ layout (std140) uniform ParticleGpuValidationUBO { 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; }; @@ -27,12 +29,23 @@ uniform sampler2D u_state_texture; 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 < 1.5) { + 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) { // 40-05: a_pos.x packs State texel X plus a static line endpoint. @@ -43,10 +56,13 @@ void main() { 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; - vec4 field0 = texture(u_field_texture, position); - vec4 field1 = texture(u_next_field_texture, position); - vec2 velocity = mix(field0.rg * 2.0 - 1.0, field1.rg * 2.0 - 1.0, u_marker.x); - bool valid = field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity)); + 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); + 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; @@ -72,6 +88,8 @@ void main() { 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; }; @@ -93,31 +111,68 @@ vec3 particleSpeedColor(float speed) { 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))); +} + +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)); +} + void main() { - vec4 previous = texture(u_previous, v_uv); 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) { // 40-01: State A/B samples the uploaded OfflineFixture Particle Field. // RG stores field-normalized 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. - vec2 position = u_mask.w > 0.5 ? v_uv : previous.rg; + 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. - vec4 field0 = texture(u_field_texture, position); - vec4 field1 = texture(u_next_field_texture, position); - vec2 velocity = mix(field0.rg * 2.0 - 1.0, field1.rg * 2.0 - 1.0, u_marker.x); - if (field0.a < 0.5 || field1.a < 0.5) { - position = v_uv; + 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); } // Field V remains north-positive. Mercator Y increases southward, so // convert only at the physical-velocity -> map-displacement boundary. vec2 mercatorVelocity = vec2(velocity.x, -velocity.y); - position = fract(position + mercatorVelocity * u_pass.y * 0.004); - if (any(isnan(velocity))) position = v_uv; + position = position + mercatorVelocity * u_pass.y * 0.00004; + 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 diff --git a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp index cea4263b0..8664c7d80 100644 --- a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp +++ b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp @@ -13,9 +13,14 @@ struct alignas(16) ParticleGpuValidationUBO { // Formal RasterParticle visual controls: particle opacity, canonical // streak length, trail opacity, and Trail A/B fade multiplier. std::array visual; + // west, north, east, south in normalized Web Mercator world coordinates. + // Field textures are regional; they must never be sampled as global UVs. + std::array fieldBounds; + // Per-texture signed velocity decoding scales in canonical m/s. + std::array fieldScale; std::array matrix; }; -static_assert(sizeof(ParticleGpuValidationUBO) == 8 * 16); +static_assert(sizeof(ParticleGpuValidationUBO) == 10 * 16); } // namespace shaders } // namespace mbgl diff --git a/include/mbgl/style/layers/weather_color_layer.hpp b/include/mbgl/style/layers/weather_color_layer.hpp new file mode 100644 index 000000000..cf1c1bee0 --- /dev/null +++ b/include/mbgl/style/layers/weather_color_layer.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace mbgl::style { + +// A field-speed colour display. It deliberately remains distinct from +// RasterParticleLayer: both consume the same canonical field source but own +// separate GPU resources and draw passes. +class WeatherColorLayer final : public Layer { +public: + WeatherColorLayer(const std::string& layerID, const std::string& sourceID); + ~WeatherColorLayer() override; + float getOpacity() const; + void setOpacity(float); + class Impl; + const Impl& impl() const; + Mutable mutableImpl() const; + +private: + explicit WeatherColorLayer(Immutable); + std::optional setPropertyInternal(const std::string&, const conversion::Convertible&) override; + StyleProperty getProperty(const std::string&) const override; + Value serialize() const override; + std::unique_ptr cloneRef(const std::string&) const override; + Mutable mutableBaseImpl() const override; +}; + +} // namespace mbgl::style diff --git a/platform/android/MapLibreAndroid/src/cpp/style/layers/layer_manager.cpp b/platform/android/MapLibreAndroid/src/cpp/style/layers/layer_manager.cpp index 1d57dd157..fc4d8529b 100644 --- a/platform/android/MapLibreAndroid/src/cpp/style/layers/layer_manager.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/style/layers/layer_manager.cpp @@ -21,6 +21,7 @@ #include "location_indicator_layer.hpp" #include #include +#include namespace mbgl { @@ -30,6 +31,7 @@ LayerManagerAndroid::LayerManagerAndroid() { // VALIDATION ONLY: JSON style access; intentionally no Android product API/JNI peer. addLayerTypeCoreOnly(std::make_unique()); addLayerTypeCoreOnly(std::make_unique()); + addLayerTypeCoreOnly(std::make_unique()); #if defined(MBGL_LAYER_FILL_DISABLE_RUNTIME) addLayerTypeCoreOnly(std::make_unique()); #elif !defined(MBGL_LAYER_FILL_DISABLE_ALL) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index a3c939b08..49356e73c 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -7,7 +7,9 @@ import android.widget.PopupMenu import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng +import org.maplibre.android.geometry.LatLngBounds import org.maplibre.android.maps.MapView import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style @@ -34,6 +36,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var timeWindowMode = false private var productionFeedMode = false private var hostSubmissionMode = false + private var realWeatherMode = false private var styleLoadEpoch = 0L private var styleReady = false // Display mirror only; Core remains the playback state authority. @@ -54,6 +57,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { trailFade = savedInstanceState?.getFloat(STATE_TRAIL_FADE, DEFAULT_TRAIL_FADE) ?: DEFAULT_TRAIL_FADE speedColorEnabled = savedInstanceState?.getBoolean(STATE_SPEED_COLOR, true) ?: true timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false + hostSubmissionMode = savedInstanceState?.getBoolean(STATE_HOST_SUBMISSION, false) ?: false + realWeatherMode = savedInstanceState?.getBoolean(STATE_REAL_WEATHER, false) ?: false super.onCreate(savedInstanceState) Log.i( TAG, @@ -101,6 +106,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { timeWindowMode = false productionFeedMode = false hostSubmissionMode = false + realWeatherMode = false fixtureMode = FIXTURE_UNIFORM_EAST if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_EAST") } @@ -108,6 +114,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { timeWindowMode = false productionFeedMode = false hostSubmissionMode = false + realWeatherMode = false fixtureMode = FIXTURE_UNIFORM_NORTH if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_NORTH") } @@ -115,6 +122,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { timeWindowMode = false productionFeedMode = false hostSubmissionMode = false + realWeatherMode = false fixtureMode = FIXTURE_VORTEX if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "VORTEX") } @@ -122,6 +130,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { timeWindowMode = true productionFeedMode = true hostSubmissionMode = false + realWeatherMode = false fixtureMode = FIXTURE_UNIFORM_EAST timeMix = 0.0f timelineState = "NOT READY" @@ -146,6 +155,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { findViewById