renderer: add generic scalar field color path

This commit is contained in:
OpenAI Codex
2026-09-10 22:41:55 +08:00
parent 9dabb95e44
commit 09ead7f201
10 changed files with 125 additions and 56 deletions

View File

@@ -149,9 +149,10 @@ float mercatorLatitudeRadians(float worldY) {
return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY))); return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY)));
} }
vec3 weatherSpeedColor(float speed) { vec3 weatherDefaultValueColor(float value) {
// Fixed 0..30 m/s scale: no per-frame or per-dataset auto stretching. // Historical default ramp endpoints are 0 and 30 generic field-value
float t = clamp(speed / 30.0, 0.0, 1.0); // 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 slow = vec3(0.08, 0.18, 0.65);
vec3 medium = vec3(0.08, 0.80, 0.78); vec3 medium = vec3(0.08, 0.80, 0.78);
vec3 fast = vec3(0.98, 0.22, 0.10); vec3 fast = vec3(0.98, 0.22, 0.10);
@@ -184,15 +185,15 @@ bool fieldVelocityAt(vec2 position, out vec2 velocity) {
length(velocity) > 0.00001; length(velocity) > 0.00001;
} }
vec4 weatherRampColor(float speed) { vec4 weatherRampColor(float value) {
int count = int(u_weather_ramp.x); 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]; vec4 color = u_weather_colors[0];
for (int i = 1; i < 8; ++i) { for (int i = 1; i < 8; ++i) {
if (i >= count) break; if (i >= count) break;
float lo = u_weather_stops[i - 1].x; float lo = u_weather_stops[i - 1].x;
float hi = u_weather_stops[i].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]; color = u_weather_colors[i];
} }
return color; return color;
@@ -207,11 +208,20 @@ void main() {
vec4 field0 = texture(u_field_texture, uv); vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv); vec4 field1 = texture(u_next_field_texture, uv);
if (field0.a < 0.5 || field1.a < 0.5) discard; if (field0.a < 0.5 || field1.a < 0.5) discard;
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x, float fieldValue;
(field1.rg * 2.0 - 1.0) * u_field_scale.y, if (u_weather_ramp.y > 0.5) {
u_marker.x); vec2 vectorValue = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
if (any(isnan(velocity))) discard; (field1.rg * 2.0 - 1.0) * u_field_scale.y,
vec4 color = weatherRampColor(length(velocity)); 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; float alpha = color.a * u_visual.x;
fragColor = vec4(color.rgb * alpha, alpha); fragColor = vec4(color.rgb * alpha, alpha);
return; return;

View File

@@ -19,7 +19,8 @@ struct alignas(16) ParticleGpuValidationUBO {
// Preferred respawn domain. It is the expanded stable viewport intersected // Preferred respawn domain. It is the expanded stable viewport intersected
// with fieldBounds, or fieldBounds when that intersection is unusable. // with fieldBounds, or fieldBounds when that intersection is unusable.
std::array<float, 4> respawnBounds; std::array<float, 4> 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. // z is viewportDensityActive for the formal validation path.
std::array<float, 4> fieldScale; std::array<float, 4> fieldScale;
// Host-owned base colours. They are independent from WeatherColor stops. // Host-owned base colours. They are independent from WeatherColor stops.

View File

@@ -3,8 +3,14 @@
#include <mbgl/style/layer.hpp> #include <mbgl/style/layer.hpp>
#include <mbgl/style/layers/weather_color_stop.hpp> #include <mbgl/style/layers/weather_color_stop.hpp>
#include <cstdint>
namespace mbgl::style { 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 // A field-speed colour display. It deliberately remains distinct from
// RasterParticleLayer: both consume the same canonical field source but own // RasterParticleLayer: both consume the same canonical field source but own
// separate GPU resources and draw passes. // separate GPU resources and draw passes.
@@ -16,6 +22,8 @@ public:
void setOpacity(float); void setOpacity(float);
const std::vector<WeatherColorStop>& getColorStops() const; const std::vector<WeatherColorStop>& getColorStops() const;
bool setColorStops(std::vector<WeatherColorStop>); bool setColorStops(std::vector<WeatherColorStop>);
FieldValueSource getFieldValueSource() const;
void setFieldValueSource(FieldValueSource);
class Impl; class Impl;
const Impl& impl() const; const Impl& impl() const;
Mutable<Impl> mutableImpl() const; Mutable<Impl> mutableImpl() const;

View File

@@ -12,16 +12,18 @@ inline bool validWeatherRGBA(const std::array<float, 4>& color) {
return true; return true;
} }
struct WeatherColorStop { 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<float, 4> color; std::array<float, 4> 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; constexpr size_t MaxWeatherColorStops = 8;
inline bool validWeatherColorStops(const std::vector<WeatherColorStop>& stops) { inline bool validWeatherColorStops(const std::vector<WeatherColorStop>& stops) {
if (stops.size() < 2 || stops.size() > MaxWeatherColorStops) return false; if (stops.size() < 2 || stops.size() > MaxWeatherColorStops) return false;
for (size_t i = 0; i < stops.size(); ++i) { for (size_t i = 0; i < stops.size(); ++i) {
if (!std::isfinite(stops[i].speedMps) || !validWeatherRGBA(stops[i].color) || if (!std::isfinite(stops[i].value) || !validWeatherRGBA(stops[i].color) ||
(i && stops[i - 1].speedMps >= stops[i].speedMps)) return false; (i && stops[i - 1].value >= stops[i].value)) return false;
} }
return true; return true;
} }

View File

@@ -47,9 +47,10 @@ float mercatorLatitudeRadians(float worldY) {
return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY))); return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY)));
} }
vec3 weatherSpeedColor(float speed) { vec3 weatherDefaultValueColor(float value) {
// Fixed 0..30 m/s scale: no per-frame or per-dataset auto stretching. // Historical default ramp endpoints are 0 and 30 generic field-value
float t = clamp(speed / 30.0, 0.0, 1.0); // 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 slow = vec3(0.08, 0.18, 0.65);
vec3 medium = vec3(0.08, 0.80, 0.78); vec3 medium = vec3(0.08, 0.80, 0.78);
vec3 fast = vec3(0.98, 0.22, 0.10); vec3 fast = vec3(0.98, 0.22, 0.10);
@@ -82,15 +83,15 @@ bool fieldVelocityAt(vec2 position, out vec2 velocity) {
length(velocity) > 0.00001; length(velocity) > 0.00001;
} }
vec4 weatherRampColor(float speed) { vec4 weatherRampColor(float value) {
int count = int(u_weather_ramp.x); 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]; vec4 color = u_weather_colors[0];
for (int i = 1; i < 8; ++i) { for (int i = 1; i < 8; ++i) {
if (i >= count) break; if (i >= count) break;
float lo = u_weather_stops[i - 1].x; float lo = u_weather_stops[i - 1].x;
float hi = u_weather_stops[i].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]; color = u_weather_colors[i];
} }
return color; return color;
@@ -105,11 +106,20 @@ void main() {
vec4 field0 = texture(u_field_texture, uv); vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv); vec4 field1 = texture(u_next_field_texture, uv);
if (field0.a < 0.5 || field1.a < 0.5) discard; if (field0.a < 0.5 || field1.a < 0.5) discard;
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x, float fieldValue;
(field1.rg * 2.0 - 1.0) * u_field_scale.y, if (u_weather_ramp.y > 0.5) {
u_marker.x); vec2 vectorValue = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
if (any(isnan(velocity))) discard; (field1.rg * 2.0 - 1.0) * u_field_scale.y,
vec4 color = weatherRampColor(length(velocity)); 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; float alpha = color.a * u_visual.x;
fragColor = vec4(color.rgb * alpha, alpha); fragColor = vec4(color.rgb * alpha, alpha);
return; return;

View File

@@ -1077,10 +1077,17 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
} }
return; return;
} }
// Scalar-only is a valid V3 Core snapshot, but this velocity-only // Particle advection consumes only physical U/V velocity. Scalar-only
// renderer stays fail-closed until the V3-03 texture contract exists. // fields and direction vectors are unavailable until V3-04 defines their
if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector())) { // separate advection contract.
if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector() ||
sourceField->vectorSemantics != particle::VectorSemantics::Velocity ||
sourceNextField->vectorSemantics != particle::VectorSemantics::Velocity)) {
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables(); if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
if (gpuResources) {
removeTargets(gpuResources->registeredTargets, changes);
gpuResources.reset();
}
return; return;
} }
if (!layerGroup || !isLayerRenderable()) return; if (!layerGroup || !isLayerRenderable()) return;

