Normal curves

Normal curves are lines that start on a transparent isosurface and follow the gradient of the scalar field into its interior. They are a middle ground between a 2-D section, which samples a plane, and an isosurface, which shows a single value: the curves sample the 3-D subdomain enclosed by the isosurface along a discrete set of lines, converge at local extrema, and are coloured by the scalar value, so the strength and direction of the gradient can be read off directly.

Source code: src/glsl/volume_normalcurves_initpoints.fx.glsl, src/glsl/volume_compute_normalcurves.fx.glsl, src/glsl/normal_curves_compute_shader.fx.glsl, src/glsl/tube_normalcurves.fx.glsl.

Computation of normal curves.

Fig. 50 Computation of normal curves. Seed points for the curves (green dots) are placed at the intersections between axis-aligned rays (black arrows) and the outer isosurface; only rays from two directions are shown for illustration. Grey arrows mark the regular start positions, black arrows the randomly disturbed ones. Only a single seed is allowed in each grid box of the yellow volume. Figure reproduced from Rautenhaus et al. (2015, doi:10.5194/gmd-8-2329-2015), Fig. 12, © Author(s) 2015. CC Attribution 3.0 License.

Seeding

The seeding step decides where curves start and is what makes the result look organic rather than gridded. It runs in a compute shader:

  1. Cast axis-aligned rays. Rays are cast through the data volume along all three world space axes (longitude, latitude, pressure), starting from regularly spaced positions. The spacing is a user setting and controls the overall curve density.

  2. Jitter the ray positions. If the rays started exactly on a regular grid, that regularity would be visible in the final image. Each start position is therefore displaced by a random offset, read from a distortion texture (distortTex) — grey versus black arrows in the figure.

  3. Find the isosurface crossings. Each ray is traversed with a fixed step size; when the isovalue is crossed, the exact position is refined by bisection (bisectionCorrection(), the same routine the raycaster uses). Up to eight crossings per ray are recorded, so curves also start on the far side of a surface.

  4. Enforce one seed per cell. In regions of high curvature many rays hit the isosurface at nearly the same place, which would produce a bundle of nearly identical curves. A regular volume with a cell size of the average ray spacing is laid over the scene, and a seed falling into an already occupied cell is discarded.

Integration

From each seed, a curve is integrated in parallel along the direction of the scalar field’s gradient, using a 4th-order Runge-Kutta scheme. The gradient is computed with the same routine used for isosurface shading, so curves and surface normals are consistent by construction. If an inner opaque isosurface is present, integration stops there.

Because the curves follow the gradient, they converge at local extrema of the field. Reading a normal curve image therefore means looking for the points where curves bundle together: those are the maxima (or minima) inside the outer isosurface, and their colour gives their strength.

Rendering

The resulting curves are rendered with the same technique as trajectories: a geometry shader turns each line segment into tube geometry (tube_normalcurves.fx.glsl). See Trajectory rendering.

Normal curves are also used outside the volume raycaster: the frontal surface detection integrates normal curves to collect per-vertex frontal parameters.