Three.js From Zero · Article s0-06
Hologram Shader
Hologram Shader is Article s0-06 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 0 · Article 06 · Quick Wins
The sci-fi pattern you'll re-use everywhere: vertical scanlines, fresnel rim glow, low-poly object faintly showing through, occasional glitch jitter. ~80 lines of GLSL. Wraps any geometry.
The hologram recipe
Four effects layered:
- Scanlines — horizontal bands moving slowly up the object.
- Fresnel — bright at silhouette edges, dim at the center facing the camera. The glowy rim that says "hologram."
- Stripes — vertical "interference" pattern of slightly varying opacity.
- Glitch jitter — random horizontal offset, rarely.
Step 1 — Base material setup
const mat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color('#22d3ee') } },
vertexShader, fragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
All four switches matter:
transparent: true+ alpha output enables the see-through effect.AdditiveBlendingstacks bright pixels — that's where the glow comes from.DoubleSiderenders both faces of the mesh — you'll see the inside of the model through the front.depthWrite: falsestops the hologram from occluding itself, which would kill the see-through.
Step 2 — Vertex shader (with jitter)
const vertexShader = /*glsl*/`
varying vec3 vNormal;
varying vec3 vPosition;
uniform float uTime;
// Random hash
float rand(vec2 co) { return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453); }
void main() {
vec3 pos = position;
// Glitch: rare horizontal jump
float glitchStrip = step(0.97, sin(uTime * 5.0 + pos.y * 30.0));
pos.x += (rand(vec2(pos.y, uTime)) - 0.5) * 0.15 * glitchStrip;
vNormal = normalize(normalMatrix * normal);
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
The glitch is the most-asked-about part. The trick: step(0.97, ...) means "1.0 if greater than 0.97, else 0." That activates for 3% of any given Y/time combination, so the glitch only shows up rarely — believable, not constant.
Step 3 — Fragment shader: scanlines + fresnel + stripes
const fragmentShader = /*glsl*/`
varying vec3 vNormal;
varying vec3 vPosition;
uniform float uTime;
uniform vec3 uColor;
void main() {
// Scanlines moving upward
float scan = step(0.5, fract((vPosition.y - uTime * 0.5) * 20.0));
// Vertical stripes (interference)
float stripes = pow(0.5 + sin((vPosition.y + uTime * 0.1) * 60.0) * 0.5, 3.0);
// Fresnel: bright at edges, dim where facing camera
float fresnel = pow(1.0 - max(dot(vNormal, vec3(0.0, 0.0, 1.0)), 0.0), 3.0);
// Combine: stripes only show, scanlines double up, fresnel adds rim glow
float alpha = stripes * 0.6 + fresnel * 1.0 + scan * 0.1;
gl_FragColor = vec4(uColor, alpha);
}
`;
Three signal generators (scan, stripes, fresnel), combined with weights. Tweak the weights to taste — that's the whole hologram tunable surface.
Step 4 — Apply to any mesh
const torusKnot = new THREE.Mesh(
new THREE.TorusKnotGeometry(0.8, 0.25, 128, 16),
mat,
);
scene.add(torusKnot);
The shader doesn't care what mesh it's on. Try it on a Suzanne, an imported glTF, your own logo — the hologram look transfers because it's based on normals and local-space position, both of which every mesh has.
Step 5 — Pulsing intensity
To sell the holographic "I'm not stable" feel, slowly modulate a global intensity in the loop:
renderer.setAnimationLoop((t) => {
mat.uniforms.uTime.value = t * 0.001;
torusKnot.rotation.y = t * 0.0003;
renderer.render(scene, camera);
});
Common first-time pitfalls
transparent: true on the material. Or you have opacity set somewhere else overriding the shader's alpha.smoothstep instead of step for the scan. Anti-aliasing in shaders comes from smoothing transitions.fresnel * 1.0 to fresnel * 2.0; the rim glow is what gives volumetric feel.step(0.97, ...) threshold is wrong — you wrote step(..., 0.97) (arguments swapped). Order matters: step(edge, x) returns 1 when x >= edge.Exercises
- Multiple color stops. Pass two colors as uniforms, lerp between them based on vertical position. Bottom warm, top cool.
- Boot-up animation. Animate
uTimefrom 0 to 1 over 2 seconds on page load. Use it to mask the alpha from bottom to top. Hologram appears like a tractor beam. - React to mouse. Pass mouse coordinates as a uniform; offset the scanlines based on cursor distance. The hologram "tracks" the user.
UP NEXT
S0-07 — Fireworks → GPU-driven particle explosions on click. The party-mode demo.