Try the demo on GitHub pages: https://donitzo.github.io/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.
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 viaVolumeSamplers.js
.
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.
The demo app supports reading NIfTI files, both 3D and time-varying 4D data.
Name | Sample 1 | Sample 2 |
---|---|---|
Iguana | ![]() |
![]() |
Chris MRI | ![]() |
![]() |
Soot Visibility | ![]() |
![]() |
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 | ![]() |
![]() |
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);
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.
-
Import the VolumeRenderer (update the three.js import in the file)
import VolumeRenderer from './VolumeRenderer.js';
-
Add a VolumeRenderer instance to the scene
const volumeRenderer = new VolumeRenderer(); scene.add(volumeRenderer);
-
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 usingvolumeRenderer.updateAtlasTexture(...)
. - Option B: Provide a custom distance function in
volumeRenderer.updateMaterial({ customFunction: myGLSLFunction })
.
- Option A: Call
-
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();
- The shader’s behavior is configured by defines, which you can set via
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.
Creates a new shader material based on provided options.
-
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.
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 (expectsuniform.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.
-
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.
Samples new values into the 3D atlas texture.
-
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).
- object containing:
minValue
number
– minimum sampled value.maxValue
number
– maximum sampled value.
Depth texture for volumetric depth testing.
Active only when useVolumetricDepthTest
is true
.
The world-space origin of the volume.
The world-space size of the volume.
Active only when customFunction
is provided.
The 3D texture containing packed volume data.
Active only when customFunction
is not provided.
Number of volumes packed along each axis in the atlas.
Active only when customFunction
is not provided.
Resolution (voxel count) of a single volume.
Active only when customFunction
is not provided.
The physical size of a single voxel.
Active only when customFunction
is not provided.
Minimum clipping planes (XYZ).
Maximum clipping planes (XYZ).
Total number of volumes (timesteps) stored in the atlas.
Active only when customFunction
is not provided.
The current time, represented either as a fractional volume index or as the time parameter for the custom function.
A random value used when initializing rays.
Helps “fuzz” ray starts when useRandomStart
is true
.
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
).
Horizontal palette texture for mapping sampled values to colors. Should be a horizontal palette.
Active only when renderNormals
is false
.
Minimum value used for palette mapping.
Active only when renderNormals
is false
.
Maximum value used for palette mapping.
Active only when renderNormals
is false
.
Minimum cutoff value.
Sampled values below this threshold are discarded.
Maximum cutoff value.
Sampled values above this threshold are discarded.
Range near the cutoff where alpha fades to zero.
Active only when renderMeanValue
is false
.
Multiplier applied to sampled values.
Constant added to sampled values after multiplication.
Fixed extinction coefficient used for alpha blending.
Active only when useExtinctionCoefficient
is true
,
useValueAsExtinctionCoefficient
is false
,
and renderNormals
is false
.
Multiplier applied to the extinction coefficient.
Active only when useExtinctionCoefficient
is true
and renderNormals
is false
.
Multiplier applied to the final alpha value.
Active only when renderNormals
is false
.
-
NIFTI-Reader-JS - MIT
-
Smoke created using FDS
-
Skybox by Paul Debevec
-
Chris MRI from McCausland Center for Brain Imaging — CC BY-NC 4.0
-
Desert Iguana from Dr. Jessie Maisano, University of Texas High-Resolution X-ray CT Facility Archive 0787 — CC BY-NC 4.0
If there are additional variations you would find useful, or if you find any bugs or have other feedback, please open an issue.