← Back to Blog

Jump Bubble Visualization: Thin-Film Interference Shaders for Beautiful Range Display

What if your ship's jump range indicator looked like a soap bubble floating in space—rainbow colors shifting as you rotate the camera, film-like bands flowing across its surface? That's exactly what we built for EVE Frontier Map's reachability visualization. Instead of a boring solid sphere, we created a physically-inspired thin-film interference shader that simulates the real optics of soap bubbles and oil slicks.

The Problem: Visualizing Distance in 3D Space

When a player selects a star system and wants to see how far their ship can jump, we need to display a sphere showing all reachable destinations. The naive approach—a semi-transparent colored sphere—works but looks flat and uninteresting. We wanted something that:

The answer came from physics: thin-film interference.

The Physics: How Soap Bubbles Get Their Colors

When light hits a thin transparent film (like a soap bubble), some light reflects off the top surface while some passes through and reflects off the bottom. These two reflected beams interfere with each other. Depending on the film's thickness and your viewing angle, certain wavelengths (colors) are amplified while others cancel out.

The Math Behind Iridescence

Constructive interference occurs when: 2 × n × d × cos(θ) = m × λ
Where n = refractive index, d = film thickness, θ = refracted angle, m = interference order, λ = wavelength. This equation determines which colors you see at different viewing angles.

This creates the characteristic rainbow swirls of soap bubbles—colors that shift as the bubble moves or as you change your viewpoint. We implemented this in GLSL shaders running on the GPU.

The Implementation: GLSL Shaders in Three.js

Our implementation lives in JumpRangeBubble.ts and consists of three layered elements:

1. The Main Interference Shell

The vertex shader handles position wobble (for that organic bubble feel) and calculates the Fresnel term—how much the surface faces toward or away from the camera:

// Wobble displacement along normal for organic bubble feel
float wobble1 = sin(position.x * 3.0 + uTime * uWobbleSpeed) * 0.4;
float wobble2 = sin(position.y * 4.0 + uTime * uWobbleSpeed * 1.3) * 0.3;
vec3 displacedPos = position + normal * wobbleDisplacement;

// Fresnel calculation - edges vs center visibility
float NdotV = dot(vWorldNormal, vViewDir);
vFresnel = pow(1.0 - abs(NdotV), 1.8);

2. Wavelength to RGB Conversion

We convert interference wavelengths (380-780nm) to visible colors using overlapping Gaussian curves that approximate the human eye's spectral response:

vec3 wavelengthToRGB(float wavelength) {
  float w = (wavelength - 380.0) / 400.0; // Normalize to 0-1
  
  // Red peaks around 650nm
  float r = exp(-pow((w - 0.75) * 3.5, 2.0));
  
  // Green peaks around 550nm  
  float g = exp(-pow((w - 0.45) * 3.0, 2.0));
  
  // Blue peaks around 450nm
  float b = exp(-pow((w - 0.15) * 3.0, 2.0));
  
  return vec3(r, g, b);
}

3. Theme Color Biasing

Pure physics would give us rainbow colors with no connection to the user's chosen theme. We solve this by biasing the interference output toward the theme color while preserving the iridescent variation:

vec3 biasTowardTheme(vec3 color, float themeHue, float bias) {
  vec3 themeColor = hsl2rgb(themeHue, 0.8, 0.55);
  return mix(color, color * themeColor * 2.0, bias);
}

With uColorBias at 0.45, we get recognizable theme colors (orange stays orange-ish) but with rainbow shimmer at the edges.

Flowing Patterns: Animated Thickness Variation

Real soap bubbles have constantly-shifting thickness as the film flows under gravity and surface tension. We simulate this with two noise functions blended together:

Pattern Purpose Character
flowNoise() Large-scale undulations Smooth, slow waves
spiralFlow() Spiraling bands Wraps around sphere poles

By animating the time uniform (uTime) at 0.525× real-time, the patterns flow smoothly without being dizzying. The gentle rotation (group.rotation.y = rel * 0.15) adds to the dynamic feel.

Layered Rendering for Depth

A single shell looks flat. We add two more layers for depth perception:

  1. Inner glow (98% radius, BackSide rendering): Subtle additive glow visible through the main shell
  2. Outer halo (102% radius, FrontSide): Very faint atmospheric edge

Each layer has progressively lower opacity (0.24 → 0.0375 → 0.03) to prevent the bubble from obscuring the stars inside it.

Performance Considerations

The shader runs per-fragment on a 96×64 tessellation sphere (6,144 triangles). For a typical 1080p viewport, this means:

Runtime Tweaking

During development, we exposed all uniforms to window.__efBubble* globals for real-time tuning in browser DevTools. This let us dial in thickness ranges (320-580nm) and flow speeds without rebuilding.

The Result

The jump range bubble now looks like a delicate soap film floating in space. When you zoom in close, you can see the rainbow bands flowing across its surface. When you rotate the camera, colors shift—just like a real bubble. And it all respects your theme: orange pilots get warm iridescence, blue pilots get cool tones.

This is the kind of detail that doesn't affect gameplay but makes EVE Frontier Map feel polished and alive. Sometimes the best features are the ones you don't consciously notice—they just make everything feel right.

Related Posts

thin-film interferencewebgl shadersthree.jssoap bubble effectiridescenceglsleve frontiergame visualizationoptical physics