Holographic displays are one of the most iconic visual motifs in science fiction — from the flickering blue projections in Star Wars to the glitchy, neon-drenched UI overlays of Cyberpunk 2077. In games, a convincing hologram effect combines several distinct visual layers: a translucent 3D object, horizontal scan lines, chromatic aberration, random glitch displacement, and a characteristic cyan-blue glow that feels like light projected into thin air. In this post, we'll build a complete hologram shader from scratch using raymarching to render a 3D object, then apply every classic holographic post-processing trick on top.
The beauty of this effect is that each layer is simple on its own, but when they're composited together the result is immediately recognizable and visually striking. Let's dive into the full shader first, then break down each component.
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
// ============================================================
// HOLOGRAM & GLITCH EFFECT — Raymarched 3D Holographic Display
// Features: rotating 3D object, scan lines, chromatic aberration,
// glitch displacement, Fresnel transparency, flickering
// ============================================================
// --- Utility: pseudo-random hash ---
float hash(float n) {
return fract(sin(n) * 43758.5453123);
}
float hash2(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
// --- Noise for organic variation ---
float noise(float x) {
float i = floor(x);
float f = fract(x);
f = f * f * (3.0 - 2.0 * f);
return mix(hash(i), hash(i + 1.0), f);
}
// --- Rotation matrix ---
mat2 rot2D(float a) {
float c = cos(a), s = sin(a);
return mat2(c, -s, s, c);
}
// --- SDF primitives ---
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
float sdBox(vec3 p, vec3 b) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float sdTorus(vec3 p, vec2 t) {
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
float sdOctahedron(vec3 p, float s) {
p = abs(p);
return (p.x + p.y + p.z - s) * 0.57735027;
}
// --- Smooth min for blending shapes ---
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
// --- Scene SDF: a composite holographic object ---
float sceneSDF(vec3 p) {
float t = iTime * 0.6;
// Rotate the entire object
p.xz *= rot2D(t);
p.xy *= rot2D(t * 0.3);
// Central octahedron (crystal-like core)
float octa = sdOctahedron(p, 0.55);
// Inner sphere
float sphere = sdSphere(p, 0.35);
// Orbiting torus ring
vec3 tp = p;
tp.xz *= rot2D(t * 1.5);
float torus = sdTorus(tp, vec2(0.7, 0.04));
// Second torus at a different angle
vec3 tp2 = p;
tp2.xy *= rot2D(1.5708);
tp2.xz *= rot2D(-t * 1.2);
float torus2 = sdTorus(tp2, vec2(0.75, 0.03));
// Small floating satellite spheres
vec3 sp1 = p - vec3(sin(t * 2.0) * 0.5, cos(t * 1.7) * 0.4, cos(t * 2.0) * 0.5);
float sat1 = sdSphere(sp1, 0.06);
vec3 sp2 = p - vec3(-cos(t * 1.5) * 0.55, sin(t * 2.2) * 0.35, sin(t * 1.5) * 0.55);
float sat2 = sdSphere(sp2, 0.05);
// Blend core shapes smoothly
float core = smin(octa, sphere, 0.2);
// Combine everything
float d = core;
d = min(d, torus);
d = min(d, torus2);
d = min(d, sat1);
d = min(d, sat2);
return d;
}
// --- Estimate normal via gradient ---
vec3 estimateNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
sceneSDF(p + e.xyy) - sceneSDF(p - e.xyy),
sceneSDF(p + e.yxy) - sceneSDF(p - e.yxy),
sceneSDF(p + e.yyx) - sceneSDF(p - e.yyx)
));
}
// --- Raymarching ---
float raymarch(vec3 ro, vec3 rd, out vec3 hitPos) {
float t = 0.0;
hitPos = ro;
for (int i = 0; i < 80; i++) {
vec3 p = ro + rd * t;
float d = sceneSDF(p);
if (d < 0.001) {
hitPos = p;
return t;
}
if (t > 5.0) break;
t += d;
}
return -1.0;
}
// --- Glitch: determines if a glitch event is active ---
float glitchAmount(float time) {
// Glitches happen at pseudo-random intervals
float g = 0.0;
// Large glitch every ~3-5 seconds
float trigger = step(0.85, hash(floor(time * 1.2)));
g += trigger * hash(floor(time * 15.0));
// Small micro-glitches more frequently
float micro = step(0.92, hash(floor(time * 7.0)));
g += micro * 0.3 * hash(floor(time * 30.0));
return g;
}
// --- Glitch UV displacement ---
vec2 glitchUV(vec2 uv, float time) {
float g = glitchAmount(time);
if (g > 0.01) {
// Horizontal band displacement
float band = step(0.5, hash(floor(uv.y * 12.0) + floor(time * 8.0)));
uv.x += band * g * 0.15 * (hash(floor(time * 20.0)) - 0.5);
// Occasional vertical shift
float vShift = step(0.7, hash(floor(time * 6.0) + 7.0));
uv.y += vShift * g * 0.04;
}
return uv;
}
void main() {
// ---- UV setup ----
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec2 uvOrig = uv;
// ---- Apply glitch displacement to UV ----
float glitch = glitchAmount(iTime);
uv = glitchUV(uv, iTime);
// ---- Camera setup ----
vec3 ro = vec3(0.0, 0.0, 2.2); // camera origin
vec3 rd = normalize(vec3(uv, -1.0)); // ray direction
// ---- Raymarch the scene ----
vec3 hitPos;
float dist = raymarch(ro, rd, hitPos);
// ---- Base hologram color ----
vec3 holoColor = vec3(0.0);
float alpha = 0.0;
if (dist > 0.0) {
vec3 normal = estimateNormal(hitPos);
vec3 viewDir = normalize(ro - hitPos);
// Fresnel effect: edges glow brighter (hologram transparency)
float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 2.5);
fresnel = 0.3 + 0.7 * fresnel;
// Basic directional lighting
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.5));
float diff = max(dot(normal, lightDir), 0.0) * 0.5 + 0.5;
// Hologram base color: cyan-blue palette
vec3 baseColor = mix(
vec3(0.0, 0.6, 1.0), // deep blue
vec3(0.0, 1.0, 0.9), // cyan
fresnel
);
// Add slight color variation based on height
baseColor += vec3(0.1, 0.05, 0.0) * sin(hitPos.y * 20.0 + iTime * 3.0);
holoColor = baseColor * diff * fresnel;
alpha = fresnel;
// Edge glow: boost the silhouette edges
float edgeGlow = pow(fresnel, 3.0) * 1.5;
holoColor += vec3(0.3, 0.7, 1.0) * edgeGlow;
}
// ---- Scan lines ----
// Horizontal scan lines that scroll slowly downward
float scanLine = sin(fragCoord.y * 2.5 - iTime * 4.0) * 0.5 + 0.5;
scanLine = pow(scanLine, 1.5);
// Fine detail scan lines
float fineScan = sin(fragCoord.y * 0.8 + iTime * 2.0) * 0.5 + 0.5;
float scanMask = 0.7 + 0.3 * scanLine * fineScan;
holoColor *= scanMask;
alpha *= scanMask;
// ---- Chromatic aberration ----
// Offset red and blue channels slightly for that holographic fringe
float caStrength = 0.004 + glitch * 0.02; // stronger during glitches
vec2 uvR = uvOrig + vec2(caStrength, 0.0);
vec2 uvB = uvOrig - vec2(caStrength, 0.0);
// Re-raymarch for offset channels (simplified: approximate with UV shift)
vec3 roR = ro;
vec3 rdR = normalize(vec3(glitchUV(uvR, iTime), -1.0));
vec3 hitR;
float distR = raymarch(roR, rdR, hitR);
vec3 roB = ro;
vec3 rdB = normalize(vec3(glitchUV(uvB, iTime), -1.0));
vec3 hitB;
float distB = raymarch(roB, rdB, hitB);
// Apply chromatic aberration to red and blue channels
if (distR > 0.0) {
vec3 nR = estimateNormal(hitR);
float fR = pow(1.0 - max(dot(nR, normalize(ro - hitR)), 0.0), 2.5);
holoColor.r = mix(holoColor.r, (0.3 + 0.7 * fR) * scanMask, 0.5);
}
if (distB > 0.0) {
vec3 nB = estimateNormal(hitB);
float fB = pow(1.0 - max(dot(nB, normalize(ro - hitB)), 0.0), 2.5);
holoColor.b = mix(holoColor.b, (0.3 + 0.7 * fB) * scanMask, 0.5);
}
// ---- Global flicker ----
// Simulate power instability: occasional brightness dips
float flicker = 1.0;
flicker *= 0.92 + 0.08 * sin(iTime * 60.0); // high-frequency
flicker *= 0.95 + 0.05 * noise(iTime * 8.0); // low-frequency wobble
flicker *= 1.0 - 0.3 * step(0.93, hash(floor(iTime * 4.0))); // random dropout
holoColor *= flicker;
alpha *= flicker;
// ---- Glitch color corruption ----
if (glitch > 0.3) {
// During heavy glitches, inject random color bands
float bandY = floor(fragCoord.y / 8.0);
float bandHash = hash(bandY + floor(iTime * 12.0));
if (bandHash > 0.7) {
holoColor = mix(holoColor, vec3(0.0, 1.0, 0.8) * 1.5, glitch * 0.4);
}
if (bandHash < 0.15) {
holoColor *= 0.1; // dark bands
}
}
// ---- Holographic base glow (ground plane suggestion) ----
float groundGlow = exp(-abs(uv.y + 0.6) * 8.0) * 0.3;
groundGlow *= 0.5 + 0.5 * sin(uv.x * 40.0 + iTime * 2.0); // interference pattern
vec3 glowColor = vec3(0.1, 0.4, 0.8) * groundGlow;
// ---- Ambient holographic particles (floating motes) ----
float particles = 0.0;
for (int i = 0; i < 8; i++) {
float fi = float(i);
vec2 ppos = vec2(
sin(iTime * 0.5 + fi * 1.7) * 0.4,
sin(iTime * 0.3 + fi * 2.3) * 0.6
);
float d = length(uv - ppos);
particles += 0.002 / (d * d + 0.001);
}
vec3 particleColor = vec3(0.2, 0.6, 1.0) * particles * 0.03;
// ---- Compose final color ----
vec3 finalColor = holoColor + glowColor + particleColor;
// Subtle vignette
float vig = 1.0 - 0.3 * length(uvOrig * 0.8);
finalColor *= vig;
// Tone mapping: prevent overblown highlights
finalColor = finalColor / (1.0 + finalColor);
// Output with slight overall transparency for compositing feel
float finalAlpha = clamp(alpha + groundGlow + particles * 0.1, 0.0, 1.0);
gl_FragColor = vec4(finalColor, finalAlpha);
}
This shader combines six distinct layers to create a convincing hologram. Each one can be tuned independently, making the effect highly customizable for your game's aesthetic. Let's walk through the key concepts behind each layer.
At the heart of the hologram is a raymarched scene built from signed distance function (SDF) primitives. We combine an octahedron core, an inner sphere, two orbiting torus rings, and small satellite spheres — all blended and rotated over time. The smin function gives us smooth blending between the core shapes, creating an organic, crystal-like form that feels right for a holographic projection.
// Smooth minimum blends two SDF shapes with a soft transition
// k controls the blending radius — larger values = smoother joins
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
// Blend the octahedron and sphere into a single organic core
float core = smin(octa, sphere, 0.2);
// Then add hard-edged ring geometry
float d = min(core, torus);
The smooth minimum is critical for making the holographic object look like a single cohesive projection rather than a collection of separate shapes. When applied to the octahedron and sphere, it creates rounded intersections that catch the Fresnel edge glow beautifully.
Scan lines are the single most recognizable trait of holographic displays. They simulate the raster pattern of a CRT or projector by darkening horizontal bands across the image. We use two overlapping sine waves at different frequencies: a coarse wave for visible rolling bands and a finer one for subtle texture. Both scroll over time to create the feeling of active projection.
// Coarse scan lines: visible rolling bands that move downward float scanLine = sin(fragCoord.y * 2.5 - iTime * 4.0) * 0.5 + 0.5; scanLine = pow(scanLine, 1.5); // sharpen the falloff // Fine detail scan lines: adds subtle high-frequency texture float fineScan = sin(fragCoord.y * 0.8 + iTime * 2.0) * 0.5 + 0.5; // Combined mask: 70% base brightness + 30% modulation float scanMask = 0.7 + 0.3 * scanLine * fineScan;
The pow on the coarse scan line sharpens the dark-to-bright transition, making the bands more defined. The 70/30 split ensures the scan lines are visible but don't obliterate the underlying object — a common mistake is making scan lines too strong, which kills readability. In a game UI context, you might lower the modulation to 10-15% so the hologram remains legible.
Chromatic aberration simulates the way real optical systems fail to focus all wavelengths of light to the same point. For holograms, it creates that telltale red-blue fringe along edges that screams "projected light." We offset the red and blue channels horizontally by re-raymarching with slightly shifted UV coordinates.
// Base CA offset + extra displacement during glitches float caStrength = 0.004 + glitch * 0.02; // Shift UV for red channel rightward, blue leftward vec2 uvR = uvOrig + vec2(caStrength, 0.0); vec2 uvB = uvOrig - vec2(caStrength, 0.0); // Raymarch each offset to get per-channel geometry hits vec3 rdR = normalize(vec3(glitchUV(uvR, iTime), -1.0)); float distR = raymarch(roR, rdR, hitR);
The key detail here is that chromatic aberration strength increases during glitch events. This ties the effects together — when the hologram glitches, the optics also destabilize, which is exactly what you'd expect from a malfunctioning projection system. For a cheaper alternative in a game shader, you could sample a texture at three offset UVs rather than re-raymarching.
Convincing glitches need to feel random but not chaotic. Our glitch system uses a two-tier approach: large glitches that disrupt the image every few seconds, and subtle micro-glitches that add constant low-level instability. The timing is driven by hashing floor(time * rate), which creates step-function randomness — the glitch state holds steady for a fraction of a second before changing.
float glitchAmount(float time) {
float g = 0.0;
// Large glitch: ~15% chance each 0.83-second window
float trigger = step(0.85, hash(floor(time * 1.2)));
g += trigger * hash(floor(time * 15.0));
// Micro-glitch: ~8% chance each 0.14-second window
float micro = step(0.92, hash(floor(time * 7.0)));
g += micro * 0.3 * hash(floor(time * 30.0));
return g;
}
The UV displacement itself works by slicing the screen into horizontal bands and shifting each one randomly during a glitch event. The floor(uv.y * 12.0) creates about 12 horizontal slices, and each slice independently decides whether to shift. This gives us the distinctive "torn scanline" look of digital corruption. Tuning the step thresholds (0.85, 0.92) controls how frequent each tier of glitch is — push them closer to 1.0 for rarer, more dramatic events.
The Fresnel effect is what makes the hologram feel volumetric and translucent rather than solid. In real physics, surfaces reflect more light at glancing angles. For holograms, we invert this intuition: edges (glancing angles) glow brighter, while face-on surfaces fade toward transparency. This creates the classic "wireframe glow" look of sci-fi projections.
// Fresnel: 1.0 at edges (normal perpendicular to view), 0.0 face-on float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 2.5); // Remap so face-on areas still have 30% visibility fresnel = 0.3 + 0.7 * fresnel; // Use Fresnel to drive both color intensity and alpha holoColor = baseColor * diff * fresnel; alpha = fresnel;
The exponent (2.5) controls how sharp the edge glow falloff is. Lower values like 1.0 give a soft, diffuse glow; higher values like 4.0 create a tight edge outline. The 0.3 base floor prevents the object from becoming completely invisible when viewed head-on. This base value is a critical tuning parameter — in gameplay, you might increase it to 0.5 or higher so the hologram remains readable at all angles.
The final compositing stage is where everything comes together. The order of operations matters: we apply scan lines and chromatic aberration first (they affect the object), then global flicker (affects everything uniformly), then glitch corruption (affects random bands), and finally ambient effects like ground glow and particles. This layering order ensures that each effect interacts naturally with the others.
// Layer 1: Object with Fresnel shading holoColor = baseColor * diff * fresnel; // Layer 2: Scan lines modulate the object holoColor *= scanMask; // Layer 3: Chromatic aberration shifts R/B channels holoColor.r = mix(holoColor.r, fresnelR * scanMask, 0.5); // Layer 4: Global flicker dims everything together holoColor *= flicker; // Layer 5: Glitch corruption overrides random bands holoColor = mix(holoColor, glitchColor, glitch * 0.4); // Layer 6: Additive ambient effects finalColor = holoColor + groundGlow + particleColor;
Notice that the object layers use multiplicative blending (darkening via scan lines and flicker), while the ambient effects use additive blending. This is intentional — the scan lines and flicker should feel like they're part of the projection system affecting the object, while the ground glow and particles are separate light sources that add to the scene. Mixing these blending modes is key to a convincing holographic look.
When adapting this effect for a real game, consider these practical adjustments. First, the triple-raymarch for chromatic aberration is expensive; in production, you would render the hologram to a texture in one pass, then apply chromatic aberration as a cheap UV-offset in a second pass. Second, the glitch intensity should respond to gameplay — a damaged hologram projector could increase the glitch threshold, while a pristine one would have almost none. Third, the scan line frequency should scale with screen resolution; the magic numbers in this shader assume a specific pixel density. Finally, alpha output is critical for compositing: this shader outputs premultiplied alpha, so make sure your blend mode is set to ONE, ONE_MINUS_SRC_ALPHA rather than standard alpha blending.
The entire effect is driven by just two uniforms — resolution and time — making it trivial to integrate into any WebGL or game engine pipeline. Swap the SDF scene for your own geometry, adjust the color palette from cyan-blue to match your game's hologram style, and you have a production-ready holographic display effect.