Deep Ocean Explorer
2025Browser-based real-time underwater world with hand-written GLSL shaders for volumetric light shafts, caustics, and jellyfish bioluminescence. Depth-driven lighting system, 10 NPC jellyfish with 8 migration patterns, and a player-controlled jellyfish with 6-DOF movement.

Deep Ocean Explorer is an ambient interactive 3D underwater world running entirely in the browser. The player navigates as a jellyfish through a procedurally lit ocean populated by NPC jellyfish swarms, fish schools, coral, seaweed, and layered atmospheric effects. Every visual effect — volumetric light shafts, caustics, bioluminescence, depth-based fog — is a hand-written GLSL ShaderMaterial with no post-processing library dependencies. The project was built as a demonstration of real-time WebGL depth: how many independent shader systems can be composed coherently in a browser.
Most browser-based 3D experiences rely on HDR environment maps and pre-baked lighting to look good. This approach looks photorealistic but is passive — lighting doesn't respond to the player's position or depth. The challenge was to build an underwater world where every atmospheric system (light scattering, caustics, fog, bioluminescence) is fully dynamic and depth-driven, while keeping the frame rate above 60fps on a mid-range laptop.
Build a real-time 3D underwater experience that demonstrates: (1) custom GLSL shader authorship without a post-processing pipeline; (2) a depth-driven dynamic lighting system with smooth zone transitions; (3) NPC entity behavior with a state machine; (4) player movement and interaction; (5) performance-aware rendering with documented tradeoffs.
- ◈Solo developer — shader authorship, scene architecture, NPC behavior, deployment
- 01All shaders inlined as TypeScript template literals — no .glsl files, no shader loading pipeline, no compilation failures from missing assets on GitHub Pages. Every THREE.ShaderMaterial is defined inside useMemo() hooks, ensuring shaders are compiled once and cached by the GPU.
- 02dFdx/dFdy derivatives for caustics — instead of a second noise sample to compute the water surface gradient, GPU screen-space derivatives give the exact gradient of the noise field for free. This is the approach used in production ocean renderers: 1/( 1 + dot(gradient, gradient) × 20) produces bright caustic lines at surface curvature peaks.
- 03Depth-based lighting as a data structure — five named zones (surface, shallow, medium, deep, abyss) each define a target light color and intensity. Every frame, the system lerps current values toward the active zone target. Adding a new zone requires one object entry, not new conditional logic.
- 04AdditiveBlending + depthWrite:false on all effect layers — volumetric shafts, caustics, god rays, and particles all use additive blending and skip depth writing. This allows unlimited layering of transparent effects without Z-fighting or overdraw artifacts, which is the correct approach for atmospheric effects that should accumulate light.
- 05Player position as the single source of truth — the JellyfishCharacter component propagates its Three.js world position to Scene.tsx via onPositionChange callback, which distributes it to all 11 downstream components as props. This avoids global state while ensuring every depth-sensitive system receives the same ground truth.
- 06HDR environment replaced with hand-rolled lighting — the @react-three/drei Environment component (which fetches HDR from a remote CDN) caused 429 rate-limit errors on GitHub Pages. Removing it and building DepthBasedLighting from scratch eliminated the external dependency and gave full control over the lighting grammar.
- →Remote CDN dependencies in static deployments are a reliability risk — the HDR fetch failure was a production bug that only appeared on GitHub Pages, not in local development. Replacing it with procedural lighting was both a fix and a feature: the lighting became richer and more controllable.
- →Screen-space derivatives (dFdx/dFdy) are underused in creative WebGL work — they provide mathematically precise gradients with no performance cost, enabling caustic simulation that would otherwise require a full ray-march.
- →NPC behavior state machines need randomization to feel alive — deterministic jellyfish patterns looked robotic. Adding ±20% random variation to timer durations and 15% probability of pattern mutation made the swarm feel organic.
- →Depth-driven systems need smooth transitions, not hard thresholds — early versions had abrupt lighting changes at zone boundaries. Switching to per-frame lerp eliminated the visual pop and made depth feel like a continuous variable rather than a step function.
Live — deployed to GitHub Pages