the hexagonal tiling in this force field is really clean. did you consider doing the impact ripples as a separate additive pass instead of baking them into the main shader? i ask because in my game i need multiple simultaneous hits and tracking all the impact points in a single shader gets unwieldy past about 4-5 hits. a separate pass with additive blending would let you instance it.
Force Field / Energy Shield Effect — Raymarched Hexagonal Barrier with Fresnel Glow & Impact Ripples
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.
Complete Energy Shield Shader
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);
}
How Ray-Sphere Intersection Works
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.
Hexagonal Grid on a Curved Surface
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.
Fresnel Rim Glow — The Key to Convincing Shields
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.
Impact Ripples — Reactive Shield Hits
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.
Energy Crackling and Travelling Waves
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.
Compositing: Additive Blending and Atmosphere
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.
Really solid work on this one. I tried something similar a while back and the impact ripple falloff was the part that kept biting me — mine looked like someone dropping a stone in water instead of an energy shield hit.
One thing that helped me was adding a slight chromatic separation to the ripple wavefront. Instead of one uniform color, offset the hex pattern lookup by a tiny amount per channel at the ripple edge:
// at the ripple wavefront float rEdge = ripple(uv + rippleDir * 0.003); float gEdge = ripple(uv); float bEdge = ripple(uv - rippleDir * 0.003); vec3 rippleCol = vec3(rEdge, gEdge, bEdge) * edgeIntensity;
Gives it that sci-fi energy dispersion look. Tiny detail but it reads well on dark backgrounds. Also curious — are you computing the hex grid with the pointy-top or flat-top orientation? I've found flat-top tends to look better for shields because the flat edges at the top/bottom feel more "engineered."
the hex tiling is really clean, props for getting the edge detection right because thats usually the annoying part lol
one thing tho - for the impact ripples have you tried using multiple ring waves instead of a single expanding circle? i layer like 3-4 rings with slightly different speeds and it gives this really cool interference pattern. each ring has its own decay rate too so they fade out naturally
also if you want the force field to react to projectiles hitting it you can pass in hit positions as a uniform array and loop through them in the shader. i keep a buffer of the last 8 impacts with timestamps and it handles rapid fire hits without any popping