View File

@@ -35,22 +35,34 @@ std::array<float, 4> bounds(const particle::ParticleFieldSnapshot& field) {
static_cast<float>((field.east + 180.0) / 360.0), static_cast<float>(mercatorY(field.south))}; static_cast<float>((field.east + 180.0) / 360.0), static_cast<float>(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; float result = 1.0f;
for (size_t index = 0; index < field.valid.size(); ++index) if (field.valid[index]) { for (size_t index = 0; index < field.valid.size(); ++index) if (field.valid[index]) {
result = std::max(result, std::abs(field.vector[index * 2])); if (valueSource == style::FieldValueSource::Scalar) {
result = std::max(result, std::abs(field.vector[index * 2 + 1])); 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; return result;
} }
std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot& field, const float fieldScale) { std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot& field,
const float fieldScale,
style::FieldValueSource valueSource) {
auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height}); auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
for (uint32_t y = 0; y < field.height; ++y) for (uint32_t x = 0; x < field.width; ++x) { for (uint32_t y = 0; y < field.height; ++y) for (uint32_t x = 0; x < field.width; ++x) {
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2; const auto cell = static_cast<size_t>(y) * field.width + x;
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels; const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
result->data[pixel] = static_cast<uint8_t>(std::clamp(field.vector[cell] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] : field.vector[cell * 2];
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(field.vector[cell + 1] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1];
result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
result->data[pixel + 2] = 0; result->data[pixel + 2] = 0;
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0; result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
} }
@@ -69,13 +81,14 @@ struct Resources {
gfx::Texture2DPtr texture1; gfx::Texture2DPtr texture1;
float scale0 = 1.0f; float scale0 = 1.0f;
float scale1 = 1.0f; float scale1 = 1.0f;
style::FieldValueSource valueSource = style::FieldValueSource::VectorMagnitude;
}; };
class Tweaker final : public gfx::DrawableTweaker { class Tweaker final : public gfx::DrawableTweaker {
public: public:
Tweaker(std::shared_ptr<Resources> resources_, float mix_, float opacity_, int16_t wrap_, Tweaker(std::shared_ptr<Resources> resources_, float mix_, float opacity_, int16_t wrap_,
std::vector<style::WeatherColorStop> stops_) std::vector<style::WeatherColorStop> stops_, style::FieldValueSource valueSource_)
: resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)) {} : resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)), valueSource(valueSource_) {}
void init(gfx::Drawable&) override {} void init(gfx::Drawable&) override {}
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture); drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture);
@@ -94,8 +107,9 @@ public:
.matrix = util::cast<float>(parameters.matrixForTile({wrap, {0, 0, 0}})), .matrix = util::cast<float>(parameters.matrixForTile({wrap, {0, 0, 0}})),
}; };
ubo.weatherRamp[0] = static_cast<float>(stops.size()); ubo.weatherRamp[0] = static_cast<float>(stops.size());
ubo.weatherRamp[1] = valueSource == style::FieldValueSource::VectorMagnitude ? 1.0f : 0.0f;
for (size_t i = 0; i < stops.size(); ++i) { 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; ubo.weatherColors[i] = stops[i].color;
} }
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context); drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
@@ -106,16 +120,17 @@ private:
float opacity; float opacity;
int16_t wrap; int16_t wrap;
std::vector<style::WeatherColorStop> stops; std::vector<style::WeatherColorStop> stops;
style::FieldValueSource valueSource;
}; };
bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) { bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) {
return particle::sameFieldGpuContent(a, b); return particle::sameFieldGpuContent(a, b);
} }
bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field, bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource,
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
outputScale = scale(field); outputScale = scale(field, valueSource);
outputImage = image(field, outputScale); outputImage = image(field, outputScale, valueSource);
outputTexture = context.createTexture2D(); outputTexture = context.createTexture2D();
if (!outputTexture) return false; if (!outputTexture) return false;
outputTexture->setImage(outputImage); outputTexture->setImage(outputImage);
@@ -173,9 +188,8 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context&
resources.reset(); resources.reset();
return; return;
} }
// V3-02 accepts scalar snapshots into the Source timeline, but the const auto valueSource = static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
// existing velocity-only renderer must remain unavailable until V3-03. if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
if (!sourceField->hasVector() || !sourceNextField->hasVector()) {
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables(); if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
resources.reset(); resources.reset();
return; return;
@@ -195,23 +209,27 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context&
state = std::make_shared<Resources>(); state->shader = context.getGenericShader(shaders, ShaderName); state = std::make_shared<Resources>(); state->shader = context.getGenericShader(shaders, ShaderName);
state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(RenderStaticData::heatmapTextureVertices()); state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(RenderStaticData::heatmapTextureVertices());
state->segments = RenderStaticData::heatmapTextureSegments(); state->segments = RenderStaticData::heatmapTextureSegments();
if (!state->shader || !uploadField(context, *sourceField, state->image0, state->texture0, state->scale0) || state->valueSource = valueSource;
!uploadField(context, *sourceNextField, state->image1, state->texture1, state->scale1)) return; 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; state->t0 = *sourceField; state->t1 = *sourceNextField; resources = state;
Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID +
" T1=" + state->t1.fieldID + " scale=0..30mps"); " T1=" + state->t1.fieldID + " valueSource=" +
} else if (!same(state->t0, *sourceField) || !same(state->t1, *sourceNextField)) { (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; const auto old0 = state->t0; const auto old1 = state->t1;
auto install = [&](const particle::ParticleFieldSnapshot& wanted, particle::ParticleFieldSnapshot& assigned, auto install = [&](const particle::ParticleFieldSnapshot& wanted, particle::ParticleFieldSnapshot& assigned,
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
if (same(old0, wanted)) { assigned = wanted; outputImage = state->image0; outputTexture = state->texture0; outputScale = state->scale0; return true; } // The decoded bundle may be identical while its selected GPU
if (same(old1, wanted)) { assigned = wanted; outputImage = state->image1; outputTexture = state->texture1; outputScale = state->scale1; return true; } // representation changes (scalar versus vector magnitude).
assigned = wanted; return uploadField(context, assigned, outputImage, outputTexture, outputScale); 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<PremultipliedImage> i0, i1; gfx::Texture2DPtr x0, x1; float s0 = 1, s1 = 1; particle::ParticleFieldSnapshot t0, t1; std::shared_ptr<PremultipliedImage> i0, i1; gfx::Texture2DPtr x0, x1; float s0 = 1, s1 = 1;
if (!install(*sourceField, t0, i0, x0, s0) || !install(*sourceNextField, t1, i1, x1, s1)) return; 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->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); 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; 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->texture0, shaders::idParticleGpuValidationFieldTexture);
builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture); builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture);
builder->addTweaker(std::make_shared<Tweaker>(state, mix, opacity, wrap, builder->addTweaker(std::make_shared<Tweaker>(state, mix, opacity, wrap,
static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops)); builder->flush(context); static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops, valueSource)); builder->flush(context);
auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front())); auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front()));
} }
} }

