feat: add real WeatherV2 Android composite validation
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:
@@ -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
|
||||
|
||||
34
docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md
Normal file
34
docs/50_Step记录/STEP-055_Android真实WeatherV2风场综合可视化_v1.md
Normal file
@@ -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,不在本阶段单独修复。
|
||||
76
docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md
Normal file
76
docs/60_测试/STEP-055_Android真实WeatherV2风场综合可视化_人工验收指令_v1.md
Normal file
@@ -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 <APK_PATH>
|
||||
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 判断。
|
||||
55
docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md
Normal file
55
docs/报告/STEP-055_Android真实WeatherV2风场综合可视化_Codex完成报告_v1.md
Normal file
@@ -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。
|
||||
10
include/mbgl/layermanager/weather_color_layer_factory.hpp
Normal file
10
include/mbgl/layermanager/weather_color_layer_factory.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <mbgl/layermanager/layer_factory.hpp>
|
||||
namespace mbgl {
|
||||
class WeatherColorLayerFactory final : public LayerFactory {
|
||||
public:
|
||||
const style::LayerTypeInfo* getTypeInfo() const noexcept override;
|
||||
std::unique_ptr<style::Layer> createLayer(const std::string&, const style::conversion::Convertible&) noexcept override;
|
||||
std::unique_ptr<RenderLayer> createRenderLayer(Immutable<style::Layer::Impl>) noexcept override;
|
||||
};
|
||||
} // namespace mbgl
|
||||
@@ -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
|
||||
|
||||
@@ -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<float, 4> 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<float, 4> fieldBounds;
|
||||
// Per-texture signed velocity decoding scales in canonical m/s.
|
||||
std::array<float, 4> fieldScale;
|
||||
std::array<float, 16> matrix;
|
||||
};
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 8 * 16);
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 10 * 16);
|
||||
|
||||
} // namespace shaders
|
||||
} // namespace mbgl
|
||||
|
||||
29
include/mbgl/style/layers/weather_color_layer.hpp
Normal file
29
include/mbgl/style/layers/weather_color_layer.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/style/layer.hpp>
|
||||
|
||||
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<Impl> mutableImpl() const;
|
||||
|
||||
private:
|
||||
explicit WeatherColorLayer(Immutable<Impl>);
|
||||
std::optional<conversion::Error> setPropertyInternal(const std::string&, const conversion::Convertible&) override;
|
||||
StyleProperty getProperty(const std::string&) const override;
|
||||
Value serialize() const override;
|
||||
std::unique_ptr<Layer> cloneRef(const std::string&) const override;
|
||||
Mutable<Layer::Impl> mutableBaseImpl() const override;
|
||||
};
|
||||
|
||||
} // namespace mbgl::style
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "location_indicator_layer.hpp"
|
||||
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/raster_particle_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/weather_color_layer_factory.hpp>
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
@@ -30,6 +31,7 @@ LayerManagerAndroid::LayerManagerAndroid() {
|
||||
// VALIDATION ONLY: JSON style access; intentionally no Android product API/JNI peer.
|
||||
addLayerTypeCoreOnly(std::make_unique<ParticleValidationLayerFactory>());
|
||||
addLayerTypeCoreOnly(std::make_unique<RasterParticleLayerFactory>());
|
||||
addLayerTypeCoreOnly(std::make_unique<WeatherColorLayerFactory>());
|
||||
#if defined(MBGL_LAYER_FILL_DISABLE_RUNTIME)
|
||||
addLayerTypeCoreOnly(std::make_unique<FillLayerFactory>());
|
||||
#elif !defined(MBGL_LAYER_FILL_DISABLE_ALL)
|
||||
|
||||
@@ -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<Button>(R.id.validationProductionInvalid).setOnClickListener {
|
||||
productionFeedMode = true
|
||||
hostSubmissionMode = false
|
||||
realWeatherMode = false
|
||||
timeWindowMode = true
|
||||
timelineState = "NOT READY"
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "INVALID_PRODUCTION_METADATA")
|
||||
@@ -154,6 +164,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
timeWindowMode = true
|
||||
productionFeedMode = false
|
||||
hostSubmissionMode = true
|
||||
realWeatherMode = false
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
timeMix = 0.0f
|
||||
timelineState = "NOT READY"
|
||||
@@ -162,6 +173,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationHostFieldDuplicate).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) submitHostValidationField(3, duplicate = true)
|
||||
}
|
||||
findViewById<Button>(R.id.validationRealWeather).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
productionFeedMode = false
|
||||
hostSubmissionMode = true
|
||||
realWeatherMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
timeMix = 0.0f
|
||||
timelineState = "NOT READY"
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "REAL_WEATHER")
|
||||
}
|
||||
findViewById<Button>(R.id.validationHostFieldInvalid).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) submitInvalidHostValidationField()
|
||||
}
|
||||
@@ -189,7 +210,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationMenuVisual).setOnClickListener { showMenu(it, "VIS", intArrayOf(R.id.validationVisualDefault, R.id.validationVisualColor, R.id.validationVisualSizeDown, R.id.validationVisualSizeUp, R.id.validationVisualLengthDown, R.id.validationVisualLengthUp, R.id.validationVisualOpacityDown, R.id.validationVisualOpacityUp, R.id.validationVisualTrailDown, R.id.validationVisualTrailUp, R.id.validationVisualFadeDown, R.id.validationVisualFadeUp)) }
|
||||
findViewById<Button>(R.id.validationMenuCount).setOnClickListener { showMenu(it, "COUNT", intArrayOf(R.id.validationParticle256, R.id.validationParticle1024, R.id.validationParticle4096, R.id.validationParticle16384, R.id.validationParticle65536, R.id.validationParticle262144, R.id.validationParticle1048576)) }
|
||||
findViewById<Button>(R.id.validationMenuMap).setOnClickListener { showMenu(it, "MAP", intArrayOf(R.id.validationGlobal, R.id.validationOceanA, R.id.validationOceanB, R.id.validationDateline, R.id.validationWorldWrap)) }
|
||||
findViewById<Button>(R.id.validationMenuMore).setOnClickListener { showMenu(it, "MORE", intArrayOf(R.id.validationActive, R.id.validationStatic, R.id.validationDisable, R.id.validationEnable, R.id.validationUniformEast, R.id.validationUniformNorth, R.id.validationVortex, R.id.validationTimeWindow, R.id.validationHostFieldSubmit, R.id.validationProductionInvalid, R.id.validationHostFieldDuplicate, R.id.validationHostFieldInvalid, R.id.validationAdvanceWindow, R.id.validationDuplicateWindow)) }
|
||||
findViewById<Button>(R.id.validationMenuMore).setOnClickListener { showMenu(it, "MORE", intArrayOf(R.id.validationActive, R.id.validationStatic, R.id.validationDisable, R.id.validationEnable, R.id.validationUniformEast, R.id.validationUniformNorth, R.id.validationVortex, R.id.validationTimeWindow, R.id.validationHostFieldSubmit, R.id.validationRealWeather, R.id.validationProductionInvalid, R.id.validationHostFieldDuplicate, R.id.validationHostFieldInvalid, R.id.validationAdvanceWindow, R.id.validationDuplicateWindow)) }
|
||||
}
|
||||
|
||||
private fun showMenu(anchor: android.view.View, title: String, actions: IntArray) {
|
||||
@@ -239,7 +260,46 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun actionLabel(id: Int): String = when (id) {
|
||||
R.id.validationVisualDefault -> "VIS DEFAULT"; R.id.validationVisualSizeDown -> "SIZE -"; R.id.validationVisualSizeUp -> "SIZE +"; R.id.validationVisualLengthDown -> "LENGTH -"; R.id.validationVisualLengthUp -> "LENGTH +"; R.id.validationVisualOpacityDown -> "OPACITY -"; R.id.validationVisualOpacityUp -> "OPACITY +"; R.id.validationVisualTrailDown -> "TRAIL -"; R.id.validationVisualTrailUp -> "TRAIL +"; R.id.validationVisualFadeDown -> "FADE -"; R.id.validationVisualFadeUp -> "FADE +"; R.id.validationVisualColor -> "COLOR"; R.id.validationParticle256 -> "256"; R.id.validationParticle1024 -> "1K"; R.id.validationParticle4096 -> "4K"; R.id.validationParticle16384 -> "16K"; R.id.validationParticle65536 -> "64K"; R.id.validationParticle262144 -> "256K"; R.id.validationParticle1048576 -> "1M"; R.id.validationGlobal -> "GLOBAL"; R.id.validationOceanA -> "OCEAN A"; R.id.validationOceanB -> "OCEAN B"; R.id.validationDateline -> "DATELINE"; R.id.validationWorldWrap -> "WORLD WRAP"; R.id.validationActive -> "ACTIVE"; R.id.validationStatic -> "STATIC"; R.id.validationDisable -> "DISABLE"; R.id.validationEnable -> "ENABLE"; R.id.validationUniformEast -> "UNIFORM EAST"; R.id.validationUniformNorth -> "UNIFORM NORTH"; R.id.validationVortex -> "VORTEX"; R.id.validationTimeWindow -> "PRODUCTION FEED"; R.id.validationHostFieldSubmit -> "HOST FIELD"; R.id.validationProductionInvalid -> "INVALID PROD"; R.id.validationHostFieldDuplicate -> "HOST DUPLICATE"; R.id.validationHostFieldInvalid -> "HOST INVALID"; R.id.validationAdvanceWindow -> "ADVANCE WINDOW"; else -> "DUPLICATE T1"
|
||||
R.id.validationTimeWindow -> "PRODUCTION FEED"
|
||||
R.id.validationHostFieldSubmit -> "HOST FIELD"
|
||||
R.id.validationRealWeather -> "REAL WEATHER"
|
||||
R.id.validationProductionInvalid -> "INVALID PROD"
|
||||
R.id.validationHostFieldDuplicate -> "HOST DUPLICATE"
|
||||
R.id.validationHostFieldInvalid -> "HOST INVALID"
|
||||
R.id.validationParticle1048576 -> "1M"
|
||||
R.id.validationParticle262144 -> "256K"
|
||||
R.id.validationParticle65536 -> "64K"
|
||||
R.id.validationParticle16384 -> "16K"
|
||||
R.id.validationParticle4096 -> "4K"
|
||||
R.id.validationParticle1024 -> "1K"
|
||||
R.id.validationParticle256 -> "256"
|
||||
R.id.validationActive -> "ACTIVE"
|
||||
R.id.validationStatic -> "STATIC"
|
||||
R.id.validationDisable -> "DISABLE"
|
||||
R.id.validationEnable -> "ENABLE"
|
||||
R.id.validationUniformEast -> "UNIFORM EAST"
|
||||
R.id.validationUniformNorth -> "UNIFORM NORTH"
|
||||
R.id.validationVortex -> "VORTEX"
|
||||
R.id.validationAdvanceWindow -> "ADVANCE WINDOW"
|
||||
R.id.validationGlobal -> "GLOBAL"
|
||||
R.id.validationOceanA -> "OCEAN A"
|
||||
R.id.validationOceanB -> "OCEAN B"
|
||||
R.id.validationDateline -> "DATELINE"
|
||||
R.id.validationWorldWrap -> "WORLD WRAP"
|
||||
R.id.validationVisualDefault -> "VIS DEFAULT"
|
||||
R.id.validationVisualSizeDown -> "SIZE -"
|
||||
R.id.validationVisualSizeUp -> "SIZE +"
|
||||
R.id.validationVisualLengthDown -> "LENGTH -"
|
||||
R.id.validationVisualLengthUp -> "LENGTH +"
|
||||
R.id.validationVisualOpacityDown -> "OPACITY -"
|
||||
R.id.validationVisualOpacityUp -> "OPACITY +"
|
||||
R.id.validationVisualTrailDown -> "TRAIL -"
|
||||
R.id.validationVisualTrailUp -> "TRAIL +"
|
||||
R.id.validationVisualFadeDown -> "FADE -"
|
||||
R.id.validationVisualFadeUp -> "FADE +"
|
||||
R.id.validationVisualColor -> "COLOR"
|
||||
R.id.validationDuplicateWindow -> "DUPLICATE T1"
|
||||
else -> "ACTION"
|
||||
}
|
||||
|
||||
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
|
||||
@@ -252,7 +312,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
maplibreMap.setStyle(Style.Builder().fromJson(validationStyle(active, enabled, reason))) {
|
||||
styleReady = true
|
||||
if (hostSubmissionMode) {
|
||||
submitHostValidationDataset()
|
||||
if (realWeatherMode) submitRealWeatherDataset() else submitHostValidationDataset()
|
||||
} else if (timeWindowMode) {
|
||||
timelineState = "READY"
|
||||
}
|
||||
@@ -408,6 +468,39 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_DATASET_SUBMIT results=${results.joinToString(",")} timeline=$timelineState")
|
||||
}
|
||||
|
||||
private fun submitRealWeatherDataset() {
|
||||
val cycles = arrayOf("2026080700", "2026080706", "2026080712", "2026080718")
|
||||
val root = "integration/particle-weatherv2/ecmwf-hres-wind-20260807"
|
||||
val results = cycles.mapIndexed { index, cycle ->
|
||||
try {
|
||||
val metadataPath = "$root/$cycle/metadata.json"
|
||||
val payloadPath = "$root/$cycle/field.f32"
|
||||
val metadata = assets.open(metadataPath).bufferedReader().use { it.readText() }
|
||||
val payload = assets.open(payloadPath).use { it.readBytes() }
|
||||
Log.i(TAG, "ParticleField: event=REAL_WEATHER_DATASET_LOAD index=$index metadata=$metadataPath payloadBytes=${payload.size}")
|
||||
maplibreMap.submitRasterArraySourceHostField(PARTICLE_SOURCE_ID, metadata, payload)
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "ParticleField: event=REAL_WEATHER_SUBMIT_REJECTED index=$index reason=ASSET_READ_FAILED", error)
|
||||
MapLibreMap.HOST_FIELD_REJECTED_ADAPTER
|
||||
}
|
||||
}
|
||||
timelineState = if (results.size == 4 && results.all { it > 0 }) "READY" else "NOT READY"
|
||||
fitRealWeatherBounds()
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleField: event=REAL_WEATHER_INITIAL_WINDOW_READY results=${results.joinToString(",")} timeline=$timelineState")
|
||||
Log.i(TAG, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY source=$PARTICLE_SOURCE_ID scale=0..30mps")
|
||||
Log.i(TAG, "ParticleField: event=REAL_WEATHER_COMPOSITE_READY source=$PARTICLE_SOURCE_ID basemap=OSM color=weather-color particle=raster-particle")
|
||||
}
|
||||
|
||||
private fun fitRealWeatherBounds() {
|
||||
val bounds = LatLngBounds.Builder()
|
||||
.include(LatLng(0.0, 112.5))
|
||||
.include(LatLng(55.77657301866769, 157.5))
|
||||
.build()
|
||||
maplibreMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 48))
|
||||
Log.i(TAG, "ParticleField: event=REAL_WEATHER_CAMERA_FIT bounds=112.5,0.0,157.5,55.77657301866769")
|
||||
}
|
||||
|
||||
private fun submitHostValidationField(index: Int, duplicate: Boolean): Int {
|
||||
if (!styleReady || !hostSubmissionMode) {
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=HOST_MODE_NOT_READY index=$index")
|
||||
@@ -466,7 +559,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
} else {
|
||||
"EAST"
|
||||
}
|
||||
val playback = if (timeWindowMode) " · ${if (hostSubmissionMode) "HOST" else if (productionFeedMode) "PRODUCTION" else "FIXTURE"} · TL $timelineState · $timelineSpeed" else ""
|
||||
val playback = if (timeWindowMode) " · ${if (realWeatherMode) "REAL WEATHER" else if (hostSubmissionMode) "HOST" else if (productionFeedMode) "PRODUCTION" else "FIXTURE"} · TL $timelineState · $timelineSpeed" else ""
|
||||
return "$mask · $runtime · $fields$playback · MIX ${(timeMix * 100).toInt()}% · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
|
||||
}
|
||||
|
||||
@@ -487,6 +580,20 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val weatherColorLayer = if (realWeatherMode && enabled) {
|
||||
"""{"id":"weather-color-real","type":"weather-color","source":"particle-fixture",
|
||||
"paint":{"weather-color-opacity":0.62}},"""
|
||||
} else ""
|
||||
val validationDecorations = if (realWeatherMode) "" else """
|
||||
{"id":"validation-below","type":"circle","source":"validation-point",
|
||||
"paint":{"circle-radius":46,"circle-color":"#247ba0"}},
|
||||
"""
|
||||
val validationHud = if (realWeatherMode) """
|
||||
{"id":"validation-real-weather-hud","type":"background","paint":{"background-opacity":0.0}}
|
||||
""" else """
|
||||
{"id":"validation-above","type":"circle","source":"validation-point",
|
||||
"paint":{"circle-radius":16,"circle-color":"#ffe066"}}
|
||||
"""
|
||||
val source = if (hostSubmissionMode) {
|
||||
"""{"type":"raster-array","host-field-submission":true}"""
|
||||
} else if (productionFeedMode) {
|
||||
@@ -516,6 +623,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
"name": "Particle Core Layer Validation",
|
||||
"sources": {
|
||||
"particle-fixture": $source,
|
||||
"osm-basemap": {"type":"raster","tiles":["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],"tileSize":256,"maxzoom":19},
|
||||
"validation-point": {
|
||||
"type": "geojson",
|
||||
"data": {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]}}
|
||||
@@ -523,11 +631,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
},
|
||||
"layers": [
|
||||
{"id":"validation-background","type":"background","paint":{"background-color":"#102030"}},
|
||||
{"id":"validation-below","type":"circle","source":"validation-point",
|
||||
"paint":{"circle-radius":46,"circle-color":"#247ba0"}},
|
||||
${if (realWeatherMode) "{\"id\":\"osm-basemap\",\"type\":\"raster\",\"source\":\"osm-basemap\",\"paint\":{\"raster-opacity\":1.0}}," else ""}
|
||||
$validationDecorations
|
||||
$weatherColorLayer
|
||||
$particleLayer
|
||||
{"id":"validation-above","type":"circle","source":"validation-point",
|
||||
"paint":{"circle-radius":16,"circle-color":"#ffe066"}}
|
||||
$validationHud
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
@@ -554,6 +662,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
outState.putFloat(STATE_TRAIL_FADE, trailFade)
|
||||
outState.putBoolean(STATE_SPEED_COLOR, speedColorEnabled)
|
||||
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
|
||||
outState.putBoolean(STATE_HOST_SUBMISSION, hostSubmissionMode)
|
||||
outState.putBoolean(STATE_REAL_WEATHER, realWeatherMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
mapView.onSaveInstanceState(outState)
|
||||
}
|
||||
@@ -579,6 +689,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val STATE_TRAIL_FADE = "particle_validation.trail_fade"
|
||||
private const val STATE_SPEED_COLOR = "particle_validation.speed_color"
|
||||
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
|
||||
private const val STATE_HOST_SUBMISSION = "particle_validation.host_submission"
|
||||
private const val STATE_REAL_WEATHER = "particle_validation.real_weather"
|
||||
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
||||
private const val PARTICLE_SOURCE_ID = "particle-fixture"
|
||||
private const val MASK_GLOBAL = 0
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationActive"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationStatic"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDisable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationEnable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationGlobal"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanA"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanB"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformEast"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformNorth"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVortex"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationTimeWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix0"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix50"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix100"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle16384"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle65536"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle262144"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1048576"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldSubmit"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldDuplicate"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldSubmit"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationRealWeather"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldDuplicate"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <mbgl/layermanager/location_indicator_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/raster_particle_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/weather_color_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/raster_layer_factory.hpp>
|
||||
#include <mbgl/layermanager/symbol_layer_factory.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
@@ -54,6 +55,7 @@ LayerManagerDefault::LayerManagerDefault() {
|
||||
// VALIDATION ONLY: 30-01 source-less Core RenderLayer experiment.
|
||||
addLayerType(std::make_unique<ParticleValidationLayerFactory>());
|
||||
addLayerType(std::make_unique<RasterParticleLayerFactory>());
|
||||
addLayerType(std::make_unique<WeatherColorLayerFactory>());
|
||||
#if !defined(MBGL_LAYER_FILL_DISABLE_ALL)
|
||||
addLayerType(std::make_unique<FillLayerFactory>());
|
||||
#endif
|
||||
|
||||
@@ -3,6 +3,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;
|
||||
};
|
||||
|
||||
@@ -24,31 +26,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
|
||||
|
||||
@@ -10,6 +10,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;
|
||||
};
|
||||
|
||||
@@ -17,12 +19,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.
|
||||
@@ -33,10 +46,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;
|
||||
|
||||
13
src/mbgl/layermanager/weather_color_layer_factory.cpp
Normal file
13
src/mbgl/layermanager/weather_color_layer_factory.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include <mbgl/layermanager/weather_color_layer_factory.hpp>
|
||||
#include <mbgl/renderer/layers/render_weather_color_layer.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer_impl.hpp>
|
||||
namespace mbgl {
|
||||
const style::LayerTypeInfo* WeatherColorLayerFactory::getTypeInfo() const noexcept { return style::WeatherColorLayer::Impl::staticTypeInfo(); }
|
||||
std::unique_ptr<style::Layer> WeatherColorLayerFactory::createLayer(const std::string& id, const style::conversion::Convertible& value) noexcept {
|
||||
const auto source = getSource(value); return source ? std::make_unique<style::WeatherColorLayer>(id, *source) : nullptr;
|
||||
}
|
||||
std::unique_ptr<RenderLayer> WeatherColorLayerFactory::createRenderLayer(Immutable<style::Layer::Impl> impl) noexcept {
|
||||
return std::make_unique<RenderWeatherColorLayer>(std::move(impl));
|
||||
}
|
||||
} // namespace mbgl
|
||||
@@ -121,15 +121,21 @@ const char* fixtureID(const uint8_t mode) {
|
||||
}
|
||||
|
||||
std::shared_ptr<PremultipliedImage> makeVelocityFieldImage(const particle::ParticleFieldSnapshot& field) {
|
||||
float scale = 1.0f;
|
||||
for (size_t index = 0; index < field.valid.size(); ++index) {
|
||||
if (!field.valid[index]) continue;
|
||||
scale = std::max(scale, std::abs(field.velocity[index * 2]));
|
||||
scale = std::max(scale, std::abs(field.velocity[index * 2 + 1]));
|
||||
}
|
||||
auto image = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
|
||||
for (uint32_t y = 0; y < field.height; ++y) {
|
||||
for (uint32_t x = 0; x < field.width; ++x) {
|
||||
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2;
|
||||
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
|
||||
image->data[pixel + 0] = static_cast<uint8_t>(std::clamp(field.velocity[cell] * 0.5f + 0.5f, 0.0f, 1.0f) *
|
||||
image->data[pixel + 0] = static_cast<uint8_t>(std::clamp(field.velocity[cell] / scale * 0.5f + 0.5f, 0.0f, 1.0f) *
|
||||
255.0f);
|
||||
image->data[pixel + 1] = static_cast<uint8_t>(
|
||||
std::clamp(field.velocity[cell + 1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
|
||||
std::clamp(field.velocity[cell + 1] / scale * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
|
||||
image->data[pixel + 2] = 0;
|
||||
image->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
|
||||
}
|
||||
@@ -137,6 +143,28 @@ std::shared_ptr<PremultipliedImage> makeVelocityFieldImage(const particle::Parti
|
||||
return image;
|
||||
}
|
||||
|
||||
float fieldVelocityScale(const particle::ParticleFieldSnapshot& field) {
|
||||
float scale = 1.0f;
|
||||
for (size_t index = 0; index < field.valid.size(); ++index) {
|
||||
if (!field.valid[index]) continue;
|
||||
scale = std::max(scale, std::abs(field.velocity[index * 2]));
|
||||
scale = std::max(scale, std::abs(field.velocity[index * 2 + 1]));
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
double mercatorY(const double latitude) {
|
||||
const auto radians = latitude * M_PI / 180.0;
|
||||
return (1.0 - std::log(std::tan(radians) + 1.0 / std::cos(radians)) / M_PI) * 0.5;
|
||||
}
|
||||
|
||||
std::array<float, 4> fieldBounds(const particle::ParticleFieldSnapshot& field) {
|
||||
return {static_cast<float>((field.west + 180.0) / 360.0),
|
||||
static_cast<float>(mercatorY(field.north)),
|
||||
static_cast<float>((field.east + 180.0) / 360.0),
|
||||
static_cast<float>(mercatorY(field.south))};
|
||||
}
|
||||
|
||||
// VALIDATION ONLY: deterministic global fixture with a diagonal coast. Alpha
|
||||
// 255 means OCEAN, alpha 0 means LAND. It is intentionally tiny and is not a
|
||||
// production coastline source.
|
||||
@@ -205,6 +233,8 @@ public:
|
||||
uint64_t gpuFieldGeneration = 0;
|
||||
uint64_t nextFieldGeneration = 0;
|
||||
uint64_t gpuNextFieldGeneration = 0;
|
||||
float fieldScale = 1.0f;
|
||||
float nextFieldScale = 1.0f;
|
||||
uint64_t fieldUploadCount = 0;
|
||||
uint64_t resetCount = 0;
|
||||
float timeMix = 0.0f;
|
||||
@@ -404,6 +434,8 @@ public:
|
||||
resources->particleLength,
|
||||
resources->trailOpacity,
|
||||
resources->formalDisplay ? resources->trailFade : 0.965f},
|
||||
.fieldBounds = fieldBounds(resources->field),
|
||||
.fieldScale = {resources->fieldScale, resources->nextFieldScale, 0.0f, 0.0f},
|
||||
.matrix = util::cast<float>(parameters.matrixForTile({worldWrap, {0, 0, 0}})),
|
||||
};
|
||||
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
|
||||
@@ -894,9 +926,9 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
owned->nextField = owned->field;
|
||||
owned->nextFieldGeneration = owned->fieldGeneration;
|
||||
}
|
||||
owned->fieldImage = makeVelocityFieldImage(owned->field);
|
||||
owned->fieldImage = makeVelocityFieldImage(owned->field);
|
||||
owned->fieldTexture = context.createTexture2D();
|
||||
owned->nextFieldImage = makeVelocityFieldImage(owned->nextField);
|
||||
owned->nextFieldImage = makeVelocityFieldImage(owned->nextField);
|
||||
owned->nextFieldTexture = context.createTexture2D();
|
||||
if (!owned->fieldImage || !owned->fieldTexture || !owned->nextFieldImage || !owned->nextFieldTexture) {
|
||||
Log::Error(Event::Render, "ParticleGPU: field resource creation failed");
|
||||
@@ -910,7 +942,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
" renderLayerInstanceId=" + std::to_string(owned->renderLayerInstanceId) +
|
||||
" resourceGeneration=" + std::to_string(owned->generation));
|
||||
owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
owned->nextFieldTexture->setImage(owned->nextFieldImage);
|
||||
Log::Info(Event::Render,
|
||||
@@ -920,11 +952,13 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
" renderLayerInstanceId=" + std::to_string(owned->renderLayerInstanceId) +
|
||||
" resourceGeneration=" + std::to_string(owned->generation));
|
||||
owned->nextFieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
owned->gpuFieldGeneration = owned->fieldGeneration;
|
||||
owned->gpuNextFieldGeneration = owned->nextFieldGeneration;
|
||||
owned->fieldUploadCount = 2;
|
||||
owned->fieldUploadCount = 2;
|
||||
owned->fieldScale = fieldVelocityScale(owned->field);
|
||||
owned->nextFieldScale = fieldVelocityScale(owned->nextField);
|
||||
owned->fixtureMode = fixtureMode;
|
||||
} catch (const std::exception& error) {
|
||||
Log::Error(Event::Render, "ParticleField: fixture load failed: " + std::string(error.what()));
|
||||
@@ -1053,14 +1087,16 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
const bool windowOverlap = reuseOldT0 || reuseOldT1 || reuseNewT0 || reuseNewT1;
|
||||
const auto upload = [&](const particle::ParticleFieldSnapshot& field,
|
||||
std::shared_ptr<PremultipliedImage>& image,
|
||||
gfx::Texture2DPtr& texture) {
|
||||
gfx::Texture2DPtr& texture,
|
||||
float& scale) {
|
||||
image = makeVelocityFieldImage(field);
|
||||
texture = context.createTexture2D();
|
||||
if (!image || !texture) return false;
|
||||
texture->setImage(image);
|
||||
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
scale = fieldVelocityScale(field);
|
||||
++resources->fieldUploadCount;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=FIELD_UPLOAD reason=NEW_CONTENT fieldUploadCount=" +
|
||||
@@ -1074,11 +1110,13 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
const char* destination,
|
||||
particle::ParticleFieldSnapshot& assigned,
|
||||
std::shared_ptr<PremultipliedImage>& image,
|
||||
gfx::Texture2DPtr& texture) {
|
||||
gfx::Texture2DPtr& texture,
|
||||
float& scale) {
|
||||
if (particle::sameFieldGpuContent(oldT0, wanted)) {
|
||||
assigned = wanted;
|
||||
image = resources->fieldImage;
|
||||
texture = resources->fieldTexture;
|
||||
scale = resources->fieldScale;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=FIELD_REUSE from=OLD_T0 to=" + std::string(destination) +
|
||||
" oldGeneration=" + std::to_string(oldT0.generation) +
|
||||
@@ -1091,6 +1129,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
assigned = wanted;
|
||||
image = resources->nextFieldImage;
|
||||
texture = resources->nextFieldTexture;
|
||||
scale = resources->nextFieldScale;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=FIELD_REUSE from=OLD_T1 to=" + std::string(destination) +
|
||||
" oldGeneration=" + std::to_string(oldT1.generation) +
|
||||
@@ -1100,7 +1139,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
return true;
|
||||
}
|
||||
assigned = wanted;
|
||||
return upload(assigned, image, texture);
|
||||
return upload(assigned, image, texture, scale);
|
||||
};
|
||||
particle::ParticleFieldSnapshot assignedT0;
|
||||
particle::ParticleFieldSnapshot assignedT1;
|
||||
@@ -1108,8 +1147,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
std::shared_ptr<PremultipliedImage> imageT1;
|
||||
gfx::Texture2DPtr textureT0;
|
||||
gfx::Texture2DPtr textureT1;
|
||||
if (!takeSlot(nextT0, "NEW_T0", assignedT0, imageT0, textureT0) ||
|
||||
!takeSlot(nextT1, "NEW_T1", assignedT1, imageT1, textureT1)) {
|
||||
float scaleT0 = 1.0f;
|
||||
float scaleT1 = 1.0f;
|
||||
if (!takeSlot(nextT0, "NEW_T0", assignedT0, imageT0, textureT0, scaleT0) ||
|
||||
!takeSlot(nextT1, "NEW_T1", assignedT1, imageT1, textureT1, scaleT1)) {
|
||||
Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed");
|
||||
return;
|
||||
}
|
||||
@@ -1119,6 +1160,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
resources->nextFieldImage = std::move(imageT1);
|
||||
resources->fieldTexture = std::move(textureT0);
|
||||
resources->nextFieldTexture = std::move(textureT1);
|
||||
resources->fieldScale = scaleT0;
|
||||
resources->nextFieldScale = scaleT1;
|
||||
resources->fieldGeneration = resources->field.generation;
|
||||
resources->nextFieldGeneration = resources->nextField.generation;
|
||||
resources->gpuFieldGeneration = resources->fieldGeneration;
|
||||
|
||||
191
src/mbgl/renderer/layers/render_weather_color_layer.cpp
Normal file
191
src/mbgl/renderer/layers/render_weather_color_layer.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
#include <mbgl/renderer/layers/render_weather_color_layer.hpp>
|
||||
|
||||
#include <mbgl/gfx/color_mode.hpp>
|
||||
#include <mbgl/gfx/context.hpp>
|
||||
#include <mbgl/gfx/cull_face_mode.hpp>
|
||||
#include <mbgl/gfx/drawable_builder.hpp>
|
||||
#include <mbgl/gfx/drawable_tweaker.hpp>
|
||||
#include <mbgl/gfx/shader_registry.hpp>
|
||||
#include <mbgl/gfx/texture2d.hpp>
|
||||
#include <mbgl/gfx/vertex_attribute.hpp>
|
||||
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
|
||||
#include <mbgl/renderer/layer_group.hpp>
|
||||
#include <mbgl/renderer/render_static_data.hpp>
|
||||
#include <mbgl/renderer/sources/render_raster_array_source.hpp>
|
||||
#include <mbgl/shaders/particle_gpu_validation_ubo.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer_impl.hpp>
|
||||
#include <mbgl/util/image.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
#include <mbgl/util/mat4.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace mbgl {
|
||||
namespace {
|
||||
constexpr auto ShaderName = "ParticleGpuValidationShader";
|
||||
|
||||
double mercatorY(const double latitude) {
|
||||
const auto radians = latitude * M_PI / 180.0;
|
||||
return (1.0 - std::log(std::tan(radians) + 1.0 / std::cos(radians)) / M_PI) * 0.5;
|
||||
}
|
||||
|
||||
std::array<float, 4> bounds(const particle::ParticleFieldSnapshot& field) {
|
||||
return {static_cast<float>((field.west + 180.0) / 360.0), static_cast<float>(mercatorY(field.north)),
|
||||
static_cast<float>((field.east + 180.0) / 360.0), static_cast<float>(mercatorY(field.south))};
|
||||
}
|
||||
|
||||
float scale(const particle::ParticleFieldSnapshot& field) {
|
||||
float result = 1.0f;
|
||||
for (size_t index = 0; index < field.valid.size(); ++index) if (field.valid[index]) {
|
||||
result = std::max(result, std::abs(field.velocity[index * 2]));
|
||||
result = std::max(result, std::abs(field.velocity[index * 2 + 1]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot& field, const float fieldScale) {
|
||||
auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
|
||||
for (uint32_t y = 0; y < field.height; ++y) for (uint32_t x = 0; x < field.width; ++x) {
|
||||
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2;
|
||||
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
|
||||
result->data[pixel] = static_cast<uint8_t>(std::clamp(field.velocity[cell] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(field.velocity[cell + 1] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
result->data[pixel + 2] = 0;
|
||||
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Resources {
|
||||
gfx::ShaderProgramBasePtr shader;
|
||||
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
|
||||
SegmentVector segments;
|
||||
particle::ParticleFieldSnapshot t0;
|
||||
particle::ParticleFieldSnapshot t1;
|
||||
std::shared_ptr<PremultipliedImage> image0;
|
||||
std::shared_ptr<PremultipliedImage> image1;
|
||||
gfx::Texture2DPtr texture0;
|
||||
gfx::Texture2DPtr texture1;
|
||||
float scale0 = 1.0f;
|
||||
float scale1 = 1.0f;
|
||||
};
|
||||
|
||||
class Tweaker final : public gfx::DrawableTweaker {
|
||||
public:
|
||||
Tweaker(std::shared_ptr<Resources> resources_, float mix_, float opacity_, int16_t wrap_)
|
||||
: resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_) {}
|
||||
void init(gfx::Drawable&) override {}
|
||||
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
|
||||
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture);
|
||||
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationFieldTexture);
|
||||
drawable.setTexture(resources->texture1, shaders::idParticleGpuValidationNextFieldTexture);
|
||||
const shaders::ParticleGpuValidationUBO ubo = {
|
||||
.pass = {5.0f, 0.0f, 0.0f, 0.0f}, .marker = {mix, 0.0f, 0.0f, 0.0f}, .mask = {0, 0, 0, 0},
|
||||
.visual = {opacity, 0, 0, 0}, .fieldBounds = bounds(resources->t0),
|
||||
.fieldScale = {resources->scale0, resources->scale1, 0, 0},
|
||||
.matrix = util::cast<float>(parameters.matrixForTile({wrap, {0, 0, 0}})),
|
||||
};
|
||||
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
|
||||
}
|
||||
private:
|
||||
std::shared_ptr<Resources> resources;
|
||||
float mix;
|
||||
float opacity;
|
||||
int16_t wrap;
|
||||
};
|
||||
|
||||
bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) {
|
||||
return particle::sameFieldGpuContent(a, b);
|
||||
}
|
||||
|
||||
bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field,
|
||||
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
|
||||
outputScale = scale(field);
|
||||
outputImage = image(field, outputScale);
|
||||
outputTexture = context.createTexture2D();
|
||||
if (!outputTexture) return false;
|
||||
outputTexture->setImage(outputImage);
|
||||
outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear,
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
RenderWeatherColorLayer::RenderWeatherColorLayer(Immutable<style::Layer::Impl> impl_)
|
||||
: RenderLayer(makeMutable<style::WeatherColorLayerProperties>(
|
||||
impl_, static_cast<const style::WeatherColorLayer::Impl&>(*impl_).opacity)) {}
|
||||
RenderWeatherColorLayer::~RenderWeatherColorLayer() = default;
|
||||
void RenderWeatherColorLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
auto properties = makeMutable<style::WeatherColorLayerProperties>(
|
||||
baseImpl, static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity);
|
||||
passes = RenderPass::Translucent;
|
||||
properties->renderPasses = mbgl::underlying_type(passes);
|
||||
evaluatedProperties = std::move(properties);
|
||||
}
|
||||
bool RenderWeatherColorLayer::needsRepaint() const { return isLayerRenderable(); }
|
||||
void RenderWeatherColorLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) { RenderLayer::markLayerRenderable(willRender, changes); }
|
||||
void RenderWeatherColorLayer::layerRemoved(UniqueChangeRequestVec& changes) { resources.reset(); RenderLayer::layerRemoved(changes); }
|
||||
void RenderWeatherColorLayer::markContextDestroyed() { resources.reset(); }
|
||||
void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters) {
|
||||
if (!parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) return;
|
||||
const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source);
|
||||
sourceTimeline = source.impl().timeline;
|
||||
if (sourceTimeline) {
|
||||
const auto frame = sourceTimeline->frame(); sourceField = *frame.t0; sourceNextField = *frame.t1;
|
||||
} else if (source.impl().field) {
|
||||
sourceField = *source.impl().field; sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field;
|
||||
}
|
||||
}
|
||||
void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& context, const TransformState& transformState,
|
||||
const std::shared_ptr<UpdateParameters>&, const RenderTree&, UniqueChangeRequestVec& changes) {
|
||||
if (!layerGroup) if (auto group = context.createLayerGroup(layerIndex, 1, getID())) setLayerGroup(std::move(group), changes);
|
||||
if (!layerGroup || !isLayerRenderable() || !sourceField || !sourceNextField) return;
|
||||
if (sourceTimeline) { const auto frame = sourceTimeline->frame(); sourceField = *frame.t0; sourceNextField = *frame.t1; }
|
||||
auto state = std::static_pointer_cast<Resources>(resources);
|
||||
if (!state) {
|
||||
state = std::make_shared<Resources>(); state->shader = context.getGenericShader(shaders, ShaderName);
|
||||
state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(RenderStaticData::heatmapTextureVertices());
|
||||
state->segments = RenderStaticData::heatmapTextureSegments();
|
||||
if (!state->shader || !uploadField(context, *sourceField, state->image0, state->texture0, state->scale0) ||
|
||||
!uploadField(context, *sourceNextField, state->image1, state->texture1, state->scale1)) return;
|
||||
state->t0 = *sourceField; state->t1 = *sourceNextField; resources = state;
|
||||
Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID +
|
||||
" T1=" + state->t1.fieldID + " scale=0..30mps");
|
||||
} else if (!same(state->t0, *sourceField) || !same(state->t1, *sourceNextField)) {
|
||||
const auto old0 = state->t0; const auto old1 = state->t1;
|
||||
auto install = [&](const particle::ParticleFieldSnapshot& wanted, particle::ParticleFieldSnapshot& assigned,
|
||||
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
|
||||
if (same(old0, wanted)) { assigned = wanted; return true; }
|
||||
if (same(old1, wanted)) { assigned = wanted; outputImage = state->image1; outputTexture = state->texture1; outputScale = state->scale1; return true; }
|
||||
assigned = wanted; return uploadField(context, assigned, outputImage, outputTexture, outputScale);
|
||||
};
|
||||
particle::ParticleFieldSnapshot t0, t1; std::shared_ptr<PremultipliedImage> i0, i1; gfx::Texture2DPtr x0, x1; float s0 = 1, s1 = 1;
|
||||
if (!install(*sourceField, t0, i0, x0, s0) || !install(*sourceNextField, t1, i1, x1, s1)) return;
|
||||
state->t0 = std::move(t0); state->t1 = std::move(t1); state->image0 = std::move(i0); state->image1 = std::move(i1);
|
||||
state->texture0 = std::move(x0); state->texture1 = std::move(x1); state->scale0 = s0; state->scale1 = s1;
|
||||
Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID);
|
||||
}
|
||||
const float mix = sourceTimeline ? sourceTimeline->frame().mix : 0.0f;
|
||||
const float opacity = static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity;
|
||||
auto& group = static_cast<LayerGroup&>(*layerGroup); group.clearDrawables();
|
||||
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
||||
const auto centerWrap = static_cast<int16_t>(std::floor((center.longitude() + 180.0) / 360.0));
|
||||
for (int16_t wrap = centerWrap - 1; wrap <= centerWrap + 1; ++wrap) {
|
||||
auto attributes = context.createVertexAttributeArray();
|
||||
if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute))
|
||||
attribute->setSharedRawData(state->vertices, offsetof(HeatmapTextureLayoutVertex, a1), 0, sizeof(HeatmapTextureLayoutVertex), gfx::AttributeDataType::Short2);
|
||||
auto builder = context.createDrawableBuilder(getID() + "-weather-color-" + std::to_string(wrap));
|
||||
builder->setShader(state->shader); builder->setRenderPass(RenderPass::Translucent); builder->setEnableDepth(false);
|
||||
builder->setColorMode(gfx::ColorMode::alphaBlended()); builder->setCullFaceMode(gfx::CullFaceMode::disabled());
|
||||
builder->setVertexAttributes(std::move(attributes)); builder->setRawVertices({}, state->vertices->elements(), gfx::AttributeDataType::Short2);
|
||||
builder->setSegments(gfx::Triangles(), RenderStaticData::quadTriangleIndices().vector(), state->segments.data(), state->segments.size());
|
||||
builder->setTexture(state->texture0, shaders::idParticleGpuValidationPreviousTexture);
|
||||
builder->setTexture(state->texture0, shaders::idParticleGpuValidationFieldTexture);
|
||||
builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture);
|
||||
builder->addTweaker(std::make_shared<Tweaker>(state, mix, opacity, wrap)); builder->flush(context);
|
||||
auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front()));
|
||||
}
|
||||
}
|
||||
} // namespace mbgl
|
||||
32
src/mbgl/renderer/layers/render_weather_color_layer.hpp
Normal file
32
src/mbgl/renderer/layers/render_weather_color_layer.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/renderer/render_layer.hpp>
|
||||
#include <mbgl/style/sources/raster_array_source.hpp>
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
class RenderWeatherColorLayer final : public RenderLayer {
|
||||
public:
|
||||
explicit RenderWeatherColorLayer(Immutable<style::Layer::Impl>);
|
||||
~RenderWeatherColorLayer() override;
|
||||
void update(gfx::ShaderRegistry&, gfx::Context&, const TransformState&, const std::shared_ptr<UpdateParameters>&,
|
||||
const RenderTree&, UniqueChangeRequestVec&) override;
|
||||
|
||||
private:
|
||||
void transition(const TransitionParameters&) override {}
|
||||
void evaluate(const PropertyEvaluationParameters&) override;
|
||||
bool hasTransition() const override { return false; }
|
||||
bool hasCrossfade() const override { return false; }
|
||||
bool needsRepaint() const override;
|
||||
void prepare(const LayerPrepareParameters&) override;
|
||||
void markLayerRenderable(bool, UniqueChangeRequestVec&) override;
|
||||
void layerRemoved(UniqueChangeRequestVec&) override;
|
||||
void markContextDestroyed() override;
|
||||
|
||||
std::shared_ptr<void> resources;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceField;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
|
||||
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
|
||||
};
|
||||
|
||||
} // namespace mbgl
|
||||
@@ -9,8 +9,8 @@ namespace mbgl::style {
|
||||
|
||||
class RasterParticleLayer::Impl final : public Layer::Impl {
|
||||
public:
|
||||
Impl(const std::string& id, const std::string& source)
|
||||
: Layer::Impl(id, source) {}
|
||||
Impl(const std::string& layerID, const std::string& sourceID)
|
||||
: Layer::Impl(layerID, sourceID) {}
|
||||
Impl(const Impl&) = default;
|
||||
bool hasLayoutDifference(const Layer::Impl&) const override { return false; }
|
||||
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override {}
|
||||
|
||||
61
src/mbgl/style/layers/weather_color_layer.cpp
Normal file
61
src/mbgl/style/layers/weather_color_layer.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include <mbgl/style/layers/weather_color_layer.hpp>
|
||||
|
||||
#include <mbgl/style/conversion/constant.hpp>
|
||||
#include <mbgl/style/conversion_impl.hpp>
|
||||
#include <mbgl/style/layer_observer.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer_impl.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace mbgl::style {
|
||||
namespace {
|
||||
constexpr const char* OpacityProperty = "weather-color-opacity";
|
||||
const LayerTypeInfo typeInfo{.type = "weather-color",
|
||||
.source = LayerTypeInfo::Source::Required,
|
||||
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
|
||||
.layout = LayerTypeInfo::Layout::NotRequired,
|
||||
.fadingTiles = LayerTypeInfo::FadingTiles::NotRequired,
|
||||
.crossTileIndex = LayerTypeInfo::CrossTileIndex::NotRequired,
|
||||
.tileKind = LayerTypeInfo::TileKind::NotRequired};
|
||||
} // namespace
|
||||
|
||||
const LayerTypeInfo* WeatherColorLayer::Impl::staticTypeInfo() noexcept { return &typeInfo; }
|
||||
WeatherColorLayer::WeatherColorLayer(const std::string& id, const std::string& source)
|
||||
: Layer(makeMutable<Impl>(id, source)) {}
|
||||
WeatherColorLayer::WeatherColorLayer(Immutable<Impl> impl_) : Layer(std::move(impl_)) {}
|
||||
WeatherColorLayer::~WeatherColorLayer() { weakFactory.invalidateWeakPtrs(); }
|
||||
const WeatherColorLayer::Impl& WeatherColorLayer::impl() const { return static_cast<const Impl&>(*baseImpl); }
|
||||
Mutable<WeatherColorLayer::Impl> WeatherColorLayer::mutableImpl() const { return makeMutable<Impl>(impl()); }
|
||||
float WeatherColorLayer::getOpacity() const { return impl().opacity; }
|
||||
void WeatherColorLayer::setOpacity(float value) {
|
||||
const auto clamped = std::clamp(value, 0.0f, 1.0f);
|
||||
if (clamped == getOpacity()) return;
|
||||
auto next = mutableImpl();
|
||||
next->opacity = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
std::optional<conversion::Error> WeatherColorLayer::setPropertyInternal(const std::string& name,
|
||||
const conversion::Convertible& value) {
|
||||
if (name != OpacityProperty) return conversion::Error{"unknown weather-color property: " + name};
|
||||
conversion::Error error;
|
||||
auto converted = conversion::convert<float>(value, error);
|
||||
if (!converted || !std::isfinite(*converted)) return conversion::Error{"weather-color-opacity must be finite"};
|
||||
setOpacity(*converted);
|
||||
return std::nullopt;
|
||||
}
|
||||
StyleProperty WeatherColorLayer::getProperty(const std::string& name) const {
|
||||
return name == OpacityProperty ? StyleProperty(Value{getOpacity()}, StyleProperty::Kind::Constant) : StyleProperty{};
|
||||
}
|
||||
Value WeatherColorLayer::serialize() const {
|
||||
auto result = Layer::serialize();
|
||||
serializeProperty(result, getProperty(OpacityProperty), OpacityProperty, true);
|
||||
return result;
|
||||
}
|
||||
std::unique_ptr<Layer> WeatherColorLayer::cloneRef(const std::string& id) const {
|
||||
auto next = mutableImpl(); next->id = id; return std::unique_ptr<Layer>(new WeatherColorLayer(std::move(next)));
|
||||
}
|
||||
Mutable<Layer::Impl> WeatherColorLayer::mutableBaseImpl() const { return staticMutableCast<Layer::Impl>(mutableImpl()); }
|
||||
|
||||
} // namespace mbgl::style
|
||||
27
src/mbgl/style/layers/weather_color_layer_impl.hpp
Normal file
27
src/mbgl/style/layers/weather_color_layer_impl.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/style/layer_impl.hpp>
|
||||
#include <mbgl/style/layer_properties.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer.hpp>
|
||||
|
||||
namespace mbgl::style {
|
||||
|
||||
class WeatherColorLayer::Impl final : public Layer::Impl {
|
||||
public:
|
||||
Impl(const std::string& layerID, const std::string& sourceID) : Layer::Impl(layerID, sourceID) {}
|
||||
Impl(const Impl&) = default;
|
||||
bool hasLayoutDifference(const Layer::Impl&) const override { return false; }
|
||||
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override {}
|
||||
float opacity = 0.62f;
|
||||
DECLARE_LAYER_TYPE_INFO;
|
||||
};
|
||||
|
||||
class WeatherColorLayerProperties final : public LayerProperties {
|
||||
public:
|
||||
WeatherColorLayerProperties(Immutable<Layer::Impl> impl, float opacity_)
|
||||
: LayerProperties(std::move(impl)), opacity(opacity_) {}
|
||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||
float opacity;
|
||||
};
|
||||
|
||||
} // namespace mbgl::style
|
||||
@@ -219,7 +219,7 @@ RasterArraySource::RasterArraySource(std::string id,
|
||||
std::string nextFixtureID,
|
||||
std::string nextMetadataURL,
|
||||
std::string nextFieldURL,
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields)
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFieldDescriptors)
|
||||
: Source(makeMutable<Impl>(std::move(id),
|
||||
std::move(fixtureID),
|
||||
std::move(metadataURL),
|
||||
@@ -227,10 +227,10 @@ RasterArraySource::RasterArraySource(std::string id,
|
||||
std::move(nextFixtureID),
|
||||
std::move(nextMetadataURL),
|
||||
std::move(nextFieldURL),
|
||||
std::move(runtimeFields))) {}
|
||||
std::move(runtimeFieldDescriptors))) {}
|
||||
|
||||
RasterArraySource::RasterArraySource(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFields)
|
||||
: Source(makeMutable<Impl>(std::move(id), std::move(localFields))) {}
|
||||
RasterArraySource::RasterArraySource(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFieldDescriptors)
|
||||
: Source(makeMutable<Impl>(std::move(id), std::move(localFieldDescriptors))) {}
|
||||
|
||||
RasterArraySource::RasterArraySource(std::string id)
|
||||
: Source(makeMutable<Impl>(std::move(id))) {}
|
||||
@@ -786,7 +786,12 @@ bool RasterArraySource::controlRuntimeTimeline(const int command, const float va
|
||||
}
|
||||
|
||||
bool RasterArraySource::supportsLayerType(const LayerTypeInfo* info) const {
|
||||
return info && std::string(info->type) == "raster-particle";
|
||||
if (!info) return false;
|
||||
const std::string type{info->type};
|
||||
// Both displays consume the exact same canonical Source-owned snapshots.
|
||||
// WeatherColorLayer is intentionally a separate renderer, not a second
|
||||
// source, cache, timeline or field normalizer.
|
||||
return type == "raster-particle" || type == "weather-color";
|
||||
}
|
||||
|
||||
Mutable<Source::Impl> RasterArraySource::createMutable() const noexcept {
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace mbgl::style {
|
||||
|
||||
class RasterArraySource::Impl final : public Source::Impl {
|
||||
public:
|
||||
Impl(std::string id,
|
||||
Impl(std::string sourceID,
|
||||
std::string fixtureID_,
|
||||
std::string metadataURL_,
|
||||
std::string fieldURL_,
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
std::string nextMetadataURL_ = {},
|
||||
std::string nextFieldURL_ = {},
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields_ = {})
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)),
|
||||
: Source::Impl(SourceType::RasterArray, std::move(sourceID)),
|
||||
fixtureID(std::move(fixtureID_)),
|
||||
metadataURL(std::move(metadataURL_)),
|
||||
fieldURL(std::move(fieldURL_)),
|
||||
@@ -24,10 +24,10 @@ public:
|
||||
nextMetadataURL(std::move(nextMetadataURL_)),
|
||||
nextFieldURL(std::move(nextFieldURL_)),
|
||||
runtimeFields(std::move(runtimeFields_)) {}
|
||||
Impl(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFields_)
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)), localFields(std::move(localFields_)) {}
|
||||
explicit Impl(std::string id)
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)),
|
||||
Impl(std::string sourceID, std::vector<RasterArrayLocalFieldDescriptor> localFields_)
|
||||
: Source::Impl(SourceType::RasterArray, std::move(sourceID)), localFields(std::move(localFields_)) {}
|
||||
explicit Impl(std::string sourceID)
|
||||
: Source::Impl(SourceType::RasterArray, std::move(sourceID)),
|
||||
hostFieldSubmission(true) {}
|
||||
Impl(const Impl&) = default;
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <mbgl/style/layers/line_layer_impl.hpp>
|
||||
#include <mbgl/style/layers/raster_layer.hpp>
|
||||
#include <mbgl/style/layers/raster_layer_impl.hpp>
|
||||
#include <mbgl/style/layers/raster_particle_layer_impl.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer_impl.hpp>
|
||||
#include <mbgl/style/source_impl.hpp>
|
||||
#include <mbgl/style/sources/custom_geometry_source.hpp>
|
||||
#include <mbgl/style/sources/geojson_source.hpp>
|
||||
@@ -175,6 +177,12 @@ TEST(Source, RasterArrayRenderItem) {
|
||||
EXPECT_FALSE(item->hasRenderPass(RenderPass::Translucent));
|
||||
}
|
||||
|
||||
TEST(Source, RasterArraySharesCanonicalSnapshotsWithWeatherColorLayer) {
|
||||
style::RasterArraySource source{"source", "fixture", "metadata", "field"};
|
||||
EXPECT_TRUE(source.supportsLayerType(style::RasterParticleLayer::Impl::staticTypeInfo()));
|
||||
EXPECT_TRUE(source.supportsLayerType(style::WeatherColorLayer::Impl::staticTypeInfo()));
|
||||
}
|
||||
|
||||
TEST(Source, RasterArraySnapshotsUseProcessWideGenerations) {
|
||||
SourceTest test;
|
||||
const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json");
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <mbgl/style/layers/raster_layer.hpp>
|
||||
#include <mbgl/style/layers/raster_layer_impl.hpp>
|
||||
#include <mbgl/style/layers/raster_particle_layer.hpp>
|
||||
#include <mbgl/style/layers/weather_color_layer.hpp>
|
||||
#include <mbgl/style/layers/symbol_layer.hpp>
|
||||
#include <mbgl/style/layers/symbol_layer_impl.hpp>
|
||||
#include <mbgl/style/style_impl.hpp>
|
||||
@@ -162,6 +163,16 @@ TEST(Layer, RasterParticleVisualProperties) {
|
||||
EXPECT_FLOAT_EQ(0.995f, layer.getTrailFade());
|
||||
}
|
||||
|
||||
TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
|
||||
WeatherColorLayer layer{"weather-color", "raster-array"};
|
||||
ASSERT_STREQ("weather-color", layer.getTypeInfo()->type);
|
||||
EXPECT_FLOAT_EQ(0.62f, layer.getOpacity());
|
||||
layer.setOpacity(0.35f);
|
||||
EXPECT_FLOAT_EQ(0.35f, layer.getOpacity());
|
||||
layer.setOpacity(4.0f);
|
||||
EXPECT_FLOAT_EQ(1.0f, layer.getOpacity());
|
||||
}
|
||||
|
||||
TEST(Layer, FillProperties) {
|
||||
auto layer = std::make_unique<FillLayer>("fill", "source");
|
||||
ASSERT_STREQ("fill", layer->getTypeInfo()->type);
|
||||
|
||||
@@ -404,3 +404,9 @@
|
||||
- 只读核对 WeatherV2 `bin_export.py`、`decode_wind()`、QBIN/NSFT export 后,确认 `wind_vectors.bin` 为完整拼接 `WVECBIN1` grid:row-major U/V/speed float32-le planes + valid uint8 plane,U=eastward、V=northward,单位 m/s;QBIN/NSFT 均由同一 decoded U/V Field 生成。
|
||||
- 从 run `2026080600` 生成真实 ECMWF HRES `2026080700/06/12/18` 四帧。原始日本及周边 bounds 为 112.5/0.0/157.5/55.776573,`513×769`,不裁剪、不重采样;输出 canonical interleaved U/V Android assets,每帧 3,155,976 bytes。
|
||||
- 转换器独立放在 `tools/weatherv2_wind_canonical/`,保留 valid/noData:invalid 写既有 -9999 sentinel,不伪造为 0 风;四帧 hash 不同,重复 T0 hash 相同,source→canonical 抽样误差 0。未改 Core/Renderer/Android,也未执行 Android/Jest;等待下一阶段集中 Android 集成验收。
|
||||
# STEP-055 — Android 真实 WeatherV2 风场色场/粒子综合可视化(IMPLEMENTED / WAITING_ANDROID_VALIDATION)
|
||||
|
||||
- 复用 STEP-054 的真实 ECMWF HRES WeatherV2 canonical T0–T3(513×769、原始 validTime、U eastward/V northward、m/s),未重生成或重采样。
|
||||
- 新增独立 `weather-color` 层:同一 canonical T0/T1 的 U/V 先插值再计算风速,固定 0–30 m/s 色标;与 RasterParticleLayer 保持分离。
|
||||
- `MORE → REAL WEATHER` 通过既有 Host Submission Contract 一键提交真实字段,使用 OSM 无 Key 底图、自动 fit metadata bounds,并恢复旋转/Activity 重建后的可见内容。
|
||||
- Android 综合验收待用户手工完成;详见 STEP-055 人工验收指令。
|
||||
|
||||
Reference in New Issue
Block a user