top of page

Water Simulation

Two different approaches to the simulation of water waves are, for one thing, the depiction of water via the Gerstner wave formula or, on the other hand, the simulation of waves by the Fast-Fourier-Transform algorithm. Both were implemented in this Unreal Engine 4 project.

Content

1. Project Summary and Structure

2. Shared Material Functions

3. Workflow to implement the different approaches

4. Beaufort Scale

5. Conclusion

6. Future Plans



Project Summary and Structure

Water and wave generation are recurring elements in multiple games, with each different visual representation and interaction. The topic has been present in my work since the development of Tropical Gunfight in 2021. Here, Gerstner Waves were used to display the ocean surrounding the island. The shader was then refined, including the implementation of basic sub-surface shattering. To further familiarize me with the topic of water simulation in ocean shaders, I wrote my bachelor's thesis about the different approaches in computer games and implemented another version of the Gerstner Waves as well as a wave simulation based on Jerry Tessendorfs Fast Fourier Transform Algorithm.

The ocean shaders were developed in Unreal Engine 4 using blueprints, C++, and HLSL.

Furthermore, I plan to continue this project and implement multiple features and reworks.

Waves simulated with the Fast-Fourier-Transform algorithm and the Gerstner formula are kept separate and therefore developed in different shaders. The base structure of the material components, however, is similar.

To change the simulated wave type, it has to be chosen in BP_Ocean, which manages the ocean simulation in general. The ocean parameters can also be altered here.

In the blueprint, the respective ocean type is selected via a switch in both the construction script and in the event graph.


Shared Material Functions

The watercolor of both materials is determined in the material function MF_WaterColor. Hereby, the color is dependent on both the transparency of the water as well as a set angle of light incidence, which is used for the sub-surface shattering. To enhance the readability of the material, this was written as a function.

[mf-watercolor]

It is then parameterized in the shader by using material parameter collections that both shaders share.

To calculate the transparency of the material, MF_Opacity was written. Since depth fade is used, the material must be tagged as transparent or post-process. Furthermore, it is also used within MF_Color to determine the watercolor based on depth.

The simulated ocean can be vast, so it was necessary to check when waves must be simulated. MF_DistanceFade is used for veneering the material in the distance, taking the current player position into account.

In order to diversify the water surface, normal maps are used to add movement and texture to the waves. Two normal maps are animated with the motion chaos function.


Workflow to implement the different approaches

Before the Gerstner Wave Formula and the Fast-Fourier-Transform Algorithm were implemented in the Unreal Engine, both were written in pseudo-code. This was to structure the development and to make scripting the methods easier.


Gerstner Pseudo Code

The implementation of wave based on the Gerstner Formula was fairly straightforward. First, a single function called GerstnerWave is created. It contains the calculation of the Gerstner Formula.

To enhance the readability of the code, the calculation of the wavelength is in another function.

Since only one wave is created per calculation, it was necessary to create a wave set consisting of multiple callings of the GerstnerWave function.



Gerstner in the Unreal Engine

The Gerstner wave formula was implemented using only blueprints and multiple materials. Parameters are read from two material parameter collections.

The complete material shader is displayed below.

Hereby, the simulation of waves is processed by the world displacement.

Waves, as well as the corresponding normals, are calculated in MF_GerstnerWave.

To prohibit a slope greater than 1, HLSL code is used to check the input value. This is due to the fact that Gerstner waves are derived from circles, and should the slope be too high, it overlaps, causing breaks in the geometry.

In MF_GerstnerWaveSet, a wave set is generated. This is achieved by calling MF_GerstnerWave a total of six times with each slightly different parameters.

The function to generate the set in the ocean material is called two times. Therefore a total of twelve waves are simulated. The input parameters are different to enhance the diverseness of the ocean.


The wave pattern is characteristic for waves generated with the Gerstner Formula with smooth wave surface and generally rounded peaks.

It is possible to use different input parameters to simulate various ocean types.



Fast-Fourier-Transform Pseudo Code

Unlike the implementation of the Gerstner waves simulating an ocean via the Fast-Fourier-Transform Algorithm required a more complex approach and execution. The first function, called FFT, generates the spectrum based on the input parameters. Complex numbers are used to display the values.

