Tuesday, March 10, 2015

Ocean Water Simulation and Rendering

Water is a great way to show off a graphics engine. There are so many interesting ways to implement water, and with enough care the water can be made to look very real, while at the same time taking only a few milliseconds of frame time with all the effects. This past week I've been working on improving the quality of 3DWorld's ocean water simulation and rendering with GPU-based waves. I've decided to use OpenGL 4 tessellation shaders for creating the wave mesh from a low-resolution grid of quads, one per mesh tile. This isn't the first time I've used tessellation shaders - I replaced the fixed planet meshes in universe mode with hardware tessellated meshes last month, right after my universe blog post. I should make another post on universe mode later with the new planets, as the mesh is both better looking and faster to generate and draw.

The water effect includes the following features, all in realtime (> 200 frames per second):
  • True Fresnel reflection of the entire scene (ground, trees, plants, clouds, sky, sun, moon)
  • Wavelength-dependent underwater scattering along sun->ground->water surface path
  • Large scale waves using mesh deformation with dynamic level of detail (GPU shader)
  • Detailed waves implemented with animated high resolution normal maps (precomputed)
  • Raindrop splashes + choppy waves to simulate stormy weather
  • Underwater caustics using a precomputed animated caustics light map
  • Underwater and above water colored, weather based fog effects
 Here is a screenshot of the ocean from far above. You can clearly see the high detail of the waves, and how the ocean can stretch to the horizon. This is a shader and textures based approach that can render any size of ocean to an approximate xy plane on the screen.

View of water from above, showing highly detailed waves, depth-based color attenuation/scattering, and fog to the horizon.

I spent a great deal of time making the reflections look nice. The entire scene (ground, vegetation, sky, etc.) is rendered in two high level passes. The first pass uses a mirroring about the Z (up) axis to a reflection texture at half screen resolution in X and Y. The second pass renders the normal scene landscape and water. The water shader uses procedural noise to bias the reflection texture lookups slightly to produce the dynamic wave distortion seen in the reflections. The bias depends on wind and weather effects to simulate different wave conditions on the water surface.

The water itself is actually rendered in two passes. First, the terrain under the water is drawn. Since the water is approximated as a flat plane at constant z-value (up), the ground terrain fragment shader can determine the depth of water above the fragment (pixel) being rendered using simple math. It can then trace the path from the position of the light source (sun or moon) to the terrain point underwater, then from that point to the camera/eye. The point where the rays enter and exit the water can easily be calculated, and the optical path lengths from light->ground and ground->camera can be computed. This is used to determine the depth-dependent attenuation and scattering + refracted term, which is rendered as part of the ground, not the water itself.

The second pass renders the water plane itself using a different shader. This pass uses alpha blending and adds the water surface effects: reflections, specular highlights, foam, and view angle-dependent term that adds a green tint to the water at shallow angles. The water surface shader has access to the terrain heightmap and can calculate the water depth at every fragment (pixel). This is used to discard/clip fragments where the water is under the mesh, to add foam near the shoreline, and to make the water more transparent near the shore so that the water/beach transition is smoother. The shader uses different constants and textures for shallow vs. deep water and does a linear blend between the two modes in between. I have even added a set of runtime controls that allow the user to adjust water color, opacity/transparency, muddiness, wave amplitude and frequency, viscosity, etc. in realtime. Yes, there is even a mode to render lava.

