There are lots of wonderful things in life. One of them is not this site’s lack of an autosave feature while writing posts. One of them is, however, Unreal Engine 4. Another great thing is that UE4 is open-source. Another great thing is that companies like nVidia port a bunch of their tech over to the engine like HairWorks, WaveWorks, HBAO+, VXGI, and Flex.
What’s less great about these integrations is that most of them are out of date (with the exception of, I believe, VXGI). Luckily, the Unreal Engine community is just excellent, and has picked up the slack in some regards:
- Realtime Dynamic GI + Reflections + AO + Emissive plugin – AHR
- Water Rendering: 1 and 2.
So, my first step in getting better water rendering in Sacrilege (I had been using one of the water rendering solutions listed above) was to try out NVIDIA’s WaveWorks. It… It wasn’t a pretty experiment. The default material left a lot to be desired, I thought, so I took an afternoon/evening to spiffy it up a bit. This is a photo chronology of what followed:
Basically, it was a whole bucket of failure ending with some passable ocean coloring and some not-terrible foam. I, being me, wanted to get a bit more out of water rendering than what I have in the past. Which is to say I wanted to have a semblance of an idea of what was actually going on, rather than just blindly using a third-party library. And I wasn’t happy enough with nVidia’s WaveWorks to just sit content in my own ignorance of water rendering techniques. I had to…
Get my feet wet.
Now that that’s out of the way: I started at the source. I knew I needed to read up on some fellow named Gerstner. I knew this primarily because of my work with Sundog Software’s excellent Triton Ocean Rendering system that I used back in my Unity days. Which, by the way, if you’re using Unity, just grab the plugin. It’s pricey, but it’s absolutely worth the money and Sundog Software’s CEO is one of the most fast-responding and nicest dudes I’ve met on the Internet. Anyway. Back to Gerstner. As it turns out, Gerstner was not some CGI specialist from the late eighties/early nineties like you might imagine (at least, I did), but rather was František Josef Gerstner, a Mathematician and Astronomer who lived from 1756-1832. His most well-known publication was A Theory of Waves. So, yeah, you can see where this is going. Gerstner discovered the trochoidal waves in 1802 — waves which had the property of sharp crests and flat troughs. Ideal for, say, a fluid simulation.
And that’s all well and good, but I’m not a Mathematician. Far from it. I majored in English. So, I looked to something a bit more palatable: GPU Gems I‘s “Effective Water Simulation from Physical Models” by Mark Finch. The chapter written in that book (which I remember purchasing when it came out because I was a giant nerd back then too) goes into far more detail about the Gerstner wave’s applications to fluid dynamics and rendering.
Still: a whole lot of math.
Then I found a language I speak in: material graphs. So, you know, thanks… Whoever you are.
But what I quickly realized is that graph can only get you so far. About yea far, in fact:
That’s almost a single Gerstner wave rolling up on the shore of a nearby coastline. So, two things about this:
- It’s very important with these types of waves that you displace in the X/Y (or X/Z, if you’re in a Y-up engine). Without that step, there’s no pinch occurring whatsoever, which makes these all little more than sine waves.
- A single Gerstner wave is really boring.
- Generating a good Gerstner cluster procedurally is… tricky.
- There is something called the Beaufort Scale — generated, weirdly enough, just 3 years after the Gerstner wave that will greatly simplify the way you parameterize your ocean values. That’s talk for another time, but know that it exists and will make your life easier.
There is also the notion of variance proposed in the not-at-all-stuffily named “Real-Time Parametric Shallow Wave Simulation” which provides several equations for steepness and wavelength variance for waves as they approach a nearby coast. I used this paper to modulate the base Gerstner wave function referred to earlier to take depth into consideration for all of its calculations and thereby give the feeling of waves washing up on an ocean shore.
So… That’s one singular Gerstner wave. One. In order to generate a semi-interesting ocean simulation you need to generate Gerstner “clusters” of several waves chaotically bashing up against one another to create the tumultuous and chaotic oceans we all know and love. One solution to this is to generate a unique parameter set for four-plus waves and use that as the basis for a simulation. But, being me, I needed everything to be a bit more procedurally-controllable than that. Especially when you consider that we’re dealing with roughly nine parameters per wave. That’s a lot of different parameters to randomize per cluster. I also wanted my ocean simulation to be entirely material-driven (otherwise, it’s very easy to use random number generators). So, to some extent, there almost has to be a human hand controlling the simulation. Unless, of course, you just set a seed variable that’s based on time.
And thus is born a Gerstner cluster:
And the corresponding material graph (don’t worry, I’ll link all this at the end):
And if you toss a normal map on that ol’ chap with four-way chaotic movement (either stolen from the water rendering community project on the Unreal Engine forums or from one of Epic’s water samples), this almost starts looking like water:
These micro-normals in tandem with a “large wave” normal map – all panning using the same four-way chaotic movement – create enough surface chaos that the whole simulation starts to feel alive. It’s when you start layering in height-based wave cap foam that a major issue becomes apparent with the whole simulation:
Yeah, it’s repetitive as hell. And, I mean, if you were to only look at a given tile, it all looks pretty good, but this simulation has to represent an entire ocean of chaos and individualism. A tiny little-bitty tile that is immediately recognizable from shore isn’t going to cut it. Rather than revisit the Gerstner cluster code, I chose a somewhat different approach: I modulate the height of each wave by a value from a macro noise texture that’s panning across the surface of the ocean. This, in tandem with all the other noise being created by the micro/macro normal, really helps sell the material simulation.
Now, for the final touch, layer in some shore foam, and we got ourselves an ocean simulation:
Oh, also, it really helps that I added real-time image-space reflection, subsurface scattering, refraction, and multiple ocean colors. Just, you know, fyi. Eff why eye. Here’s the final material graph:
It has some… Optimizations that need to be made.