Navigate

Search topics across all sections

GitHub
Shader Recipes

Hologram Effect

Like a sci-fi forcefield brought to life. The hologram effect combines three ingredients: fresnel glow that makes edges bright and centers transparent, animated scan lines sweeping vertically, and random glitch flickers. Together they make any 3D shape look like a holographic projection.

You set up the fresnel effect but it only glows on one side of the mesh. When you orbit around, the glow stays on the same face instead of appearing on the silhouette edges as expected.

terminal
Fresnel glow is fixed to one side of the mesh regardless of camera angle. Edges do not glow.

Real-world

Think about a sci-fi forcefield in a movie. It is not a solid object -- it is translucent in the middle but glows bright at the edges where your line of sight is nearly parallel to the surface. That edge glow is the fresnel effect, and it is the backbone of every hologram shader.

Now add scan lines -- horizontal stripes that scroll upward, like old CRT monitors or a laser scanning the object. These are just a sine wave applied to the Y position: wherever the sine is above 0.5, the surface is bright; below 0.5, it is dark.

Finally, throw in occasional glitch flickers -- random horizontal slices that flash bright for a single frame, as if the holographic projector is slightly unstable. That is the complete recipe: fresnel + scanlines + glitch.

How the Hologram Works

Three effects layered together in the fragment shader.

Vertex Shader

Pass position, normal, worldPos

Fresnel

Edge glow from view angle

Scan Lines

Scrolling horizontal stripes

Glitch

Random brightness flashes

Building the Hologram Step by Step

Each effect is simple on its own. The magic is in how they combine.

Step 1 -- Set up the vertex shader with world-space data

vertex.glslGLSL
varying vec3 vPosition;
varying vec3 vNormal;
varying vec3 vWorldPosition;

void main() {
  vPosition = position;
  vNormal = normalize(normalMatrix * normal);

  vec4 worldPos = modelMatrix * vec4(position, 1.0);
  vWorldPosition = worldPos.xyz;

  gl_Position = projectionMatrix
    * modelViewMatrix * vec4(position, 1.0);
}

The vertex shader must pass three things: the local position (for scanlines), the world-space normal (for fresnel), and the world-space position (for the view direction calculation). The normalMatrix correctly transforms normals even with non-uniform scaling.

Step 2 -- Calculate the fresnel effect

fragment.glslGLSL
// View direction: camera to surface
vec3 viewDir = normalize(
  cameraPosition - vWorldPosition
);

// Fresnel: bright when view grazes the surface
float fresnel = pow(
  1.0 - abs(dot(viewDir, vNormal)),
  uFresnelPower
);

// fresnelPower=1: wide glow
// fresnelPower=5: tight edge-only glow

The dot product between the view direction and surface normal is 1.0 when looking straight at the surface and 0.0 at grazing angles. Subtracting from 1.0 inverts it so edges are bright. The power exponent controls how quickly the glow falls off -- higher values make a tighter, more dramatic edge glow.

Step 3 -- Add scrolling scan lines

fragment.glslGLSL
// Scan lines based on Y position + time
float scanline = step(0.5,
  sin(vPosition.y * uScanlineCount
    + uTime * uScanlineSpeed)
  * 0.5 + 0.5
);

// Result: alternating bright/dark horizontal bands
// that scroll upward over time

The sine wave creates a smooth oscillation along the Y axis. We remap it from [-1, 1] to [0, 1] then threshold it with step() at 0.5 to get sharp on/off bands. Adding uTime multiplied by speed makes the pattern scroll upward each frame.

Step 4 -- Layer in glitch and flicker

fragment.glslGLSL
// Glitch: random bright slices
float block = floor(vPosition.y * 20.0 + uTime * 3.0);
float glitch = step(
  1.0 - uGlitchIntensity * 0.15,
  fract(sin(block * 12.9898) * 43758.5453)
);

