Wind Field¶
This example combines a synthetic wind-speed field with vectors and streamlines.
Preview¶
Run And Adapt¶
Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.
| Route | Availability | Command or action |
|---|---|---|
| C | Canonical native source | just example-c showcases/wind_field (build and run), or rerun ./build/examples/c/showcases/wind_field |
| Python | Available | python3 -m examples.python.gallery.showcases.wind_field |
| Browser | Live WebGPU route | Open live example |
Use this example as capability or integration evidence, not as a minimal copy-paste template. Start from the nearest supported, copy-safe example and add this feature after verifying the linked API reference.
What To Look For¶
The sampled field stores wind speed over a kilometer-scale domain, vectors show local direction and magnitude, streamlines trace the flow, and a fixed probe plus colorbar report speed in m/s. During live playback, compare how the image, vector glyphs, and paths update together from the same procedural wind model.
This workflow is useful for geophysical or fluid-like data where scalar magnitude and direction need to be read in one coordinated panel.
Source¶
#!/usr/bin/env python3
"""Synthetic wind-speed field with vectors and streamlines."""
from __future__ import annotations
import ctypes
from dataclasses import dataclass
import numpy as np
import datoviz as dvz
from examples.python.gallery import common as ex
FIELD_WIDTH = 384
FIELD_HEIGHT = 240
VECTOR_COLS = 43
VECTOR_ROWS = 27
STREAMLINE_COUNT = 76
STREAMLINE_POINT_COUNT = 128
PROBE_SEGMENTS = 36
COLORMAP_LUT_SIZE = 256
DOMAIN_X_MIN = -620.0
DOMAIN_X_MAX = +620.0
DOMAIN_Y_MIN = -390.0
DOMAIN_Y_MAX = +390.0
PROBE_X = +345.0
PROBE_Y = +14.0
TAU = 2.0 * np.pi
FRAME_BG = dvz.DvzColor(14, 17, 23, 255)
PANEL_BG = dvz.DvzColor(22, 27, 34, 255)
GRID = dvz.DvzColor(48, 54, 61, 160)
TEXT = dvz.DvzColor(201, 209, 217, 255)
CYAN = dvz.DvzColor(76, 201, 240, 255)
MINT = dvz.DvzColor(128, 255, 219, 255)
AMBER = dvz.DvzColor(255, 183, 3, 255)
ERROR = dvz.DvzColor(239, 71, 111, 255)
@dataclass
class WindParams:
time_scale: float = 1.0
speed_max_mps: float = 80.0
storm_center_x_km: float = 10.0
storm_center_y_km: float = 30.0
storm_drift_x_km: float = 40.0
storm_drift_y_km: float = 20.0
storm_drift_rate_x: float = 0.25
storm_drift_rate_y: float = 0.50
storm_drift_phase_y: float = 1.8
eye_radius_km: float = 125.0
vortex_strength_mps: float = 76.0
spiral_radius_km: float = 550.0
inflow_strength_mps: float = -9.5
breathing_amplitude: float = 0.060
breathing_rate: float = 0.35
background_u_mps: float = 16.0
background_u_y_gradient: float = 0.005
background_u_wave_mps: float = 0.0
background_u_wave_rate: float = 0.40
background_u_wave_phase: float = 0.400000
background_v_mps: float = -8.0
background_v_wave_mps: float = 8.5
background_v_wave_k: float = 0.008000
background_v_wave_rate: float = 0.10
background_v_wave_phase: float = 0.700000
shear_strength_mps: float = 4.5
shear_wave_k: float = 0.005500
shear_rate: float = 0.15
shear_y_center_km: float = -125.000000
shear_y_radius_km: float = 310.000000
cross_wind_strength_mps: float = 4.5
cross_wind_wave_k: float = 0.007000
cross_wind_rate: float = 0.25
cross_wind_phase: float = -0.300000
cross_wind_x_center_km: float = -220.000000
cross_wind_x_radius_km: float = 520.000000
terrain_friction: float = 0.24
scalar_terrain_mix_mps: float = 6.5
vector_scale: float = 0.50
vector_alpha_base: float = 30.0
vector_alpha_range: float = 70.0
vector_width_base_px: float = 2.0
vector_width_range_px: float = 1.8
streamline_seed_wobble_km: float = 60.0
streamline_seed_wobble_rate: float = 0.50
streamline_inner_rotation_rate: float = 0.15
streamline_inner_radius_km: float = 106.000000
streamline_inner_radius_jitter_km: float = 4.100000
streamline_inner_y_scale: float = 0.780000
streamline_alpha_outer: float = 118.0
streamline_alpha_inner: float = 166.0
streamline_width_outer_px: float = 1.6
streamline_width_inner_px: float = 1.5
streamline_step_outer_km: float = 6.0
streamline_step_inner_km: float = 6.0
streamline_min_speed_mps: float = 7.0
def _clamp01(value):
return np.clip(value, 0.0, 1.0)
def _u8(value):
return np.clip(255.0 * _clamp01(value) + 0.5, 0.0, 255.0).astype(np.uint8)
def _alpha_u8(value):
return int(np.clip(value + 0.5, 0.0, 255.0))
def _mix(a, b, t):
return a + (b - a) * t
def _color_tuple(color, alpha: int | None = None):
return (
color.r,
color.g,
color.b,
color.a if alpha is None else int(np.clip(alpha, 0, 255)),
)
def _terrain_mask(x, y):
n0 = np.sin(0.0062 * x + 0.0028 * y)
n1 = np.sin(0.0110 * x - 0.0075 * y + 1.8)
ridge = 0.45 * n0 + 0.30 * n1 + 0.20 * np.sin(0.0048 * (x + 1.7 * y))
return _clamp01(0.48 + 0.55 * ridge)
def _wind_sample(x, y, params: WindParams, time_s: float):
center_x = params.storm_center_x_km + params.storm_drift_x_km * np.sin(
params.storm_drift_rate_x * time_s
)
center_y = params.storm_center_y_km + params.storm_drift_y_km * np.cos(
params.storm_drift_rate_y * time_s + params.storm_drift_phase_y
)
dx = x - center_x
dy = y - center_y
r = np.sqrt(dx * dx + dy * dy) + 1e-3
eye_radius = max(params.eye_radius_km, 1e-3)
rr = r / eye_radius
breathing = 1.0 + params.breathing_amplitude * np.sin(params.breathing_rate * time_s)
vortex = params.vortex_strength_mps * breathing * rr * np.exp(0.5 * (1.0 - rr * rr))
spiral_radius = max(params.spiral_radius_km, 1e-3)
spiral = np.exp(-(r * r) / (2.0 * spiral_radius * spiral_radius))
inflow = params.inflow_strength_mps * spiral
terrain = _terrain_mask(x, y)
u = (
params.background_u_mps
+ params.background_u_y_gradient * y
+ params.background_u_wave_mps
* np.sin(params.background_u_wave_rate * time_s + params.background_u_wave_phase)
)
v = params.background_v_mps + params.background_v_wave_mps * np.sin(
params.background_v_wave_k * x
+ params.background_v_wave_phase
+ params.background_v_wave_rate * time_s
)
u += -vortex * dy / r + inflow * dx / r
v += +vortex * dx / r + inflow * dy / r
shear = params.shear_strength_mps * np.sin(
params.shear_wave_k * (x - 0.8 * y) + params.shear_rate * time_s
)
shear_dy = y - params.shear_y_center_km
shear_radius = max(params.shear_y_radius_km, 1e-3)
u += shear * np.exp(-(shear_dy * shear_dy) / (2.0 * shear_radius * shear_radius))
cross_dx = x - params.cross_wind_x_center_km
cross_radius = max(params.cross_wind_x_radius_km, 1e-3)
v += (
params.cross_wind_strength_mps
* np.sin(params.cross_wind_wave_k * y + params.cross_wind_phase + params.cross_wind_rate * time_s)
* np.exp(-(cross_dx * cross_dx) / (2.0 * cross_radius * cross_radius))
)
friction = 1.0 - params.terrain_friction * terrain
u *= friction
v *= friction
speed = np.sqrt(u * u + v * v)
direction = np.degrees(np.arctan2(u, v))
direction = np.where(direction < 0.0, direction + 360.0, direction)
return u, v, speed, direction
def _wind_colormap_values(speed_mps, params: WindParams):
t = _clamp01(speed_mps / max(params.speed_max_mps, 1e-3))
stops = (
(0.00, FRAME_BG),
(0.24, dvz.DvzColor(23, 65, 92, 255)),
(0.52, CYAN),
(0.72, MINT),
(0.90, AMBER),
(1.00, ERROR),
)
out = np.zeros(np.shape(t) + (4,), dtype=np.uint8)
for (left_t, left), (right_t, right) in zip(stops[:-1], stops[1:], strict=True):
mask = (t >= left_t) & (t <= right_t if right_t == 1.0 else t < right_t)
local = _clamp01((t - left_t) / max(right_t - left_t, 1e-6))
out[..., 0] = np.where(mask, _u8(_mix(left.r / 255.0, right.r / 255.0, local)), out[..., 0])
out[..., 1] = np.where(mask, _u8(_mix(left.g / 255.0, right.g / 255.0, local)), out[..., 1])
out[..., 2] = np.where(mask, _u8(_mix(left.b / 255.0, right.b / 255.0, local)), out[..., 2])
out[..., 3] = np.where(mask, 255, out[..., 3])
return out
def _flow_color(speed_mps, alpha, midpoint: float, gamma: float, params: WindParams):
normalized = _clamp01(speed_mps / max(params.speed_max_mps, 1e-3))
t = normalized**gamma
local0 = _clamp01(t / midpoint)
local1 = _clamp01((t - midpoint) / (1.0 - midpoint))
low = np.column_stack(
[
_u8(_mix(CYAN.r / 255.0, MINT.r / 255.0, local0)),
_u8(_mix(CYAN.g / 255.0, MINT.g / 255.0, local0)),
_u8(_mix(CYAN.b / 255.0, MINT.b / 255.0, local0)),
]
)
high = np.column_stack(
[
_u8(_mix(MINT.r / 255.0, AMBER.r / 255.0, local1)),
_u8(_mix(MINT.g / 255.0, AMBER.g / 255.0, local1)),
_u8(_mix(MINT.b / 255.0, AMBER.b / 255.0, local1)),
]
)
rgb = np.where((t < midpoint)[:, None], low, high)
return np.column_stack([rgb, np.asarray(alpha, dtype=np.uint8)]).astype(np.uint8)
def _scalar_field(params: WindParams, time_s: float):
x = np.linspace(DOMAIN_X_MIN, DOMAIN_X_MAX, FIELD_WIDTH, dtype=np.float32)
y = np.linspace(DOMAIN_Y_MIN, DOMAIN_Y_MAX, FIELD_HEIGHT, dtype=np.float32)
xx, yy = np.meshgrid(x, y)
_u, _v, speed, _direction = _wind_sample(xx, yy, params, time_s)
terrain = _terrain_mask(xx, yy)
values = _clamp01((speed + params.scalar_terrain_mix_mps * terrain) / params.speed_max_mps)
return np.ascontiguousarray((values * params.speed_max_mps).astype(np.float32))
def _vector_data(params: WindParams, time_s: float):
x = np.linspace(DOMAIN_X_MIN, DOMAIN_X_MAX, VECTOR_COLS, dtype=np.float32)
y = np.linspace(DOMAIN_Y_MIN, DOMAIN_Y_MAX, VECTOR_ROWS, dtype=np.float32)
xx, yy = np.meshgrid(x, y)
u, v, speed, _direction = _wind_sample(xx.ravel(), yy.ravel(), params, time_s)
positions = np.column_stack([xx.ravel(), yy.ravel(), np.full(VECTOR_COLS * VECTOR_ROWS, 0.03)])
vectors = np.column_stack([params.vector_scale * u, params.vector_scale * v, np.zeros_like(u)])
alpha = params.vector_alpha_base + params.vector_alpha_range * _clamp01(
speed / max(params.vortex_strength_mps, 1e-3)
)
colors = _flow_color(speed, [_alpha_u8(a) for a in alpha], 0.44, 0.78, params)
widths = params.vector_width_base_px + params.vector_width_range_px * _clamp01(
speed / max(params.speed_max_mps, 1e-3)
)
return (
np.ascontiguousarray(positions, dtype=np.float32),
np.ascontiguousarray(vectors, dtype=np.float32),
np.ascontiguousarray(colors, dtype=np.uint8),
np.ascontiguousarray(widths, dtype=np.float32),
)
def _streamline_data(params: WindParams, time_s: float):
total = STREAMLINE_COUNT * STREAMLINE_POINT_COUNT
positions = np.zeros((total, 3), dtype=np.float32)
colors = np.zeros((total, 4), dtype=np.uint8)
widths = np.zeros(total, dtype=np.float32)
subpaths = np.full(STREAMLINE_COUNT, STREAMLINE_POINT_COUNT, dtype=np.uint32)
for line in range(STREAMLINE_COUNT):
band = line / (STREAMLINE_COUNT - 1)
upper_band = band**0.68
x = (
DOMAIN_X_MIN
+ 78.0
+ 54.0 * (line % 8)
+ params.streamline_seed_wobble_km
* np.sin(params.streamline_seed_wobble_rate * time_s + 3.1 * band)
)
y = _mix(DOMAIN_Y_MIN + 132.0, DOMAIN_Y_MAX - 42.0, upper_band)
if line >= STREAMLINE_COUNT // 2:
inner = line - STREAMLINE_COUNT // 2
a = TAU * inner / (STREAMLINE_COUNT // 2) + params.streamline_inner_rotation_rate * time_s
radius = params.streamline_inner_radius_km + params.streamline_inner_radius_jitter_km * (
inner % 7
)
x = params.storm_center_x_km + radius * np.cos(a)
y = params.storm_center_y_km + params.streamline_inner_y_scale * radius * np.sin(a)
active = True
held_position = np.zeros(3, dtype=np.float32)
held_color = np.array(_color_tuple(CYAN, 0), dtype=np.uint8)
for point in range(STREAMLINE_POINT_COUNT):
idx = line * STREAMLINE_POINT_COUNT + point
if not active:
positions[idx] = held_position
colors[idx] = held_color
continue
positions[idx] = (x, y, 0.02)
held_position = positions[idx].copy()
u, v, speed, _direction = _wind_sample(x, y, params, time_s)
alpha = (
params.streamline_alpha_outer
if line < STREAMLINE_COUNT // 2
else params.streamline_alpha_inner
)
colors[idx] = _flow_color(np.array([speed]), [_alpha_u8(alpha)], 0.36, 0.64, params)[0]
held_color = colors[idx].copy()
held_color[3] = 0
widths[idx] = (
params.streamline_width_outer_px
if line < STREAMLINE_COUNT // 2
else params.streamline_width_inner_px
)
norm = max(float(speed), params.streamline_min_speed_mps)
step = (
params.streamline_step_outer_km
if line < STREAMLINE_COUNT // 2
else params.streamline_step_inner_km
)
x += step * float(u) / norm
y += step * float(v) / norm
if x < DOMAIN_X_MIN or x > DOMAIN_X_MAX or y < DOMAIN_Y_MIN or y > DOMAIN_Y_MAX:
active = False
return positions, colors, widths, subpaths
def _configure_panel(panel) -> None:
padding = dvz.DvzPanelReserve()
padding.left_px = 66.0
padding.right_px = 12.0
padding.bottom_px = 12.0
padding.top_px = 12.0
if dvz.dvz_panel_set_padding(panel, ctypes.byref(padding)) != 0:
raise RuntimeError("dvz_panel_set_padding() failed")
desc = dvz.dvz_panel_view2d_desc()
desc.mode = dvz.DVZ_PANEL_VIEW2D_CONTAIN
desc.aspect = dvz.DVZ_PANEL_VIEW2D_ASPECT_EQUAL
desc.padding = 0.02
desc.domain_x[:] = (DOMAIN_X_MIN, DOMAIN_X_MAX)
desc.domain_y[:] = (DOMAIN_Y_MIN, DOMAIN_Y_MAX)
desc.has_domain_x = True
desc.has_domain_y = True
if dvz.dvz_panel_set_view2d(panel, ctypes.byref(desc)) != 0:
raise RuntimeError("dvz_panel_set_view2d() failed")
def _attach(panel, visual, z_layer: int) -> None:
attach = dvz.dvz_visual_attach_desc()
attach.z_layer = z_layer
attach.coord_space = dvz.DVZ_VISUAL_COORD_DATA
if dvz.dvz_panel_add_visual(panel, visual, ctypes.byref(attach)) != 0:
raise RuntimeError("dvz_panel_add_visual() failed")
def _add_scale(scene, params: WindParams):
desc = dvz.dvz_scale_desc()
desc.kind = dvz.DVZ_SCALE_CONTINUOUS
desc.label = b"wind speed"
desc.unit = b"m/s"
scale = dvz.dvz_scale(scene, ctypes.byref(desc))
if not scale:
raise RuntimeError("dvz_scale() failed")
fmt = dvz.dvz_format_desc()
fmt.precision = 0
fmt.trim_trailing_zeros = True
if dvz.dvz_scale_set_format(scale, ctypes.byref(fmt)) != 0:
raise RuntimeError("dvz_scale_set_format() failed")
if dvz.dvz_scale_set_domain(scale, 0.0, params.speed_max_mps) != 0:
raise RuntimeError("dvz_scale_set_domain() failed")
if dvz.dvz_scale_set_view_range(scale, 0.0, params.speed_max_mps) != 0:
raise RuntimeError("dvz_scale_set_view_range() failed")
speeds = np.linspace(0.0, params.speed_max_mps, COLORMAP_LUT_SIZE, dtype=np.float32)
rgba = _wind_colormap_values(speeds, params)
colors = (dvz.DvzColor * COLORMAP_LUT_SIZE)(
*(dvz.DvzColor(int(r), int(g), int(b), int(a)) for r, g, b, a in rgba)
)
colormap = dvz.dvz_colormap_custom(scene, b"showcase_wind_speed", colors, COLORMAP_LUT_SIZE)
if not colormap:
raise RuntimeError("dvz_colormap_custom() failed")
if dvz.dvz_scale_set_colormap(scale, colormap) != 0:
raise RuntimeError("dvz_scale_set_colormap() failed")
return scale
def _add_colorbar(panel, scale) -> None:
desc = dvz.dvz_colorbar_desc()
desc.orientation = dvz.DVZ_COLORBAR_ORIENTATION_VERTICAL
desc.anchor = dvz.DVZ_SCENE_ANCHOR_PANEL_LEFT
desc.title = b"m/s"
desc.reserve_px = 66.0
desc.ramp_width_px = 24.0
desc.plot_gap_px = 10.0
desc.tick_length_px = 5.0
desc.label_gap_px = 4.0
desc.text_renderer = dvz.DVZ_TEXT_RENDERER_MSDF_ATLAS
colorbar = dvz.dvz_colorbar(panel, scale, ctypes.byref(desc))
if not colorbar:
raise RuntimeError("dvz_colorbar() failed")
fmt = dvz.dvz_format_desc()
fmt.precision = 0
fmt.trim_trailing_zeros = True
if dvz.dvz_colorbar_set_format(colorbar, ctypes.byref(fmt)) != 0:
raise RuntimeError("dvz_colorbar_set_format() failed")
def _add_image(scene, panel, scale, params: WindParams) -> None:
values = _scalar_field(params, 0.0)
positions = np.array(
[
[DOMAIN_X_MIN, DOMAIN_Y_MIN, 0.0],
[DOMAIN_X_MIN, DOMAIN_Y_MAX, 0.0],
[DOMAIN_X_MAX, DOMAIN_Y_MIN, 0.0],
[DOMAIN_X_MAX, DOMAIN_Y_MAX, 0.0],
],
dtype=np.float32,
)
texcoords = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], dtype=np.float32)
image = dvz.dvz_image(scene, 0)
if not image:
raise RuntimeError("dvz_image() failed")
if dvz.dvz_visual_set_data_many(image, {"position": positions, "texcoords": texcoords}) != 0:
raise RuntimeError("dvz_visual_set_data_many(image) failed")
if dvz.dvz_visual_set_scale(image, b"color", scale) != 0:
raise RuntimeError("dvz_visual_set_scale(image) failed")
field = dvz.dvz_sampled_field_from_array(scene, values)
if dvz.dvz_visual_set_field(image, b"field", field) != 0:
raise RuntimeError("dvz_visual_set_field(image) failed")
if dvz.dvz_visual_set_depth_test(image, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(image) failed")
_attach(panel, image, 0)
def _add_vectors(scene, panel, params: WindParams) -> None:
positions, vectors, colors, widths = _vector_data(params, 0.0)
visual = dvz.dvz_vector(scene, 0)
if not visual:
raise RuntimeError("dvz_vector() failed")
style = dvz.dvz_vector_style()
style.end_cap = dvz.DVZ_SEGMENT_CAP_TRIANGLE_OUT
if dvz.dvz_vector_set_style(visual, ctypes.byref(style)) != 0:
raise RuntimeError("dvz_vector_set_style() failed")
if dvz.dvz_visual_set_data_many(
visual,
{
"position": positions,
"vector": vectors,
"color": colors,
"stroke_width_px": widths,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(vector) failed")
if dvz.dvz_visual_set_depth_test(visual, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(vector) failed")
_attach(panel, visual, 2)
def _add_streamlines(scene, panel, params: WindParams) -> None:
positions, colors, widths, subpaths = _streamline_data(params, 0.0)
path = dvz.dvz_path(scene, 0)
if not path:
raise RuntimeError("dvz_path() failed")
if dvz.dvz_visual_set_data_many(
path,
{
"position": positions,
"color": colors,
"stroke_width_px": widths,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(path) failed")
if dvz.dvz_path_set_subpaths(
path, STREAMLINE_COUNT, subpaths.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
) != 0:
raise RuntimeError("dvz_path_set_subpaths() failed")
if dvz.dvz_path_set_caps(path, dvz.DVZ_SEGMENT_CAP_ROUND, dvz.DVZ_SEGMENT_CAP_ROUND) != 0:
raise RuntimeError("dvz_path_set_caps() failed")
if dvz.dvz_path_set_join(path, dvz.DVZ_PATH_JOIN_ROUND, 4.0) != 0:
raise RuntimeError("dvz_path_set_join() failed")
if dvz.dvz_visual_set_depth_test(path, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(path) failed")
_attach(panel, path, 1)
def _add_probe(scene, panel, params: WindParams) -> None:
angles = np.linspace(0.0, TAU, PROBE_SEGMENTS + 1, dtype=np.float32)
starts = np.column_stack(
[PROBE_X + 18.0 * np.cos(angles[:-1]), PROBE_Y + 14.0 * np.sin(angles[:-1])]
)
ends = np.column_stack(
[PROBE_X + 18.0 * np.cos(angles[1:]), PROBE_Y + 14.0 * np.sin(angles[1:])]
)
starts = np.column_stack([starts, np.full(PROBE_SEGMENTS, 0.05)]).astype(np.float32)
ends = np.column_stack([ends, np.full(PROBE_SEGMENTS, 0.05)]).astype(np.float32)
colors = np.tile(np.array([_color_tuple(CYAN, 235)], dtype=np.uint8), (PROBE_SEGMENTS, 1))
widths = np.full(PROBE_SEGMENTS, 2.0, dtype=np.float32)
ring = dvz.dvz_segment(scene, 0)
if not ring:
raise RuntimeError("dvz_segment(probe ring) failed")
if dvz.dvz_visual_set_data_many(
ring,
{
"position_start": starts,
"position_end": ends,
"color": colors,
"stroke_width_px": widths,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(probe ring) failed")
if dvz.dvz_segment_set_caps(ring, dvz.DVZ_SEGMENT_CAP_ROUND, dvz.DVZ_SEGMENT_CAP_ROUND) != 0:
raise RuntimeError("dvz_segment_set_caps(probe ring) failed")
if dvz.dvz_visual_set_depth_test(ring, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(probe ring) failed")
_attach(panel, ring, 3)
dot = dvz.dvz_point(scene, 0)
if not dot:
raise RuntimeError("dvz_point(probe dot) failed")
dot_position = np.array([[PROBE_X, PROBE_Y, 0.06]], dtype=np.float32)
dot_color = np.array([_color_tuple(CYAN, 245)], dtype=np.uint8)
dot_diameter = np.array([7.0], dtype=np.float32)
if dvz.dvz_visual_set_data_many(
dot,
{
"position": dot_position,
"color": dot_color,
"diameter_px": dot_diameter,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(probe dot) failed")
ex.set_filled_point_style(dot)
if dvz.dvz_visual_set_depth_test(dot, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(probe dot) failed")
_attach(panel, dot, 4)
_u, _v, speed, direction = _wind_sample(PROBE_X, PROBE_Y, params, 0.0)
_add_readout(panel, f"Wind Speed {float(speed):.1f} m/s Dir {float(direction):.0f} deg".encode())
def _add_readout(panel, text: bytes) -> None:
overlay = dvz.dvz_overlay(panel, 0)
if not overlay:
raise RuntimeError("dvz_overlay() failed")
style = dvz.dvz_overlay_card_style()
style.background_color = dvz.DvzColor(PANEL_BG.r, PANEL_BG.g, PANEL_BG.b, 226)
style.text_color = TEXT
style.padding_px[:] = (12.0, 7.0)
style.min_width_px = 322.0
style.height_px = 32.0
style.glyph_advance_px = 7.5
style.text_size_px = 14.0
style.text_renderer = dvz.DVZ_TEXT_RENDERER_MSDF_ATLAS
style.max_text_chars = 96
desc = dvz.dvz_overlay_card_desc()
desc.text = text
desc.placement = dvz.DVZ_OVERLAY_CARD_PLACEMENT_BOTTOM_RIGHT
desc.offset_px[:] = (-112.0, -46.0)
card = dvz.dvz_overlay_card(overlay, ctypes.byref(desc))
if not card:
raise RuntimeError("dvz_overlay_card() failed")
if dvz.dvz_overlay_card_set_style(card, ctypes.byref(style)) != 0:
raise RuntimeError("dvz_overlay_card_set_style() failed")
def _build_scene():
params = WindParams()
scene, figure, panel = ex.scene_panel()
dvz.dvz_panel_set_background_color(panel, PANEL_BG)
_configure_panel(panel)
scale = _add_scale(scene, params)
_add_colorbar(panel, scale)
_add_image(scene, panel, scale, params)
_add_streamlines(scene, panel, params)
_add_vectors(scene, panel, params)
_add_probe(scene, panel, params)
return scene, figure, panel
def _configure_view(view, scene, panel) -> None:
desc = dvz.dvz_panzoom_desc()
desc.controller_flags = dvz.DVZ_PANZOOM_FLAGS_KEEP_ASPECT
controller = dvz.dvz_panzoom(scene, ctypes.byref(desc))
if not controller:
raise RuntimeError("dvz_panzoom() failed")
if dvz.dvz_view_bind_controller(view, panel, controller, dvz.DVZ_DIM_MASK_XY) != 0:
raise RuntimeError("dvz_view_bind_controller() failed")
def main() -> None:
scene, figure, panel = _build_scene()
def configure(view) -> None:
_configure_view(view, scene, panel)
ex.run_with_view(scene, figure, "Wind Field", configure)
if __name__ == "__main__":
main()
/*
* Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project root for details.
* SPDX-License-Identifier: MIT
*/
/* wind_field - This example combines a synthetic wind-speed field with vectors and streamlines.
*
* What to look for: the sampled field stores wind speed over a kilometer-scale domain, vectors show
* local direction and magnitude, streamlines trace the flow, and a fixed probe plus colorbar report
* speed in m/s. During live playback, compare how the image, vector glyphs, and paths update
* together from the same procedural wind model.
*
* This workflow is useful for geophysical or fluid-like data where scalar magnitude and direction
* need to be read in one coordinated panel.
*
* Scenario: showcases_wind_field
* Style: showcase, graphite_cyan, 1280x720 window target
*
* Build: just example-c showcases/wind_field
* Run: ./build/examples/c/showcases/wind_field --live
* Smoke: ./build/examples/c/showcases/wind_field --png
*/
/*************************************************************************************************/
/* Includes */
/*************************************************************************************************/
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "_alloc.h"
#include "_assertions.h"
#include "datoviz/scene.h"
#include "example_common.h"
#include "example_style.h"
#include "example_tuner.h"
#include "runner/scenario_runner.h"
DvzScenarioSpec dvz_showcase_wind_field_scenario(void);
/*************************************************************************************************/
/* Constants */
/*************************************************************************************************/
#define WIDTH EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define FIELD_WIDTH 384u
#define FIELD_HEIGHT 240u
#define VECTOR_COLS 43u
#define VECTOR_ROWS 27u
#define VECTOR_COUNT (VECTOR_COLS * VECTOR_ROWS)
#define STREAMLINE_COUNT 76u
#define STREAMLINE_POINT_COUNT 128u
#define STREAMLINE_TOTAL_COUNT (STREAMLINE_COUNT * STREAMLINE_POINT_COUNT)
#define PROBE_SEGMENTS 36u
#define COLORMAP_LUT_SIZE 256u
#define ANIMATION_STRIDE 2u
#define ANIMATION_FPS 60.0f
#define DOMAIN_X_MIN_KM -620.0f
#define DOMAIN_X_MAX_KM +620.0f
#define DOMAIN_Y_MIN_KM -390.0f
#define DOMAIN_Y_MAX_KM +390.0f
#define PROBE_X_KM +345.0f
#define PROBE_Y_KM +14.0f
static const float TAU = 6.28318530718f;
/*************************************************************************************************/
/* Structs */
/*************************************************************************************************/
typedef struct WindSample
{
float u;
float v;
float speed;
float direction_deg;
} WindSample;
typedef struct WindShowcaseParams
{
float time_scale;
float speed_max_mps;
float storm_center_x_km;
float storm_center_y_km;
float storm_drift_x_km;
float storm_drift_y_km;
float storm_drift_rate_x;
float storm_drift_rate_y;
float storm_drift_phase_y;
float eye_radius_km;
float vortex_strength_mps;
float spiral_radius_km;
float inflow_strength_mps;
float breathing_amplitude;
float breathing_rate;
float background_u_mps;
float background_u_y_gradient;
float background_u_wave_mps;
float background_u_wave_rate;
float background_u_wave_phase;
float background_v_mps;
float background_v_wave_mps;
float background_v_wave_k;
float background_v_wave_rate;
float background_v_wave_phase;
float shear_strength_mps;
float shear_wave_k;
float shear_rate;
float shear_y_center_km;
float shear_y_radius_km;
float cross_wind_strength_mps;
float cross_wind_wave_k;
float cross_wind_rate;
float cross_wind_phase;
float cross_wind_x_center_km;
float cross_wind_x_radius_km;
float terrain_friction;
float scalar_terrain_mix_mps;
float vector_scale;
float vector_alpha_base;
float vector_alpha_range;
float vector_width_base_px;
float vector_width_range_px;
float streamline_seed_wobble_km;
float streamline_seed_wobble_rate;
float streamline_inner_rotation_rate;
float streamline_inner_radius_km;
float streamline_inner_radius_jitter_km;
float streamline_inner_y_scale;
float streamline_alpha_outer;
float streamline_alpha_inner;
float streamline_width_outer_px;
float streamline_width_inner_px;
float streamline_step_outer_km;
float streamline_step_inner_km;
float streamline_min_speed_mps;
} WindShowcaseParams;
typedef struct WindShowcaseState
{
DvzPanel* panel;
DvzScale* scale;
DvzSampledField* field;
DvzVisual* vectors;
DvzVisual* streamlines;
float* values;
WindShowcaseParams params;
ExampleTuner tuner;
float current_time_s;
} WindShowcaseState;
/* Wind field settings tuned defaults */
static const WindShowcaseParams WIND_PARAMS_SHOWCASE = {
.time_scale = 1.0f,
.speed_max_mps = 80.0f,
.storm_center_x_km = 10.0f,
.storm_center_y_km = 30.0f,
.storm_drift_x_km = 40.0f,
.storm_drift_y_km = 20.0f,
.storm_drift_rate_x = 0.25f,
.storm_drift_rate_y = 0.50f,
.storm_drift_phase_y = 1.8f,
.eye_radius_km = 125.0f,
.vortex_strength_mps = 76.0f,
.spiral_radius_km = 550.0f,
.inflow_strength_mps = -9.5f,
.breathing_amplitude = 0.060f,
.breathing_rate = 0.35f,
.background_u_mps = 16.0f,
.background_u_y_gradient = 0.005f,
.background_u_wave_mps = 0.0f,
.background_u_wave_rate = 0.40f,
.background_u_wave_phase = 0.400000f,
.background_v_mps = -8.0f,
.background_v_wave_mps = 8.5f,
.background_v_wave_k = 0.008000f,
.background_v_wave_rate = 0.10f,
.background_v_wave_phase = 0.700000f,
.shear_strength_mps = 4.5f,
.shear_wave_k = 0.005500f,
.shear_rate = 0.15f,
.shear_y_center_km = -125.000000f,
.shear_y_radius_km = 310.000000f,
.cross_wind_strength_mps = 4.5f,
.cross_wind_wave_k = 0.007000f,
.cross_wind_rate = 0.25f,
.cross_wind_phase = -0.300000f,
.cross_wind_x_center_km = -220.000000f,
.cross_wind_x_radius_km = 520.000000f,
.terrain_friction = 0.24f,
.scalar_terrain_mix_mps = 6.5f,
.vector_scale = 0.50f,
.vector_alpha_base = 30.0f,
.vector_alpha_range = 70.0f,
.vector_width_base_px = 2.0f,
.vector_width_range_px = 1.8f,
.streamline_seed_wobble_km = 60.0f,
.streamline_seed_wobble_rate = 0.50f,
.streamline_inner_rotation_rate = 0.15f,
.streamline_inner_radius_km = 106.000000f,
.streamline_inner_radius_jitter_km = 4.100000f,
.streamline_inner_y_scale = 0.780000f,
.streamline_alpha_outer = 118.0f,
.streamline_alpha_inner = 166.0f,
.streamline_width_outer_px = 1.6f,
.streamline_width_inner_px = 1.5f,
.streamline_step_outer_km = 6.0f,
.streamline_step_inner_km = 6.0f,
.streamline_min_speed_mps = 7.0f,
};
/*************************************************************************************************/
/* Helpers */
/*************************************************************************************************/
/**
* Clamp a float to the unit interval.
*
* @param value input value
* @return clamped value
*/
static float _clamp01(float value)
{
if (value < 0.0f)
return 0.0f;
if (value > 1.0f)
return 1.0f;
return value;
}
/**
* Convert a normalized float channel to an 8-bit channel.
*
* @param value normalized channel value
* @return clamped 8-bit channel
*/
static uint8_t _u8(float value)
{
return (uint8_t)(255.0f * _clamp01(value) + 0.5f);
}
/**
* Convert a float alpha value to an 8-bit channel.
*
* @param value alpha in [0, 255]
* @return clamped 8-bit channel
*/
static uint8_t _alpha_u8(float value)
{
if (value < 0.0f)
return 0u;
if (value > 255.0f)
return 255u;
return (uint8_t)(value + 0.5f);
}
/**
* Snap a float to a display-oriented step.
*
* @param value input value
* @param min lower bound
* @param max upper bound
* @param step snap increment
* @return snapped and clamped value
*/
static float _snap_float_step(float value, float min, float max, float step)
{
if (step > 0.0f)
value = roundf(value / step) * step;
if (value < min)
return min;
if (value > max)
return max;
return value;
}
/**
* Show a wind-parameter slider with a rounded display increment.
*
* @param gui GUI
* @param label slider label
* @param value edited value
* @param min lower bound
* @param max upper bound
* @param step snap increment
* @param format display format
* @return whether the value changed
*/
static bool _wind_gui_slider(
DvzGui* gui, const char* label, float* value, float min, float max, float step,
const char* format)
{
if (!dvz_gui_slider_float_format(gui, label, value, min, max, format))
return false;
*value = _snap_float_step(*value, min, max, step);
return true;
}
/**
* Linearly interpolate two floats.
*
* @param a first value
* @param b second value
* @param t interpolation parameter
* @return interpolated value
*/
static float _mix(float a, float b, float t)
{
return a + (b - a) * t;
}
/**
* Return a smooth pseudo terrain mask in the weather domain.
*
* The mask is not cartographic data; it gives the synthetic wind field a darker land/sea-like
* structure and a weak friction term so arrows do not look like pure mathematical noise.
*
* @param x domain X coordinate in km
* @param y domain Y coordinate in km
* @return terrain mask in [0, 1]
*/
static float _terrain_mask(float x, float y)
{
const float n0 = sinf(0.0062f * x + 0.0028f * y);
const float n1 = sinf(0.0110f * x - 0.0075f * y + 1.8f);
const float ridge = 0.45f * n0 + 0.30f * n1 + 0.20f * sinf(0.0048f * (x + 1.7f * y));
return _clamp01(0.48f + 0.55f * ridge);
}
/**
* Sample the synthetic weather wind field.
*
* @param x domain X coordinate in km
* @param y domain Y coordinate in km
* @param params procedural model parameters
* @param time_s deterministic animation time in seconds
* @return wind sample with vector, speed, and direction
*/
static WindSample
_wind_sample(float x, float y, const WindShowcaseParams* params, float time_s)
{
ANN(params);
const float center_x =
params->storm_center_x_km + params->storm_drift_x_km *
sinf(params->storm_drift_rate_x * time_s);
const float center_y =
params->storm_center_y_km + params->storm_drift_y_km *
cosf(params->storm_drift_rate_y * time_s +
params->storm_drift_phase_y);
const float dx = x - center_x;
const float dy = y - center_y;
const float r = sqrtf(dx * dx + dy * dy) + 1e-3f;
const float eye_radius = fmaxf(params->eye_radius_km, 1e-3f);
const float rr = r / eye_radius;
const float breathing =
1.0f + params->breathing_amplitude * sinf(params->breathing_rate * time_s);
const float vortex =
params->vortex_strength_mps * breathing * rr * expf(0.5f * (1.0f - rr * rr));
const float spiral_radius = fmaxf(params->spiral_radius_km, 1e-3f);
const float spiral = expf(-(r * r) / (2.0f * spiral_radius * spiral_radius));
const float inflow = params->inflow_strength_mps * spiral;
const float terrain = _terrain_mask(x, y);
float u = params->background_u_mps + params->background_u_y_gradient * y +
params->background_u_wave_mps *
sinf(params->background_u_wave_rate * time_s +
params->background_u_wave_phase);
float v = params->background_v_mps +
params->background_v_wave_mps *
sinf(
params->background_v_wave_k * x + params->background_v_wave_phase +
params->background_v_wave_rate * time_s);
u += -vortex * dy / r + inflow * dx / r;
v += +vortex * dx / r + inflow * dy / r;
const float shear = params->shear_strength_mps *
sinf(params->shear_wave_k * (x - 0.8f * y) +
params->shear_rate * time_s);
const float shear_dy = y - params->shear_y_center_km;
const float shear_radius = fmaxf(params->shear_y_radius_km, 1e-3f);
u += shear * expf(-(shear_dy * shear_dy) / (2.0f * shear_radius * shear_radius));
const float cross_dx = x - params->cross_wind_x_center_km;
const float cross_radius = fmaxf(params->cross_wind_x_radius_km, 1e-3f);
v += params->cross_wind_strength_mps *
sinf(
params->cross_wind_wave_k * y + params->cross_wind_phase +
params->cross_wind_rate * time_s) *
expf(-(cross_dx * cross_dx) / (2.0f * cross_radius * cross_radius));
const float friction = 1.0f - params->terrain_friction * terrain;
u *= friction;
v *= friction;
const float speed = sqrtf(u * u + v * v);
float dir = atan2f(u, v) * 180.0f / 3.14159265359f;
if (dir < 0.0f)
dir += 360.0f;
return (WindSample){.u = u, .v = v, .speed = speed, .direction_deg = dir};
}
/**
* Map one wind speed to the showcase colormap.
*
* @param speed_mps wind speed in m/s
* @param params procedural model parameters
* @return output RGBA8 color
*/
static DvzColor _wind_colormap(float speed_mps, const WindShowcaseParams* params)
{
ANN(params);
const float t = _clamp01(speed_mps / fmaxf(params->speed_max_mps, 1e-3f));
DvzColor c0 = dvz_color_rgb(14, 17, 23);
DvzColor c1 = dvz_color_rgb(23, 65, 92);
DvzColor c2 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
DvzColor c3 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
DvzColor c4 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_WARNING);
DvzColor c5 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ERROR);
DvzColor a = c0;
DvzColor b = c1;
float local = t / 0.24f;
if (t >= 0.24f && t < 0.52f)
{
a = c1;
b = c2;
local = (t - 0.24f) / 0.28f;
}
else if (t >= 0.52f && t < 0.72f)
{
a = c2;
b = c3;
local = (t - 0.52f) / 0.20f;
}
else if (t >= 0.72f && t < 0.90f)
{
a = c3;
b = c4;
local = (t - 0.72f) / 0.18f;
}
else if (t >= 0.90f)
{
a = c4;
b = c5;
local = (t - 0.90f) / 0.10f;
}
local = _clamp01(local);
return dvz_color_rgba(
_u8(_mix((float)a.r / 255.0f, (float)b.r / 255.0f, local)),
_u8(_mix((float)a.g / 255.0f, (float)b.g / 255.0f, local)),
_u8(_mix((float)a.b / 255.0f, (float)b.b / 255.0f, local)), 255u);
}
/**
* Map one wind speed to a constrained cyan/mint/amber overlay color.
*
* @param speed_mps wind speed in m/s
* @param alpha output alpha
* @param midpoint normalized speed where mint is reached
* @param gamma nonlinear contrast factor applied to normalized speed
* @param params procedural model parameters
* @return output RGBA8 color
*/
static DvzColor
_wind_flow_color(
float speed_mps, uint8_t alpha, float midpoint, float gamma,
const WindShowcaseParams* params)
{
ANN(params);
const float normalized = _clamp01(speed_mps / fmaxf(params->speed_max_mps, 1e-3f));
const float t = powf(normalized, gamma);
DvzColor a = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
DvzColor b = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
float local = t / midpoint;
if (t >= midpoint)
{
a = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
b = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_WARNING);
local = (t - midpoint) / (1.0f - midpoint);
}
local = _clamp01(local);
return dvz_color_rgba(
_u8(_mix((float)a.r / 255.0f, (float)b.r / 255.0f, local)),
_u8(_mix((float)a.g / 255.0f, (float)b.g / 255.0f, local)),
_u8(_mix((float)a.b / 255.0f, (float)b.b / 255.0f, local)), alpha);
}
/**
* Map one wind speed to an arrow color.
*
* @param speed_mps wind speed in m/s
* @param alpha output alpha
* @param params procedural model parameters
* @return output RGBA8 color
*/
static DvzColor
_wind_arrow_color(float speed_mps, uint8_t alpha, const WindShowcaseParams* params)
{
return _wind_flow_color(speed_mps, alpha, 0.44f, 0.78f, params);
}
/**
* Map one wind speed to a streamline color.
*
* @param speed_mps wind speed in m/s
* @param alpha output alpha
* @param params procedural model parameters
* @return output RGBA8 color
*/
static DvzColor
_wind_streamline_color(float speed_mps, uint8_t alpha, const WindShowcaseParams* params)
{
return _wind_flow_color(speed_mps, alpha, 0.36f, 0.64f, params);
}
/**
* Fill the custom wind-speed colormap LUT.
*
* @param colors output RGBA8 LUT
* @param params procedural model parameters
*/
static void
_fill_wind_colormap(DvzColor colors[COLORMAP_LUT_SIZE], const WindShowcaseParams* params)
{
ANN(colors);
ANN(params);
for (uint32_t i = 0; i < COLORMAP_LUT_SIZE; i++)
{
const float t =
COLORMAP_LUT_SIZE > 1u ? (float)i / (float)(COLORMAP_LUT_SIZE - 1u) : 0.0f;
colors[i] = _wind_colormap(t * params->speed_max_mps, params);
}
}
/**
* Fill the scalar wind-speed field.
*
* @param values output scalar field values in m/s
* @param params procedural model parameters
* @param time_s deterministic animation time in seconds
*/
static void _fill_scalar_field(float* values, const WindShowcaseParams* params, float time_s)
{
ANN(values);
ANN(params);
for (uint32_t y = 0; y < FIELD_HEIGHT; y++)
{
const float fy = FIELD_HEIGHT > 1u ? (float)y / (float)(FIELD_HEIGHT - 1u) : 0.0f;
const float data_y = _mix(DOMAIN_Y_MIN_KM, DOMAIN_Y_MAX_KM, fy);
for (uint32_t x = 0; x < FIELD_WIDTH; x++)
{
const float fx = FIELD_WIDTH > 1u ? (float)x / (float)(FIELD_WIDTH - 1u) : 0.0f;
const float data_x = _mix(DOMAIN_X_MIN_KM, DOMAIN_X_MAX_KM, fx);
WindSample sample = _wind_sample(data_x, data_y, params, time_s);
const float terrain = _terrain_mask(data_x, data_y);
values[y * FIELD_WIDTH + x] =
_clamp01(
(sample.speed + params->scalar_terrain_mix_mps * terrain) /
fmaxf(params->speed_max_mps, 1e-3f)) *
params->speed_max_mps;
}
}
}
/**
* Copy data-space positions while assigning one Z coordinate.
*
* @param data input data positions
* @param out output data positions
* @param count number of positions
* @param z data Z coordinate to assign
*/
static void _copy_positions_with_z(const float* data, float* out, uint32_t count, float z)
{
ANN(data);
ANN(out);
for (uint32_t i = 0; i < count; i++)
{
out[3 * i + 0] = data[3 * i + 0];
out[3 * i + 1] = data[3 * i + 1];
out[3 * i + 2] = z;
}
}
/**
* Return a DATA-space visual attachment descriptor for one draw layer.
*
* @param z_layer layer used for draw ordering
* @return visual attachment descriptor
*/
static DvzVisualAttachDesc _wind_attach(int32_t z_layer)
{
DvzVisualAttachDesc attach = dvz_visual_attach_desc();
attach.z_layer = z_layer;
return attach;
}
/**
* Add the scalar wind-speed image visual.
*
* @param scene scene owning the visual
* @param panel panel receiving the visual
* @param scale scale bound to the scalar image
* @param values scalar field values
* @param out_field output sampled field
* @return true when the image was added
*/
static bool _add_wind_image(
DvzScene* scene, DvzPanel* panel, DvzScale* scale, float* values,
DvzSampledField** out_field)
{
ANN(scene);
ANN(panel);
ANN(scale);
ANN(values);
ANN(out_field);
vec3 data_positions[4] = {
{DOMAIN_X_MIN_KM, DOMAIN_Y_MIN_KM, 0.0f},
{DOMAIN_X_MIN_KM, DOMAIN_Y_MAX_KM, 0.0f},
{DOMAIN_X_MAX_KM, DOMAIN_Y_MIN_KM, 0.0f},
{DOMAIN_X_MAX_KM, DOMAIN_Y_MAX_KM, 0.0f},
};
vec2 texcoords[4] = {
{0.0f, 0.0f},
{0.0f, 1.0f},
{1.0f, 0.0f},
{1.0f, 1.0f},
};
DvzVisual* image = dvz_image(scene, 0);
if (image == NULL)
return false;
if (dvz_visual_set_data(image, "position", data_positions, 4) != 0)
return false;
if (dvz_visual_set_data(image, "texcoords", texcoords, 4) != 0)
return false;
if (dvz_visual_set_scale(image, "color", scale) != 0)
return false;
DvzSampledField* field = dvz_sampled_field(
scene, &(DvzSampledFieldDesc){DVZ_STRUCT_INIT_FIELDS(DvzSampledFieldDesc),
.dim = DVZ_FIELD_DIM_2D,
.format = DVZ_FIELD_FORMAT_R32_FLOAT,
.semantic = DVZ_FIELD_SEMANTIC_SCALAR,
.width = FIELD_WIDTH,
.height = FIELD_HEIGHT,
.depth = 1,
});
if (field == NULL)
return false;
if (dvz_sampled_field_set_data(
field, &(DvzFieldDataView){DVZ_STRUCT_INIT_FIELDS(DvzFieldDataView),
.data = values,
.bytes_per_row = FIELD_WIDTH * sizeof(float),
.rows_per_image = FIELD_HEIGHT,
}) != DVZ_OK)
{
return false;
}
if (dvz_visual_set_field(image, "field", field) != DVZ_OK)
return false;
if (dvz_visual_set_depth_test(image, false) != 0)
return false;
DvzVisualAttachDesc attach = _wind_attach(0);
if (dvz_panel_add_visual(panel, image, &attach) != 0)
return false;
*out_field = field;
return true;
}
/**
* Fill straight wind-vector visual data.
*
* @param positions output vector positions in data coordinates
* @param vectors output vector displacements in data coordinates
* @param colors output vector colors
* @param widths output vector stroke widths
* @param params procedural model parameters
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool _fill_vectors(
vec3* positions, vec3* vectors, DvzColor* colors, float* widths,
const WindShowcaseParams* params, float time_s)
{
ANN(positions);
ANN(vectors);
ANN(colors);
ANN(widths);
ANN(params);
uint32_t idx = 0;
const float step_x = (DOMAIN_X_MAX_KM - DOMAIN_X_MIN_KM) / (float)(VECTOR_COLS - 1u);
const float step_y = (DOMAIN_Y_MAX_KM - DOMAIN_Y_MIN_KM) / (float)(VECTOR_ROWS - 1u);
for (uint32_t row = 0; row < VECTOR_ROWS; row++)
{
for (uint32_t col = 0; col < VECTOR_COLS; col++)
{
const float x = DOMAIN_X_MIN_KM + (float)col * step_x;
const float y = DOMAIN_Y_MIN_KM + (float)row * step_y;
WindSample sample = _wind_sample(x, y, params, time_s);
vec3 data_start[1] = {{x, y, 0.0f}};
vec3 data_end[1] = {
{x + params->vector_scale * sample.u, y + params->vector_scale * sample.v, 0.0f}};
data_start[0][2] = 0.03f;
data_end[0][2] = 0.03f;
positions[idx][0] = data_start[0][0];
positions[idx][1] = data_start[0][1];
positions[idx][2] = data_start[0][2];
vectors[idx][0] = data_end[0][0] - data_start[0][0];
vectors[idx][1] = data_end[0][1] - data_start[0][1];
vectors[idx][2] = 0.0f;
colors[idx] = _wind_arrow_color(
sample.speed,
_alpha_u8(
params->vector_alpha_base +
params->vector_alpha_range *
_clamp01(sample.speed / fmaxf(params->vortex_strength_mps, 1e-3f))),
params);
widths[idx] =
params->vector_width_base_px +
params->vector_width_range_px *
_clamp01(sample.speed / fmaxf(params->speed_max_mps, 1e-3f));
idx++;
}
}
return true;
}
/**
* Add the retained vector field.
*
* @param scene scene owning the visual
* @param panel panel receiving the visual
* @param out_visual output vector visual
* @param params procedural model parameters
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool _add_vectors(
DvzScene* scene, DvzPanel* panel, DvzVisual** out_visual,
const WindShowcaseParams* params, float time_s)
{
ANN(scene);
ANN(panel);
ANN(out_visual);
ANN(params);
vec3* positions = (vec3*)dvz_calloc(VECTOR_COUNT, sizeof(*positions));
vec3* vectors = (vec3*)dvz_calloc(VECTOR_COUNT, sizeof(*vectors));
DvzColor* colors = (DvzColor*)dvz_calloc(VECTOR_COUNT, sizeof(*colors));
float* widths = (float*)dvz_calloc(VECTOR_COUNT, sizeof(*widths));
if (positions == NULL || vectors == NULL || colors == NULL || widths == NULL)
goto error;
if (!_fill_vectors(positions, vectors, colors, widths, params, time_s))
goto error;
DvzVisual* visual = dvz_vector(scene, 0);
if (visual == NULL)
goto error;
DvzVectorStyle style = dvz_vector_style();
style.end_cap = DVZ_SEGMENT_CAP_TRIANGLE_OUT;
if (dvz_vector_set_style(visual, &style) != 0)
goto error;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position", .data = positions, .item_count = VECTOR_COUNT},
{.attr_name = "vector", .data = vectors, .item_count = VECTOR_COUNT},
{.attr_name = "color", .data = colors, .item_count = VECTOR_COUNT},
{.attr_name = "stroke_width_px", .data = widths, .item_count = VECTOR_COUNT},
};
if (dvz_visual_set_data_many(visual, updates, 4) != 0)
goto error;
if (dvz_visual_set_depth_test(visual, false) != 0)
goto error;
DvzVisualAttachDesc attach = _wind_attach(2);
if (dvz_panel_add_visual(panel, visual, &attach) != 0)
goto error;
*out_visual = visual;
dvz_free(widths);
dvz_free(colors);
dvz_free(vectors);
dvz_free(positions);
return true;
error:
dvz_free(widths);
dvz_free(colors);
dvz_free(vectors);
dvz_free(positions);
return false;
}
/**
* Fill streamline path data by integrating through the same wind field.
*
* @param positions output path positions in data coordinates
* @param colors output path colors
* @param widths output stroke widths
* @param subpaths output subpath lengths
* @param params procedural model parameters
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool
_fill_streamlines(
vec3* positions, DvzColor* colors, float* widths, uint32_t* subpaths,
const WindShowcaseParams* params, float time_s)
{
ANN(positions);
ANN(colors);
ANN(widths);
ANN(subpaths);
ANN(params);
for (uint32_t line = 0; line < STREAMLINE_COUNT; line++)
{
subpaths[line] = STREAMLINE_POINT_COUNT;
const float band = (float)line / (float)(STREAMLINE_COUNT - 1u);
const float upper_band = powf(band, 0.68f);
float x = DOMAIN_X_MIN_KM + 78.0f + 54.0f * (float)(line % 8u) +
params->streamline_seed_wobble_km *
sinf(params->streamline_seed_wobble_rate * time_s + 3.1f * band);
float y = _mix(DOMAIN_Y_MIN_KM + 132.0f, DOMAIN_Y_MAX_KM - 42.0f, upper_band);
if (line >= STREAMLINE_COUNT / 2u)
{
const uint32_t inner = line - STREAMLINE_COUNT / 2u;
const float a =
TAU * (float)inner / (float)(STREAMLINE_COUNT / 2u) +
params->streamline_inner_rotation_rate * time_s;
const float radius =
params->streamline_inner_radius_km +
params->streamline_inner_radius_jitter_km * (float)(inner % 7u);
x = params->storm_center_x_km + radius * cosf(a);
y = params->storm_center_y_km + (params->streamline_inner_y_scale * radius) * sinf(a);
}
bool active = true;
vec3 held_position = {0};
DvzColor held_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
held_color.a = 0u;
for (uint32_t point = 0; point < STREAMLINE_POINT_COUNT; point++)
{
const uint32_t idx = line * STREAMLINE_POINT_COUNT + point;
if (!active)
{
positions[idx][0] = held_position[0];
positions[idx][1] = held_position[1];
positions[idx][2] = held_position[2];
colors[idx] = held_color;
widths[idx] = 0.0f;
continue;
}
vec3 data[1] = {{x, y, 0.0f}};
data[0][2] = 0.02f;
positions[idx][0] = data[0][0];
positions[idx][1] = data[0][1];
positions[idx][2] = data[0][2];
held_position[0] = data[0][0];
held_position[1] = data[0][1];
held_position[2] = data[0][2];
WindSample sample = _wind_sample(x, y, params, time_s);
colors[idx] = _wind_streamline_color(
sample.speed,
_alpha_u8(
line < STREAMLINE_COUNT / 2u ? params->streamline_alpha_outer
: params->streamline_alpha_inner),
params);
held_color = colors[idx];
held_color.a = 0u;
widths[idx] = line < STREAMLINE_COUNT / 2u ? params->streamline_width_outer_px
: params->streamline_width_inner_px;
const float norm = fmaxf(sample.speed, params->streamline_min_speed_mps);
const float step = line < STREAMLINE_COUNT / 2u ? params->streamline_step_outer_km
: params->streamline_step_inner_km;
x += step * sample.u / norm;
y += step * sample.v / norm;
if (x < DOMAIN_X_MIN_KM || x > DOMAIN_X_MAX_KM || y < DOMAIN_Y_MIN_KM ||
y > DOMAIN_Y_MAX_KM)
{
active = false;
}
}
}
return true;
}
/**
* Add streamlines as a subdued path overlay.
*
* @param scene scene owning the visual
* @param panel panel receiving the visual
* @param out_visual output path visual
* @param params procedural model parameters
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool
_add_streamlines(
DvzScene* scene, DvzPanel* panel, DvzVisual** out_visual,
const WindShowcaseParams* params, float time_s)
{
ANN(scene);
ANN(panel);
ANN(out_visual);
ANN(params);
vec3* positions = (vec3*)dvz_calloc(STREAMLINE_TOTAL_COUNT, sizeof(*positions));
DvzColor* colors = (DvzColor*)dvz_calloc(STREAMLINE_TOTAL_COUNT, sizeof(*colors));
float* widths = (float*)dvz_calloc(STREAMLINE_TOTAL_COUNT, sizeof(*widths));
uint32_t* subpaths = (uint32_t*)dvz_calloc(STREAMLINE_COUNT, sizeof(*subpaths));
if (positions == NULL || colors == NULL || widths == NULL || subpaths == NULL)
goto error;
if (!_fill_streamlines(positions, colors, widths, subpaths, params, time_s))
goto error;
DvzVisual* path = dvz_path(scene, 0);
if (path == NULL)
goto error;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position", .data = positions, .item_count = STREAMLINE_TOTAL_COUNT},
{.attr_name = "color", .data = colors, .item_count = STREAMLINE_TOTAL_COUNT},
{.attr_name = "stroke_width_px", .data = widths, .item_count = STREAMLINE_TOTAL_COUNT},
};
if (dvz_visual_set_data_many(path, updates, 3) != 0)
goto error;
if (dvz_path_set_subpaths(path, STREAMLINE_COUNT, subpaths) != 0)
goto error;
if (dvz_path_set_caps(path, DVZ_SEGMENT_CAP_ROUND, DVZ_SEGMENT_CAP_ROUND) != 0)
goto error;
if (dvz_path_set_join(path, DVZ_PATH_JOIN_ROUND, 4.0f) != 0)
goto error;
if (dvz_visual_set_depth_test(path, false) != 0)
goto error;
DvzVisualAttachDesc attach = _wind_attach(1);
if (dvz_panel_add_visual(panel, path, &attach) != 0)
goto error;
*out_visual = path;
dvz_free(subpaths);
dvz_free(widths);
dvz_free(colors);
dvz_free(positions);
return true;
error:
dvz_free(subpaths);
dvz_free(widths);
dvz_free(colors);
dvz_free(positions);
return false;
}
/**
* Add a fixed probe crosshair and readout card.
*
* @param scene scene owning marker visuals
* @param panel panel receiving overlays
* @param params procedural model parameters
* @return true on success
*/
static bool _add_probe(DvzScene* scene, DvzPanel* panel, const WindShowcaseParams* params)
{
ANN(scene);
ANN(panel);
ANN(params);
vec3 data_starts[PROBE_SEGMENTS] = {{0}};
vec3 data_ends[PROBE_SEGMENTS] = {{0}};
vec3 starts[PROBE_SEGMENTS] = {{0}};
vec3 ends[PROBE_SEGMENTS] = {{0}};
DvzColor colors[PROBE_SEGMENTS] = {{0}};
float widths[PROBE_SEGMENTS] = {0};
const float radius_x = 18.0f;
const float radius_y = 14.0f;
const DvzColor cyan = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
for (uint32_t i = 0; i < PROBE_SEGMENTS; i++)
{
const float a0 = TAU * (float)i / (float)PROBE_SEGMENTS;
const float a1 = TAU * (float)(i + 1u) / (float)PROBE_SEGMENTS;
data_starts[i][0] = PROBE_X_KM + radius_x * cosf(a0);
data_starts[i][1] = PROBE_Y_KM + radius_y * sinf(a0);
data_ends[i][0] = PROBE_X_KM + radius_x * cosf(a1);
data_ends[i][1] = PROBE_Y_KM + radius_y * sinf(a1);
colors[i] = cyan;
colors[i].a = 235u;
widths[i] = 2.0f;
}
_copy_positions_with_z((const float*)data_starts, (float*)starts, PROBE_SEGMENTS, 0.05f);
_copy_positions_with_z((const float*)data_ends, (float*)ends, PROBE_SEGMENTS, 0.05f);
DvzVisual* ring = dvz_segment(scene, 0);
if (ring == NULL)
return false;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position_start", .data = starts, .item_count = PROBE_SEGMENTS},
{.attr_name = "position_end", .data = ends, .item_count = PROBE_SEGMENTS},
{.attr_name = "color", .data = colors, .item_count = PROBE_SEGMENTS},
{.attr_name = "stroke_width_px", .data = widths, .item_count = PROBE_SEGMENTS},
};
if (dvz_visual_set_data_many(ring, updates, 4) != 0)
return false;
if (dvz_segment_set_caps(ring, DVZ_SEGMENT_CAP_ROUND, DVZ_SEGMENT_CAP_ROUND) != 0)
return false;
if (dvz_visual_set_depth_test(ring, false) != 0)
return false;
DvzVisualAttachDesc ring_attach = _wind_attach(3);
if (dvz_panel_add_visual(panel, ring, &ring_attach) != 0)
return false;
vec3 data_dot[1] = {{PROBE_X_KM, PROBE_Y_KM, 0.0f}};
vec3 dot_position[1] = {{0}};
_copy_positions_with_z((const float*)data_dot, (float*)dot_position, 1, 0.06f);
DvzVisual* dot = dvz_point(scene, 0);
if (dot == NULL)
return false;
DvzColor dot_color[1] = {cyan};
dot_color[0].a = 245u;
float diameter_px[1] = {7.0f};
DvzVisualDataUpdate dot_updates[] = {
{.attr_name = "position", .data = dot_position, .item_count = 1},
{.attr_name = "color", .data = dot_color, .item_count = 1},
{.attr_name = "diameter_px", .data = diameter_px, .item_count = 1},
};
if (dvz_visual_set_data_many(dot, dot_updates, 3) != 0)
return false;
if (dvz_visual_set_depth_test(dot, false) != 0)
return false;
DvzVisualAttachDesc dot_attach = _wind_attach(4);
if (dvz_panel_add_visual(panel, dot, &dot_attach) != 0)
return false;
WindSample sample = _wind_sample(PROBE_X_KM, PROBE_Y_KM, params, 0.0f);
char readout[96] = {0};
snprintf(
readout, sizeof(readout), "Wind Speed %.1f m/s Dir %.0f deg", sample.speed,
sample.direction_deg);
DvzOverlay* overlay = dvz_overlay(panel, 0);
if (overlay == NULL)
return false;
DvzOverlayCardStyle card_style = dvz_overlay_card_style();
DvzColor panel_bg = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_PANEL_BG);
DvzColor text = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
card_style.background_color = dvz_color_rgba(panel_bg.r, panel_bg.g, panel_bg.b, 226u);
card_style.text_color = text;
card_style.padding_px[0] = 12.0f;
card_style.padding_px[1] = 7.0f;
card_style.min_width_px = 322.0f;
card_style.height_px = 32.0f;
card_style.glyph_advance_px = 7.5f;
card_style.text_size_px = 14.0f;
card_style.text_renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
card_style.max_text_chars = 96u;
DvzOverlayCard* card = dvz_overlay_card(
overlay,
&(DvzOverlayCardDesc){DVZ_STRUCT_INIT_FIELDS(DvzOverlayCardDesc),
.text = readout,
.placement = DVZ_OVERLAY_CARD_PLACEMENT_BOTTOM_RIGHT,
.offset_px = {-112.0f, -46.0f},
});
return card != NULL && dvz_overlay_card_set_style(card, &card_style) == 0;
}
/**
* Create the shared wind-speed color scale.
*
* @param scene scene owning scale resources
* @param params procedural model parameters
* @return created scale, or NULL on failure
*/
static DvzScale* _add_wind_scale(DvzScene* scene, const WindShowcaseParams* params)
{
ANN(scene);
ANN(params);
DvzScale* scale = dvz_scale(
scene, &(DvzScaleDesc){DVZ_STRUCT_INIT_FIELDS(DvzScaleDesc),
.kind = DVZ_SCALE_CONTINUOUS,
.label = "wind speed",
.unit = "m/s",
});
if (scale == NULL)
return NULL;
dvz_scale_set_format(
scale, &(DvzFormatDesc){DVZ_STRUCT_INIT_FIELDS(DvzFormatDesc),
.precision = 0,
.trim_trailing_zeros = true});
dvz_scale_set_domain(scale, 0.0, params->speed_max_mps);
dvz_scale_set_view_range(scale, 0.0, params->speed_max_mps);
DvzColor colors[COLORMAP_LUT_SIZE] = {0};
_fill_wind_colormap(colors, params);
DvzColormap* colormap =
dvz_colormap_custom(scene, "showcase_wind_speed", colors, COLORMAP_LUT_SIZE);
if (colormap == NULL)
return NULL;
dvz_scale_set_colormap(scale, colormap);
return scale;
}
/**
* Add the wind-speed colorbar.
*
* @param panel panel receiving the colorbar
* @param scale scale bound to the colorbar
* @return created colorbar, or NULL on failure
*/
static DvzColorbar* _add_wind_colorbar(DvzPanel* panel, DvzScale* scale)
{
ANN(panel);
ANN(scale);
DvzColorbar* colorbar = dvz_colorbar(
panel, scale,
&(DvzColorbarDesc){DVZ_STRUCT_INIT_FIELDS(DvzColorbarDesc),
.orientation = DVZ_COLORBAR_ORIENTATION_VERTICAL,
.anchor = DVZ_SCENE_ANCHOR_PANEL_LEFT,
.title = "m/s",
.reserve_px = 66.0f,
.ramp_width_px = 24.0f,
.plot_gap_px = 10.0f,
.tick_length_px = 5.0f,
.label_gap_px = 4.0f,
.text_renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS,
});
if (colorbar != NULL)
{
dvz_colorbar_set_format(
colorbar, &(DvzFormatDesc){DVZ_STRUCT_INIT_FIELDS(DvzFormatDesc), .precision = 0, .trim_trailing_zeros = true});
}
return colorbar;
}
/**
* Update the scalar sampled field with the animated wind speed.
*
* @param state showcase animation state
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool _update_wind_image(WindShowcaseState* state, float time_s)
{
if (state == NULL || state->field == NULL || state->values == NULL)
return false;
_fill_scalar_field(state->values, &state->params, time_s);
return dvz_sampled_field_update_region(
state->field,
(DvzFieldRegion){
.x = 0,
.y = 0,
.z = 0,
.width = FIELD_WIDTH,
.height = FIELD_HEIGHT,
.depth = 1,
},
&(DvzFieldDataView){DVZ_STRUCT_INIT_FIELDS(DvzFieldDataView),
.data = state->values,
.bytes_per_row = FIELD_WIDTH * sizeof(float),
.rows_per_image = FIELD_HEIGHT,
}) == DVZ_OK;
}
/**
* Update the vector overlay from the animated wind field.
*
* @param state showcase animation state
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool _update_vectors(WindShowcaseState* state, float time_s)
{
if (state == NULL || state->panel == NULL || state->vectors == NULL)
return false;
vec3* positions = (vec3*)dvz_calloc(VECTOR_COUNT, sizeof(*positions));
vec3* vectors = (vec3*)dvz_calloc(VECTOR_COUNT, sizeof(*vectors));
DvzColor* colors = (DvzColor*)dvz_calloc(VECTOR_COUNT, sizeof(*colors));
float* widths = (float*)dvz_calloc(VECTOR_COUNT, sizeof(*widths));
bool ok = false;
if (positions == NULL || vectors == NULL || colors == NULL || widths == NULL)
goto cleanup;
if (!_fill_vectors(positions, vectors, colors, widths, &state->params, time_s))
goto cleanup;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position", .data = positions, .item_count = VECTOR_COUNT},
{.attr_name = "vector", .data = vectors, .item_count = VECTOR_COUNT},
{.attr_name = "color", .data = colors, .item_count = VECTOR_COUNT},
{.attr_name = "stroke_width_px", .data = widths, .item_count = VECTOR_COUNT},
};
ok = dvz_visual_set_data_many(state->vectors, updates, 4) == 0;
cleanup:
dvz_free(widths);
dvz_free(colors);
dvz_free(vectors);
dvz_free(positions);
return ok;
}
/**
* Update the streamline overlay from the animated wind field.
*
* @param state showcase animation state
* @param time_s deterministic animation time in seconds
* @return true on success
*/
static bool _update_streamlines(WindShowcaseState* state, float time_s)
{
if (state == NULL || state->panel == NULL || state->streamlines == NULL)
return false;
vec3* positions = (vec3*)dvz_calloc(STREAMLINE_TOTAL_COUNT, sizeof(*positions));
DvzColor* colors = (DvzColor*)dvz_calloc(STREAMLINE_TOTAL_COUNT, sizeof(*colors));
float* widths = (float*)dvz_calloc(STREAMLINE_TOTAL_COUNT, sizeof(*widths));
uint32_t* subpaths = (uint32_t*)dvz_calloc(STREAMLINE_COUNT, sizeof(*subpaths));
bool ok = false;
if (positions == NULL || colors == NULL || widths == NULL || subpaths == NULL)
goto cleanup;
if (!_fill_streamlines(positions, colors, widths, subpaths, &state->params, time_s))
goto cleanup;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position", .data = positions, .item_count = STREAMLINE_TOTAL_COUNT},
{.attr_name = "color", .data = colors, .item_count = STREAMLINE_TOTAL_COUNT},
{.attr_name = "stroke_width_px", .data = widths, .item_count = STREAMLINE_TOTAL_COUNT},
};
ok = dvz_visual_set_data_many(state->streamlines, updates, 3) == 0;
cleanup:
dvz_free(subpaths);
dvz_free(widths);
dvz_free(colors);
dvz_free(positions);
return ok;
}
/**
* Advance the deterministic weather animation.
*
* @param ctx scenario context
* @param user_data showcase animation state
*/
static void _scenario_frame(DvzScenarioContext* ctx, void* user_data)
{
WindShowcaseState* state = (WindShowcaseState*)user_data;
if (ctx == NULL || state == NULL)
return;
const uint32_t frame_index =
ctx->preview_mode ? (uint32_t)ctx->preview_frame_index + 1u
: (uint32_t)ctx->frame_index + 1u;
const uint32_t update_stride = ctx->preview_mode ? 1u : ANIMATION_STRIDE;
if (frame_index % update_stride != 0)
return;
const float time_s =
ctx->preview_mode ? (float)dvz_scenario_preview_time(ctx)
: (float)frame_index / ANIMATION_FPS;
state->current_time_s = time_s;
const float model_time_s = state->current_time_s * state->params.time_scale;
if (!_update_wind_image(state, model_time_s))
return;
if (!_update_streamlines(state, model_time_s))
return;
(void)_update_vectors(state, model_time_s);
}
/**
* Apply current wind parameter edits to retained visuals.
*
* @param user wind showcase state
*/
static void _wind_params_apply(void* user)
{
WindShowcaseState* state = (WindShowcaseState*)user;
if (state == NULL)
return;
if (state->scale != NULL)
{
dvz_scale_set_domain(state->scale, 0.0, state->params.speed_max_mps);
dvz_scale_set_view_range(state->scale, 0.0, state->params.speed_max_mps);
}
const float model_time_s = state->current_time_s * state->params.time_scale;
if (!_update_wind_image(state, model_time_s))
return;
if (!_update_streamlines(state, model_time_s))
return;
(void)_update_vectors(state, model_time_s);
}
/**
* Reset editable wind parameters to the showcase defaults.
*
* @param user wind showcase state
*/
static void _wind_params_reset(void* user)
{
WindShowcaseState* state = (WindShowcaseState*)user;
if (state == NULL)
return;
state->params = WIND_PARAMS_SHOWCASE;
_wind_params_apply(user);
}
/**
* Draw wind model controls in the example tuner.
*
* @param gui GUI
* @param user wind showcase state
* @return whether parameters changed
*/
static bool _wind_params_gui(DvzGui* gui, void* user)
{
WindShowcaseState* state = (WindShowcaseState*)user;
if (gui == NULL || state == NULL)
return false;
bool changed = false;
WindShowcaseParams* p = &state->params;
dvz_gui_separator_text(gui, "Time and scale");
changed |= _wind_gui_slider(gui, "Time scale", &p->time_scale, 0.1f, 8.0f, 0.1f, "%.1f");
changed |= _wind_gui_slider(gui, "Speed max", &p->speed_max_mps, 30.0f, 140.0f, 1.0f, "%.0f");
dvz_gui_separator_text(gui, "Storm");
changed |= _wind_gui_slider(
gui, "Center X", &p->storm_center_x_km, DOMAIN_X_MIN_KM, DOMAIN_X_MAX_KM, 5.0f,
"%.0f");
changed |= _wind_gui_slider(
gui, "Center Y", &p->storm_center_y_km, DOMAIN_Y_MIN_KM, DOMAIN_Y_MAX_KM, 5.0f,
"%.0f");
changed |= _wind_gui_slider(gui, "Drift X", &p->storm_drift_x_km, 0.0f, 140.0f, 5.0f, "%.0f");
changed |= _wind_gui_slider(gui, "Drift Y", &p->storm_drift_y_km, 0.0f, 100.0f, 5.0f, "%.0f");
changed |=
_wind_gui_slider(gui, "Drift rate X", &p->storm_drift_rate_x, 0.0f, 1.0f, 0.05f, "%.2f");
changed |=
_wind_gui_slider(gui, "Drift rate Y", &p->storm_drift_rate_y, 0.0f, 1.0f, 0.05f, "%.2f");
changed |=
_wind_gui_slider(gui, "Drift phase Y", &p->storm_drift_phase_y, 0.0f, TAU, 0.1f, "%.1f");
changed |=
_wind_gui_slider(gui, "Eye radius", &p->eye_radius_km, 30.0f, 260.0f, 5.0f, "%.0f");
changed |=
_wind_gui_slider(gui, "Vortex", &p->vortex_strength_mps, 10.0f, 150.0f, 1.0f, "%.0f");
changed |= _wind_gui_slider(
gui, "Spiral radius", &p->spiral_radius_km, 80.0f, 700.0f, 10.0f, "%.0f");
changed |= _wind_gui_slider(gui, "Inflow", &p->inflow_strength_mps, -35.0f, 5.0f, 0.5f, "%.1f");
changed |=
_wind_gui_slider(gui, "Breathing", &p->breathing_amplitude, 0.0f, 0.35f, 0.005f, "%.3f");
changed |= _wind_gui_slider(gui, "Breathing rate", &p->breathing_rate, 0.0f, 1.5f, 0.05f, "%.2f");
dvz_gui_separator_text(gui, "Background");
changed |= _wind_gui_slider(gui, "Base U", &p->background_u_mps, -20.0f, 45.0f, 0.5f, "%.1f");
changed |= _wind_gui_slider(
gui, "U/Y gradient", &p->background_u_y_gradient, -0.05f, 0.05f, 0.005f,
"%.3f");
changed |= _wind_gui_slider(gui, "U wave", &p->background_u_wave_mps, 0.0f, 12.0f, 0.5f, "%.1f");
changed |=
_wind_gui_slider(gui, "U wave rate", &p->background_u_wave_rate, 0.0f, 1.5f, 0.05f, "%.2f");
changed |= _wind_gui_slider(gui, "Base V", &p->background_v_mps, -30.0f, 30.0f, 0.5f, "%.1f");
changed |= _wind_gui_slider(gui, "V wave", &p->background_v_wave_mps, 0.0f, 16.0f, 0.5f, "%.1f");
changed |=
_wind_gui_slider(gui, "V wave rate", &p->background_v_wave_rate, 0.0f, 1.5f, 0.05f, "%.2f");
changed |= _wind_gui_slider(gui, "Shear", &p->shear_strength_mps, 0.0f, 30.0f, 0.5f, "%.1f");
changed |= _wind_gui_slider(gui, "Shear rate", &p->shear_rate, 0.0f, 1.5f, 0.05f, "%.2f");
changed |= _wind_gui_slider(
gui, "Cross wind", &p->cross_wind_strength_mps, 0.0f, 20.0f, 0.5f, "%.1f");
changed |=
_wind_gui_slider(gui, "Cross rate", &p->cross_wind_rate, 0.0f, 1.5f, 0.05f, "%.2f");
changed |=
_wind_gui_slider(gui, "Terrain friction", &p->terrain_friction, 0.0f, 0.6f, 0.01f, "%.2f");
changed |= _wind_gui_slider(
gui, "Terrain color", &p->scalar_terrain_mix_mps, 0.0f, 20.0f, 0.5f, "%.1f");
dvz_gui_separator_text(gui, "Vectors");
changed |= _wind_gui_slider(gui, "Vector scale", &p->vector_scale, 0.2f, 3.0f, 0.05f, "%.2f");
changed |=
_wind_gui_slider(gui, "Vector alpha", &p->vector_alpha_base, 20.0f, 255.0f, 5.0f, "%.0f");
changed |= _wind_gui_slider(
gui, "Vector alpha range", &p->vector_alpha_range, 0.0f, 160.0f, 5.0f, "%.0f");
changed |=
_wind_gui_slider(gui, "Vector width", &p->vector_width_base_px, 0.5f, 8.0f, 0.1f, "%.1f");
changed |= _wind_gui_slider(
gui, "Vector width range", &p->vector_width_range_px, 0.0f, 6.0f, 0.1f, "%.1f");
dvz_gui_separator_text(gui, "Streamlines");
changed |= _wind_gui_slider(
gui, "Seed wobble", &p->streamline_seed_wobble_km, 0.0f, 100.0f, 5.0f, "%.0f");
changed |= _wind_gui_slider(
gui, "Seed wobble rate", &p->streamline_seed_wobble_rate, 0.0f, 1.5f, 0.05f,
"%.2f");
changed |= _wind_gui_slider(
gui, "Inner rotation", &p->streamline_inner_rotation_rate, 0.0f, 2.0f, 0.05f,
"%.2f");
changed |= _wind_gui_slider(
gui, "Outer alpha", &p->streamline_alpha_outer, 0.0f, 255.0f, 5.0f, "%.0f");
changed |= _wind_gui_slider(
gui, "Inner alpha", &p->streamline_alpha_inner, 0.0f, 255.0f, 5.0f, "%.0f");
changed |= _wind_gui_slider(
gui, "Outer width", &p->streamline_width_outer_px, 0.3f, 6.0f, 0.1f, "%.1f");
changed |= _wind_gui_slider(
gui, "Inner width", &p->streamline_width_inner_px, 0.3f, 6.0f, 0.1f, "%.1f");
changed |= _wind_gui_slider(
gui, "Outer step", &p->streamline_step_outer_km, 1.0f, 18.0f, 0.5f, "%.1f");
changed |= _wind_gui_slider(
gui, "Inner step", &p->streamline_step_inner_km, 1.0f, 18.0f, 0.5f, "%.1f");
changed |= _wind_gui_slider(
gui, "Minimum speed", &p->streamline_min_speed_mps, 0.5f, 30.0f, 0.5f, "%.1f");
return changed;
}
/**
* Print pasteable C defaults for the current wind parameters.
*
* @param fp output stream
* @param user wind showcase state
*/
static void _wind_params_print_c(FILE* fp, void* user)
{
WindShowcaseState* state = (WindShowcaseState*)user;
if (state == NULL)
return;
if (fp == NULL)
fp = stdout;
const WindShowcaseParams* p = &state->params;
fprintf(fp, "static const WindShowcaseParams WIND_PARAMS_SHOWCASE = {\n");
#define PRINT_PARAM_FMT(name, fmt) fprintf(fp, " ." #name " = " fmt "f,\n", (double)p->name)
#define PRINT_PARAM(name) PRINT_PARAM_FMT(name, "%.6f")
PRINT_PARAM_FMT(time_scale, "%.1f");
PRINT_PARAM_FMT(speed_max_mps, "%.1f");
PRINT_PARAM_FMT(storm_center_x_km, "%.1f");
PRINT_PARAM_FMT(storm_center_y_km, "%.1f");
PRINT_PARAM_FMT(storm_drift_x_km, "%.1f");
PRINT_PARAM_FMT(storm_drift_y_km, "%.1f");
PRINT_PARAM_FMT(storm_drift_rate_x, "%.2f");
PRINT_PARAM_FMT(storm_drift_rate_y, "%.2f");
PRINT_PARAM_FMT(storm_drift_phase_y, "%.1f");
PRINT_PARAM_FMT(eye_radius_km, "%.1f");
PRINT_PARAM_FMT(vortex_strength_mps, "%.1f");
PRINT_PARAM_FMT(spiral_radius_km, "%.1f");
PRINT_PARAM_FMT(inflow_strength_mps, "%.1f");
PRINT_PARAM_FMT(breathing_amplitude, "%.3f");
PRINT_PARAM_FMT(breathing_rate, "%.2f");
PRINT_PARAM_FMT(background_u_mps, "%.1f");
PRINT_PARAM_FMT(background_u_y_gradient, "%.3f");
PRINT_PARAM_FMT(background_u_wave_mps, "%.1f");
PRINT_PARAM_FMT(background_u_wave_rate, "%.2f");
PRINT_PARAM(background_u_wave_phase);
PRINT_PARAM_FMT(background_v_mps, "%.1f");
PRINT_PARAM_FMT(background_v_wave_mps, "%.1f");
PRINT_PARAM(background_v_wave_k);
PRINT_PARAM_FMT(background_v_wave_rate, "%.2f");
PRINT_PARAM(background_v_wave_phase);
PRINT_PARAM_FMT(shear_strength_mps, "%.1f");
PRINT_PARAM(shear_wave_k);
PRINT_PARAM_FMT(shear_rate, "%.2f");
PRINT_PARAM(shear_y_center_km);
PRINT_PARAM(shear_y_radius_km);
PRINT_PARAM_FMT(cross_wind_strength_mps, "%.1f");
PRINT_PARAM(cross_wind_wave_k);
PRINT_PARAM_FMT(cross_wind_rate, "%.2f");
PRINT_PARAM(cross_wind_phase);
PRINT_PARAM(cross_wind_x_center_km);
PRINT_PARAM(cross_wind_x_radius_km);
PRINT_PARAM_FMT(terrain_friction, "%.2f");
PRINT_PARAM_FMT(scalar_terrain_mix_mps, "%.1f");
PRINT_PARAM_FMT(vector_scale, "%.2f");
PRINT_PARAM_FMT(vector_alpha_base, "%.1f");
PRINT_PARAM_FMT(vector_alpha_range, "%.1f");
PRINT_PARAM_FMT(vector_width_base_px, "%.1f");
PRINT_PARAM_FMT(vector_width_range_px, "%.1f");
PRINT_PARAM_FMT(streamline_seed_wobble_km, "%.1f");
PRINT_PARAM_FMT(streamline_seed_wobble_rate, "%.2f");
PRINT_PARAM_FMT(streamline_inner_rotation_rate, "%.2f");
PRINT_PARAM(streamline_inner_radius_km);
PRINT_PARAM(streamline_inner_radius_jitter_km);
PRINT_PARAM(streamline_inner_y_scale);
PRINT_PARAM_FMT(streamline_alpha_outer, "%.1f");
PRINT_PARAM_FMT(streamline_alpha_inner, "%.1f");
PRINT_PARAM_FMT(streamline_width_outer_px, "%.1f");
PRINT_PARAM_FMT(streamline_width_inner_px, "%.1f");
PRINT_PARAM_FMT(streamline_step_outer_km, "%.1f");
PRINT_PARAM_FMT(streamline_step_inner_km, "%.1f");
PRINT_PARAM_FMT(streamline_min_speed_mps, "%.1f");
#undef PRINT_PARAM
#undef PRINT_PARAM_FMT
fprintf(fp, "};\n");
}
/*************************************************************************************************/
/* Scenario callbacks */
/*************************************************************************************************/
/**
* Initialize the synthetic weather field showcase scenario.
*
* @param ctx scenario context
* @param out_user scenario state output
* @return whether initialization succeeded
*/
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
if (ctx == NULL)
return false;
if (out_user != NULL)
*out_user = NULL;
bool ok = false;
WindShowcaseState* state = (WindShowcaseState*)dvz_calloc(1, sizeof(*state));
if (state == NULL)
return false;
if (out_user != NULL)
*out_user = state;
state->params = WIND_PARAMS_SHOWCASE;
state->tuner = example_tuner("Wind field settings");
ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
EXAMPLE_CHECK(ctx->figure != NULL, "dvz_figure() failed");
example_tuner_figure(&state->tuner, ctx->figure);
(void)example_tuner_add_component(
&state->tuner, "Wind model", state, NULL, _wind_params_gui, _wind_params_apply,
_wind_params_reset, _wind_params_print_c);
DvzPanel* panel = dvz_panel_full(ctx->figure);
EXAMPLE_CHECK(panel != NULL, "dvz_panel_full() failed");
bool configured = example_configure_equal_aspect_panel(
panel,
(DvzDataDomain){.min = DOMAIN_X_MIN_KM, .max = DOMAIN_X_MAX_KM},
(DvzDataDomain){.min = DOMAIN_Y_MIN_KM, .max = DOMAIN_Y_MAX_KM}, 0.02);
EXAMPLE_CHECK(configured, "failed to configure equal-aspect wind panel");
DvzScale* scale = _add_wind_scale(ctx->scene, &state->params);
EXAMPLE_CHECK(scale != NULL, "_add_wind_scale() failed");
state->scale = scale;
DvzColorbar* colorbar = _add_wind_colorbar(panel, scale);
EXAMPLE_CHECK(colorbar != NULL, "_add_wind_colorbar() failed");
state->values = (float*)dvz_calloc((DvzSize)FIELD_WIDTH * FIELD_HEIGHT, sizeof(float));
EXAMPLE_CHECK(state->values != NULL, "wind scalar field allocation failed");
_fill_scalar_field(state->values, &state->params, 0.0f);
state->panel = panel;
ok = _add_wind_image(ctx->scene, panel, scale, state->values, &state->field);
EXAMPLE_CHECK(ok, "_add_wind_image() failed");
ok = _add_streamlines(ctx->scene, panel, &state->streamlines, &state->params, 0.0f);
EXAMPLE_CHECK(ok, "_add_streamlines() failed");
ok = _add_vectors(ctx->scene, panel, &state->vectors, &state->params, 0.0f);
EXAMPLE_CHECK(ok, "_add_vectors() failed");
ok = _add_probe(ctx->scene, panel, &state->params);
EXAMPLE_CHECK(ok, "_add_probe() failed");
DvzPanzoomDesc panzoom_desc = dvz_panzoom_desc();
panzoom_desc.controller_flags = DVZ_PANZOOM_FLAGS_KEEP_ASPECT;
DvzPanzoom* panzoom = dvz_scenario_panzoom(ctx, panel, &panzoom_desc, DVZ_DIM_MASK_XY);
EXAMPLE_CHECK(panzoom != NULL, "failed to create or bind panzoom controller");
(void)panzoom;
ok = true;
cleanup:
return ok;
}
/**
* Attach live-only wind model tuning controls to the native view.
*
* @param ctx scenario context
* @param app app
* @param view native view
* @param user scenario state
* @return whether the tuner was attached or intentionally skipped
*/
static bool _scenario_native_view(DvzScenarioContext* ctx, DvzApp* app, DvzView* view, void* user)
{
(void)app;
WindShowcaseState* state = (WindShowcaseState*)user;
if (
ctx == NULL || ctx->presentation != DVZ_RUNNER_PRESENT_GLFW || state == NULL ||
view == NULL)
return true;
return example_tuner_attach(&state->tuner, view);
}
/**
* Destroy the synthetic weather field showcase scenario state.
*
* @param ctx scenario context
* @param user scenario state
*/
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
(void)ctx;
WindShowcaseState* state = (WindShowcaseState*)user;
if (state == NULL)
return;
dvz_free(state->values);
dvz_free(state);
}
/**
* Return the synthetic weather field showcase scenario.
*
* @return scenario specification
*/
DvzScenarioSpec dvz_showcase_wind_field_scenario(void)
{
return (DvzScenarioSpec){
.id = "showcases_wind_field",
.title = "Wind Field",
.width = WIDTH,
.height = HEIGHT,
.fps = 60.0,
.requirements = DVZ_SCENARIO_REQ_CONTROLLER | DVZ_SCENARIO_REQ_PANZOOM |
DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
.init = _scenario_init,
.frame = _scenario_frame,
.destroy = _scenario_destroy,
};
}
/*************************************************************************************************/
/* Functions */
/*************************************************************************************************/
/**
* Run the synthetic weather field showcase through the native scenario runner.
*
* @param argc command-line argument count
* @param argv command-line argument vector
* @return process exit code
*/
#ifndef DVZ_EXAMPLE_NO_MAIN
int main(int argc, char** argv)
{
DvzScenarioSpec spec = dvz_showcase_wind_field_scenario();
if (example_cli_wants_live_gui(argc, argv))
spec.native_view = _scenario_native_view;
return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details
- ID:
showcases_wind_field - Category:
showcase - Lane:
showcases - Status:
supported - Source:
examples/c/showcases/wind_field.c - Approved adaptation starter:
no - Python source:
examples/python/gallery/showcases/wind_field.py - Python adaptation: Available
- Browser support: Live in browser
- WebGPU live route:
examples/webgpu/live.html?id=showcases_wind_field - Browser capability tags:
image,vector,path,colorbar,panzoom - Validation:
smoke+screenshot
Tags
scientific, synthetic, scalar-field, vector-field, wind-field, colorbar, capture
Data
| Field | Value |
|---|---|
kind |
synthetic |