Force fields and energy shields are one of the most iconic visual effects in sci-fi games. From the shimmering barriers in Halo to the hexagonal shields in Overwatch, these effects combine several fascinating shader techniques: sphere ray-intersection, hexagonal tiling on curved surfaces, Fresnel rim lighting, animated impact ripples, and energy crackling. In this post we'll build a complete, visually rich energy shield from scratch in a single fragment shader.
The approach uses analytical ray-sphere intersection rather than traditional raymarching distance fields. This gives us exact hit points on the shield surface, which we then project into a hexagonal UV space to create the honeycomb pattern. Multiple layers of animation — pulsing hex edges, travelling ripple waves, random energy crackles, and a strong Fresnel rim glow — combine to sell the illusion of a living, reactive energy barrier.
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
// ============================================================
// FORCE FIELD / ENERGY SHIELD
// A raymarched hexagonal energy barrier with Fresnel glow,
// impact ripples, and energy crackling effects.
// ============================================================
#define PI 3.14159265
#define TWO_PI 6.28318530
// --- Utility functions ---
// Simple hash for pseudo-random values
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
// 2D rotation matrix
mat2 rot2(float a) {
float c = cos(a), s = sin(a);
return mat2(c, -s, s, c);
}
// Smooth noise from hash
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// Fractional Brownian Motion for energy crackling
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
mat2 r = rot2(0.5);
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p = r * p * 2.0;
a *= 0.5;
}
return v;
}
// --- Ray-Sphere Intersection ---
// Returns distances (near, far) or (-1,-1) if no hit
vec2 sphereIntersect(vec3 ro, vec3 rd, vec3 center, float radius) {
vec3 oc = ro - center;
float b = dot(oc, rd);
float c = dot(oc, oc) - radius * radius;
float disc = b * b - c;
if (disc < 0.0) return vec2(-1.0);
float sq = sqrt(disc);
return vec2(-b - sq, -b + sq);
}
// --- Hexagonal grid on a sphere ---
// Convert 3D point on sphere to hex grid coordinates
// Returns: xy = hex cell center, z = distance to nearest edge
vec3 hexGrid(vec2 uv, float scale) {
uv *= scale;
// Hex grid constants
const vec2 s = vec2(1.0, 1.7320508); // 1, sqrt(3)
// Two offset grids
vec4 hC = floor(vec4(uv, uv - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 hF = vec4(uv - hC.xy * s, uv - (hC.zw + 0.5) * s);
// Pick closer center
vec2 cellId;
vec2 cellUV;
if (dot(hF.xy, hF.xy) < dot(hF.zw, hF.zw)) {
cellId = hC.xy;
cellUV = hF.xy;
} else {
cellId = hC.zw + 0.5;
cellUV = hF.zw;
}
// Distance to hex edge
vec2 a = abs(cellUV);
float edgeDist = max(a.x * 0.5 + a.y * 0.8660254, a.x);
float hexEdge = 0.5 - edgeDist;
return vec3(cellId, hexEdge);
}
// --- Fresnel effect ---
// Strong glow at glancing angles, transparent when facing camera
float fresnelEffect(vec3 normal, vec3 viewDir, float power) {
return pow(1.0 - abs(dot(normal, viewDir)), power);
}
// --- Impact ripple effect ---
// Simulates hits at predefined animated positions
float impactRipple(vec3 pos, vec3 impactPos, float time, float age) {
float dist = length(pos - impactPos);
float rippleRadius = age * 1.2;
float ripple = sin((dist - rippleRadius) * 25.0) * 0.5 + 0.5;
float envelope = smoothstep(rippleRadius - 0.15, rippleRadius, dist)
* smoothstep(rippleRadius + 0.15, rippleRadius, dist);
float fade = exp(-age * 2.5);
return ripple * envelope * fade;
}
void main() {
// Normalized coordinates
vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
// Camera setup — orbit around the shield
float camAngle = iTime * 0.3;
vec3 ro = vec3(sin(camAngle) * 3.5, 1.0 + sin(iTime * 0.2) * 0.3, cos(camAngle) * 3.5);
vec3 target = vec3(0.0, 0.0, 0.0);
vec3 fwd = normalize(target - ro);
vec3 right = normalize(cross(fwd, vec3(0.0, 1.0, 0.0)));
vec3 up = cross(right, fwd);
vec3 rd = normalize(fwd * 1.5 + right * uv.x + up * uv.y);
// Shield properties
vec3 shieldCenter = vec3(0.0, 0.0, 0.0);
float shieldRadius = 1.5;
// Background — dark space with subtle gradient
vec3 col = vec3(0.02, 0.03, 0.06);
col += 0.03 * smoothstep(0.5, 0.0, length(uv));
// Faint background stars
vec2 starUV = gl_FragCoord.xy * 0.01;
float stars = step(0.98, hash(floor(starUV * 80.0)));
col += stars * 0.4;
// Intersect ray with shield sphere
vec2 t = sphereIntersect(ro, rd, shieldCenter, shieldRadius);
if (t.x > 0.0) {
// We hit the shield — compute both entry and exit points
// to render both front and back faces for transparency
// --- Front face ---
vec3 posF = ro + rd * t.x;
vec3 normalF = normalize(posF - shieldCenter);
// Spherical UV mapping for hex grid
float thetaF = atan(normalF.z, normalF.x);
float phiF = asin(normalF.y);
vec2 sphereUVF = vec2(thetaF / TWO_PI + 0.5, phiF / PI + 0.5);
// Hex grid pattern
vec3 hexF = hexGrid(sphereUVF * vec2(2.0, 1.0), 8.0);
float hexEdgeF = smoothstep(0.0, 0.05, hexF.z);
float hexLineF = 1.0 - hexEdgeF;
// Unique value per hex cell for animation variety
float cellHashF = hash(hexF.xy);
// Animated hex cell fill — random cells pulse
float pulse = sin(iTime * 3.0 + cellHashF * TWO_PI) * 0.5 + 0.5;
float cellGlow = smoothstep(0.7, 1.0, pulse) * 0.3;
// Fresnel rim glow — bright at edges
float fresnel = fresnelEffect(normalF, -rd, 3.0);
// Energy crackling — animated noise on the surface
vec2 crackleUV = sphereUVF * 6.0 + iTime * 0.3;
float crackle = fbm(crackleUV);
crackle = smoothstep(0.45, 0.65, crackle) * 0.4;
// Impact ripple effects — three animated impact points
// Each impact loops with different timing
float impact = 0.0;
for (int i = 0; i < 3; i++) {
float fi = float(i);
float cycleTime = mod(iTime + fi * 2.1, 4.0);
// Impact positions orbit around the shield
float impAngle = fi * 2.094 + iTime * 0.1;
vec3 impPos = shieldCenter + normalize(vec3(
sin(impAngle) * 0.8,
cos(fi * 1.5) * 0.5,
cos(impAngle) * 0.8
)) * shieldRadius;
impact += impactRipple(posF, impPos, iTime, cycleTime);
}
// Travelling energy wave — scanline going up
float wave = sin(normalF.y * 15.0 - iTime * 4.0) * 0.5 + 0.5;
wave = smoothstep(0.85, 1.0, wave) * 0.3;
// Shield color palette — cyan/blue energy
vec3 shieldColor = vec3(0.1, 0.6, 1.0);
vec3 impactColor = vec3(0.4, 0.9, 1.0);
vec3 edgeColor = vec3(0.3, 0.8, 1.0);
vec3 crackleColor = vec3(0.6, 0.85, 1.0);
// Compose shield appearance
float alpha = 0.0;
// Hex grid lines
alpha += hexLineF * 0.6;
vec3 shieldCol = hexLineF * edgeColor * 0.8;
// Pulsing cell fill
alpha += cellGlow;
shieldCol += cellGlow * shieldColor;
// Fresnel rim
alpha += fresnel * 0.7;
shieldCol += fresnel * edgeColor * 1.2;
// Energy crackle
alpha += crackle;
shieldCol += crackle * crackleColor;
// Impact ripples
alpha += impact * 1.5;
shieldCol += impact * impactColor * 2.0;
// Travelling wave
alpha += wave;
shieldCol += wave * shieldColor * 0.8;
// Base transparency
alpha += 0.04;
shieldCol += shieldColor * 0.04;
// Clamp alpha
alpha = clamp(alpha, 0.0, 0.95);
// --- Back face (subtle inner glow) ---
vec3 posB = ro + rd * t.y;
vec3 normalB = normalize(posB - shieldCenter);
float fresnelB = fresnelEffect(-normalB, -rd, 4.0);
shieldCol += fresnelB * shieldColor * 0.15;
alpha += fresnelB * 0.1;
// Blend shield over background
col = mix(col, shieldCol, alpha);
// Additive bloom around the shield
col += shieldCol * alpha * 0.15;
}
// Outer glow around the shield (even when not directly hit)
// Use a slightly larger sphere test for the glow
vec2 tGlow = sphereIntersect(ro, rd, shieldCenter, shieldRadius + 0.15);
if (tGlow.x > 0.0 && t.x < 0.0) {
vec3 glowPos = ro + rd * tGlow.x;
vec3 glowNorm = normalize(glowPos - shieldCenter);
float glowFresnel = fresnelEffect(glowNorm, -rd, 5.0);
col += vec3(0.05, 0.2, 0.5) * glowFresnel * 0.3;
}
// Proximity glow — distance-based atmospheric glow
vec3 toCenter = shieldCenter - ro;
float closest = length(cross(toCenter, rd));
float proxGlow = smoothstep(shieldRadius + 0.8, shieldRadius, closest);
col += vec3(0.02, 0.08, 0.2) * proxGlow * 0.5;
// Tone mapping and gamma
col = col / (1.0 + col);
col = pow(col, vec3(0.9));
gl_FragColor = vec4(col, 1.0);
}
Unlike signed distance field raymarching (which iterates step by step), our shield uses an analytical ray-sphere intersection. This is much cheaper for a simple sphere and gives us exact, pixel-perfect hit points. We solve the quadratic equation formed by substituting the ray equation into the sphere equation:
// Ray: P = ro + rd * t // Sphere: |P - center|^2 = radius^2 // Substituting gives: t^2 + 2*b*t + c = 0 vec3 oc = ro - center; float b = dot(oc, rd); float c = dot(oc, oc) - radius * radius; float disc = b * b - c; // disc < 0 means no intersection // otherwise: t_near = -b - sqrt(disc), t_far = -b + sqrt(disc)
Getting both the near and far intersection points is key for our shield — we render the front face with the hex pattern and effects, and add a subtle inner glow on the back face. This two-layer approach gives the shield a sense of volume and translucency rather than looking like a flat painted sphere.
The hexagonal pattern is the visual hallmark of sci-fi energy shields. To tile hexagons onto a sphere, we first compute spherical UV coordinates (theta and phi angles from the surface normal), then pass those into a 2D hex grid function. The hex grid works by testing two offset rectangular grids and picking whichever cell center is closer to the sample point:
// Spherical UV from surface normal float theta = atan(normal.z, normal.x); // longitude float phi = asin(normal.y); // latitude vec2 sphereUV = vec2(theta / TWO_PI + 0.5, phi / PI + 0.5); // Hex grid: two offset rectangular grids vec4 hC = floor(vec4(uv, uv - vec2(0.5, 1.0)) / s.xyxy) + 0.5; vec4 hF = vec4(uv - hC.xy * s, uv - (hC.zw + 0.5) * s); // Pick closer center based on distance // Edge distance = max(|x|*0.5 + |y|*sqrt(3)/2, |x|)
The edge distance value returned by the hex function is what creates those sharp, glowing grid lines. By applying smoothstep to this distance, we can control line thickness and softness. Each cell also gets a unique hash value from its grid coordinates, which drives the per-cell pulse animation — making random hexagons light up and fade independently.
The Fresnel effect is arguably the single most important ingredient for a convincing energy shield. It makes the shield nearly transparent when viewed head-on but brightly glowing at glancing angles (the silhouette edges). This matches our intuition for how thin, translucent energy membranes would behave and is physically motivated by real optical behavior at material boundaries:
float fresnelEffect(vec3 normal, vec3 viewDir, float power) {
// dot(N, V) = 1 when facing camera, 0 at silhouette
// 1 - dot gives us 0 at center, 1 at edges
// The power exponent controls how tight the rim glow is
return pow(1.0 - abs(dot(normal, viewDir)), power);
}
A power of 2-3 gives a broad, soft glow suitable for a translucent barrier. Higher powers (4-6) concentrate the glow tightly at the edges, good for a more subtle "glass dome" look. In our shader, we use a power of 3 for the front face and 4 for the back face inner glow, so the back face contribution is subtler.
Static shields look lifeless. Impact ripples make the shield feel reactive and dynamic. Each ripple is a ring of energy expanding outward from a hit point on the sphere surface. The math is straightforward — measure the geodesic distance from the current fragment to the impact point, then create a ring pattern that expands over time:
float impactRipple(vec3 pos, vec3 impactPos, float time, float age) {
float dist = length(pos - impactPos);
float rippleRadius = age * 1.2; // ring expands over time
// Sine wave creates concentric rings
float ripple = sin((dist - rippleRadius) * 25.0) * 0.5 + 0.5;
// Envelope constrains energy to a thin ring
float envelope = smoothstep(rippleRadius - 0.15, rippleRadius, dist)
* smoothstep(rippleRadius + 0.15, rippleRadius, dist);
// Exponential fade as the ripple ages
float fade = exp(-age * 2.5);
return ripple * envelope * fade;
}
We run three impacts simultaneously, each on a looping timer with staggered offsets. The impact positions slowly orbit the shield so the effect stays dynamic. In a real game, you would feed actual hit positions and times through uniforms, but for this demo the self-animated approach keeps things self-contained.
Two more layers of animation add life to the shield. The energy crackle uses fractional Brownian motion (layered noise) sampled across the sphere surface with a time offset, then thresholded to create bright "arcs" that flicker and shift. The travelling wave is a simple sine pattern based on the Y component of the surface normal, scrolling upward over time — this creates the classic "energy scanline" look seen in many games:
// Energy crackling — FBM noise thresholded into bright arcs vec2 crackleUV = sphereUV * 6.0 + iTime * 0.3; float crackle = fbm(crackleUV); crackle = smoothstep(0.45, 0.65, crackle) * 0.4; // Travelling wave — horizontal energy scanline float wave = sin(normalF.y * 15.0 - iTime * 4.0) * 0.5 + 0.5; wave = smoothstep(0.85, 1.0, wave) * 0.3;
The smoothstep thresholding is critical for both effects. Without it, the noise would produce a uniform murky glow. With the threshold, only the peaks of the noise pattern become visible, creating sharp, bright features against a mostly transparent shield surface.
All the shield layers (hex lines, cell glow, Fresnel rim, crackles, impacts, waves) are summed into a combined alpha and color value, then blended over the background using a standard mix. An extra additive pass col += shieldCol * alpha * 0.15 adds a subtle bloom effect. Beyond the shield surface itself, we also add proximity glow — a soft atmospheric light based on how close the ray passes to the shield, even if it misses. This halo effect makes the shield feel like it's radiating energy into the surrounding space.
To integrate this into a game engine, you would typically render the shield as a transparent sphere mesh with a similar fragment shader, using the engine's depth buffer for occlusion and actual game-driven uniforms for impact positions, shield health (controlling opacity and color shifts), and team colors. The core techniques — hex grids, Fresnel glow, and impact ripples — transfer directly.