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.

← Three.js From ZeroS0-06 · Quick Wins

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.

— fps

The hologram recipe

Four effects layered:

  1. Scanlines — horizontal bands moving slowly up the object.
  2. Fresnel — bright at silhouette edges, dim at the center facing the camera. The glowy rim that says "hologram."
  3. Stripes — vertical "interference" pattern of slightly varying opacity.
  4. 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.
  • AdditiveBlending stacks bright pixels — that's where the glow comes from.
  • DoubleSide renders both faces of the mesh — you'll see the inside of the model through the front.
  • depthWrite: false stops 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

"Hologram has no see-through — it's solid." You forgot transparent: true on the material. Or you have opacity set somewhere else overriding the shader's alpha.
"Scanlines are jagged." Try smoothstep instead of step for the scan. Anti-aliasing in shaders comes from smoothing transitions.
"It looks like a flat blue blob with no depth." Fresnel weight is too low. Crank fresnel * 1.0 to fresnel * 2.0; the rim glow is what gives volumetric feel.
"Glitch is constant, not rare." step(0.97, ...) threshold is wrong — you wrote step(..., 0.97) (arguments swapped). Order matters: step(edge, x) returns 1 when x >= edge.

Exercises

  1. Multiple color stops. Pass two colors as uniforms, lerp between them based on vertical position. Bottom warm, top cool.
  2. Boot-up animation. Animate uTime from 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.
  3. React to mouse. Pass mouse coordinates as a uniform; offset the scanlines based on cursor distance. The hologram "tracks" the user.