From 09ead7f201497f696f049dc5f5c3eac754b4bbef Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 10 Sep 2026 22:41:55 +0800 Subject: [PATCH] renderer: add generic scalar field color path --- .../shaders/gl/particle_gpu_validation.hpp | 32 ++++++--- .../shaders/particle_gpu_validation_ubo.hpp | 3 +- .../mbgl/style/layers/weather_color_layer.hpp | 8 +++ .../mbgl/style/layers/weather_color_stop.hpp | 10 +-- shaders/particle_gpu_validation.fragment.glsl | 32 ++++++--- .../render_particle_validation_layer.cpp | 13 +++- .../layers/render_weather_color_layer.cpp | 70 ++++++++++++------- src/mbgl/style/layers/weather_color_layer.cpp | 8 +++ .../style/layers/weather_color_layer_impl.hpp | 1 + test/style/style_layer.test.cpp | 4 ++ 10 files changed, 125 insertions(+), 56 deletions(-) diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index b970d4a04..afd745755 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -149,9 +149,10 @@ float mercatorLatitudeRadians(float worldY) { return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY))); } -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 weatherDefaultValueColor(float value) { + // Historical default ramp endpoints are 0 and 30 generic field-value + // units. Hosts provide explicit stops whenever another scale is needed. + float t = clamp(value / 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); @@ -184,15 +185,15 @@ bool fieldVelocityAt(vec2 position, out vec2 velocity) { length(velocity) > 0.00001; } -vec4 weatherRampColor(float speed) { +vec4 weatherRampColor(float value) { int count = int(u_weather_ramp.x); - if (count < 2) return vec4(weatherSpeedColor(speed), 1.0); + if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0); vec4 color = u_weather_colors[0]; for (int i = 1; i < 8; ++i) { if (i >= count) break; float lo = u_weather_stops[i - 1].x; float hi = u_weather_stops[i].x; - if (speed <= hi) return mix(color, u_weather_colors[i], clamp((speed - lo) / (hi - lo), 0.0, 1.0)); + if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0)); color = u_weather_colors[i]; } return color; @@ -207,11 +208,20 @@ void main() { 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; - vec4 color = weatherRampColor(length(velocity)); + float fieldValue; + if (u_weather_ramp.y > 0.5) { + vec2 vectorValue = 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(vectorValue))) discard; + fieldValue = length(vectorValue); + } else { + fieldValue = mix((field0.r * 2.0 - 1.0) * u_field_scale.x, + (field1.r * 2.0 - 1.0) * u_field_scale.y, + u_marker.x); + if (isnan(fieldValue)) discard; + } + vec4 color = weatherRampColor(fieldValue); float alpha = color.a * u_visual.x; fragColor = vec4(color.rgb * alpha, alpha); return; diff --git a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp index 7db0a9921..06434cbb1 100644 --- a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp +++ b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp @@ -19,7 +19,8 @@ struct alignas(16) ParticleGpuValidationUBO { // Preferred respawn domain. It is the expanded stable viewport intersected // with fieldBounds, or fieldBounds when that intersection is unusable. std::array respawnBounds; - // Per-texture signed velocity decoding scales in canonical m/s. + // Per-texture signed vector decoding scales. WeatherColor also uses these + // for its explicitly selected generic scalar/value plane. // z is viewportDensityActive for the formal validation path. std::array fieldScale; // Host-owned base colours. They are independent from WeatherColor stops. diff --git a/include/mbgl/style/layers/weather_color_layer.hpp b/include/mbgl/style/layers/weather_color_layer.hpp index 90bedcb9d..0c719688c 100644 --- a/include/mbgl/style/layers/weather_color_layer.hpp +++ b/include/mbgl/style/layers/weather_color_layer.hpp @@ -3,8 +3,14 @@ #include #include +#include + namespace mbgl::style { +// The renderer must be told which canonical plane produces color values. +// Defaulting to VectorMagnitude preserves the v1 Wind color behavior. +enum class FieldValueSource : uint8_t { Scalar, VectorMagnitude }; + // 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. @@ -16,6 +22,8 @@ public: void setOpacity(float); const std::vector& getColorStops() const; bool setColorStops(std::vector); + FieldValueSource getFieldValueSource() const; + void setFieldValueSource(FieldValueSource); class Impl; const Impl& impl() const; Mutable mutableImpl() const; diff --git a/include/mbgl/style/layers/weather_color_stop.hpp b/include/mbgl/style/layers/weather_color_stop.hpp index dc761068c..91465fe82 100644 --- a/include/mbgl/style/layers/weather_color_stop.hpp +++ b/include/mbgl/style/layers/weather_color_stop.hpp @@ -12,16 +12,18 @@ inline bool validWeatherRGBA(const std::array& color) { return true; } struct WeatherColorStop { - float speedMps; + // A generic field value. Its unit and meaning come from the explicitly + // selected field value source, never from this color ramp. + float value; std::array color; - bool operator==(const WeatherColorStop& other) const { return speedMps == other.speedMps && color == other.color; } + bool operator==(const WeatherColorStop& other) const { return value == other.value && color == other.color; } }; constexpr size_t MaxWeatherColorStops = 8; inline bool validWeatherColorStops(const std::vector& stops) { if (stops.size() < 2 || stops.size() > MaxWeatherColorStops) return false; for (size_t i = 0; i < stops.size(); ++i) { - if (!std::isfinite(stops[i].speedMps) || !validWeatherRGBA(stops[i].color) || - (i && stops[i - 1].speedMps >= stops[i].speedMps)) return false; + if (!std::isfinite(stops[i].value) || !validWeatherRGBA(stops[i].color) || + (i && stops[i - 1].value >= stops[i].value)) return false; } return true; } diff --git a/shaders/particle_gpu_validation.fragment.glsl b/shaders/particle_gpu_validation.fragment.glsl index 05dd1efb3..3d28f8f95 100644 --- a/shaders/particle_gpu_validation.fragment.glsl +++ b/shaders/particle_gpu_validation.fragment.glsl @@ -47,9 +47,10 @@ float mercatorLatitudeRadians(float worldY) { return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY))); } -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 weatherDefaultValueColor(float value) { + // Historical default ramp endpoints are 0 and 30 generic field-value + // units. Hosts provide explicit stops whenever another scale is needed. + float t = clamp(value / 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); @@ -82,15 +83,15 @@ bool fieldVelocityAt(vec2 position, out vec2 velocity) { length(velocity) > 0.00001; } -vec4 weatherRampColor(float speed) { +vec4 weatherRampColor(float value) { int count = int(u_weather_ramp.x); - if (count < 2) return vec4(weatherSpeedColor(speed), 1.0); + if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0); vec4 color = u_weather_colors[0]; for (int i = 1; i < 8; ++i) { if (i >= count) break; float lo = u_weather_stops[i - 1].x; float hi = u_weather_stops[i].x; - if (speed <= hi) return mix(color, u_weather_colors[i], clamp((speed - lo) / (hi - lo), 0.0, 1.0)); + if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0)); color = u_weather_colors[i]; } return color; @@ -105,11 +106,20 @@ void main() { 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; - vec4 color = weatherRampColor(length(velocity)); + float fieldValue; + if (u_weather_ramp.y > 0.5) { + vec2 vectorValue = 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(vectorValue))) discard; + fieldValue = length(vectorValue); + } else { + fieldValue = mix((field0.r * 2.0 - 1.0) * u_field_scale.x, + (field1.r * 2.0 - 1.0) * u_field_scale.y, + u_marker.x); + if (isnan(fieldValue)) discard; + } + vec4 color = weatherRampColor(fieldValue); float alpha = color.a * u_visual.x; fragColor = vec4(color.rgb * alpha, alpha); return; diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 3e8db3d55..86f436b49 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -1077,10 +1077,17 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, } return; } - // Scalar-only is a valid V3 Core snapshot, but this velocity-only - // renderer stays fail-closed until the V3-03 texture contract exists. - if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector())) { + // Particle advection consumes only physical U/V velocity. Scalar-only + // fields and direction vectors are unavailable until V3-04 defines their + // separate advection contract. + if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector() || + sourceField->vectorSemantics != particle::VectorSemantics::Velocity || + sourceNextField->vectorSemantics != particle::VectorSemantics::Velocity)) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); + if (gpuResources) { + removeTargets(gpuResources->registeredTargets, changes); + gpuResources.reset(); + } return; } if (!layerGroup || !isLayerRenderable()) return; diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index 46238392d..f059d26b9 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -35,22 +35,34 @@ std::array bounds(const particle::ParticleFieldSnapshot& field) { static_cast((field.east + 180.0) / 360.0), static_cast(mercatorY(field.south))}; } -float scale(const particle::ParticleFieldSnapshot& field) { +bool supportsValueSource(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) { + return valueSource == style::FieldValueSource::Scalar ? field.hasScalar() : field.hasVector(); +} + +float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) { 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.vector[index * 2])); - result = std::max(result, std::abs(field.vector[index * 2 + 1])); + if (valueSource == style::FieldValueSource::Scalar) { + result = std::max(result, std::abs(field.scalar[index])); + } else { + result = std::max(result, std::abs(field.vector[index * 2])); + result = std::max(result, std::abs(field.vector[index * 2 + 1])); + } } return result; } -std::shared_ptr image(const particle::ParticleFieldSnapshot& field, const float fieldScale) { +std::shared_ptr image(const particle::ParticleFieldSnapshot& field, + const float fieldScale, + style::FieldValueSource valueSource) { auto result = std::make_shared(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(y) * field.width + x) * 2; + const auto cell = static_cast(y) * field.width + x; const auto pixel = (static_cast(y) * field.width + x) * PremultipliedImage::channels; - result->data[pixel] = static_cast(std::clamp(field.vector[cell] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); - result->data[pixel + 1] = static_cast(std::clamp(field.vector[cell + 1] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); + const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] : field.vector[cell * 2]; + const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1]; + result->data[pixel] = static_cast(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); + result->data[pixel + 1] = static_cast(std::clamp(second / 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; } @@ -69,13 +81,14 @@ struct Resources { gfx::Texture2DPtr texture1; float scale0 = 1.0f; float scale1 = 1.0f; + style::FieldValueSource valueSource = style::FieldValueSource::VectorMagnitude; }; class Tweaker final : public gfx::DrawableTweaker { public: Tweaker(std::shared_ptr resources_, float mix_, float opacity_, int16_t wrap_, - std::vector stops_) - : resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)) {} + std::vector stops_, style::FieldValueSource valueSource_) + : resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)), valueSource(valueSource_) {} void init(gfx::Drawable&) override {} void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture); @@ -94,8 +107,9 @@ public: .matrix = util::cast(parameters.matrixForTile({wrap, {0, 0, 0}})), }; ubo.weatherRamp[0] = static_cast(stops.size()); + ubo.weatherRamp[1] = valueSource == style::FieldValueSource::VectorMagnitude ? 1.0f : 0.0f; for (size_t i = 0; i < stops.size(); ++i) { - ubo.weatherStops[i][0] = stops[i].speedMps; + ubo.weatherStops[i][0] = stops[i].value; ubo.weatherColors[i] = stops[i].color; } drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context); @@ -106,16 +120,17 @@ private: float opacity; int16_t wrap; std::vector stops; + style::FieldValueSource valueSource; }; 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& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { - outputScale = scale(field); - outputImage = image(field, outputScale); +bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource, + std::shared_ptr& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { + outputScale = scale(field, valueSource); + outputImage = image(field, outputScale, valueSource); outputTexture = context.createTexture2D(); if (!outputTexture) return false; outputTexture->setImage(outputImage); @@ -173,9 +188,8 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& resources.reset(); return; } - // V3-02 accepts scalar snapshots into the Source timeline, but the - // existing velocity-only renderer must remain unavailable until V3-03. - if (!sourceField->hasVector() || !sourceNextField->hasVector()) { + const auto valueSource = static_cast(*baseImpl).valueSource; + if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); resources.reset(); return; @@ -195,23 +209,27 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& state = std::make_shared(); state->shader = context.getGenericShader(shaders, ShaderName); state->vertices = std::make_shared>(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->valueSource = valueSource; + if (!state->shader || !uploadField(context, *sourceField, valueSource, state->image0, state->texture0, state->scale0) || + !uploadField(context, *sourceNextField, valueSource, 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)) { + " T1=" + state->t1.fieldID + " valueSource=" + + (valueSource == style::FieldValueSource::Scalar ? "SCALAR" : "VECTOR_MAGNITUDE")); + } else if (state->valueSource != valueSource || !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& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { - if (same(old0, wanted)) { assigned = wanted; outputImage = state->image0; outputTexture = state->texture0; outputScale = state->scale0; 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); + // The decoded bundle may be identical while its selected GPU + // representation changes (scalar versus vector magnitude). + if (state->valueSource == valueSource && same(old0, wanted)) { assigned = wanted; outputImage = state->image0; outputTexture = state->texture0; outputScale = state->scale0; return true; } + if (state->valueSource == valueSource && same(old1, wanted)) { assigned = wanted; outputImage = state->image1; outputTexture = state->texture1; outputScale = state->scale1; return true; } + assigned = wanted; return uploadField(context, assigned, valueSource, outputImage, outputTexture, outputScale); }; particle::ParticleFieldSnapshot t0, t1; std::shared_ptr 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; + state->texture0 = std::move(x0); state->texture1 = std::move(x1); state->scale0 = s0; state->scale1 = s1; state->valueSource = valueSource; Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID); } const float mix = sourceTimeline ? timelineFrame.mix : 0.0f; @@ -232,7 +250,7 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& builder->setTexture(state->texture0, shaders::idParticleGpuValidationFieldTexture); builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture); builder->addTweaker(std::make_shared(state, mix, opacity, wrap, - static_cast(*baseImpl).colorStops)); builder->flush(context); + static_cast(*baseImpl).colorStops, valueSource)); builder->flush(context); auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front())); } } diff --git a/src/mbgl/style/layers/weather_color_layer.cpp b/src/mbgl/style/layers/weather_color_layer.cpp index a55e07392..c39b41dda 100644 --- a/src/mbgl/style/layers/weather_color_layer.cpp +++ b/src/mbgl/style/layers/weather_color_layer.cpp @@ -47,6 +47,14 @@ bool WeatherColorLayer::setColorStops(std::vector stops) { observer->onLayerChanged(*this); return true; } +FieldValueSource WeatherColorLayer::getFieldValueSource() const { return impl().valueSource; } +void WeatherColorLayer::setFieldValueSource(FieldValueSource valueSource) { + if (valueSource == getFieldValueSource()) return; + auto next = mutableImpl(); + next->valueSource = valueSource; + baseImpl = std::move(next); + observer->onLayerChanged(*this); +} std::optional WeatherColorLayer::setPropertyInternal(const std::string& name, const conversion::Convertible& value) { if (name != OpacityProperty) return conversion::Error{"unknown weather-color property: " + name}; diff --git a/src/mbgl/style/layers/weather_color_layer_impl.hpp b/src/mbgl/style/layers/weather_color_layer_impl.hpp index 8de2e7dd7..d4dcc2094 100644 --- a/src/mbgl/style/layers/weather_color_layer_impl.hpp +++ b/src/mbgl/style/layers/weather_color_layer_impl.hpp @@ -15,6 +15,7 @@ public: float opacity = 0.62f; // Empty preserves the historical shader palette until a Host supplies stops. std::vector colorStops; + FieldValueSource valueSource = FieldValueSource::VectorMagnitude; DECLARE_LAYER_TYPE_INFO; }; diff --git a/test/style/style_layer.test.cpp b/test/style/style_layer.test.cpp index c1dd5de7d..25ef747bb 100644 --- a/test/style/style_layer.test.cpp +++ b/test/style/style_layer.test.cpp @@ -181,6 +181,9 @@ TEST(Layer, RasterParticleVisualProperties) { TEST(Layer, WeatherColorUsesIndependentFixedOpacity) { WeatherColorLayer layer{"weather-color", "raster-array"}; ASSERT_STREQ("weather-color", layer.getTypeInfo()->type); + EXPECT_EQ(FieldValueSource::VectorMagnitude, layer.getFieldValueSource()); + layer.setFieldValueSource(FieldValueSource::Scalar); + EXPECT_EQ(FieldValueSource::Scalar, layer.getFieldValueSource()); EXPECT_FLOAT_EQ(0.62f, layer.getOpacity()); layer.setOpacity(0.35f); EXPECT_FLOAT_EQ(0.35f, layer.getOpacity()); @@ -192,6 +195,7 @@ TEST(Layer, WeatherColorUsesIndependentFixedOpacity) { EXPECT_EQ(layer.getColorStops(), valid); EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}})); EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {0.0f, {1, 0, 0, 1}}})); + EXPECT_TRUE(layer.setColorStops({{-2.0f, {0, 0, 1, 1}}, {3.0f, {1, 0, 0, 1}}})); EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {10.0f, {std::numeric_limits::quiet_NaN(), 0, 0, 1}}})); EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {1.0f, {0, 0, 1, 1}}, {2.0f, {0, 0, 1, 1}}, {3.0f, {0, 0, 1, 1}}, {4.0f, {0, 0, 1, 1}}, {5.0f, {0, 0, 1, 1}},