In reality there is actually one more pass. When the camera is just above the water, and GPU waves are enabled, you can see the water surface through a partially transparent wave peak. The eye vector goes through the front side of a wave, out the back side, and into another wave behind the first one. It so happens that when the camera is close to the water, the view of the ocean is at a grazing angle, where the Fresnel term weights the reflection term from the second rendering pass more than the refracted term from the mesh drawing pass. In this case, the final color is nearly an additive blend of the ground and reflected colors. This will doubly add the reflected contribution from the front and back waves, making the water surface appear oddly brighter than it should be. The attenuation of light through the volume of the front wave is missing. If there is more than one wave visible, it looks even worse. This can be partially fixed by enabling back face culling, which removes the back face of the front wave. In order to remove the water behind the front wave, an additional drawing pass is used. First, the water is drawn in a Z/depth pre-pass with no color information. Then the water is drawn again with depth testing enabled using a color pass (with water fragment shader enabled). This second pass uses a depth compare of LESS_EQUAL so that only the front face of the water (nearest wave) is drawn. The water behind the nearest wave fails the depth test and those fragments are discarded so they don't contribute to the final color. This involves running the tessellation shaders twice, but most of the work is in the fragment shader, which is only run once, so this has a minimal impact on frame rate.

Here is a screenshot of the ocean from near the water surface. The detailed waves add realism to the water, and the lower frequency rolling waves add depth to the image. It's hard to tell from a screenshot, but the surface of the water is animated with multiple layers of waves for a natural look with very low tiling, aliasing, and repetitive artifacts. The shader is actually quite complex and uses a dozen different textures for the wave normals, caustics patterns, etc.


Ocean water just above wave peaks showing closeup of waves, scene reflections, and specular sun reflection.

 Here is a video I uploaded to YouTube showing the water in realtime. The in-game water is much less blurry!


I made some adjustments to the environment to get a nice screenshot near sunset with a planet and asteroid belt in the background. I can use the universe mode planet rendering as a dynamic background in ground/terrain mode, and even have the sun, planets, and moons move in the background in realtime. There is a key to switch between universe and ground mode that lets you fly your spaceship around to find a planet to land on, then generate the terrain using the planet's temperature, water level, vegetation, atmosphere, gravity, etc. This is the same location as the previous screenshot, but on a different planet where there is more water, a thinner atmosphere, and larger but dimmer sun.

Ocean near sunset with isolated islands, specular reflections, and distant planet + asteroids.

I have also implemented weather effects including rain, snow, wind, fog, and lightning. Here is an image of the same ocean on a cloudy, rainy day. The rain can be clearly seen, along with many small ripples in the water, choppy waves, and more high frequency noise in the reflection. There are some gaps in the clouds that allow light rays to shine down on the mesh and water. I'll have to say more about this later as it's getting a bit off topic.

Rainy day with choppy waves and raindrop splashes. Note the mostly cloudy skies and thick fog in the distance.

Here is a wireframe view of the ocean surface mesh. I'm using tessellation shaders to dynamically subdivide a very coarse mesh based on distance to the camera. The ocean mesh has higher resolution near the camera - but it doesn't have to be very high to achieve a nice sine-based wave effect. The waves are low amplitude to reduce any obvious repetitive patterns, aliasing, and other artifacts. Even these small waves add a lot of realism, at very low framerate cost. I had to use a mixture of 4 different sine waves to break up the repetition and reduce popping artifacts when the level of mesh detail changes.

Wireframe mesh of ocean water surface showing GPU waves implemented with OpenGL 4 hardware tessellation shaders.

Finally, an underwater view. The bottom of the ocean's surface waves can be seen, and the dense fog limits visibility. I have added "fake" caustics on the ocean floor using another animated texture that matches the detailed wave texture above and is animated at the same rate. This view also uses the same wavelength-dependent attenuation with slightly different constant factors. I procedurally placed some rocks and seaweed under the water to make it look a bit more interesting.

Underwater view showing caustics and normal mapping on the ocean floor, underwater fog, and distance based per-wavelength color attenuation.
Overall, I'm pretty happy with this approach to rendering ocean water. It's a mix of real physics for the "easier" and more efficient effects, and precomputed, texture-based solutions for the traditionally more expensive effects such as wave peaks and underwater caustics. It looks like a real ocean, and responds in realistic ways to environmental effects such as lighting and weather. It's also fast enough that it can coexist with all the other terrain features without taking too much frame time. Now I just need to figure out how to have dynamic objects interact with and float on the water surface. I do have that for my older small area CPU-based water, but that's a topic for another post.

No comments:

Post a Comment