// Flicker: subtle overall brightness pulsing
float flicker = 0.85 + 0.15
  * sin(uTime * 15.0)
  * sin(uTime * 7.3);

// Combine all effects
float alpha = (fresnel * 0.6 + 0.15)
  + scanline * 0.2;
alpha *= flicker;
alpha += glitch * 0.5;

The glitch divides the mesh into horizontal blocks and randomly brightens some of them each frame. The pseudo-random function fract(sin(x) * big_number) is a classic GPU trick for generating randomness. The flicker multiplies two offset sine waves for an organic brightness oscillation.

What you just learned

The fresnel effect uses dot(viewDir, normal) to make edges glow bright while centers stay transparent.

Scan lines are just sin(y * count + time * speed) thresholded with step() for sharp bands.

Pseudo-random glitch uses fract(sin(x) * 43758.5453) -- a classic GPU random number trick.

Additive blending + depthWrite=false makes overlapping transparent parts glow instead of occlude.

All three effects (fresnel, scanlines, glitch) are computed entirely in the fragment shader -- no textures needed.

Question

The fresnel effect relies on dot(viewDirection, normal). What happens to the fresnel glow if you use a completely flat plane instead of a curved mesh like the torus knot? Would you still see edge glow, and why or why not?

Think about it...

You apply the hologram shader to a cube, but the fresnel glow shows hard edges at the cube's corners instead of smooth gradients. Why?

Hint: Think about how normals differ between a smooth sphere and a hard-edged cube...

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Set scanlineCount to 5 — chunky lines

Try This!

Beginner

Max glitchIntensity — glitchy!

Try This!

Beginner

Set opacity to 0.1 — barely visible

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Fresnel calculation using model-space normal
The dot product needs world-space vectors to work correctly
Don't do this
Hologram.tsxTSX
// In fragment shader:
vec3 viewDir = normalize(
  cameraPosition - vPosition // model space!
);
float fresnel = pow(
  1.0 - dot(viewDir, normal), 2.0
);
// Fresnel rotates with the object!
cameraPosition is in world space, so you must also transform the vertex position and normal to world space before computing the dot product. Using model-space position creates a fresnel that rotates with the object instead of staying fixed relative to the camera.
2
Missing additive blending for glow
Default blending makes the hologram look like frosted glass
Don't do this
Hologram.tsxTSX
<shaderMaterial
  transparent
  // Using default NormalBlending
  vertexShader={vs}
  fragmentShader={fs}
/>
// Looks opaque and solid, not glowing
Additive blending adds the fragment color to whatever is behind it instead of replacing it. This makes overlapping transparent parts glow brighter, which is essential for the holographic look. Also set depthWrite={false} so back faces render through front faces.
3
Scanlines not moving because uTime is missing
The shader compiles but scanlines are frozen in place
Don't do this
fragment.glslGLSL
// Fragment shader:
float scanline = step(0.5,
  sin(vPosition.y * 20.0) * 0.5 + 0.5
);
// Static stripes -- not animated!
Without adding uTime to the sine input, the scanlines are computed from a fixed position and never change. Adding uTime * speed offsets the sine wave each frame, creating the scrolling scan line effect. Make sure uTime is updated in useFrame.

Best Practices

Use abs() in fresnel dot

Without abs(), back-facing normals produce negative dot products that break the fresnel calculation. abs() ensures it works from both sides when using DoubleSide rendering.

Keep glitch subtle

Too much glitch intensity makes the hologram unreadable. Start with intensity around 1-2 and only increase for dramatic effects. The best holograms are mostly stable with occasional glitches.

Pair with dark backgrounds

Additive blending adds light, so holograms glow best against dark backgrounds. On a white background, additive blending has no visible effect since you cannot add to white.

Choose curved geometry

Fresnel looks best on rounded shapes with smooth normals. Torus knots, spheres, and organic shapes work great. For hard-edge geometry, compute smooth normals first.