So… we have finally come to this… eventually every game developer must deal, one way or another, with water on their game. It’s our time.

When thinking about our forest, water, lakes and even seas were something that we wanted to add to spice a little the topography of this location… the ideas were many, but it always comes to how “good” your water is… so you don’t break immersion (huh…).

Objectives:

Our goals were laid down at the very moment our subsequent levels were created… water is a key part on the first arc. We needed the following:

  • Water that can react to collisions (do ripple waves)
  • Water that can simulate oceans and high tides.
  • Water that doesn’t look out of place.

So let’s take it step by step… this tutorial will cover the first two points… the physical behaviour of our water, but for that.. we must first solve our first question…

Water: sprite? shader?

Whatever the case.. we must respect this first “art” approach given by our artists.

 

Water visual idea

2D Water vs 3D:

Having a 2D game, the answer seemed obvious (duh.. 2D brah). This would imply creating 2D sprites that animate the water (those should tile)… and when the character hits the water.. it creates a splash animation. Using this approach, has two issues:

  • Water cannot be “simulated” and if we wanted to add movement, we needed more 2D assets per movement type.
  • Tiling on our game can be a little hard to accomplish due to the nature of our art work… altough it is a solved issue, still requires a little time to set up the water paint masks to tile.

Besides… it just didn’t give us that “alive” feeling we are seeking for our game.

So… 3D it is! Having extreme care with the art style not being torn apart by this choice (see: next part – water: the reckoning) we started the creation of the physics simulation.

 

Chosing the main function – Sine vs Gerstner Waves.

While investigating this topic we came across two awesome articles that helped us quite a lot with this part: GPU Gems (for understanding the math behind the gerstner wave) and OceanShader with Gerstner Waves (for implementing the gerstner wave on UE4 material editor).

While those two articles help quite a lot specially on ocean shaders, we only ever used a small fraction of the logic there, but what those tutorials fail to mention is the core concept of a traveling wave.

Assuming not everyone is familiar with the core concepts we will do a little TL;DR:

  • Function: think of it as a little machine that eats values and spits values.
  • Sine function: a “machine” that receives any number and returns a value between -1 and 1, it’s cyclical, meaning that when it does a loop, it simply repeats itself with the new values.. for example inputing “pi” will give you a value of 0… while inputing “pi/2) will give you 1… etc

Here… is dangerous to go alone, take a graph:

The sine function.

 

So with this we have an idea of how the sine behaves… But what happens when one inputs different type of data?

  • If inside our sine, we feed it the current time: We would see the sine going up and down… repeating itself.
  • If we instead, feed it “distance” data: We would see a sine that’s stationary (doesn’t move) but has a different value on each point… like an still picture of a ripple wave (frozen in time).
  • If we feed it with time AND distance: We have the concept of a traveling wave.

For better visualization take a look at this article

So, having the idea of the function y(x,t) = A * Sin(kx – wt), we just need to order it a little to be of better use for us.

  • k is the length constant
  • w is the “time” constant
  • A is amplitude

After a little reordering (check the prior article)

With:

  • Lambda: the length of the wave.
  • S = the speed of the wave

y(x,t) = A*Sin(kx-wt) = A*Sin(2*pi/Lambda * x – v*2pi/Lambda * t)

Is a equation that uses variables that are more easily modificable for our artists (we only wanted to deal with length and speed).

With this we only need to talk about the other wave, the Gerstner Wave.

 

Gerstner Wave:

So.. the issue with the sine wave, it’s that is a really simple wave (the simplest wave actually!… every other wave can be represented as a sum of sines), the issue with only using sines fo this.. is that they are good for small water bodies, but seas tend to have another “shape”. While sine waves have a rounded tip and rounded bottom.. seas tend to have a pointy tip and rounded bottom… the Gerstner Wave is a good approximation for that (more info on GPU Gems).

Credit to Gpu Gems

 

We won’t be talking much about them… but they are basically a modified sine wave that can receive a “Steepness” value for controlling precisely that pointy tip.

With this two solutions we proceed on creating those material functions (using UE4).

Traveling Sine Function

Gerstner Wave Function

 

Ok! Main component complete, we now have a wave that travels! For the Gerstner Wave… our jorney ends here… because we won’t do ripple effects with that function and only use it to simulate high seas.. here’s the result of the function being hooked up to the World Diplacement Node:

Sea displacement

 

Ok that’s awesome!… but we’re not yet done… we will bid farewell the Gerstner Wave and use the sine function to transform it in a “Ripple Wave Function”.

 

The Sinc Function but not quite

Thinking about a ripple wave in general will lead to the following mental image: “a large peak at the point of impact, that diminishes when time and distance passes”.

So… do you think this is a good enough approximation of a “ripple” wave?

Yep… looks about right

 

This is called the “Sinc Function”, a fancy name for just saying: “Hey.. take this sin(x) and multiply it by 1/x (the rational function)”, it basically works by taking the argument (input) of the sin(x) function and dividing the sine wave by it, as 1/x is greater the closer to zero it gets, this approximates quite well the “large peak that diminishes.”

So… we just grab what we feed the sine wave and divide our result by that? Sounds easy! Well, not quite, this is what happens when you do that

Traveling Sinc Wave

Basically we’re making the Sinc wave move away from the center, not diminish in time and distance… that’s a Traveling Sinc Wave.

For the Sinc Wave to work like we want we will modify the Sinc Wave to be like this

Sinc_2(x) = A*Sin(2*pi/Lambda * x – v*2pi/Lambda * t) /(x * friction + t * decay+ smoothness)

This new function works just like the sinc but with a few modifications.. those are

  • The argument of the sine function is not the same as the divisor, so it won’t translate into a traveling Sinc function.
  • Friction: a factor that makes the distance matter more or less on the diminish of the sinc function. For greater values, the wave loses energy faster when moving away from the impact point.
  • Decay: Same as friction but on time… the wave dies quickly if this value is high enough… but for a zero value decay, the wave never decays.
  • Smoothness: This variable was created in order to smooth the sharp peak on the origin when the time equals zero, by shifting the 1/x function to the right (farther from infinity). Use a greater value if you need a small peak, use 0 if you want a big sharp spiky, dangerous and extreme peak.

This new function will give us this result:

Sinc wave without filter

 

This is ALMOST what we want… but if you take a look at a tiny detail…the wave isn’t “born” on the center and then travel to the borders, it kinda just “pops” into existence, and this looks really weird.

So… with the sinc wave complete, let’s patch this up with the last function to add.

 

The Step function as a Lowpass Filter.

The final step (huhx2) on our efforts to make the water shader will be the creation of a “Filter” something that lets some values pass, but destroys other values, what we want to do is, with the velocity and time alive of the wave, filter out everything that the wave hasn’t reached out yet (to avoid the pop up). This means that every vertex point that is greater than the distance traveled by the wave gets filtered out, in other words: “every point greater than v*t must equal 0”. This, translated into material nodes look like this (trying to avoid if/else here):

Step function on Material Nodes (divisor is the distance of the current vertex)

 

Multiplying this final function will finally let us have a proper traveling sinc wave:

Proper Traveling Sinc Wave

 

With that we have a good traveling wave that can be (hopefully) used on the game. By creating a material instance that has 3 Traveling Waves and with 3 different set of material inputs for each one, we created a blueprint that (on actor hit) cycles through those waves setting the Impact Point and Impact Time so the Waves react to objects thrown to them (the creation of the larger material is actually easy to do, so it won’t be covered here, although if requested, we can later edit this post to add screenshots to that material).

This is the result of our wave reaction mesh:

Waves… done

Little Note: Don’t forget to use the times since the impact as the sin(kx-wt) time, if you just input the game time, every wave you create will be in phase… and it just looks weird.

With this we wrap the first part of our water shader tutorial, next issue we will discuss how we changed from this wireframe, to something actually usable in game.

So, if anybody of you actually enjoyed this tutorial, please let us know on the comments… also adding us at out twitter @CritFailStudio and Facebook /CriticalFailureStudio and giving us your support and ideas for next tutorials and game feedback will help us greatly to keep on this weekly posts.

 

Cheers!

2 Responses

  1. Cornelius Carl dijo:

    hey there! Really enjoyed this tutorial. Any chance to see the full graph/ get a download for the material?

    • HeavyBullets dijo:

      The graphs or material nodes will be given on part 2.. which will be uploaded this friday (as they complete those functions)

      I have been making some changes to the formulas, so they will be a tad different, but mantaining the same overall logic

Agregar un comentario

Su dirección de correo no se hará público. Los campos requeridos están marcados *