The oceanographic spectrum used to simulate the waves is the Phillips spectrum by analogy with Tessendorf's paper.

To calculate the wave height field, two functions are needed. They generate it for h(0)k and h(0)-k and use a random Gaussian number and the Phillips spectrum, amongst other things.

Fourier components are calculated over time and with h(0)k and h(0)-k.



FFT in the Unreal Engine

The simulation of FFT waves is executed in BP_Ocean. Hereby, multiple separate materials, as well as material functions, are needed.

The complete material shader is displayed below. The generated Fast-Fourier-Transform normals and displacement are used in this shader.

The majority of the FFT calculation is done in BP_Ocean, with multiple steps needed. The ocean is initialized in the construction script. A dynamic instance of the ocean materials is created and assigned to the mesh here.

The main components of the ocean simulation are defined in begin play and in event tick. Since the FFT is written on render targets that are then used in the material, they are generated on begin play. Furthermore, dynamic materials of different components, such as the Philips components, are created here. Render targets used for calculation are also generated here. The Philips spectrum was implemented as a material with blueprints, and the equation from Tessendorf was used. With a switch, both Philips components can be calculated in the same material.

A switch is also used in the butterfly material to distinguish between vertical and horizontal butterflies.

The Fourier components are realized as axis shifts. The same material is used for both the x and y-axis, and they can be chosen by a component mask. The calculation for the z-axis is similar, and the only difference is that the Phillips components are used to display the height field.


The FFT function is called for x,y, and z coordinates in event tick. Two render targets are used as input parameters, and the result is written on texture write. The calculation is displayed below:

The parameters of the dynamic material are determined by render textures called flip and flop.

After every stage, a variable that determines the coordinate on where to write on the texture is incremented.


Beaufort Scale

In order to visualize the variety of the FFT and the possible parameters, the Beaufort scale was implemented. In general, this is an empirical measure that relates wind speed to observed conditions at sea or on land. The strength ranges are divided into 13 categories, with 0 being a calm, mirror-like sea and 12 representing hurricane-like weather.

To implement the scale in Unreal, an enum E_BeaufortScale and a material parameter collection MPC_BeaufortScale were created. The strength ranges are managed in the enum, and the values per range are held by the material parameter collection.

For the user to be able to choose between either setting their own values for the FFT or using the Beaufort Scale, a boolean is used to check the preferred input. It is then switched on the chosen number.

For every possible choice, the values are then set. Here it is important to note that if the values for the corresponding strength range are set as a minimum and maximum value, a random float in range is chosen.


The achieved pattern is characteristic of waves generated by the FFT. Therefore, a diverse wave pattern and a great number of waves are displayed.

With the preferred simulation method set to the Beaufort scale, the wave pattern changes gradually depending on the number.


Conclusion

The implementation of the different wave types presented other challenges for each. And the essential realization of the FFT and the Gerstner waves were on a different difficulty levels. While the FFT required more functions and a deeper understanding, and multiple attempts, the simulation via the Gerstner formula brought problems with the geometry meshes and the tessellation.

In general, a more complex and diverse wave spectrum can be simulated more easily with the Fast-Fourier-Transform algorithm. However, to achieve a similar wave pattern with the current approach of the Gerstner waves, a great multitude of waves is needed. Furthermore, it is still necessary to optimize the Gerstner waves in teams of the mesh, because geometrical issues are present.


Future Plans

To broaden my horizons, I plan to continue working on this project and implement multiple missing features. Newly added content will then also be documented here.

In the following, a list with the planned features is displayed.


  • Simulation of waves via the Beaufort Scale [DONE]

  • Simulation of white caps via the Jacobian matrix

  • Rework of the wave generation in C++

  • Implementation of buoyancy and collision detection in general

  • Implementation of underwater camera



What went well

  • Gained a deeper insight into the topic of water waves and wave generation

  • Research of the theoretical basis and current methods and possibilities

What went wrong

  • Rework of the project structure due to C++ problems


Project Gallery

bottom of page