Come with me on a journey to design a water caustics-like effect for my nautical-themed D&D campaign website.

The Problem

I run a D&D campaign called Against the Tide — a nautical adventure full of sea monsters, cursed ships, and saltwater misery. The campaign website needed to feel like it was underwater, and nothing sells that feeling quite like water caustics: those shimmering, dancing light patterns you see rippling across the seafloor on a sunny day.

The question was how to actually pull that off in a browser without it becoming a weekend-destroying rabbit hole.

Spoiler: it became a weekend-destroying rabbit hole. But a fun one.

Research: How Hard Could This Be?

My first stop was figuring out what I was even getting myself into. A quick research pass turned up some humbling results.

There's this deep-dive on real-time water caustics rendering by Martin Renou, complete with a full 3D Three.js demo. It's genuinely impressive and genuinely overkill for a campaign website. Then there's the Disney Animation research on water caustics, which reminded me that very smart people have spent entire careers on this problem. There's even a Blender tutorial for pre-rendering the effect if you're in the 3D art world.

The takeaway: real caustics simulation is hard. Like, PhD-level hard. Time to figure out how to fake it convincingly.

Weighing My Options

After the research phase, I had a rough menu of approaches:

  1. Full browser simulation — 3D graphics, shaders, external libraries. The "right" way, but almost certainly more work than I wanted to do.
  2. Pre-rendered video or animated image — Render the effect offline and use it as a CSS background. Practical, but it felt like cheating in the wrong direction (too static, too heavy).
  3. Static image textures — Just grab a caustic texture image and animate it with CSS. Quick, dirty, possibly effective.
  4. JavaScript + Canvas 2D simulation — Write a particle or wave simulation from scratch. Interesting, but a lot of custom code to maintain.
  5. WebGL shaders — A step up from Canvas, using the GPU directly for a procedural effect. Shiny, new (to me), and promising.

Options 1 and 2 felt out of scope for a weekend project. Option 3 caught my eye because of the sheer bang-for-buck potential — a good caustic texture animated subtly can go a long way. But option 5, WebGL shaders, had that irresistible "I've been meaning to learn this" energy. So naturally, I went with the shiny thing.

Detour: Image Textures and CSS Tomfoolery

Before diving into WebGL, I took a quick detour with the texture approach just to see what was possible. The idea was simple: take a caustic texture image, layer two copies of it, and slowly pan and zoom them at slightly different speeds to create the illusion of movement.

It worked better than expected. With some careful tweaking of opacity and blend modes, the result was subtle enough to feel atmospheric without screaming "I slapped a JPG on a div." You can see the experiment in the CodePen below:

If I ever need a lightweight fallback, this is it. But onward to shaders.

WebGL: The Real Deal

I went to Claude with a prompt and got back a WebGL shader implementation that was, honestly, extremely promising right out of the gate. The procedural caustic pattern looked great — the kind of organic, rippling light you'd actually see on a shallow seafloor.

There was one thing that immediately felt wrong, though: the GLSL shader source code was shoved into a JavaScript template literal string. It works, but it feels gross — no syntax highlighting, no editor support, just a wall of shader code masquerading as a string. It's the kind of thing that's fine for a demo but would bother me every time I opened the file. Something to address if this ever gets properly productionized.

The Mobile Problem

Everything looked great on desktop. Then I pulled up my phone.

Desktop gave me beautiful, shimmering caustics. Mobile gave me... something that looked more like a bad screensaver from 2003. The patterns were blockier, less smooth, and the overall effect lost the organic quality that made it work.

Desktop Mobile
Desktop Version Mobile Version

After some digging, the culprit turned out to be floating-point precision. Mobile GPUs default to mediump (medium precision) for floats, which isn't quite enough for the smooth gradient calculations the shader relies on. Switching to precision highp float at the top of the fragment shader brought the mobile rendering in line with desktop.

precision highp float;

One line. That's it. The tradeoff is that high precision is more taxing on mobile hardware, so it's worth keeping an eye on performance — but for a background effect that's largely aesthetic, it seems like a reasonable call.

Putting It All Together

With the shader working across devices, I started integrating it into an actual page layout. I added a parchment-like paper texture for the main content body, letting it float over the animated water background. The title sits above the paper, suspended in the water itself, which gives the whole thing a layered, almost physical feeling — like reading a document that's been dropped into a tide pool.

The caustic effect blends nicely with the overall aesthetic, and getting it to interact with the text elements (rather than just hiding behind them) is something I kept iterating on. I'm happy with where it landed.

I also snuck in a little easter egg: there's a pirate coin in the header that you can drag around. Go ahead, give it a try.

Takeaways

This was a fun one. A few things I walked away with:

WebGL shaders are more approachable than they look. If you've been putting off learning GLSL because it seems intimidating, a weekend project like this is a great entry point. The basics click fast once you have a working example to tinker with.

The precision issue is worth knowing about up front. If you're writing shaders that will run on mobile, test early and set your precision expectations deliberately rather than relying on defaults.

The texture + CSS approach is still a solid option for simpler cases. Not everything needs a shader. Sometimes a well-animated PNG is the right tool for the job.

And finally: D&D websites are an excellent excuse to learn new web technologies. Highly recommend.