View File

@@ -47,6 +47,14 @@ bool WeatherColorLayer::setColorStops(std::vector<WeatherColorStop> stops) {
observer->onLayerChanged(*this); observer->onLayerChanged(*this);
return true; 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<conversion::Error> WeatherColorLayer::setPropertyInternal(const std::string& name, std::optional<conversion::Error> WeatherColorLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) { const conversion::Convertible& value) {
if (name != OpacityProperty) return conversion::Error{"unknown weather-color property: " + name}; if (name != OpacityProperty) return conversion::Error{"unknown weather-color property: " + name};

View File

@@ -15,6 +15,7 @@ public:
float opacity = 0.62f; float opacity = 0.62f;
// Empty preserves the historical shader palette until a Host supplies stops. // Empty preserves the historical shader palette until a Host supplies stops.
std::vector<WeatherColorStop> colorStops; std::vector<WeatherColorStop> colorStops;
FieldValueSource valueSource = FieldValueSource::VectorMagnitude;
DECLARE_LAYER_TYPE_INFO; DECLARE_LAYER_TYPE_INFO;
}; };

View File

@@ -181,6 +181,9 @@ TEST(Layer, RasterParticleVisualProperties) {
TEST(Layer, WeatherColorUsesIndependentFixedOpacity) { TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
WeatherColorLayer layer{"weather-color", "raster-array"}; WeatherColorLayer layer{"weather-color", "raster-array"};
ASSERT_STREQ("weather-color", layer.getTypeInfo()->type); 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()); EXPECT_FLOAT_EQ(0.62f, layer.getOpacity());
layer.setOpacity(0.35f); layer.setOpacity(0.35f);
EXPECT_FLOAT_EQ(0.35f, layer.getOpacity()); EXPECT_FLOAT_EQ(0.35f, layer.getOpacity());
@@ -192,6 +195,7 @@ TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
EXPECT_EQ(layer.getColorStops(), valid); 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}}}));
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {0.0f, {1, 0, 0, 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<float>::quiet_NaN(), 0, 0, 1}}})); EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {10.0f, {std::numeric_limits<float>::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}}, 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}}, {3.0f, {0, 0, 1, 1}}, {4.0f, {0, 0, 1, 1}}, {5.0f, {0, 0, 1, 1}},