Skip to content

Donitzo/three.js-volume-renderer

Repository files navigation

three.js Volume Renderer

A lightweight volume renderer for three.js that uses raymarching to render procedurally defined or data-driven 3D volumes in real time.

Description

The volume renderer is implemented as a single VolumeRenderer class that extends THREE.Mesh with a raymarching fragment shader. You can either provide your own 3D volumetric data or supply a custom function in GLSL to create complex procedural shapes or surfaces.

The renderer works as a fullscreen postprocessing effect which renders on top of existing geometry.

The volume renderer features:

  • Shader features can be toggled at compile-time using #define directives, keeping it lightweight and versatile for different use cases, e.g. for in-game smoke, MRI scans, and other volumetric data.
  • Normal estimation for lighting.
  • Depth testing.
  • Clip planes.
  • Color palettes with transparent cutoff range.
  • Extinction coefficients for translucency.
  • Rendering of static or animated 3D volume data atlas texture. This could for example be an MRI or smoke.
  • Sampling from THREE.Mesh surfaces as volumetric shapes via VolumeSamplers.js.

What is Raymarching?

Raymarching is a rendering technique where, for each pixel, we cast a ray into a scene and advance it in small steps (imagine a cone of WxH rays shooting out from the camera). At each step along the ray, we sample volume data (e.g. density, extinction coefficient, distance function) and accumulate it (e.g., via alpha blending or by estimating a mean value) until the ray exits the volume. Unlike surface-based raymarchers that stop at the first hit, this approach processes the entire volume along the ray.

When alpha blending is used, an extinction coefficient determines how much light is absorbed at each step, allowing you to see through semi-transparent volumes like smoke or mist. If lighting is enabled, we also estimate a normal at each step by computing the forward difference of the volume data, letting you illuminate the volume with directional or point lights.

This raymarcher always takes a fixed number of steps along the ray, constrained to the intersecting volume. In the worst case, the ray spans the diagonal of the volume, and those steps are evenly distributed across that distance. Using a fixed step count ensures consistent loop length and predictable performance.

NIfTI Files

The demo app supports reading NIfTI files, both 3D and time-varying 4D data.

Name Sample 1 Sample 2
Iguana
Chris MRI
Soot Visibility

Samples

Below are some sample outputs generated by different distance functions. Each row shows an animated view alongside its corresponding normal rendering.

Name Animation Normals
Pulsing Sphere
Square Sphere
Doughnut
Rings
Twister
Gyroid
Tunnel
Mandelbulb
Wobbly Sphere
Surface
Smoke

Turning a Three.Mesh into a volume

The provided VolumeSamplers.js utility class can turn a manifold THREE.Mesh (or just its geometry) into a Signed Distance Field (SDF) volume. Here's an example that bakes a mesh into a 32³ atlas texture covering the volume:

import VolumeSamplers from './VolumeSamplers.js';

const sampler = VolumeSamplers.createMeshInstanceSdfSampler(mesh);

volumeRenderer.createAtlasTexture(
    new THREE.Vector3(32, 32, 32),
    new THREE.Vector3(-1, -1, -1),
    new THREE.Vector3(2 / 32, 2 / 32, 2 / 32),
    1
);

