Build a Light Pillar Effect in React with WebGL
Render a dramatic volumetric light beam rising from the ground using raymarching in a WebGL fragment shader.
Volumetric light is one of those effects that makes people stop scrolling. It looks physical, like the light is actually occupying space. LightPillar achieves this in the browser with a raymarching fragment shader built on Three.js. No 3D models, no textures, just math.
The final result
What we are building
A glowing vertical beam of light that rises from the base of the canvas and softens at the top. The beam has animated density variation, a color gradient from bottom to top, and an optional mouse-interactive rotation.
Setting up
npm install threeimport * as THREE from 'three';
import React, { useRef, useEffect, useState } from 'react';Building the component
The component generates the fragment shader dynamically, injecting quality settings as GLSL constants:
const settings = qualitySettings[quality];
const fragmentShader = `
// ...
const int ITERATIONS = ${settings.iterations};
const int WAVE_ITERATIONS = ${settings.waveIterations};
const float STEP_MULT = ${settings.stepMultiplier.toFixed(1)};
// ...
`;This sidesteps the GLSL limitation of not allowing loop bounds to be runtime values. By injecting the constant at compile time, the GPU can unroll the loop efficiently.
The raymarching loop is the heart of the effect. For each pixel, a ray is marched from a virtual camera position:
for(int i = 0; i < ITERATIONS; i++) {
vec3 pos = origin + direction * depth;
// Rotate position around Y axis
float newX = pos.x * rotCos - pos.z * rotSin;
float newZ = pos.x * rotSin + pos.z * rotCos;
pos.x = newX;
pos.z = newZ;
vec3 deformed = pos;
deformed.y *= uPillarHeight;
deformed = deformed + vec3(0.0, uTime, 0.0);
// Apply wave octaves
for(int j = 0; j < WAVE_ITERATIONS; j++) {
vec3 oscillation = cos(deformed.zxy * frequency - phase);
deformed += oscillation * amplitude;
frequency *= 2.0;
amplitude *= 0.5;
}
float fieldDistance = length(cosinePair) - 0.2;
float radialBound = length(pos.xz) - uPillarWidth;
// Smooth union of field and radial bounds
float k = 4.0;
float h = max(k - abs(-radialBound - (-fieldDistance)), 0.0);
fieldDistance = -(min(-radialBound, -fieldDistance) - h * h * 0.25 / k);The smooth union h * h * 0.25 / k blends the pillar's cylindrical boundary with the wave field, creating the organic edges. Each step accumulates color based on the current gradient and field proximity:
vec3 gradient = mix(uBottomColor, uTopColor, smoothstep(15.0, -15.0, pos.y));
color += gradient / fieldDistance;Dividing by fieldDistance is the key: small distances (near the surface) contribute large color values, and large distances contribute almost nothing. This naturally creates the glow falloff.
After raymarching, the accumulated color is compressed with tanh to keep it in a displayable range:
color = tanh(color * uGlowAmount / widthNormalization);The Three.js setup uses separate useEffect hooks for initialization and prop updates, so changing colors or intensity does not tear down and rebuild the WebGL context:
useEffect(() => {
// One-time setup: renderer, geometry, material
}, [webGLSupported, quality]);
useEffect(() => {
if (!materialRef.current) return;
materialRef.current.uniforms.uTopColor.value = parseColor(topColor);
}, [topColor]);How to use it
<div className="relative h-screen bg-black">
<LightPillar
topColor="#5227FF"
bottomColor="#FF9FFC"
intensity={1.0}
rotationSpeed={0.3}
pillarWidth={3.0}
glowAmount={0.005}
mixBlendMode="screen"
/>
<div className="relative z-10">Your content</div>
</div>The mixBlendMode="screen" style keeps dark areas transparent when compositing over a dark background.
Key takeaways
- Injecting loop counts as GLSL compile-time constants via template literals gives you quality presets without runtime branching overhead.
- The smooth union operation blends two signed distance fields so the pillar boundary feels organic rather than hard-edged.
- Separating init and update into multiple
useEffecthooks lets you change props without destroying the WebGL context.