Volumes#
A volume is the object whose appearance is decided almost entirely by how light is
transported rather than by what a surface does, which makes it the one place where the
choice of renderer changes the picture rather than the polish. Everything on this page
applies to k3d.volume; mip follows it where it says so.
Materials#
Volumetric objects (volume, mip) carry the same two knobs for the
specular highlight of their isodensity surface (default roughness=0.25) -
lower roughness makes noisy gradients sparkle like wet tissue, which may even
be desired. metalness tints and strengthens the highlight with the
transfer-function colour; it never darkens the body, because a volume has no
environment reflection to replace the lost diffuse light with.
Light and the environment#
Volumetric data (volume, mip) and the points 3d impostors read the same
environment: diffuse light from the map’s spherical harmonics plus one dominant
directional light distilled from it, so a directional HDRI models volumes consistently
with every mesh in the scene.
In advanced a volume also contributes to the occlusion pass - the shell where its
accumulated opacity crosses one half - so dense structures cast and receive contact
shadows like real surfaces. The two knobs are plot.ao_radius and
plot.ao_strength, described in Renderers.
Composing with geometry#
Since 3.0.0 a volume composes correctly with meshes that intersect it
when depth peeling is enabled (plot.depth_peels >= 3 - fewer layers make
the segmentation too coarse to be predictable). The ray march is split into
segments bounded by the peel layers, so geometry inside the volume occludes
and is occluded sample-accurately, in both renderers:
Volumetric data (volume, mip) and the points 3d impostors read the
same environment: diffuse light from the map’s spherical harmonics plus one
dominant directional light distilled from it, so a directional HDRI models
volumes consistently with every mesh in the scene.
A volume through each renderer#
A volume is where the three differ most, because it is the one object whose appearance is
almost entirely a question of how light is transported rather than of what the surface
does. The same cardiac CT below, at the same alpha_coef, the same transfer function,
the same camera and the same environment:
Every one of the four is a full HD render - click it to open it at full size, because the
difference between advanced and cinematic lives in detail a page-width figure loses.
Two things are deliberately not held constant, and both are worth knowing. The volume’s
shadow is off in the first image and on in the second, which is the point of having
both. And light_scale is 2.25, which lifts only the traced image, because the raster
shader does not read it: the march lights every sample locally and counts the environment
twice while the tracer attenuates, so at this density an unlifted traced image is much the
darkest of the four and the comparison would be about exposure instead of about light.
In the path tracer#
A volume is part of the light simulation. Its bounding box sits in the
acceleration structure as the boundary of a medium, and inside it every ray -
camera rays, bounced rays, shadow rays - is tracked through the 3D texture with
the same transfer function the other renderers use. How much matter a ray meets
matches the raster exactly: alpha_coef and the opacity function give the same
optical depth here as in advanced, so a volume tuned there keeps its density
here and only the lighting changes. Geometry inside the gas is shadowed by it and
occludes it, the gas appears in reflections, and light scattered inside it is
the environment’s.
Where the density changes sharply the medium is shaded as a surface: at every
collision the tracer measures the change of the normalised intensity over one
gradient_step and, with probability \(1 - e^{-8\,m}\) for a change
\(m\), treats the point as a rough dielectric facing the falling density,
with the transfer function colour and the volume’s own roughness and
metalness. Bone and skin in a CT get highlights, Fresnel and an orientation;
soft tissue with gentle gradients stays gas. Gas events scatter forward with a
Henyey-Greenstein phase function (asymmetry 0.85, as measured tissue does), so light
reaches deeper than an isotropic phase would let it:
What to expect from a physically traced volume, as opposed to the ray march:
the march shades every sample locally and without occlusion, and lights it with the environment counted twice - its harmonics plus a directional light distilled from the same map - so a dense volume glows with its interior colours. The tracer attenuates instead, so the two agree only where the volume is thin: on a CT scan at
alpha_coef15 the traced image is the brighter of the two, at 200 it is a third of the march. Loweralpha_coefor a brighter colour map does here what it does to real fog, androughnessandmetalnessshape the highlights;the medium has its own exposure,
light_scaleon the volume, because a brighter environment lifts the whole scene while a dense volume needs more light than the geometry around it. It multiplies what reaches an event inside the medium, so every ratio in the image survives - a crevice stays as much darker than an exposed surface as it was - and only the exposure moves. It cannot rescue a pitch-black deep interior: where no light arrives there is nothing to multiply, andcinematic_bouncesis what carries light in there;samplesis not used: there is no fixed step to set. The tracker draws the distance to the next collision from the density itself, which needs no sampling rate and introduces no error of its own -cinematic_samplestrades noise for time instead, andalpha_coefsets the cost, since it sets how often a ray is interrupted;the surface decision is made per collision from the local gradient, so a transfer function that ramps gently over a boundary gives a softer, gassier edge than one that steps;
gradient_stepsets the scale of the gradient, in the volume’s mean edge length, and is the same knob the other renderers use;cost follows the density in front of the ray, not the densest voxel in the box. The tracker keeps a grid of macrocells, each holding the largest extinction any point inside it can have, and walks that grid: a cell of air is crossed in one step, and a thin region is stepped at its own rate. On a 512x512x1319 CT scan with an opacity function rising across its colour range, 40% of the cells hold nothing at all and the mean majorant is a ninth of the global one, so an average ray tests about a ninth as many collisions - in exchange for a few hundred cell crossings, which are far cheaper than a sample of a volume too large to sit in cache. A ray straight through the densest bone still saves close to half, which is the honest limit: dense matter costs, and
alpha_coefstill sets how often a ray inside it is interrupted;one volume per plot is traced; any further
volumestays on the raster overlay with a warning. So does a volume with a mask (mask,mask_opacities): the medium does not read the mask, and tracing the volume as if the mask were not there would be worse than not tracing it. So does a volume that carries colour per voxel, for the reason given above - the medium reads one channel as density and takes its colour from a transfer function, and there is no transfer function there to read;the medium needs two more texture units than the tracer’s surfaces do, one for its data and one for its majorant grid; a context that cannot provide fifteen keeps the volume on the raster overlay and says so once in the console.
mip is a maximum-intensity projection, a diagnostic view rather than a
physical one, and stays what it was: ray-marched as in advanced, stopping at
the first traced surface, and composited over the traced image outside the
light simulation.
Colour per voxel#
A scan can measure colour rather than a quantity to map. The Visible Human cryosections are
photographs, and an RGB-encoded NIfTI carries them as three bytes a voxel. A 4D array of
uint8 shaped [z, y, x, 3] or [z, y, x, 4] passed to volume, volume_slice
or mip is drawn as the colour it is. Nothing is left for a colormap to do, so color_map
and color_range are refused with a warning rather than quietly ignored, and the uint8 is
kept rather than cast - a cast to float32 would quadruple a photographic volume without
adding precision the data has.
One thing has to be invented, and it is the alpha: the march has to know where to stop, and
colour does not say. It comes from Rec. 709 luminance shaped by opacity_function, which is
therefore the whole transfer function here, and the same luminance feeds the gradient the shader
lights with, so a colour volume shades like any other.
Where that ramp rises matters more than it would for a scalar field, and it is the one thing to
know before reaching for this. A volume is sampled trilinearly, so every surface has a rim where
the texture fades in. A scalar field hides it: whatever value the ray stops at, the colormap turns
it into a full-intensity colour. Here the value is the colour, so a ray stopping halfway up the
rim paints a half-bright one. Measured on a white ball, a ramp rising from 0.02 renders it at 76
levels, the same ball with the ramp rising from 0.45 renders at 249, and with
interpolation=False the low ramp renders at 255, because there is no rim to stop in. Start the
ramp where the data is already itself.
mip maximises that same luminance and keeps the colour of the voxel that reached it, because a
maximum has to be a maximum of something. On this head that is bone and teeth, through the skin,
in the colours they have:
The path tracer has no medium for this. Its density is a single channel and its colour comes from a transfer function, so an RGB volume stays on the rasterised layer with a warning, alongside a masked volume and any volume past the first.
Reading one is a question for the file, not for k3d. SimpleITK returns RGB24 straight as
[z, y, x, 3] uint8, the order a volume is indexed in; nibabel hands the same file over as
a structured dtype, one uint8 field per channel, which needs a view before it is an
ordinary array. A film needs neither: (frames, height, width, 3) is already the shape, with
time where depth usually goes. examples/volume_rgb.ipynb loads this head and ends with ten
seconds of video as a space-time block.
Note
The head on this page is visiblehuman.nii.gz from niivue-images, an RGB24 NIfTI of Visible Human Project cryosection photographs
(U.S. National Library of Medicine). 196 x 240 x 256 voxels at 1 mm, 36 MB unpacked.