A reusable WebGL2 GLSL shader for Three.js. It generates animated abstract thermal-camera forms: metaball fields, domain warp, noise, deformation, and a 1D thermal gradient. It is not an application — drop the shaders and uniforms into any site.
ShaderMaterial + GLSL3)import * as THREE from "three";
import {
VERTEX_SHADER,
FRAGMENT_SHADER,
createUniforms,
} from "./js/thermal-forms.js";
const material = new THREE.ShaderMaterial({
glslVersion: THREE.GLSL3,
vertexShader: VERTEX_SHADER,
fragmentShader: FRAGMENT_SHADER,
uniforms: createUniforms(THREE),
toneMapped: false,
});
Or createThermalMaterial(THREE, params), which is the same
ShaderMaterial with defaults filled in. Put it on a
PlaneGeometry(2, 2) with an orthographic camera from
-1..1 for a fullscreen field. Set
uResolution to the drawing-buffer size so aspect correction
stays correct.
Each fragment builds an aspect-corrected coordinate, domain-warps it
with 3D noise, and sums up to 12 metaball sources. The scalar field is
remapped with threshold, contrast and smoothness, then sampled through a
1D thermal LUT. 0 is cold / black, 1 is hot.
The LUT is sampled at (1.0 - thermal, 0.5).
The same seed + parameters + time always produce the same
frame. Source positions are packed on the CPU from the integer seed;
uSeed is a derived spatial offset, not that integer.
| Uniform | Type | Role |
|---|---|---|
uResolution |
vec2 | Drawing buffer size in pixels. Used for aspect correction. |
uTime |
float | Animation time in seconds. |
uLoop |
float | 0 = free-running time. 1 = closed loop using duration/origin. |
uDuration |
float | Loop period in seconds when uLoop is 1. |
uLoopOrigin |
float | Time at the start of a closed loop window. |
uSeed |
float | Spatial noise offset. Set via seedToOffset(seed)[0]. |
uScale |
float | Form scale. Larger values zoom out. |
uOffset |
vec2 | Pan in aspect-corrected field space. |
uDetail |
float | Domain-warp frequency. |
uComplexity |
float | Warp octaves and cavity modulation. |
uDistortion |
float | Spatial warp amplitude. |
uContrast |
float | Field remap gamma. |
uThreshold |
float | Field floor before remap. |
uSmoothness |
float | Soft edge width after remap. |
uGlow |
float | In-shader thermal halo mix. |
uMotionSpeed |
float | Time scale for noise and source motion. |
uMotionAmount |
float | Source drift amplitude. |
uDeformation |
float | Flow, spin, and extra 3D-noise warp. |
uPulse |
float | Source radius / strength breathing. |
uTurnover |
float | Presence fade and extra-source mix. |
uSourceCount |
int | Packed sources in use, 1–12. |
uBaseCount |
int | Primary source count before turnover extras. |
uSources[12] |
vec4 | xy position, z radius, w strength. |
uSourceXform[12] |
vec4 | x aspect, y rotation, z local distortion, w phase. |
uGradient |
sampler2D | 1×N LUT. Sampled at (1.0 - thermal, 0.5). |
uUseImage |
float | 0 = procedural metaballs. 1 = sample image/video luma. |
uSourceImage |
sampler2D | Source image or video frame. Luma drives the thermal field. |
uImageField |
sampler2D | Optional softened field when uInventFalloff is 1. |
uImageSize |
vec2 | Source size in pixels. Used for aspect-correct sampling. |
uInventFalloff |
float | 1 = remapped soft field. 0 = direct luma remap. |
uInvert |
float | 1 = invert source luma before remap. |
Metaball layout is deterministic. Call
generateSourcePool(seed, amount) then
packSources(uniforms, seed, amount), or let
createUniforms / setThermalParams do it.
amount is 1–12 and becomes uBaseCount. The
pool always holds 12 slots so turnover can fade extras in.
Optional media mode replaces the procedural metaball field with the luminance of an image or video. Domain warp, scale, offset, threshold, contrast, smoothness, deformation and the thermal LUT still apply.
import {
createThermalMaterial,
createSourceTexture,
createVideoSourceTexture,
setThermalSource,
clearThermalSource,
} from "./js/thermal-forms.js";
const material = createThermalMaterial(THREE);
// Still image
const image = await loadImage(file);
const texture = createSourceTexture(THREE, image);
setThermalSource(material, texture, image.naturalWidth, image.naturalHeight, {
inventFalloff: false,
invert: false,
});
// Video frame (advance uTime; VideoTexture updates each frame)
const video = document.createElement("video");
video.src = url;
video.muted = true;
video.loop = true;
await video.play();
const videoTex = createVideoSourceTexture(THREE, video);
setThermalSource(material, videoTex, video.videoWidth, video.videoHeight);
// Back to procedural forms
clearThermalSource(material);
Set uImageSize to the true media size so aspect correction
matches the source. For a soft invented falloff, bind a blurred copy to
uImageField and leave uInventFalloff = 1; for
direct luma sampling set uInventFalloff = 0 and reuse the
same texture for both samplers. See
example 04.
Default stops (hot → cold):
0% #830100,
20% #FF2E01,
40% #FFD702,
60% #7FFF81,
80% #04D3FF,
90% #0F29FF,
100% #000000.
Build a 1024×1 DataTexture with
createGradientTexture(THREE, stops).
For a clip that returns to its first frame, set uLoop = 1,
uDuration to the period, and uLoopOrigin to the
start time. Motion then orbits extra noise axes instead of yo-yoing.
glsl/thermal-forms.vert.glsl — ShaderMaterial vertexglsl/thermal-forms.frag.glsl — assembled fragmentjs/thermal-forms.js — uniforms, seed, sources, gradientexamples/ — Three.js ShaderMaterial demosdocs/ — this pageThis package is only the thermal-forms shader and a thin Three.js helper. It does not include application UI, Tweakpane, presets, PNG/MP4 export, overlays, or any backend. Image/video support is limited to the public uniforms and helper APIs above — you supply the textures yourself.