Surface Grid¶
This example turns a generated height field into a lit mesh and wireframe.
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/surface_grid (build and run), or rerun ./build/examples/c/showcases/surface_grid |
| Python | Available | python3 -m examples.python.gallery.showcases.surface_grid |
| 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¶
SURFACE_ROWS by SURFACE_COLS height and color arrays are converted to geometry, rendered as a material mesh, and reused to derive unique edges for the segment wireframe. In the preview, compare the smooth shaded surface with the overlaid grid lines while rotating the arcball camera.
This workflow is useful for scientific surfaces where the same gridded data should support both shape perception and explicit mesh topology.
Source¶
#!/usr/bin/env python3
"""Generated height field rendered as a lit mesh with a wireframe overlay."""
from __future__ import annotations
import ctypes
import numpy as np
import datoviz as dvz
from examples.python.gallery import common as ex
SURFACE_ROWS = 80
SURFACE_COLS = 96
INITIAL_ANGLES = (ctypes.c_float * 3)(0.58, -0.18, 0.22)
PANEL_BG = dvz.DvzColor(13, 18, 24, 255)
COLOR_LOW = dvz.DvzColor(42, 132, 148, 255)
COLOR_HIGH = dvz.DvzColor(88, 214, 238, 255)
WIREFRAME = dvz.DvzColor(14, 34, 38, 92)
def _surface_data():
rows = np.arange(SURFACE_ROWS, dtype=np.float64)
cols = np.arange(SURFACE_COLS, dtype=np.float64)
y = -1.0 + 2.0 * rows[:, None] / (SURFACE_ROWS - 1)
x = -1.0 + 2.0 * cols[None, :] / (SURFACE_COLS - 1)
r2 = x * x + y * y
ridge = 0.34 * np.cos(15.0 * np.sqrt(r2)) * np.exp(-1.25 * r2)
saddle = 0.13 * (x * x - 0.70 * y * y) * np.exp(-0.85 * r2)
ripple = 0.050 * np.sin(15.5 * x + 2.0 * y) * np.cos(10.0 * y - 2.5 * x)
heights = (ridge + saddle + ripple).astype(np.float64)
t = np.clip((heights + 0.26) / 0.58, 0.0, 1.0)
colors = np.zeros((SURFACE_ROWS, SURFACE_COLS, 4), dtype=np.uint8)
colors[..., 0] = ((1.0 - t) * COLOR_LOW.r + t * COLOR_HIGH.r).astype(np.uint8)
colors[..., 1] = ((1.0 - t) * COLOR_LOW.g + t * COLOR_HIGH.g).astype(np.uint8)
colors[..., 2] = ((1.0 - t) * COLOR_LOW.b + t * COLOR_HIGH.b).astype(np.uint8)
colors[..., 3] = 235
return np.ascontiguousarray(heights.ravel()), np.ascontiguousarray(colors.reshape(-1, 4))
def _grid_positions(heights):
rows = np.arange(SURFACE_ROWS, dtype=np.float64)
cols = np.arange(SURFACE_COLS, dtype=np.float64)
row, col = np.meshgrid(rows, cols, indexing="ij")
positions = np.zeros((SURFACE_ROWS * SURFACE_COLS, 3), dtype=np.float64)
positions[:, 0] = -2.70 + col.ravel() * (2.0 * 2.70 / (SURFACE_COLS - 1))
positions[:, 1] = 1.18 * heights
positions[:, 2] = +2.00 - row.ravel() * (2.0 * 2.00 / (SURFACE_ROWS - 1))
return positions
def _surface_geometry(heights, colors):
desc = dvz.dvz_geometry_surface_grid_desc()
desc.rows = SURFACE_ROWS
desc.cols = SURFACE_COLS
desc.heights = heights.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
desc.colors = colors.ctypes.data_as(ctypes.POINTER(dvz.DvzColor))
desc.origin[:] = (-2.70, 0.0, +2.00)
desc.col_basis[:] = (2.0 * 2.70 / (SURFACE_COLS - 1), 0.0, 0.0)
desc.row_basis[:] = (0.0, 0.0, -2.0 * 2.00 / (SURFACE_ROWS - 1))
desc.height_axis[:] = (0.0, 1.0, 0.0)
desc.height_scale = 1.18
geometry = dvz.dvz_geometry_surface_grid(ctypes.byref(desc))
if not geometry:
raise RuntimeError("dvz_geometry_surface_grid() failed")
return geometry
def _surface_material():
material = dvz.dvz_phong_material_desc()
material.phong.ambient = 0.34
material.phong.diffuse = 0.72
material.phong.specular = 0.22
material.phong.shininess = 40.0
material.light_direction[:] = (-0.32, -0.58, -0.74)
return material
def _add_surface(scene, panel, geometry) -> None:
mesh = dvz.dvz_mesh(scene, 0)
if not mesh:
raise RuntimeError("dvz_mesh() failed")
material = _surface_material()
if dvz.dvz_visual_set_material(mesh, ctypes.byref(material)) != 0:
raise RuntimeError("dvz_visual_set_material() failed")
if dvz.dvz_mesh_set_geometry(mesh, geometry) != 0:
raise RuntimeError("dvz_mesh_set_geometry() failed")
ex.add_visual(panel, mesh)
def _add_wireframe(scene, panel, geometry, positions) -> None:
edges = dvz.dvz_geometry_edges(geometry)
if not edges:
raise RuntimeError("dvz_geometry_edges() failed")
try:
edge_data = edges.contents
count = int(edge_data.edge_count)
if count == 0:
raise RuntimeError("empty geometry edge list")
starts = np.zeros((count, 3), dtype=np.float32)
ends = np.zeros((count, 3), dtype=np.float32)
colors = np.empty((count, 4), dtype=np.uint8)
widths = np.full(count, 1.10, dtype=np.float32)
for i in range(count):
edge = edge_data.edges[i]
starts[i] = positions[int(edge.v0)]
ends[i] = positions[int(edge.v1)]
starts[i, 1] += 0.003
ends[i, 1] += 0.003
colors[:] = (WIREFRAME.r, WIREFRAME.g, WIREFRAME.b, WIREFRAME.a)
wire = dvz.dvz_segment(scene, 0)
if not wire:
raise RuntimeError("dvz_segment() failed")
if dvz.dvz_visual_set_data_many(
wire,
{
"position_start": starts,
"position_end": ends,
"color": colors,
"stroke_width_px": widths,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(wireframe) failed")
if dvz.dvz_segment_set_caps(wire, dvz.DVZ_SEGMENT_CAP_BUTT, dvz.DVZ_SEGMENT_CAP_BUTT) != 0:
raise RuntimeError("dvz_segment_set_caps() failed")
if dvz.dvz_visual_set_alpha_mode(wire, dvz.DVZ_ALPHA_BLENDED) != 0:
raise RuntimeError("dvz_visual_set_alpha_mode(wireframe) failed")
ex.add_visual(panel, wire)
finally:
dvz.dvz_geometry_edges_destroy(edges)
def _setup_camera(panel) -> None:
camera = dvz.dvz_camera_desc()
camera.view.eye[:] = (3.25, 2.25, 3.20)
camera.view.target[:] = (0.0, 0.0, 0.0)
camera.view.up[:] = (0.0, 1.0, 0.0)
camera.projection.fov_y = 0.58
camera.projection.near_clip = 0.05
camera.projection.far_clip = 100.0
if dvz.dvz_panel_set_camera_desc(panel, ctypes.byref(camera)) != 0:
raise RuntimeError("dvz_panel_set_camera_desc() failed")
def _build_scene():
heights, colors = _surface_data()
positions = _grid_positions(heights)
geometry = _surface_geometry(heights, colors)
try:
scene, figure, panel = ex.scene_panel()
dvz.dvz_panel_set_background_color(panel, PANEL_BG)
_setup_camera(panel)
_add_surface(scene, panel, geometry)
_add_wireframe(scene, panel, geometry, positions)
return scene, figure, panel
finally:
dvz.dvz_geometry_destroy(geometry)
def _configure_view(view, scene, panel) -> None:
arcball = dvz.dvz_view_arcball(view, panel, None)
if not arcball:
raise RuntimeError("dvz_view_arcball() failed")
if dvz.dvz_arcball_set(arcball, INITIAL_ANGLES) != 0:
raise RuntimeError("dvz_arcball_set() failed")
def main() -> None:
scene, figure, panel = _build_scene()
def configure(view) -> None:
_configure_view(view, scene, panel)
ex.run_with_view(scene, figure, "Surface Grid", 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
*/
/* surface_grid - This example turns a generated height field into a lit mesh and wireframe.
*
* What to look for: SURFACE_ROWS by SURFACE_COLS height and color arrays are converted to
* geometry, rendered as a material mesh, and reused to derive unique edges for the segment
* wireframe. In the preview, compare the smooth shaded surface with the overlaid grid lines while
* rotating the arcball camera.
*
* This workflow is useful for scientific surfaces where the same gridded data should support both
* shape perception and explicit mesh topology.
*
* Scenario: showcases_surface_grid
* Style: showcase, graphite_cyan, 1280x720 window target
*
* Build: just example-c showcases/surface_grid
* Run: ./build/examples/c/showcases/surface_grid --live
* Smoke: ./build/examples/c/showcases/surface_grid --png
*/
/*************************************************************************************************/
/* Includes */
/*************************************************************************************************/
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include "_alloc.h"
#include "_assertions.h"
#include "datoviz/geom.h"
#include "datoviz/scene.h"
#include "example_common.h"
#include "example_style.h"
#include "runner/scenario_runner.h"
/*************************************************************************************************/
/* Forward declarations */
/*************************************************************************************************/
DvzScenarioSpec dvz_showcase_surface_grid_scenario(void);
/*************************************************************************************************/
/* Constants */
/*************************************************************************************************/
#define WIDTH EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define SURFACE_ROWS 80u
#define SURFACE_COLS 96u
/*************************************************************************************************/
/* Structs */
/*************************************************************************************************/
typedef struct SurfaceGridState
{
DvzGeometry* geometry;
DvzGeometryEdges* edges;
} SurfaceGridState;
/*************************************************************************************************/
/* Helpers */
/*************************************************************************************************/
/**
* Fill a deterministic height field and color ramp.
*
* @param heights output heights
* @param colors output colors
*/
static void _surface_data(double* heights, DvzColor* colors)
{
ANN(heights);
ANN(colors);
for (uint32_t row = 0; row < SURFACE_ROWS; row++)
{
const double y = -1.0 + 2.0 * (double)row / (double)(SURFACE_ROWS - 1u);
for (uint32_t col = 0; col < SURFACE_COLS; col++)
{
const double x = -1.0 + 2.0 * (double)col / (double)(SURFACE_COLS - 1u);
const double r2 = x * x + y * y;
const double ridge = 0.34 * cos(15.0 * sqrt(r2)) * exp(-1.25 * r2);
const double saddle = 0.13 * (x * x - 0.70 * y * y) * exp(-0.85 * r2);
const double ripple = 0.050 * sin(15.5 * x + 2.0 * y) * cos(10.0 * y - 2.5 * x);
const double z = ridge + saddle + ripple;
const uint32_t idx = row * SURFACE_COLS + col;
heights[idx] = z;
const double t = fmin(1.0, fmax(0.0, (z + 0.26) / 0.58));
const DvzColor lo = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
const DvzColor hi = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
colors[idx] = dvz_color_rgba(
(uint8_t)((1.0 - t) * lo.r + t * hi.r), (uint8_t)((1.0 - t) * lo.g + t * hi.g),
(uint8_t)((1.0 - t) * lo.b + t * hi.b), 235);
}
}
}
/**
* Add the lit surface mesh.
*
* @param scene scene owning visuals
* @param panel target panel
* @param geometry surface-grid geometry
* @return true on success
*/
static bool _add_surface(DvzScene* scene, DvzPanel* panel, const DvzGeometry* geometry)
{
ANN(scene);
ANN(panel);
ANN(geometry);
DvzVisual* mesh = dvz_mesh(scene, 0);
if (mesh == NULL)
return false;
DvzMaterialDesc material = example_default_phong_material_desc();
if (dvz_visual_set_material(mesh, &material) != 0)
return false;
if (dvz_mesh_set_geometry(mesh, geometry) != 0)
return false;
return dvz_panel_add_visual(panel, mesh, NULL) == 0;
}
/**
* Add a segment wireframe derived from unique geometry edges.
*
* @param scene scene owning visuals
* @param panel target panel
* @param geometry source geometry
* @param edges derived edge list
* @return true on success
*/
static bool _add_wireframe(
DvzScene* scene, DvzPanel* panel, const DvzGeometry* geometry, const DvzGeometryEdges* edges)
{
ANN(scene);
ANN(panel);
ANN(geometry);
ANN(edges);
if (edges->edge_count == 0)
return false;
vec3* starts = (vec3*)dvz_calloc(edges->edge_count, sizeof(vec3));
vec3* ends = (vec3*)dvz_calloc(edges->edge_count, sizeof(vec3));
DvzColor* colors = (DvzColor*)dvz_calloc(edges->edge_count, sizeof(DvzColor));
float* widths = (float*)dvz_calloc(edges->edge_count, sizeof(float));
if (starts == NULL || ends == NULL || colors == NULL || widths == NULL)
goto error;
const DvzColor wire_color = dvz_color_rgba(14, 34, 38, 92);
for (uint32_t i = 0; i < edges->edge_count; i++)
{
const DvzGeometryEdge* edge = &edges->edges[i];
const double* p0 = geometry->positions[edge->v0];
const double* p1 = geometry->positions[edge->v1];
starts[i][0] = (float)p0[0];
starts[i][1] = (float)(p0[1] + 0.003);
starts[i][2] = (float)p0[2];
ends[i][0] = (float)p1[0];
ends[i][1] = (float)(p1[1] + 0.003);
ends[i][2] = (float)p1[2];
colors[i] = wire_color;
widths[i] = 1.10f;
}
DvzVisual* wire = dvz_segment(scene, 0);
if (wire == NULL)
goto error;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position_start", .data = starts, .item_count = edges->edge_count},
{.attr_name = "position_end", .data = ends, .item_count = edges->edge_count},
{.attr_name = "color", .data = colors, .item_count = edges->edge_count},
{.attr_name = "stroke_width_px", .data = widths, .item_count = edges->edge_count},
};
if (dvz_visual_set_data_many(wire, updates, 4) != 0)
goto error;
if (dvz_segment_set_caps(wire, DVZ_SEGMENT_CAP_BUTT, DVZ_SEGMENT_CAP_BUTT) != 0)
goto error;
if (dvz_panel_add_visual(panel, wire, NULL) != 0)
goto error;
dvz_free(starts);
dvz_free(ends);
dvz_free(colors);
dvz_free(widths);
return true;
error:
dvz_free(starts);
dvz_free(ends);
dvz_free(colors);
dvz_free(widths);
return false;
}
/*************************************************************************************************/
/* Scenario callbacks */
/*************************************************************************************************/
/**
* Initialize the surface-grid showcase.
*
* @param ctx scenario context
* @param out_user scenario state output
* @return true on success
*/
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
if (ctx == NULL)
return false;
SurfaceGridState* state = (SurfaceGridState*)dvz_calloc(1, sizeof(*state));
if (state == NULL)
return false;
if (out_user != NULL)
*out_user = state;
const uint32_t vertex_count = SURFACE_ROWS * SURFACE_COLS;
double* heights = (double*)dvz_calloc(vertex_count, sizeof(double));
DvzColor* colors = (DvzColor*)dvz_calloc(vertex_count, sizeof(DvzColor));
if (heights == NULL || colors == NULL)
goto error;
_surface_data(heights, colors);
DvzGeometrySurfaceGridDesc desc = dvz_geometry_surface_grid_desc();
desc.rows = SURFACE_ROWS;
desc.cols = SURFACE_COLS;
desc.heights = heights;
desc.colors = colors;
desc.origin[0] = -2.7;
desc.origin[1] = 0.0;
desc.origin[2] = +2.00;
desc.col_basis[0] = 2 * 2.70 / (double)(SURFACE_COLS - 1u);
desc.col_basis[1] = 0.0;
desc.col_basis[2] = 0.0;
desc.row_basis[0] = 0.0;
desc.row_basis[1] = 0.0;
desc.row_basis[2] = -2 * 2.00 / (double)(SURFACE_ROWS - 1u);
desc.height_axis[0] = 0.0;
desc.height_axis[1] = 1.0;
desc.height_axis[2] = 0.0;
desc.height_scale = 1.18;
state->geometry = dvz_geometry_surface_grid(&desc);
dvz_free(heights);
dvz_free(colors);
heights = NULL;
colors = NULL;
if (state->geometry == NULL)
goto error;
state->edges = dvz_geometry_edges(state->geometry);
if (state->edges == NULL)
goto error;
ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
if (ctx->figure == NULL)
goto error;
DvzPanel* panel = dvz_panel_full(ctx->figure);
if (panel == NULL)
goto error;
example_graphite_cyan_set_panel_background(panel);
if (example_set_default_3d_camera(panel, 1.80f) == NULL)
goto error;
if (!_add_surface(ctx->scene, panel, state->geometry))
goto error;
if (!_add_wireframe(ctx->scene, panel, state->geometry, state->edges))
goto error;
DvzController* controller = dvz_arcball(ctx->scene, NULL);
if (controller == NULL)
goto error;
if (dvz_scenario_bind_controller(ctx, panel, controller, DVZ_DIM_MASK_XYZ) != 0)
goto error;
return true;
error:
dvz_free(heights);
dvz_free(colors);
return false;
}
/**
* Destroy the surface-grid showcase state.
*
* @param ctx scenario context
* @param user scenario state
*/
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
(void)ctx;
SurfaceGridState* state = (SurfaceGridState*)user;
if (state == NULL)
return;
if (state->edges != NULL)
dvz_geometry_edges_destroy(state->edges);
if (state->geometry != NULL)
dvz_geometry_destroy(state->geometry);
dvz_free(state);
}
/**
* Return the surface-grid showcase scenario specification.
*
* @return scenario specification
*/
DvzScenarioSpec dvz_showcase_surface_grid_scenario(void)
{
return (DvzScenarioSpec){
.id = "showcases_surface_grid",
.title = "Surface Grid",
.width = WIDTH,
.height = HEIGHT,
.fps = 60.0,
.requirements =
DVZ_SCENARIO_REQ_MESH_VISUAL | DVZ_SCENARIO_REQ_CONTROLLER | DVZ_SCENARIO_REQ_ARCBALL,
.init = _scenario_init,
.destroy = _scenario_destroy,
};
}
/*************************************************************************************************/
/* Functions */
/*************************************************************************************************/
/**
* Run the surface-grid 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_surface_grid_scenario();
return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details
- ID:
showcases_surface_grid - Category:
showcase - Lane:
showcases - Status:
supported - Source:
examples/c/showcases/surface_grid.c - Approved adaptation starter:
no - Python source:
examples/python/gallery/showcases/surface_grid.py - Python adaptation: Available
- Browser support: Live in browser
- WebGPU live route:
examples/webgpu/live.html?id=showcases_surface_grid - Browser capability tags:
mesh,segment,arcball - Validation:
smoke+screenshot
Tags
scientific, synthetic, surface-grid, mesh, wireframe, arcball
Data
| Field | Value |
|---|---|
kind |
synthetic |