volumeRenderer.updateAtlasTexture((xi, yi, zi, x, y, z, t) => sampler(x, y, z);

Installation

Copy VolumeRenderer.js into your project and import it. You most likely have to update the Three.js import path in the file.

To run the demo app, simply clone this repo and host it on a local server.

Instructions

  1. Import the VolumeRenderer (update the three.js import in the file)

    import VolumeRenderer from './VolumeRenderer.js';
  2. Add a VolumeRenderer instance to the scene

    const volumeRenderer = new VolumeRenderer();
    scene.add(volumeRenderer);
  3. Load or define volume data

    • Option A: Call volumeRenderer.createAtlasTexture(...) to set up a 3D texture for your data, then fill it with actual values using volumeRenderer.updateAtlasTexture(...).
    • Option B: Provide a custom distance function in volumeRenderer.updateMaterial({ customFunction: myGLSLFunction }).
  4. Update shader defines and uniforms

    • The shader’s behavior is configured by defines, which you can set via volumeRenderer.updateMaterial(...).
    • There are many different uniforms to configure under volumeRenderer.uniforms.
    • Update these uniforms each frame in your main loop:
      volumeRenderer.uniforms.time.value += dt;
      volumeRenderer.uniforms.random.value = Math.random();

Note: Keep in mind that the number of ray steps has a large impact on performance and quality. 32 steps is a reasonable compromise, but it can look okay at even less steps.

VolumeRenderer API

updateMaterial(options = {})

Creates a new shader material based on provided options.

Parameters

  • options.customFunction string|null A custom GLSL function that overrides the default volume sampling.

    When provided, this function is injected into the fragment shader to calculate the sampled scalar value at a given voxel position and time step.

    Expected Signature

    float sampleValue(float x, float y, float z, float t) {
      // {your custom code goes here, do not include function definition}
      // x, y, z: local position inside the volume (in world-space units relative to volumeOrigin)
      // t:       current time
      // return:  scalar value at that point
    }
  • options.useVolumetricDepthTest boolean (default: false) Enables volumetric depth testing (expects uniform.depthTexture to be set).

  • options.useExtinctionCoefficient boolean (default: true) Enables extinction coefficient for alpha blending.

  • options.useValueAsExtinctionCoefficient boolean (default: false) Uses the sampled value as the extinction coefficient (such as when you use CE as a scalar field).

  • options.usePointLights boolean (default: false) Enables point lights (normals are estimated, decreases performance).

  • options.useDirectionalLights boolean (default: false) Enables directional lights (normals are estimated, decreases performance).

  • options.useRandomStart boolean (default: true) Randomizes ray start position to soften edges.

  • options.renderMeanValue boolean (default: false) Renders the mean value across the volume instead of alpha blending.

  • options.invertNormals boolean (default: false) Inverts all surface normals.

  • options.renderNormals boolean (default: false) Renders surface normals at the first hit (normals are estimated, decreases performance).

  • options.raySteps number (default: 64) Number of ray steps for sampling. Scales linearly with performance.


createAtlasTexture(volumeResolution, volumeOrigin, voxelSize, timeCount, textureFilter = THREE.LinearFilter)

Creates a half-precision 3D atlas texture and updates uniforms.

Parameters

  • volumeResolution THREE.Vector3 3D resolution (voxel count) of one volume.

  • volumeOrigin THREE.Vector3 3D world-space origin of the volume.

  • voxelSize THREE.Vector3 Physical 3D size of one voxel.

  • timeCount number Total number of timesteps.

  • textureFilter number (default: THREE.LinearFilter) Texture interpolation mode.


updateAtlasTexture(sampler, timeOffset = null, timeCount = null)

Samples new values into the 3D atlas texture.

Parameters

  • sampler Function Function signature: (xi:number, yi:number, zi:number, x:number, y:number, z:number, ti:number) => number

  • timeOffset number|null Starting time index (default: 0).

  • timeCount number|null Number of timesteps to update (default: full atlas count).

Returns

  • object containing:
    • minValue number – minimum sampled value.
    • maxValue number – maximum sampled value.

Material uniforms

depthTexture

Depth texture for volumetric depth testing.
Active only when useVolumetricDepthTest is true.

volumeOrigin

The world-space origin of the volume.

volumeSize

The world-space size of the volume.
Active only when customFunction is provided.

volumeAtlas

The 3D texture containing packed volume data.
Active only when customFunction is not provided.

atlasResolution

Number of volumes packed along each axis in the atlas.
Active only when customFunction is not provided.

volumeResolution

Resolution (voxel count) of a single volume.
Active only when customFunction is not provided.

voxelSize

The physical size of a single voxel.
Active only when customFunction is not provided.

clipMin

Minimum clipping planes (XYZ).

clipMax

Maximum clipping planes (XYZ).

timeCount

Total number of volumes (timesteps) stored in the atlas.
Active only when customFunction is not provided.

time

The current time, represented either as a fractional volume index or as the time parameter for the custom function.

random

A random value used when initializing rays.
Helps “fuzz” ray starts when useRandomStart is true.

normalEpsilon

Real-unit epsilon used for estimating normals via forward differences.
Active when renderNormals is true,
or when renderMeanValue is false and (usePointLights or useDirectionalLights is true).

palette

Horizontal palette texture for mapping sampled values to colors. Should be a horizontal palette. Active only when renderNormals is false.

minPaletteValue

Minimum value used for palette mapping.
Active only when renderNormals is false.

maxPaletteValue

Maximum value used for palette mapping.
Active only when renderNormals is false.

minCutoffValue

Minimum cutoff value.
Sampled values below this threshold are discarded.

maxCutoffValue

Maximum cutoff value.
Sampled values above this threshold are discarded.

cutoffFadeRange

Range near the cutoff where alpha fades to zero.
Active only when renderMeanValue is false.

valueMultiplier

Multiplier applied to sampled values.

valueAdded

Constant added to sampled values after multiplication.

extinctionCoefficient

Fixed extinction coefficient used for alpha blending.
Active only when useExtinctionCoefficient is true,
useValueAsExtinctionCoefficient is false,
and renderNormals is false.

extinctionMultiplier

Multiplier applied to the extinction coefficient.
Active only when useExtinctionCoefficient is true
and renderNormals is false.

alphaMultiplier

Multiplier applied to the final alpha value.
Active only when renderNormals is false.

Attribution

Feedback & Bug Reports

If there are additional variations you would find useful, or if you find any bugs or have other feedback, please open an issue.