Making waves

June 2025

One of my goals is to make the environment feel as dynamic and alive as possible. In koi farm 1, I simulated all the waves in the scene instead of just looping a waves animation or using some shader trick to render waves; every wave in the game was at some point caused by a fish, a raindrop or the user, and waves bounce, combine and travel across the scene as time passes. For koi farm 2, I want that too, but better of course. There are a few extra things to consider this time:

  • While koi farm 1 had a very limited scene, environments can be bigger now. Simulating waves for all existing water wastes too many GPU cycles.
  • The waves need to be 3D this time.
  • Pond shapes are completely unpredictable because players design their own ponds, the water simulation needs to work in any case.
  • Simulating refraction in 3D is not trivial unless you're raytracing.

Many ingredients go into rendering believable water:

  • Objects below the water surface are colored slightly differently.
  • Some objects are partially submerged, like plants.
  • When light travels from air to water, refraction occurs. Light moves through the water surface at an angle creating distortion.
  • Water is very reflective, it should reflect the scenery around it and the sky above.
  • Waves change shape all the time, and the water needs to animate these waves at a sufficiently high resolution.
The wave simulation
The wave simulation

The wave simulation uses the same algorithm koi farm 1 used. This works well enough to create and propagate waves, and reflecting those waves back from shores with arbitrary shapes. The simulation gives me an image which contains the wave altitude on every pixel. To keep things performant, the simulation only runs for water in a fixed size area centered around the camera. When the player changes perspective, the simulation moves with the camera to simulate water wherever required. The wave simulation also only updates waves where water actually exists. To do this, I render the scene altitude from a top-down perspective, and I use this heightmap as a depth buffer while rasterizing the next wave simulation image state. The animation shows the wave height as a grayscale value; the bright dot moving from the right to the left is a fish swimming near the surface of the water causing waves.

Wave mesh distortion
Wave mesh distortion

The wave simulation algorithm gives me rather smooth waves which look similar to sine waves when viewed from the side. This is not how real waves look, and they don't feel right when rendered in 3D. The peaks of higher waves are somewhat sharp, while the spaces in between are smoother. Last month's isotropic water mesh comes in very handy: it's pretty resistant to self occlusion and warping, so I can move its vertices in the direction of the derivative of the wave simulation texture without messing up the geometry. This creates precisely the desired effect. Waves are sharp at the crest and the troughs are wider and smooth.

Ray-traced refraction
Ray-traced refraction

Rendering refraction turned out to be a major issue. When using ray-tracing techniques it's pretty trivial, but I can't rely on users running hardware with these capabilities, and the koi models will be way too complex to ray-trace efficiently. I've tried screen space refractions, where I first rendered the underwater scene to an image, and then ray-traced into that image from every water surface pixel to find out what pixel should be visible there. The resulting distortion is physically correct, but the results weren't pixel perfect because I've had to limit the ray-tracing resolution to keep frame rate within acceptable levels, and tracing colors behind koi on a pre-rendered image is impossible. This created many false positive traces where a koi pixel was hit, while in reality a pixel behind the koi had to be retrieved. It's impossible to distinguish different "layers" of colors in a single image. The image on the right shows the result of this problem. some pixels get the right color, but whenever the refracted ray goes behind something, noisy artifacts show up instead of what you should have seen. The black and white koi in the top left is a great example: many rays hit behind the koi but return koi pixels instead, which extrudes the koi upwards.

Eventually, I settled on faking refraction convincingly:

Koi refracting through the waves
  1. Underwater objects are rendered to an image. Everything underwater is skewed upwards in a way that approximates the effect of refracting light rays, excluding shadows and depth based calculations.
  2. The displacement magnitude for each water pixel is calculated based on the angle of incidence which is the angle at which the ray from the camera hits the water surface. This is a rough approximation for the distance an underwater ray travels, which affects the amount of distortion.
  3. The underwater image is rendered on the water, but a displacement is applied based on the water surface normal and the previously calculated displacement magnitude.

The video shows a test of this "fake refraction" effect. Koi displace an unrealistic amount of water above them for testing purposes. The only thing that remains are reflections of both the sky and plants on the pond shores, but since neither exist for now, I'll implement that later.