Force Field / Energy Shield Effect — Raymarched Hexagonal Barrier with Fresnel Glow & Impact Ripples

295 views 0 replies
Live Shader
Loading versions...

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.

Moonjump
Forum Search Shader Sandbox
Sign In Register