feat: add particle visual runtime controls
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:
69
docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md
Normal file
69
docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# STEP-046 Android OpenGL 正式粒子视觉与运行时渲染控制
|
||||
|
||||
日期:2026-09-03
|
||||
状态:IMPLEMENTED / WAITING FOR ANDROID VALIDATION
|
||||
|
||||
## 目标
|
||||
|
||||
在已关闭的 40-04 Runtime Field 链路上,将正式 `RasterParticleLayer` 的 full particle draw 从固定蓝色 point 扩展为 GPU 速度驱动的短 streak,并一次性提供运行时视觉属性和 Android 验收入口。
|
||||
|
||||
正式链路保持唯一:
|
||||
|
||||
```text
|
||||
RasterArraySource T0/T1
|
||||
-> GPU time interpolation
|
||||
-> State A/B
|
||||
-> Trail A/B
|
||||
-> GPU full streak draw
|
||||
-> Composite
|
||||
```
|
||||
|
||||
## 实现
|
||||
|
||||
- 每个 State texel 仍代表一颗正式粒子。静态 VBO 仅保存该 texel 的两个 line endpoint 编码;不包含 live position、direction 或 color。
|
||||
- vertex shader 从当前 State write texture 和 T0/T1 Field textures 获取位置与 `mix` 后的速度,按既有边界将 `V north-positive` 转换为 Mercator `dy=-v`,生成 canonical map-space 短 streak。无效/NoData/NaN/近零速度会 fail-closed,不绘制该 streak。
|
||||
- fragment shader 用最终插值速度 magnitude 走稳定的蓝→绿→橙 ramp;COLOR 控制可关闭 ramp 回退为固定蓝色。相同 magnitude 的 EAST/NORTH 保持同色,vortex 可显示局部速度差异。
|
||||
- 新增 Core visual paint properties:
|
||||
- `raster-particle-line-width`
|
||||
- `raster-particle-particle-length`
|
||||
- `raster-particle-particle-opacity`
|
||||
- `raster-particle-trail-opacity`
|
||||
- `raster-particle-trail-fade`
|
||||
- `raster-particle-speed-color-enabled`
|
||||
- 所有属性是 Render/Visual property,不是 Field 或 simulation state。其运行时更新经既有 `MapLibreMap -> NativeMapView -> JNI -> RasterParticleLayer` boolean thin bridge,调用强类型 Core setter;不通过 Android Java Layer peer。
|
||||
- Android Validation 页面加入 VISUAL DEFAULT、SIZE、LENGTH、OPACITY、TRAIL、FADE 与 COLOR 控制,另有直接 VORTEX fixture 与 256K capacity 档;状态栏保留当前可见参数;Activity recreation 保存这些值。
|
||||
|
||||
## 不变量与观测
|
||||
|
||||
- visual property 更新只更新 RenderLayer/UBO;不会 reload Style、reload Source、上传 Field、重建 T0/T1、reset State/Trail 或清 Trail。
|
||||
- 40-04 的 A/B→B/C→C/D、`OLD_T1 -> NEW_T0` GPU reuse、Duplicate→NOOP、`NO_PRELOADED_FIELD` fail-closed 保持不变。
|
||||
- `PARTICLE_DRAW_RESOURCES_*` 记录 `particleDrawCount=N` 与 `streakVertexCount=2N`,明确两个顶点仍是一颗粒子的线段,不改变 40-03 已验证容量 `>= 1,048,576` 的含义。
|
||||
- `VISUAL_PROPERTY_APPLIED` 记录参数、renderLayerInstanceId、resourceGeneration、fieldUploadCount、resetCount;不按 frame 高频输出。没有可靠 GPU timer,因此不虚构 GPU ms。
|
||||
|
||||
## 静态验证
|
||||
|
||||
- `node shaders/generate_shader_code.mjs` 连续运行两次,生成物稳定。
|
||||
- Linux OpenGL 精确编译:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o`:PASS。
|
||||
- 新增 `RasterParticleVisualProperties` C++ unit-level coverage,检查默认、运行时 setter 和 visual 参数边界。
|
||||
- `clang-format` 与 `git diff --check`:PASS。
|
||||
- 未运行 Android Gradle、ADB、真机或 Jest。
|
||||
|
||||
## Android 单轮验收
|
||||
|
||||
```bash
|
||||
cd /mnt/sda1/MapLibreParticleLab/platform/android
|
||||
|
||||
./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug \
|
||||
-Pandroid.injected.build.abi=x86_64 \
|
||||
--console=plain
|
||||
```
|
||||
|
||||
APK:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`
|
||||
|
||||
用户在同一 APK 中执行 T1--T8:EAST/NORTH/vortex geometry、MIX=0/50/100 连续性、VISUAL controls property-only、STATIC/Camera/Rotation、1M baseline,以及 Runtime advance/reuse/NOOP/fail-closed 回归。
|
||||
|
||||
当前 T1--T8 全部为 `PENDING USER ANDROID VALIDATION`;未将任何 Android 视觉、Camera、Mask、Lifecycle 或 capacity 结果写成 PASS。
|
||||
|
||||
下一建议阶段:用户完成这一次综合 Android 验收并提供结构化日志;若全部通过,才可进入 40-06 时间序列 Playback/preload/cache 的规划。
|
||||
|
||||
本阶段不包含 WeatherColorLayer、网络/GRIB/provider、React Native、Vulkan、iOS/Metal、生产 Land/Ocean mask、粒子箭头贴图或 APK packaging 技术债。
|
||||
66
docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md
Normal file
66
docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 40-05 Android OpenGL 正式粒子视觉与运行时渲染控制 — Codex 完成报告
|
||||
|
||||
日期:2026-09-03
|
||||
状态:IMPLEMENTED / WAITING FOR ANDROID VALIDATION
|
||||
|
||||
## 结果
|
||||
|
||||
40-05 / STEP-046 已完成代码、Android 验收入口与静态验证,尚未进行 Android 构建或实体机验收,因此不能标记 PASS。
|
||||
|
||||
### A. 正式方向 geometry
|
||||
|
||||
`RenderRasterParticleLayer` 的正式 full draw 不再画固定蓝色 point。每个 State texel 创建两个静态 line vertices;vertex shader 读取当前 State write、T0/T1 Field,并以 `timeMix` 取得最终 GPU velocity。方向由 `vec2(u, -v)` 在 Mercator 边界生成,所以继续遵守 `U=east-positive`、`V=north-positive`、`dy=-v`。没有 CPU 每粒子 direction/color、GPU readback、第二 simulation 或 Overlay。
|
||||
|
||||
### B/C. 视觉属性与速度颜色
|
||||
|
||||
Core `RasterParticleLayer` 具有 line width、streak length、particle/trail opacity、trail fade、speed-color enabled 的强类型 setter 和 style property 支持。速度色使用 shader 内蓝→绿→橙最小 ramp,输入是最终插值后的 magnitude;NoData/invalid velocity 在 vertex shader 直接不绘制。Legacy `ParticleValidationLayer` 的旧验证显示没有被改为正式视觉路径。
|
||||
|
||||
### D. Android runtime 控制
|
||||
|
||||
Android Test App 通过既有 `setStyleLayerFloatProperty()` boolean JNI bridge 调用 Core 强类型 setter,不建立 Java RasterParticleLayer peer。请求失败时 Kotlin 不写入本地 visual state;成功后才更新状态栏。新增控制:VISUAL DEFAULT、SIZE ±、LENGTH ±、OPACITY ±、TRAIL ±、FADE ±、COLOR,以及直接 VORTEX 与 256K capacity 入口。
|
||||
|
||||
### E. 观测和回归
|
||||
|
||||
资源日志现在明确区分 `particleDrawCount`(粒子数)与 `streakVertexCount`(两端点数)。visual property 变化输出一次结构化摘要,含 Field upload/reset counters 与 resource identity;没有每粒子或每帧日志,也没有虚构 GPU timer。
|
||||
|
||||
## 保持不变
|
||||
|
||||
- 40-02 T0/T1 GPU interpolation 和 MIX property-only。
|
||||
- 40-03 `N State texels -> N actual particle draw` 与已验证 capacity `>=1,048,576`。
|
||||
- 40-04 Runtime Field A/B→B/C→C/D、OLD_T1→NEW_T0 reuse、Duplicate NOOP、候选耗尽 fail-closed。
|
||||
- State/Trail ping-pong、STATIC frozen composite、mask reuse、camera/world wrap/antimeridian 和 rotation lifecycle。
|
||||
|
||||
视觉参数改变预期 `FIELD_UPLOAD delta=0`、`SIMULATION_RESET delta=0`、`STYLE_LOAD delta=0`;本实现没有 visual GPU resource rebuild。
|
||||
|
||||
## 已执行验证
|
||||
|
||||
- shader generator 连续两次运行:PASS,OpenGL generated header 已同步。
|
||||
- Linux OpenGL 精确对象编译:
|
||||
- `CMakeFiles/mbgl-core.dir/src/mbgl/style/layers/raster_particle_layer.cpp.o`
|
||||
- `CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o`
|
||||
- `test/CMakeFiles/mbgl-test.dir/style/style_layer.test.cpp.o`
|
||||
- 结果:PASS。
|
||||
- C++ `RasterParticleVisualProperties` 最小测试已加入并成功编译。
|
||||
- `clang-format`、`git diff --check`:PASS。
|
||||
|
||||
未执行:Android Gradle、ADB、安装、真机、Jest。
|
||||
|
||||
## 用户单次 Android 验收命令
|
||||
|
||||
```bash
|
||||
cd /mnt/sda1/MapLibreParticleLab/platform/android
|
||||
|
||||
./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug \
|
||||
-Pandroid.injected.build.abi=x86_64 \
|
||||
--console=plain
|
||||
```
|
||||
|
||||
APK 路径:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`
|
||||
|
||||
用户随后在一轮安装中完成 T1--T8,重点检查 EAST/NORTH/vortex streak direction、MIX 连续性、visual property-only counters、STATIC/camera/rotation、1M 和 Runtime Field 回归。
|
||||
|
||||
T1--T8 目前均为 `PENDING USER ANDROID VALIDATION`,因此 Camera/Projection、Mask、Lifecycle/Rotation 和实际 1K/16K/64K/256K/1M streak capacity 只能标记为“验收入口已完成,尚未由 Android 实机验证”。
|
||||
|
||||
## 范围外
|
||||
|
||||
未实现 WeatherColorLayer、真实天气 HTTP/GRIB/provider、React Native、Vulkan、iOS/Metal、生产 mask、粒子箭头/旋转或 APK ZIP 技术债。
|
||||
@@ -12,30 +12,54 @@ struct ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Op
|
||||
|
||||
out vec2 v_uv;
|
||||
out vec2 v_world;
|
||||
out vec2 v_particle_velocity;
|
||||
out float v_particle_valid;
|
||||
|
||||
layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp vec4 u_visual;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
uniform sampler2D u_state_texture;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
|
||||
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) {
|
||||
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
} else if (u_pass.x > 3.5) {
|
||||
// 40-03: a_pos is a static State texel index generated only when the
|
||||
// particle capacity changes. The position itself remains GPU-owned.
|
||||
vec2 stateUV = (a_pos + vec2(0.5)) / u_mask.xy;
|
||||
// 40-05: a_pos.x packs State texel X plus a static line endpoint.
|
||||
// State position, interpolated Field velocity, speed color and streak
|
||||
// orientation are all evaluated on GPU; the CPU never supplies live
|
||||
// particle position, direction, or color data.
|
||||
float endpoint = mod(a_pos.x, 2.0) * 2.0 - 1.0;
|
||||
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 mercatorVelocity = vec2(velocity.x, -velocity.y);
|
||||
float speed = length(mercatorVelocity);
|
||||
v_uv = position;
|
||||
v_world = position;
|
||||
gl_PointSize = 3.0;
|
||||
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
||||
v_particle_velocity = mercatorVelocity;
|
||||
v_particle_valid = valid && speed > 0.00001 ? 1.0 : 0.0;
|
||||
if (v_particle_valid < 0.5) {
|
||||
gl_Position = vec4(-2.0, -2.0, 0.0, 1.0);
|
||||
} else {
|
||||
vec2 direction = mercatorVelocity / speed;
|
||||
vec2 streakPosition = position + direction * endpoint * u_visual.y * 0.5;
|
||||
gl_Position = vec4(streakPosition * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix is
|
||||
// PaintParameters::matrixForTile() for the selected MapLibre world copy.
|
||||
@@ -47,6 +71,7 @@ void main() {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp vec4 u_visual;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
@@ -57,6 +82,16 @@ uniform sampler2D u_next_field_texture;
|
||||
uniform sampler2D u_state_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
in vec2 v_particle_velocity;
|
||||
in float v_particle_valid;
|
||||
|
||||
vec3 particleSpeedColor(float speed) {
|
||||
float t = clamp(speed * 0.75, 0.0, 1.0);
|
||||
vec3 slow = vec3(0.10, 0.45, 1.00);
|
||||
vec3 medium = vec3(0.10, 0.95, 0.72);
|
||||
vec3 fast = vec3(1.00, 0.48, 0.12);
|
||||
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);
|
||||
@@ -88,7 +123,7 @@ void main() {
|
||||
// Formal Trail first fades the previous target. Its full-particle point
|
||||
// draw follows in the same target and consumes every State texel.
|
||||
if (u_mask.z > 0.5) {
|
||||
fragColor = vec4(previous.rgb * u_marker.w, 1.0);
|
||||
fragColor = vec4(previous.rgb * u_visual.w, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,10 +137,12 @@ void main() {
|
||||
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
||||
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
||||
} else if (mode > 3.5) {
|
||||
// The vertex shader fetched this particle's State texel directly from
|
||||
// the current State write texture. Every point contributes one formal
|
||||
// particle draw; no CPU position data is involved per frame.
|
||||
fragColor = vec4(0.08, 0.55, 1.0, 1.0);
|
||||
if (v_particle_valid < 0.5) discard;
|
||||
float speed = length(v_particle_velocity);
|
||||
vec3 color = u_marker.y > 0.5 ? particleSpeedColor(speed) : vec3(0.08, 0.55, 1.0);
|
||||
// Trail targets are RGB accumulation buffers. Opacity changes source
|
||||
// energy only; it never alters State, Field textures, or time windows.
|
||||
fragColor = vec4(color * u_visual.x, 1.0);
|
||||
} else {
|
||||
// The formal RasterParticle layer displays the Trail texture written by
|
||||
// the current State -> Trail pass. Legacy ParticleValidation keeps its
|
||||
@@ -114,8 +151,9 @@ void main() {
|
||||
float ocean = texture(u_mask_texture, v_world).a;
|
||||
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
||||
if (u_mask.z > 0.5) {
|
||||
float alpha = clamp(intensity * 1.25, 0.0, 0.96);
|
||||
fragColor = vec4(previous.rgb * alpha, alpha);
|
||||
vec3 color = previous.rgb * u_visual.z;
|
||||
float alpha = clamp(max(color.r, max(color.g, color.b)), 0.0, 0.96);
|
||||
fragColor = vec4(color, alpha);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,12 @@ struct alignas(16) ParticleGpuValidationUBO {
|
||||
std::array<float, 4> pass;
|
||||
std::array<float, 4> marker;
|
||||
std::array<float, 4> mask;
|
||||
// Formal RasterParticle visual controls: particle opacity, canonical
|
||||
// streak length, trail opacity, and Trail A/B fade multiplier.
|
||||
std::array<float, 4> visual;
|
||||
std::array<float, 16> matrix;
|
||||
};
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 7 * 16);
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 8 * 16);
|
||||
|
||||
} // namespace shaders
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -18,6 +18,18 @@ public:
|
||||
void setTimeMix(float);
|
||||
uint32_t getParticleCount() const;
|
||||
void setParticleCount(uint32_t);
|
||||
float getLineWidth() const;
|
||||
void setLineWidth(float);
|
||||
float getParticleLength() const;
|
||||
void setParticleLength(float);
|
||||
float getParticleOpacity() const;
|
||||
void setParticleOpacity(float);
|
||||
float getTrailOpacity() const;
|
||||
void setTrailOpacity(float);
|
||||
float getTrailFade() const;
|
||||
void setTrailFade(float);
|
||||
bool isSpeedColorEnabled() const;
|
||||
void setSpeedColorEnabled(bool);
|
||||
class Impl;
|
||||
const Impl& impl() const;
|
||||
Mutable<Impl> mutableImpl() const;
|
||||
|
||||
@@ -1117,7 +1117,10 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
|
||||
auto* layer = map->getStyle().getLayer(jni::Make<std::string>(env, layerId));
|
||||
if (!layer) return false;
|
||||
const auto property = jni::Make<std::string>(env, propertyName);
|
||||
if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count") {
|
||||
if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count" &&
|
||||
property != "raster-particle-line-width" && property != "raster-particle-particle-length" &&
|
||||
property != "raster-particle-particle-opacity" && property != "raster-particle-trail-opacity" &&
|
||||
property != "raster-particle-trail-fade" && property != "raster-particle-speed-color-enabled") {
|
||||
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY");
|
||||
return false;
|
||||
}
|
||||
@@ -1126,10 +1129,14 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
|
||||
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_LAYER");
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(value)) {
|
||||
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_FLOAT");
|
||||
return false;
|
||||
}
|
||||
if (property == "raster-particle-time-mix") {
|
||||
particleLayer->setTimeMix(value);
|
||||
} else {
|
||||
if (!std::isfinite(value) || value < 0.0f || std::floor(value) != value) {
|
||||
} else if (property == "raster-particle-particle-count") {
|
||||
if (value < 0.0f || std::floor(value) != value) {
|
||||
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT");
|
||||
return false;
|
||||
}
|
||||
@@ -1139,6 +1146,22 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
|
||||
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT");
|
||||
return false;
|
||||
}
|
||||
} else if (property == "raster-particle-line-width") {
|
||||
particleLayer->setLineWidth(value);
|
||||
} else if (property == "raster-particle-particle-length") {
|
||||
particleLayer->setParticleLength(value);
|
||||
} else if (property == "raster-particle-particle-opacity") {
|
||||
particleLayer->setParticleOpacity(value);
|
||||
} else if (property == "raster-particle-trail-opacity") {
|
||||
particleLayer->setTrailOpacity(value);
|
||||
} else if (property == "raster-particle-trail-fade") {
|
||||
particleLayer->setTrailFade(value);
|
||||
} else {
|
||||
if (value != 0.0f && value != 1.0f) {
|
||||
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_COLOR_MODE");
|
||||
return false;
|
||||
}
|
||||
particleLayer->setSpeedColorEnabled(value > 0.5f);
|
||||
}
|
||||
mbgl::Log::Info(mbgl::Event::JNI,
|
||||
"ParticleGPU: event=STYLE_PROPERTY_SET layerId=" + jni::Make<std::string>(env, layerId) +
|
||||
|
||||
@@ -24,6 +24,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private var fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
private var timeMix = 0.0f
|
||||
private var particleCount = PARTICLE_COUNT_256
|
||||
private var lineWidth = DEFAULT_LINE_WIDTH
|
||||
private var particleLength = DEFAULT_PARTICLE_LENGTH
|
||||
private var particleOpacity = DEFAULT_PARTICLE_OPACITY
|
||||
private var trailOpacity = DEFAULT_TRAIL_OPACITY
|
||||
private var trailFade = DEFAULT_TRAIL_FADE
|
||||
private var speedColorEnabled = true
|
||||
private var timeWindowMode = false
|
||||
private var styleLoadEpoch = 0L
|
||||
private var styleReady = false
|
||||
@@ -35,6 +41,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST
|
||||
timeMix = savedInstanceState?.getFloat(STATE_TIME_MIX, 0.0f) ?: 0.0f
|
||||
particleCount = savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256
|
||||
lineWidth = savedInstanceState?.getFloat(STATE_LINE_WIDTH, DEFAULT_LINE_WIDTH) ?: DEFAULT_LINE_WIDTH
|
||||
particleLength = savedInstanceState?.getFloat(STATE_PARTICLE_LENGTH, DEFAULT_PARTICLE_LENGTH) ?: DEFAULT_PARTICLE_LENGTH
|
||||
particleOpacity = savedInstanceState?.getFloat(STATE_PARTICLE_OPACITY, DEFAULT_PARTICLE_OPACITY) ?: DEFAULT_PARTICLE_OPACITY
|
||||
trailOpacity = savedInstanceState?.getFloat(STATE_TRAIL_OPACITY, DEFAULT_TRAIL_OPACITY) ?: DEFAULT_TRAIL_OPACITY
|
||||
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
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.i(
|
||||
@@ -89,6 +101,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
fixtureMode = FIXTURE_UNIFORM_NORTH
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_NORTH")
|
||||
}
|
||||
findViewById<Button>(R.id.validationVortex).setOnClickListener {
|
||||
timeWindowMode = false
|
||||
fixtureMode = FIXTURE_VORTEX
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "VORTEX")
|
||||
}
|
||||
findViewById<Button>(R.id.validationTimeWindow).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
@@ -103,6 +120,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationParticle4096).setOnClickListener { setParticleCount(PARTICLE_COUNT_4096) }
|
||||
findViewById<Button>(R.id.validationParticle16384).setOnClickListener { setParticleCount(PARTICLE_COUNT_16384) }
|
||||
findViewById<Button>(R.id.validationParticle65536).setOnClickListener { setParticleCount(PARTICLE_COUNT_65536) }
|
||||
findViewById<Button>(R.id.validationParticle262144).setOnClickListener { setParticleCount(PARTICLE_COUNT_262144) }
|
||||
findViewById<Button>(R.id.validationParticle1048576).setOnClickListener { setParticleCount(PARTICLE_COUNT_1048576) }
|
||||
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) advanceRuntimeFieldWindow()
|
||||
@@ -110,6 +128,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationDuplicateWindow).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) duplicateRuntimeFieldWindow()
|
||||
}
|
||||
findViewById<Button>(R.id.validationVisualDefault).setOnClickListener { resetVisualDefaults() }
|
||||
findViewById<Button>(R.id.validationVisualSizeDown).setOnClickListener { setVisualFloat("raster-particle-line-width", lineWidth - 0.5f) { lineWidth = it.coerceIn(1.0f, 8.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualSizeUp).setOnClickListener { setVisualFloat("raster-particle-line-width", lineWidth + 0.5f) { lineWidth = it.coerceIn(1.0f, 8.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualLengthDown).setOnClickListener { setVisualFloat("raster-particle-particle-length", particleLength - 0.004f) { particleLength = it.coerceIn(0.002f, 0.080f) } }
|
||||
findViewById<Button>(R.id.validationVisualLengthUp).setOnClickListener { setVisualFloat("raster-particle-particle-length", particleLength + 0.004f) { particleLength = it.coerceIn(0.002f, 0.080f) } }
|
||||
findViewById<Button>(R.id.validationVisualOpacityDown).setOnClickListener { setVisualFloat("raster-particle-particle-opacity", particleOpacity - 0.1f) { particleOpacity = it.coerceIn(0.0f, 1.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualOpacityUp).setOnClickListener { setVisualFloat("raster-particle-particle-opacity", particleOpacity + 0.1f) { particleOpacity = it.coerceIn(0.0f, 1.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualTrailDown).setOnClickListener { setVisualFloat("raster-particle-trail-opacity", trailOpacity - 0.1f) { trailOpacity = it.coerceIn(0.0f, 1.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualTrailUp).setOnClickListener { setVisualFloat("raster-particle-trail-opacity", trailOpacity + 0.1f) { trailOpacity = it.coerceIn(0.0f, 1.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualFadeDown).setOnClickListener { setVisualFloat("raster-particle-trail-fade", trailFade - 0.05f) { trailFade = it.coerceIn(0.0f, 0.995f) } }
|
||||
findViewById<Button>(R.id.validationVisualFadeUp).setOnClickListener { setVisualFloat("raster-particle-trail-fade", trailFade + 0.05f) { trailFade = it.coerceIn(0.0f, 0.995f) } }
|
||||
findViewById<Button>(R.id.validationVisualColor).setOnClickListener { setSpeedColor(!speedColorEnabled) }
|
||||
findViewById<Button>(R.id.validationDateline).setOnClickListener {
|
||||
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
|
||||
Log.i(TAG, "ParticleValidation30_03: DATELINE preset center=0,179 zoom=2")
|
||||
@@ -196,6 +226,45 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_APPLIED requestedParticleCount=$requestedParticleCount effectiveParticleCount=$particleCount styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun setVisualFloat(property: String, requested: Float, apply: (Float) -> Unit) {
|
||||
if (!styleReady || !enabled) {
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=${if (!styleReady) "STYLE_LOADING" else "DISABLED"} property=$property styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REQUEST property=$property value=$requested styleEpoch=$styleLoadEpoch")
|
||||
if (!maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, requested)) {
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=CORE_PROPERTY_REJECTED property=$property styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
apply(requested)
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_ONLY property=$property value=$requested styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun setSpeedColor(requested: Boolean) {
|
||||
if (!styleReady || !enabled) {
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=${if (!styleReady) "STYLE_LOADING" else "DISABLED"} property=raster-particle-speed-color-enabled styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
val value = if (requested) 1.0f else 0.0f
|
||||
if (!maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, "raster-particle-speed-color-enabled", value)) {
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=CORE_PROPERTY_REJECTED property=raster-particle-speed-color-enabled styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
speedColorEnabled = requested
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_ONLY property=raster-particle-speed-color-enabled value=$value styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun resetVisualDefaults() {
|
||||
setVisualFloat("raster-particle-line-width", DEFAULT_LINE_WIDTH) { lineWidth = DEFAULT_LINE_WIDTH }
|
||||
setVisualFloat("raster-particle-particle-length", DEFAULT_PARTICLE_LENGTH) { particleLength = DEFAULT_PARTICLE_LENGTH }
|
||||
setVisualFloat("raster-particle-particle-opacity", DEFAULT_PARTICLE_OPACITY) { particleOpacity = DEFAULT_PARTICLE_OPACITY }
|
||||
setVisualFloat("raster-particle-trail-opacity", DEFAULT_TRAIL_OPACITY) { trailOpacity = DEFAULT_TRAIL_OPACITY }
|
||||
setVisualFloat("raster-particle-trail-fade", DEFAULT_TRAIL_FADE) { trailFade = DEFAULT_TRAIL_FADE }
|
||||
setSpeedColor(true)
|
||||
}
|
||||
|
||||
private fun advanceRuntimeFieldWindow() {
|
||||
val rejectReason = when {
|
||||
!styleReady -> "STYLE_LOADING"
|
||||
@@ -246,10 +315,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
"RUNTIME WINDOW"
|
||||
} else if (fixtureMode == FIXTURE_UNIFORM_NORTH) {
|
||||
"NORTH"
|
||||
} else if (fixtureMode == FIXTURE_VORTEX) {
|
||||
"VORTEX"
|
||||
} else {
|
||||
"EAST"
|
||||
}
|
||||
return "$mask · $runtime · $fields · MIX ${(timeMix * 100).toInt()}% · $particleCount particles"
|
||||
return "$mask · $runtime · $fields · 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"}"
|
||||
}
|
||||
|
||||
private fun validationStyle(active: Boolean, enabled: Boolean): String {
|
||||
@@ -262,7 +333,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
val particleLayer = if (enabled) {
|
||||
"""{"id":"particle-raster-core","type":"raster-particle","source":"particle-fixture",
|
||||
"paint":{"raster-particle-active":$active,"raster-particle-mask-mode":$maskMode,
|
||||
"raster-particle-time-mix":$timeMix,"raster-particle-particle-count":$particleCount}},"""
|
||||
"raster-particle-time-mix":$timeMix,"raster-particle-particle-count":$particleCount,
|
||||
"raster-particle-line-width":$lineWidth,"raster-particle-particle-length":$particleLength,
|
||||
"raster-particle-particle-opacity":$particleOpacity,"raster-particle-trail-opacity":$trailOpacity,
|
||||
"raster-particle-trail-fade":$trailFade,"raster-particle-speed-color-enabled":$speedColorEnabled}},"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -317,6 +391,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
outState.putInt(STATE_FIXTURE_MODE, fixtureMode)
|
||||
outState.putFloat(STATE_TIME_MIX, timeMix)
|
||||
outState.putInt(STATE_PARTICLE_COUNT, particleCount)
|
||||
outState.putFloat(STATE_LINE_WIDTH, lineWidth)
|
||||
outState.putFloat(STATE_PARTICLE_LENGTH, particleLength)
|
||||
outState.putFloat(STATE_PARTICLE_OPACITY, particleOpacity)
|
||||
outState.putFloat(STATE_TRAIL_OPACITY, trailOpacity)
|
||||
outState.putFloat(STATE_TRAIL_FADE, trailFade)
|
||||
outState.putBoolean(STATE_SPEED_COLOR, speedColorEnabled)
|
||||
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
mapView.onSaveInstanceState(outState)
|
||||
@@ -324,6 +404,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
|
||||
private fun fixtureId(): String = when (fixtureMode) {
|
||||
FIXTURE_UNIFORM_NORTH -> "uniform-north"
|
||||
FIXTURE_VORTEX -> "vortex"
|
||||
else -> "uniform-east"
|
||||
}
|
||||
|
||||
@@ -335,6 +416,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val STATE_FIXTURE_MODE = "particle_validation.fixture_mode"
|
||||
private const val STATE_TIME_MIX = "particle_validation.time_mix"
|
||||
private const val STATE_PARTICLE_COUNT = "particle_validation.particle_count"
|
||||
private const val STATE_LINE_WIDTH = "particle_validation.line_width"
|
||||
private const val STATE_PARTICLE_LENGTH = "particle_validation.particle_length"
|
||||
private const val STATE_PARTICLE_OPACITY = "particle_validation.particle_opacity"
|
||||
private const val STATE_TRAIL_OPACITY = "particle_validation.trail_opacity"
|
||||
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 PARTICLE_LAYER_ID = "particle-raster-core"
|
||||
private const val PARTICLE_SOURCE_ID = "particle-fixture"
|
||||
@@ -343,11 +430,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val MASK_OCEAN_B = 2
|
||||
private const val FIXTURE_UNIFORM_EAST = 0
|
||||
private const val FIXTURE_UNIFORM_NORTH = 1
|
||||
private const val FIXTURE_VORTEX = 2
|
||||
private const val PARTICLE_COUNT_256 = 256
|
||||
private const val PARTICLE_COUNT_1024 = 1024
|
||||
private const val PARTICLE_COUNT_4096 = 4096
|
||||
private const val PARTICLE_COUNT_16384 = 16384
|
||||
private const val PARTICLE_COUNT_65536 = 65536
|
||||
private const val PARTICLE_COUNT_262144 = 262144
|
||||
private const val PARTICLE_COUNT_1048576 = 1048576
|
||||
private const val DEFAULT_LINE_WIDTH = 2.0f
|
||||
private const val DEFAULT_PARTICLE_LENGTH = 0.018f
|
||||
private const val DEFAULT_PARTICLE_OPACITY = 1.0f
|
||||
private const val DEFAULT_TRAIL_OPACITY = 1.0f
|
||||
private const val DEFAULT_TRAIL_FADE = 0.92f
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,43 @@
|
||||
|
||||
<Button android:id="@+id/validationParticle16384" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_16384" />
|
||||
<Button android:id="@+id/validationParticle65536" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_65536" />
|
||||
<Button android:id="@+id/validationParticle262144" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_262144" />
|
||||
<Button android:id="@+id/validationParticle1048576" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_1048576" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button android:id="@+id/validationVisualDefault" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_default" />
|
||||
<Button android:id="@+id/validationVisualSizeDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_size_down" />
|
||||
<Button android:id="@+id/validationVisualSizeUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_size_up" />
|
||||
<Button android:id="@+id/validationVisualLengthDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_length_down" />
|
||||
<Button android:id="@+id/validationVisualLengthUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_length_up" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button android:id="@+id/validationVisualOpacityDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_opacity_down" />
|
||||
<Button android:id="@+id/validationVisualOpacityUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_opacity_up" />
|
||||
<Button android:id="@+id/validationVisualTrailDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_trail_down" />
|
||||
<Button android:id="@+id/validationVisualTrailUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_trail_up" />
|
||||
<Button android:id="@+id/validationVisualColor" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_color" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button android:id="@+id/validationVisualFadeDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_fade_down" />
|
||||
<Button android:id="@+id/validationVisualFadeUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_fade_up" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
@@ -57,6 +91,7 @@
|
||||
|
||||
<Button android:id="@+id/validationUniformEast" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_uniform_east" />
|
||||
<Button android:id="@+id/validationUniformNorth" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_uniform_north" />
|
||||
<Button android:id="@+id/validationVortex" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_vortex" />
|
||||
<Button android:id="@+id/validationTimeWindow" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_time_window" />
|
||||
<Button android:id="@+id/validationAdvanceWindow" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_advance_window" />
|
||||
<Button android:id="@+id/validationDuplicateWindow" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_duplicate_window" />
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<string name="particle_validation_world_wrap">WORLD WRAP</string>
|
||||
<string name="particle_validation_uniform_east">UNIFORM EAST</string>
|
||||
<string name="particle_validation_uniform_north">UNIFORM NORTH</string>
|
||||
<string name="particle_validation_vortex">VORTEX</string>
|
||||
<string name="particle_validation_time_window">T0 EAST / T1 NORTH</string>
|
||||
<string name="particle_validation_advance_window">ADVANCE WINDOW</string>
|
||||
<string name="particle_validation_duplicate_window">DUPLICATE T1</string>
|
||||
@@ -35,7 +36,20 @@
|
||||
<string name="particle_validation_particles_4096">4K</string>
|
||||
<string name="particle_validation_particles_16384">16K</string>
|
||||
<string name="particle_validation_particles_65536">64K</string>
|
||||
<string name="particle_validation_particles_262144">256K</string>
|
||||
<string name="particle_validation_particles_1048576">1M</string>
|
||||
<string name="particle_validation_visual_default">VIS DEFAULT</string>
|
||||
<string name="particle_validation_visual_size_down">SIZE −</string>
|
||||
<string name="particle_validation_visual_size_up">SIZE +</string>
|
||||
<string name="particle_validation_visual_length_down">LENGTH −</string>
|
||||
<string name="particle_validation_visual_length_up">LENGTH +</string>
|
||||
<string name="particle_validation_visual_opacity_down">OPACITY −</string>
|
||||
<string name="particle_validation_visual_opacity_up">OPACITY +</string>
|
||||
<string name="particle_validation_visual_trail_down">TRAIL −</string>
|
||||
<string name="particle_validation_visual_trail_up">TRAIL +</string>
|
||||
<string name="particle_validation_visual_fade_down">FADE −</string>
|
||||
<string name="particle_validation_visual_fade_up">FADE +</string>
|
||||
<string name="particle_validation_visual_color">COLOR</string>
|
||||
<string name="particle_validation_global_active_status">40-01 GLOBAL ACTIVE: offline field simulation and current-frame composite.</string>
|
||||
<string name="particle_validation_global_static_status">40-01 GLOBAL STATIC: zero-step bootstrap and frozen composite.</string>
|
||||
<string name="particle_validation_ocean_a_status">40-01 OCEAN_ONLY_A: shared coastal-mask consumer.</string>
|
||||
|
||||
@@ -2,6 +2,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp vec4 u_visual;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
@@ -12,6 +13,16 @@ uniform sampler2D u_next_field_texture;
|
||||
uniform sampler2D u_state_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
in vec2 v_particle_velocity;
|
||||
in float v_particle_valid;
|
||||
|
||||
vec3 particleSpeedColor(float speed) {
|
||||
float t = clamp(speed * 0.75, 0.0, 1.0);
|
||||
vec3 slow = vec3(0.10, 0.45, 1.00);
|
||||
vec3 medium = vec3(0.10, 0.95, 0.72);
|
||||
vec3 fast = vec3(1.00, 0.48, 0.12);
|
||||
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);
|
||||
@@ -43,7 +54,7 @@ void main() {
|
||||
// Formal Trail first fades the previous target. Its full-particle point
|
||||
// draw follows in the same target and consumes every State texel.
|
||||
if (u_mask.z > 0.5) {
|
||||
fragColor = vec4(previous.rgb * u_marker.w, 1.0);
|
||||
fragColor = vec4(previous.rgb * u_visual.w, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,10 +68,12 @@ void main() {
|
||||
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
||||
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
||||
} else if (mode > 3.5) {
|
||||
// The vertex shader fetched this particle's State texel directly from
|
||||
// the current State write texture. Every point contributes one formal
|
||||
// particle draw; no CPU position data is involved per frame.
|
||||
fragColor = vec4(0.08, 0.55, 1.0, 1.0);
|
||||
if (v_particle_valid < 0.5) discard;
|
||||
float speed = length(v_particle_velocity);
|
||||
vec3 color = u_marker.y > 0.5 ? particleSpeedColor(speed) : vec3(0.08, 0.55, 1.0);
|
||||
// Trail targets are RGB accumulation buffers. Opacity changes source
|
||||
// energy only; it never alters State, Field textures, or time windows.
|
||||
fragColor = vec4(color * u_visual.x, 1.0);
|
||||
} else {
|
||||
// The formal RasterParticle layer displays the Trail texture written by
|
||||
// the current State -> Trail pass. Legacy ParticleValidation keeps its
|
||||
@@ -69,8 +82,9 @@ void main() {
|
||||
float ocean = texture(u_mask_texture, v_world).a;
|
||||
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
||||
if (u_mask.z > 0.5) {
|
||||
float alpha = clamp(intensity * 1.25, 0.0, 0.96);
|
||||
fragColor = vec4(previous.rgb * alpha, alpha);
|
||||
vec3 color = previous.rgb * u_visual.z;
|
||||
float alpha = clamp(max(color.r, max(color.g, color.b)), 0.0, 0.96);
|
||||
fragColor = vec4(color, alpha);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,54 @@ layout (location = 0) in vec2 a_pos;
|
||||
|
||||
out vec2 v_uv;
|
||||
out vec2 v_world;
|
||||
out vec2 v_particle_velocity;
|
||||
out float v_particle_valid;
|
||||
|
||||
layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp vec4 u_visual;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
uniform sampler2D u_state_texture;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
|
||||
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) {
|
||||
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
} else if (u_pass.x > 3.5) {
|
||||
// 40-03: a_pos is a static State texel index generated only when the
|
||||
// particle capacity changes. The position itself remains GPU-owned.
|
||||
vec2 stateUV = (a_pos + vec2(0.5)) / u_mask.xy;
|
||||
// 40-05: a_pos.x packs State texel X plus a static line endpoint.
|
||||
// State position, interpolated Field velocity, speed color and streak
|
||||
// orientation are all evaluated on GPU; the CPU never supplies live
|
||||
// particle position, direction, or color data.
|
||||
float endpoint = mod(a_pos.x, 2.0) * 2.0 - 1.0;
|
||||
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 mercatorVelocity = vec2(velocity.x, -velocity.y);
|
||||
float speed = length(mercatorVelocity);
|
||||
v_uv = position;
|
||||
v_world = position;
|
||||
gl_PointSize = 3.0;
|
||||
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
||||
v_particle_velocity = mercatorVelocity;
|
||||
v_particle_valid = valid && speed > 0.00001 ? 1.0 : 0.0;
|
||||
if (v_particle_valid < 0.5) {
|
||||
gl_Position = vec4(-2.0, -2.0, 0.0, 1.0);
|
||||
} else {
|
||||
vec2 direction = mercatorVelocity / speed;
|
||||
vec2 streakPosition = position + direction * endpoint * u_visual.y * 0.5;
|
||||
gl_Position = vec4(streakPosition * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix is
|
||||
// PaintParameters::matrixForTile() for the selected MapLibre world copy.
|
||||
|
||||
@@ -58,16 +58,33 @@ struct ParticleSettings {
|
||||
bool usesFormalSource;
|
||||
float timeMix;
|
||||
uint32_t particleCount;
|
||||
float lineWidth;
|
||||
float particleLength;
|
||||
float particleOpacity;
|
||||
float trailOpacity;
|
||||
float trailFade;
|
||||
bool speedColorEnabled;
|
||||
};
|
||||
|
||||
ParticleSettings particleSettings(const Immutable<style::Layer::Impl>& value) {
|
||||
if (value->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo()) {
|
||||
const auto& layer = static_cast<const style::RasterParticleLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, 0, true, layer.timeMix, layer.particleCount};
|
||||
return {layer.active,
|
||||
layer.maskMode,
|
||||
0,
|
||||
true,
|
||||
layer.timeMix,
|
||||
layer.particleCount,
|
||||
layer.lineWidth,
|
||||
layer.particleLength,
|
||||
layer.particleOpacity,
|
||||
layer.trailOpacity,
|
||||
layer.trailFade,
|
||||
layer.speedColorEnabled};
|
||||
}
|
||||
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
||||
const auto& layer = static_cast<const style::ParticleValidationLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256};
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256, 2.0f, 0.018f, 1.0f, 1.0f, 0.965f, false};
|
||||
}
|
||||
|
||||
enum class ValidationPass : uint8_t {
|
||||
@@ -190,6 +207,12 @@ public:
|
||||
uint64_t resetCount = 0;
|
||||
float timeMix = 0.0f;
|
||||
uint32_t particleCount = 0;
|
||||
float lineWidth = 2.0f;
|
||||
float particleLength = 0.018f;
|
||||
float particleOpacity = 1.0f;
|
||||
float trailOpacity = 1.0f;
|
||||
float trailFade = 0.92f;
|
||||
bool speedColorEnabled = true;
|
||||
uint8_t fixtureMode = 255;
|
||||
uint8_t loggedMaskMode = 255;
|
||||
Size trailSize{};
|
||||
@@ -265,22 +288,29 @@ constexpr uint32_t MaxParticleVerticesPerSegment = std::numeric_limits<uint16_t>
|
||||
|
||||
void rebuildParticleDrawResources(const std::shared_ptr<ParticleGpuResources>& resources) {
|
||||
auto vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>();
|
||||
vertices->reserve(resources->particleCount);
|
||||
// Each State texel is represented by a two-vertex line. Its endpoints are
|
||||
// static texel indices; State position, interpolated Field velocity and
|
||||
// orientation remain GPU-owned in the vertex shader.
|
||||
vertices->reserve(resources->particleCount * 2);
|
||||
for (uint32_t particleIndex = 0; particleIndex < resources->particleCount; ++particleIndex) {
|
||||
const auto x = static_cast<int16_t>(particleIndex % resources->stateSize.width);
|
||||
const auto y = static_cast<int16_t>(particleIndex / resources->stateSize.width);
|
||||
vertices->emplace_back(HeatmapBucket::textureVertex({x, y}));
|
||||
// a_pos.x packs state X and endpoint (0=start, 1=end). State grids
|
||||
// are at most 1024 wide, so the encoded value remains a signed short.
|
||||
vertices->emplace_back(HeatmapBucket::textureVertex({static_cast<int16_t>(x * 2), y}));
|
||||
vertices->emplace_back(HeatmapBucket::textureVertex({static_cast<int16_t>(x * 2 + 1), y}));
|
||||
}
|
||||
|
||||
auto indices = std::make_shared<gfx::IndexVectorBase>();
|
||||
indices->reserve(std::min(resources->particleCount, MaxParticleVerticesPerSegment));
|
||||
for (uint32_t index = 0; index < std::min(resources->particleCount, MaxParticleVerticesPerSegment); ++index) {
|
||||
indices->reserve(MaxParticleVerticesPerSegment);
|
||||
for (uint32_t index = 0; index < MaxParticleVerticesPerSegment; ++index) {
|
||||
indices->extend(1, static_cast<uint16_t>(index));
|
||||
}
|
||||
|
||||
SegmentVector segments;
|
||||
for (uint32_t offset = 0; offset < resources->particleCount; offset += MaxParticleVerticesPerSegment) {
|
||||
const auto count = std::min(resources->particleCount - offset, MaxParticleVerticesPerSegment);
|
||||
const auto vertexCount = resources->particleCount * 2;
|
||||
for (uint32_t offset = 0; offset < vertexCount; offset += MaxParticleVerticesPerSegment) {
|
||||
const auto count = std::min(vertexCount - offset, MaxParticleVerticesPerSegment);
|
||||
segments.emplace_back(offset, 0, count, count);
|
||||
}
|
||||
resources->particleVertices = std::move(vertices);
|
||||
@@ -337,17 +367,20 @@ public:
|
||||
|
||||
const float frame = simulationStep;
|
||||
const float markerX = resources->timeMix;
|
||||
const float markerY = 0.5f + 0.32f * std::cos(frame * 0.053f);
|
||||
// RenderTarget clear is opaque black. State initialization must therefore
|
||||
// be an explicit lifecycle signal, never inferred from sampled alpha.
|
||||
const bool seedState = pass == ValidationPass::State && !resources->stateSeeded;
|
||||
const shaders::ParticleGpuValidationUBO ubo = {
|
||||
.pass = {static_cast<float>(pass), frame, maskMode == 0 ? 0.0f : 1.0f, static_cast<float>(maskMode)},
|
||||
.marker = {markerX, markerY, 0.035f, 0.965f},
|
||||
.marker = {markerX, resources->speedColorEnabled ? 1.0f : 0.0f, 0.035f, 0.965f},
|
||||
.mask = {static_cast<float>(resources->stateSize.width),
|
||||
static_cast<float>(resources->stateSize.height),
|
||||
resources->formalDisplay ? 1.0f : 0.0f,
|
||||
seedState ? 1.0f : 0.0f},
|
||||
.visual = {resources->particleOpacity,
|
||||
resources->particleLength,
|
||||
resources->trailOpacity,
|
||||
resources->formalDisplay ? resources->trailFade : 0.965f},
|
||||
.matrix = util::cast<float>(parameters.matrixForTile({worldWrap, {0, 0, 0}})),
|
||||
};
|
||||
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
|
||||
@@ -590,7 +623,7 @@ bool buildParticlePoints(gfx::Context& context,
|
||||
builder->setCullFaceMode(gfx::CullFaceMode::disabled());
|
||||
builder->setVertexAttributes(std::move(attributes));
|
||||
builder->setRawVertices({}, resources->particleVertices->elements(), gfx::AttributeDataType::Short2);
|
||||
builder->setSegments(gfx::Points(3.0f),
|
||||
builder->setSegments(gfx::Lines(resources->lineWidth),
|
||||
resources->particleIndices,
|
||||
resources->particleSegments.data(),
|
||||
resources->particleSegments.size());
|
||||
@@ -627,7 +660,13 @@ RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable<style::Layer::Imp
|
||||
settings.maskMode,
|
||||
settings.fixtureMode,
|
||||
settings.timeMix,
|
||||
settings.particleCount);
|
||||
settings.particleCount,
|
||||
settings.lineWidth,
|
||||
settings.particleLength,
|
||||
settings.particleOpacity,
|
||||
settings.trailOpacity,
|
||||
settings.trailFade,
|
||||
settings.speedColorEnabled);
|
||||
}()) {
|
||||
renderLayerInstanceId = ++nextRenderLayerInstanceId;
|
||||
formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo();
|
||||
@@ -653,8 +692,18 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
"ParticleGPU: event=LAYER_CHANGED layerId=" + getID() +
|
||||
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId));
|
||||
const auto settings = particleSettings(baseImpl);
|
||||
auto properties = makeMutable<style::ParticleRenderLayerProperties>(
|
||||
baseImpl, settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix, settings.particleCount);
|
||||
auto properties = makeMutable<style::ParticleRenderLayerProperties>(baseImpl,
|
||||
settings.active,
|
||||
settings.maskMode,
|
||||
settings.fixtureMode,
|
||||
settings.timeMix,
|
||||
settings.particleCount,
|
||||
settings.lineWidth,
|
||||
settings.particleLength,
|
||||
settings.particleOpacity,
|
||||
settings.trailOpacity,
|
||||
settings.trailFade,
|
||||
settings.speedColorEnabled);
|
||||
passes = RenderPass::Translucent;
|
||||
properties->renderPasses = mbgl::underlying_type(passes);
|
||||
evaluatedProperties = std::move(properties);
|
||||
@@ -664,7 +713,11 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
" maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + " fixture=" +
|
||||
(formalSource && sourceField ? sourceField->fixtureID : fixtureID(settings.fixtureMode)) +
|
||||
" layerIndex=" + std::to_string(getLayerIndex()) + " timeMix=" + std::to_string(settings.timeMix) +
|
||||
" particleCount=" + std::to_string(settings.particleCount) + " stage=40-03");
|
||||
" particleCount=" + std::to_string(settings.particleCount) + " lineWidth=" +
|
||||
std::to_string(settings.lineWidth) + " streakLength=" + std::to_string(settings.particleLength) +
|
||||
" particleOpacity=" + std::to_string(settings.particleOpacity) + " trailOpacity=" +
|
||||
std::to_string(settings.trailOpacity) + " trailFade=" + std::to_string(settings.trailFade) +
|
||||
" speedColor=" + (settings.speedColorEnabled ? "true" : "false") + " stage=40-05");
|
||||
}
|
||||
|
||||
bool RenderRasterParticleLayer::needsRepaint() const {
|
||||
@@ -687,7 +740,7 @@ void RenderRasterParticleLayer::layerRemoved(UniqueChangeRequestVec& changes) {
|
||||
std::to_string(gpuResources->particleCount) +
|
||||
" resourceGeneration=" + std::to_string(gpuResources->generation));
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=LAYER_REMOVED trailDrawCount=" +
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=LAYER_REMOVED streakVertexCount=" +
|
||||
std::to_string(gpuResources->particleVertices ? gpuResources->particleVertices->elements() : 0) +
|
||||
" fullParticleDraw=true resourceGeneration=" + std::to_string(gpuResources->generation));
|
||||
removeTargets(gpuResources->registeredTargets, changes);
|
||||
@@ -704,7 +757,7 @@ void RenderRasterParticleLayer::markContextDestroyed() {
|
||||
std::to_string(gpuResources->particleCount) +
|
||||
" resourceGeneration=" + std::to_string(gpuResources->generation));
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=CONTEXT_DESTROYED trailDrawCount=" +
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=CONTEXT_DESTROYED streakVertexCount=" +
|
||||
std::to_string(gpuResources->particleVertices ? gpuResources->particleVertices->elements() : 0) +
|
||||
" fullParticleDraw=true resourceGeneration=" + std::to_string(gpuResources->generation));
|
||||
Log::Info(
|
||||
@@ -852,8 +905,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_CREATE effectiveParticleCount=" +
|
||||
std::to_string(resources->particleCount) + " stateGrid=" +
|
||||
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
|
||||
" trailDrawCount=" + std::to_string(resources->particleVertices->elements()) +
|
||||
" particleIndexCount=" + std::to_string(resources->particleVertices->elements()) +
|
||||
" particleDrawCount=" + std::to_string(resources->particleCount) +
|
||||
" streakVertexCount=" + std::to_string(resources->particleVertices->elements()) +
|
||||
" fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation));
|
||||
Log::Info(
|
||||
Event::Render,
|
||||
@@ -913,7 +966,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
std::to_string(previousSize.height) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation));
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=PARTICLE_COUNT_CHANGE trailDrawCount=" +
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=PARTICLE_COUNT_CHANGE streakVertexCount=" +
|
||||
std::to_string(resources->particleVertices ? resources->particleVertices->elements() : 0) +
|
||||
" fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation));
|
||||
removeTargets(resources->stateTargets, changes);
|
||||
@@ -932,8 +985,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_REBUILD effectiveParticleCount=" +
|
||||
std::to_string(resources->particleCount) + " stateGrid=" +
|
||||
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
|
||||
" trailDrawCount=" + std::to_string(resources->particleVertices->elements()) +
|
||||
" particleIndexCount=" + std::to_string(resources->particleVertices->elements()) +
|
||||
" particleDrawCount=" + std::to_string(resources->particleCount) +
|
||||
" streakVertexCount=" + std::to_string(resources->particleVertices->elements()) +
|
||||
" fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation));
|
||||
}
|
||||
|
||||
@@ -1054,6 +1107,31 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
" resetCount=" + std::to_string(resources->resetCount));
|
||||
}
|
||||
|
||||
const bool visualChanged = resources->lineWidth != properties.lineWidth ||
|
||||
resources->particleLength != properties.particleLength ||
|
||||
resources->particleOpacity != properties.particleOpacity ||
|
||||
resources->trailOpacity != properties.trailOpacity ||
|
||||
resources->trailFade != properties.trailFade ||
|
||||
resources->speedColorEnabled != properties.speedColorEnabled;
|
||||
if (visualChanged) {
|
||||
resources->lineWidth = properties.lineWidth;
|
||||
resources->particleLength = properties.particleLength;
|
||||
resources->particleOpacity = properties.particleOpacity;
|
||||
resources->trailOpacity = properties.trailOpacity;
|
||||
resources->trailFade = properties.trailFade;
|
||||
resources->speedColorEnabled = properties.speedColorEnabled;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=VISUAL_PROPERTY_APPLIED lineWidth=" + std::to_string(resources->lineWidth) +
|
||||
" streakLength=" + std::to_string(resources->particleLength) +
|
||||
" particleOpacity=" + std::to_string(resources->particleOpacity) + " trailOpacity=" +
|
||||
std::to_string(resources->trailOpacity) + " trailFade=" + std::to_string(resources->trailFade) +
|
||||
" speedColor=" + (resources->speedColorEnabled ? "true" : "false") +
|
||||
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation) +
|
||||
" fieldUploadCount=" + std::to_string(resources->fieldUploadCount) +
|
||||
" resetCount=" + std::to_string(resources->resetCount));
|
||||
}
|
||||
|
||||
if (!resources->lifecycleModeKnown || resources->lifecycleActive != active) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleRender: lifecycle transition=" + std::string(active ? "ACTIVE" : "STATIC") +
|
||||
|
||||
@@ -13,19 +13,37 @@ public:
|
||||
uint8_t maskMode_,
|
||||
uint8_t fixtureMode_,
|
||||
float timeMix_,
|
||||
uint32_t particleCount_)
|
||||
uint32_t particleCount_,
|
||||
float lineWidth_,
|
||||
float particleLength_,
|
||||
float particleOpacity_,
|
||||
float trailOpacity_,
|
||||
float trailFade_,
|
||||
bool speedColorEnabled_)
|
||||
: LayerProperties(std::move(impl)),
|
||||
active(active_),
|
||||
maskMode(maskMode_),
|
||||
fixtureMode(fixtureMode_),
|
||||
timeMix(timeMix_),
|
||||
particleCount(particleCount_) {}
|
||||
particleCount(particleCount_),
|
||||
lineWidth(lineWidth_),
|
||||
particleLength(particleLength_),
|
||||
particleOpacity(particleOpacity_),
|
||||
trailOpacity(trailOpacity_),
|
||||
trailFade(trailFade_),
|
||||
speedColorEnabled(speedColorEnabled_) {}
|
||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||
bool active;
|
||||
uint8_t maskMode;
|
||||
uint8_t fixtureMode;
|
||||
float timeMix;
|
||||
uint32_t particleCount;
|
||||
float lineWidth;
|
||||
float particleLength;
|
||||
float particleOpacity;
|
||||
float trailOpacity;
|
||||
float trailFade;
|
||||
bool speedColorEnabled;
|
||||
};
|
||||
|
||||
} // namespace mbgl::style
|
||||
|
||||
@@ -13,6 +13,12 @@ constexpr const char* ActiveProperty = "raster-particle-active";
|
||||
constexpr const char* MaskModeProperty = "raster-particle-mask-mode";
|
||||
constexpr const char* TimeMixProperty = "raster-particle-time-mix";
|
||||
constexpr const char* ParticleCountProperty = "raster-particle-particle-count";
|
||||
constexpr const char* LineWidthProperty = "raster-particle-line-width";
|
||||
constexpr const char* ParticleLengthProperty = "raster-particle-particle-length";
|
||||
constexpr const char* ParticleOpacityProperty = "raster-particle-particle-opacity";
|
||||
constexpr const char* TrailOpacityProperty = "raster-particle-trail-opacity";
|
||||
constexpr const char* TrailFadeProperty = "raster-particle-trail-fade";
|
||||
constexpr const char* SpeedColorEnabledProperty = "raster-particle-speed-color-enabled";
|
||||
constexpr uint32_t MinimumParticleCount = 256;
|
||||
constexpr uint32_t MaximumParticleCount = 1024 * 1024;
|
||||
|
||||
@@ -91,6 +97,77 @@ void RasterParticleLayer::setParticleCount(const uint32_t particleCount) {
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getLineWidth() const {
|
||||
return impl().lineWidth;
|
||||
}
|
||||
void RasterParticleLayer::setLineWidth(float value) {
|
||||
const auto clamped = std::clamp(value, 1.0f, 8.0f);
|
||||
if (clamped != getLineWidth()) {
|
||||
auto next = mutableImpl();
|
||||
next->lineWidth = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getParticleLength() const {
|
||||
return impl().particleLength;
|
||||
}
|
||||
void RasterParticleLayer::setParticleLength(float value) {
|
||||
const auto clamped = std::clamp(value, 0.002f, 0.080f);
|
||||
if (clamped != getParticleLength()) {
|
||||
auto next = mutableImpl();
|
||||
next->particleLength = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getParticleOpacity() const {
|
||||
return impl().particleOpacity;
|
||||
}
|
||||
void RasterParticleLayer::setParticleOpacity(float value) {
|
||||
const auto clamped = std::clamp(value, 0.0f, 1.0f);
|
||||
if (clamped != getParticleOpacity()) {
|
||||
auto next = mutableImpl();
|
||||
next->particleOpacity = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getTrailOpacity() const {
|
||||
return impl().trailOpacity;
|
||||
}
|
||||
void RasterParticleLayer::setTrailOpacity(float value) {
|
||||
const auto clamped = std::clamp(value, 0.0f, 1.0f);
|
||||
if (clamped != getTrailOpacity()) {
|
||||
auto next = mutableImpl();
|
||||
next->trailOpacity = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getTrailFade() const {
|
||||
return impl().trailFade;
|
||||
}
|
||||
void RasterParticleLayer::setTrailFade(float value) {
|
||||
const auto clamped = std::clamp(value, 0.0f, 0.995f);
|
||||
if (clamped != getTrailFade()) {
|
||||
auto next = mutableImpl();
|
||||
next->trailFade = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
bool RasterParticleLayer::isSpeedColorEnabled() const {
|
||||
return impl().speedColorEnabled;
|
||||
}
|
||||
void RasterParticleLayer::setSpeedColorEnabled(bool value) {
|
||||
if (value != isSpeedColorEnabled()) {
|
||||
auto next = mutableImpl();
|
||||
next->speedColorEnabled = value;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const std::string& name,
|
||||
const conversion::Convertible& value) {
|
||||
conversion::Error error;
|
||||
@@ -124,6 +201,28 @@ std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const
|
||||
setParticleCount(static_cast<uint32_t>(*converted));
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto finiteFloat = [&](const char* message, auto setter) -> std::optional<conversion::Error> {
|
||||
auto converted = conversion::convert<float>(value, error);
|
||||
if (!converted || !std::isfinite(*converted)) return conversion::Error{message};
|
||||
(this->*setter)(*converted);
|
||||
return std::nullopt;
|
||||
};
|
||||
if (name == LineWidthProperty)
|
||||
return finiteFloat("raster-particle-line-width must be finite", &RasterParticleLayer::setLineWidth);
|
||||
if (name == ParticleLengthProperty)
|
||||
return finiteFloat("raster-particle-particle-length must be finite", &RasterParticleLayer::setParticleLength);
|
||||
if (name == ParticleOpacityProperty)
|
||||
return finiteFloat("raster-particle-particle-opacity must be finite", &RasterParticleLayer::setParticleOpacity);
|
||||
if (name == TrailOpacityProperty)
|
||||
return finiteFloat("raster-particle-trail-opacity must be finite", &RasterParticleLayer::setTrailOpacity);
|
||||
if (name == TrailFadeProperty)
|
||||
return finiteFloat("raster-particle-trail-fade must be finite", &RasterParticleLayer::setTrailFade);
|
||||
if (name == SpeedColorEnabledProperty) {
|
||||
auto converted = conversion::convert<bool>(value, error);
|
||||
if (!converted) return error;
|
||||
setSpeedColorEnabled(*converted);
|
||||
return std::nullopt;
|
||||
}
|
||||
return conversion::Error{"layer doesn't support this property"};
|
||||
}
|
||||
StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
|
||||
@@ -133,6 +232,14 @@ StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
|
||||
if (name == TimeMixProperty) return StyleProperty(Value{getTimeMix()}, StyleProperty::Kind::Constant);
|
||||
if (name == ParticleCountProperty)
|
||||
return StyleProperty(Value{static_cast<uint64_t>(getParticleCount())}, StyleProperty::Kind::Constant);
|
||||
if (name == LineWidthProperty) return StyleProperty(Value{getLineWidth()}, StyleProperty::Kind::Constant);
|
||||
if (name == ParticleLengthProperty) return StyleProperty(Value{getParticleLength()}, StyleProperty::Kind::Constant);
|
||||
if (name == ParticleOpacityProperty)
|
||||
return StyleProperty(Value{getParticleOpacity()}, StyleProperty::Kind::Constant);
|
||||
if (name == TrailOpacityProperty) return StyleProperty(Value{getTrailOpacity()}, StyleProperty::Kind::Constant);
|
||||
if (name == TrailFadeProperty) return StyleProperty(Value{getTrailFade()}, StyleProperty::Kind::Constant);
|
||||
if (name == SpeedColorEnabledProperty)
|
||||
return StyleProperty(Value{isSpeedColorEnabled()}, StyleProperty::Kind::Constant);
|
||||
return {};
|
||||
}
|
||||
Value RasterParticleLayer::serialize() const {
|
||||
@@ -141,6 +248,12 @@ Value RasterParticleLayer::serialize() const {
|
||||
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
|
||||
serializeProperty(result, getProperty(TimeMixProperty), TimeMixProperty, true);
|
||||
serializeProperty(result, getProperty(ParticleCountProperty), ParticleCountProperty, true);
|
||||
serializeProperty(result, getProperty(LineWidthProperty), LineWidthProperty, true);
|
||||
serializeProperty(result, getProperty(ParticleLengthProperty), ParticleLengthProperty, true);
|
||||
serializeProperty(result, getProperty(ParticleOpacityProperty), ParticleOpacityProperty, true);
|
||||
serializeProperty(result, getProperty(TrailOpacityProperty), TrailOpacityProperty, true);
|
||||
serializeProperty(result, getProperty(TrailFadeProperty), TrailFadeProperty, true);
|
||||
serializeProperty(result, getProperty(SpeedColorEnabledProperty), SpeedColorEnabledProperty, true);
|
||||
return result;
|
||||
}
|
||||
std::unique_ptr<Layer> RasterParticleLayer::cloneRef(const std::string& id) const {
|
||||
|
||||
@@ -18,6 +18,12 @@ public:
|
||||
uint8_t maskMode = 0;
|
||||
float timeMix = 0.0f;
|
||||
uint32_t particleCount = 256;
|
||||
float lineWidth = 2.0f;
|
||||
float particleLength = 0.018f;
|
||||
float particleOpacity = 1.0f;
|
||||
float trailOpacity = 1.0f;
|
||||
float trailFade = 0.92f;
|
||||
bool speedColorEnabled = true;
|
||||
DECLARE_LAYER_TYPE_INFO;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,6 +128,40 @@ TEST(Layer, RasterParticleCountProperties) {
|
||||
EXPECT_EQ(65536u, layer.getParticleCount());
|
||||
}
|
||||
|
||||
TEST(Layer, RasterParticleVisualProperties) {
|
||||
RasterParticleLayer layer{"particle", "raster-array"};
|
||||
|
||||
EXPECT_FLOAT_EQ(2.0f, layer.getLineWidth());
|
||||
EXPECT_FLOAT_EQ(0.018f, layer.getParticleLength());
|
||||
EXPECT_FLOAT_EQ(1.0f, layer.getParticleOpacity());
|
||||
EXPECT_FLOAT_EQ(1.0f, layer.getTrailOpacity());
|
||||
EXPECT_FLOAT_EQ(0.92f, layer.getTrailFade());
|
||||
EXPECT_TRUE(layer.isSpeedColorEnabled());
|
||||
|
||||
layer.setLineWidth(4.0f);
|
||||
layer.setParticleLength(0.030f);
|
||||
layer.setParticleOpacity(0.45f);
|
||||
layer.setTrailOpacity(0.60f);
|
||||
layer.setTrailFade(0.80f);
|
||||
layer.setSpeedColorEnabled(false);
|
||||
|
||||
EXPECT_FLOAT_EQ(4.0f, layer.getLineWidth());
|
||||
EXPECT_FLOAT_EQ(0.030f, layer.getParticleLength());
|
||||
EXPECT_FLOAT_EQ(0.45f, layer.getParticleOpacity());
|
||||
EXPECT_FLOAT_EQ(0.60f, layer.getTrailOpacity());
|
||||
EXPECT_FLOAT_EQ(0.80f, layer.getTrailFade());
|
||||
EXPECT_FALSE(layer.isSpeedColorEnabled());
|
||||
|
||||
// Visual properties are bounded, and cannot affect the existing Field or
|
||||
// simulation resource contracts through invalid values.
|
||||
layer.setLineWidth(100.0f);
|
||||
layer.setParticleLength(-1.0f);
|
||||
layer.setTrailFade(2.0f);
|
||||
EXPECT_FLOAT_EQ(8.0f, layer.getLineWidth());
|
||||
EXPECT_FLOAT_EQ(0.002f, layer.getParticleLength());
|
||||
EXPECT_FLOAT_EQ(0.995f, layer.getTrailFade());
|
||||
}
|
||||
|
||||
TEST(Layer, FillProperties) {
|
||||
auto layer = std::make_unique<FillLayer>("fill", "source");
|
||||
ASSERT_STREQ("fill", layer->getTypeInfo()->type);
|
||||
|
||||
@@ -306,3 +306,12 @@
|
||||
- MIX 保持 property-only;Duplicate 重提当前 T1 返回 NOOP,不产生 Field upload/reset/style reload;候选耗尽 fail-closed,当前 vortex/nodata 窗口继续可用。
|
||||
- `STYLE_LOAD reason=TIME_WINDOW` 仅是用户建立初始窗口的允许初始化;真正 Runtime 操作期间 styleEpoch 稳定。MIX 的 `LAYER_CHANGED` 不是 Layer recreate,最终 renderLayerInstanceId 保持 1。
|
||||
- 40-04 未改变 U/V、`dy=-v`、GPU interpolation、40-03 full particle draw 或已验证 capacity ≥ 1,048,576;真实网络/GRIB/provider、视觉阶段、WeatherColorLayer、Vulkan、iOS/Metal、生产 mask、APK 技术债仍属范围外,未进入下一阶段。
|
||||
|
||||
## 2026-09-03 — 40-05 / STEP-046 Android OpenGL 正式粒子视觉与运行时渲染控制(代码阶段)
|
||||
|
||||
- 正式 `RasterParticleLayer` full draw 从固定 blue point 扩展为短 streak:每个 State texel 对应一颗粒子、两个静态 line endpoint,GPU vertex shader 从 current State write 与 T0/T1 插值 Field 求位置、方向和速度;CPU 不持有 live particle position/direction/color,也不做 readback 或第二 simulation。
|
||||
- 继续冻结 `U=east-positive`、`V=north-positive` 与 Mercator 边界 `dy=-v`;EAST/NORTH/vortex 的视觉方向与 MIX 后最终速度一致。NoData、NaN 和近零速 fail-closed,不画 streak。
|
||||
- 新增 `raster-particle-line-width`、`particle-length`、`particle-opacity`、`trail-opacity`、`trail-fade`、`speed-color-enabled` 等 Core Render/Visual properties。它们经现有 Android→Core JNI boolean bridge 运行时更新,只改 Render/UBO,不 reload Style/Source、不上传 Field、不 reset State/Trail。
|
||||
- GPU fragment shader 使用最终插值 speed magnitude 的最小蓝→绿→橙 ramp;COLOR 控制可以回退固定蓝色。该粒子着色与未实现的 WeatherColorLayer 明确分离。
|
||||
- Android Validation 页增加 VISUAL DEFAULT、SIZE、LENGTH、OPACITY、TRAIL、FADE、COLOR 集中入口、直接 VORTEX fixture 与 256K 容量档,并保存 Activity 状态;资源日志补充 `particleDrawCount=N`、`streakVertexCount=2N` 和低频 visual property snapshot。
|
||||
- shader generator 二次幂等、Linux OpenGL Core/style test object 精确编译、format/diff 均通过;未运行 Android Gradle、ADB、真机或 Jest。当前状态只能是 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`;用户下一轮以一次 x86_64 APK 编译完成 T1--T8 综合验收后才可关闭。
|
||||
|
||||
Reference in New Issue
Block a user