Thermal Forms Shader

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.

Public uniforms

482917
6
1
0
0
0.52
0.26
0.64
1.2
0.15
0.68
0.65
1.96
0.4
0.5
0.35
0

Requirements

Quick start

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.

What the shader does

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 API

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.

Sources and seed

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.

Image or video as source

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.

Gradient

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).

Closed loops

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.

Files

Not included

This 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.