Gallery
Indigo Night Volumetric Smoke
GLSL ShaderHeavy
A slow-moving, realistic volumetric smoke effect rendered on the GPU.
#realistic#shader#glsl#smoke#indigo-night
Live preview
1 file
$
npm i three @react-three/fiber/*
Volumetric Smoke (React + react-three-fiber, GLSL)
Setup with Vite:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm i three @react-three/fiber
package.json (dependencies):
"react": "^19.0.0",
"react-dom": "^19.0.0",
"three": "^0.185.1",
"@react-three/fiber": "^9.0.0"
Next.js App Router: add "use client" as the first line of this file.
Render <SceneBackground /> inside a container with position: relative.
*/
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three";
const vertexShader = `varying vec2 vUv;
void main() { vUv = uv; gl_Position = vec4(position, 1.0); }`;
const fragmentShader = `precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform vec2 uResolution;
uniform float uLight;
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec2 mod289(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}
vec3 permute(vec3 x){return mod289(((x*34.0)+1.0)*x);}
float snoise(vec2 v){
const vec4 C=vec4(0.211324865,0.366025403,-0.577350269,0.024390243);
vec2 i=floor(v+dot(v,C.yy)); vec2 x0=v-i+dot(i,C.xx);
vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0);
vec4 x12=x0.xyxy+C.xxzz; x12.xy-=i1; i=mod289(i);
vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));
vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0);
m=m*m; m=m*m; vec3 x=2.0*fract(p*C.www)-1.0; vec3 hh=abs(x)-0.5; vec3 ox=floor(x+0.5); vec3 a0=x-ox;
m*=1.79284291-0.85373472*(a0*a0+hh*hh);
vec3 g; g.x=a0.x*x0.x+hh.x*x0.y; g.yz=a0.yz*x12.xz+hh.yz*x12.yw; return 130.0*dot(m,g);
}
float fbm(vec2 p){ float v=0.0,a=0.5; for(int i=0;i<6;i++){ v+=a*snoise(p); p*=2.0; a*=0.5; } return v; }
void main(){
float SP = 0.800;
vec3 cBg = mix(vec3(0.020,0.024,0.059),vec3(0.957,0.961,0.992),uLight);
vec3 cA = mix(vec3(0.506,0.549,0.973),vec3(0.263,0.220,0.792),uLight);
vec3 cB = mix(vec3(0.310,0.275,0.898),vec3(0.216,0.188,0.639),uLight);
vec3 cC = mix(vec3(0.647,0.706,0.988),vec3(0.310,0.275,0.898),uLight);
vec2 p = vUv * vec2(uResolution.x / uResolution.y, 1.0);
float t = uTime * SP * 0.5;
vec2 q = vec2(fbm(p * 2.0 + vec2(0.0, -t)), fbm(p * 2.0 + vec2(5.2, 1.3) - t * 0.8));
float d = fbm(p * 3.0 + q * 2.0 + vec2(0.0, -t * 0.6)) * 0.5 + 0.5;
float l = fbm(p * 3.0 + q * 2.0 + vec2(0.06, 0.10) + vec2(0.0, -t * 0.6)) * 0.5 + 0.5;
float shade = clamp((d - l) * 2.6, 0.0, 1.0);
vec3 smoke = mix(cB, cC, d);
smoke = mix(smoke, cA, shade * 0.55);
float dens = smoothstep(0.32, 0.85, d);
vec3 col = mix(cBg, smoke, dens);
gl_FragColor = vec4(col, 1.0);
}`;
/** 1 in light mode, 0 in dark. The shader mixes both palettes on it. */
function useLightScheme() {
const [light, setLight] = useState(false);
useEffect(() => {
const mq = matchMedia("(prefers-color-scheme: light)");
const sync = () => setLight(mq.matches);
sync();
mq.addEventListener("change", sync);
return () => mq.removeEventListener("change", sync);
}, []);
return light;
}
function Plane() {
const mat = useRef<THREE.ShaderMaterial>(null);
const { size } = useThree();
const light = useLightScheme();
const uniforms = useMemo(() => ({ uTime: { value: 0 }, uResolution: { value: new THREE.Vector2(size.width, size.height) }, uLight: { value: 0 } }), []);
useFrame((state) => {
if (mat.current) {
mat.current.uniforms.uTime.value = state.clock.elapsedTime;
mat.current.uniforms.uResolution.value.set(size.width, size.height);
// Eased rather than snapped, so a scheme flip crossfades.
const u = mat.current.uniforms.uLight;
u.value += ((light ? 1 : 0) - u.value) * 0.08;
}
});
return (
<mesh>
<planeGeometry args={[2, 2]} />
<shaderMaterial ref={mat} vertexShader={vertexShader} fragmentShader={fragmentShader} uniforms={uniforms} />
</mesh>
);
}
export function SceneBackground() {
return (
<Canvas style={{ position: "absolute", inset: 0 }} orthographic camera={{ position: [0, 0, 1], zoom: 1 }} dpr={[1, 2]}>
<Plane />
</Canvas>
);
}Copy or download any file. The HTML build loads Three.js from a CDN via an import map, so it runs by just opening index.html. The React build is react-three-fiber; setup notes live at the top of each file.
Variants
4 of this pattern