Realistic Oceans Using Mesh Deformation!

Making realistic waves using skinned meshes and mesh deformation!

Download here

NOTE: This tutorial is for the scripting end of mesh deformation waves. If you need to know how to make the wave itself, check out this tutorial here or download this sample model. Now onwards with the resource!

Using a formula called the Gerstner wave formula, making realistic-looking waves in 3D games have been really easy, and the end results are always stunning. In this resource, I will cover my module that simplifies skinned mesh waves, and you can get your water to work with just a few lines of code! Also, this module should only be used on the client, to prevent unnecessary server strain. (And every clientā€™s wave should be synced, as it uses UTC time)

Documentation

Wave.new(plane: instance, waveSettings: table | nil, bones: table | nil) => wave

Make a new wave instance that you can use to use update functions. If a table of all bones are not provided, it will find every bone descendant of plane and assume those to be the bones.

Default waveSettings:


{

WaveLength = 85, -- How long each wave crest should be

Gravity = 1.5, -- How gravity affects the wave

Direction = Vector2.new(0,1), -- What direction the wave moves in: Vector2.new(xDirection, zDirection)

PushPoint = nil, -- Alternative to Direction, this will be used if it exists. All waves will move away from this part

Steepness = 1, -- How steep the distance between the crest (top) and trough (bottom)

TimeModifier = 4, -- A higher modifier will make the waves move slower

MaxDistance = 15000, -- Maximum render distance, if character is past this, it will not render the wave movements. Set to math.huge if you wish to ignore this.

}

IMPORTANT: When you used to provide a direction of 0, 0 for the waveSettingā€™s direction, it would error because it did not know what direction to go to. Now, instead of erroring, if provided with an empty Vector2, it will instead generate its own directions using the perlin noise algorithm, which gives the waves the effect that it is not moving in any one direction.

Wave:ConnectRenderStepped() => Connection

Updates the wave every frame, according to waveā€™s waveSettings. This should be used to actually start the wave movement, and the :Disconnect function can be run on the returned connection to stop it at any time. (although you should probably run Wave:Refresh() after you do that, as the waves will be stuck in their last position)

Wave:Update() => nil

Updates all of the waveā€™s bones and uses wave.time as time. wave.time must be set manually.

NOTE: This function does not normally need to be used, as Wave:ConnectRenderStepped already covers this. However, if you want to code your own version of Wave:ConnectRenderStepped, then this function will come in handy.

Wave:Refresh() => nil

Updates all of the waveā€™s bones to their original positions, setting the wave to be a flat plane.

NOTE: This function does not normally need to be used, as Wave:ConnectRenderStepped already covers this for when the character is outside of MaxRenderDistance. However, if you want to code your own version of Wave:ConnectRenderStepped, then this function will come in handy.

Wave:Destroy() => nil

Destroys the wave object, useful when saving memory. This should only be used when a wave no longer needs to be used. This will not destroy the wave plane, rather any cache, functions, and connections associated with it by this module

Code Examples

Example 1

In this example, it makes a simple wave moving along the X axis using default settings.


local Wave = require(script.Wave) -- Change this to wherever the module may be located

local WaveInstance = workspace:WaitForChild("Wave"):WaitForChild("Plane") -- Change this to wherever your wave's plane is located

local TutorialWave1 = Wave.new(WaveInstance) -- I won't include the other 2 parameters, as I want it to find the bones for me and I want to use default settings

TutorialWave1:ConnectRenderStepped()

-- Tada! Your water should now have steady waves pointing in the x position

Final Result:

https://gyazo.com/b2aecc11ab3e1397e03e1e6e24dabcff

Example 2

In this example, I used my own waveSettings to make steeper and slower waves, and then disconnect it after 10 seconds (because why not).


local Wave = require(script.Wave) -- Change this to wherever the module may be located

local WaveInstance = workspace:WaitForChild("Wave"):WaitForChild("Plane") -- Change this to wherever your wave's plane is located

-- Do keep in mind that any settings you want to keep default, you do not even have to include. I however, included it just for the sake of the tutorial.

local WaveSettings = {

WaveLength = 95, -- Changed from 85 for slightly longer waves

Gravity = 1.6, -- Changed from 1.5 for slightly higher gravity

Direction = Vector2.new(1,1), -- Now it points to positive x and positive z

PushPoint = nil, -- Example of this in next example ; )

Steepness = 1.5, -- Changed from 1, so bigger waves

TimeModifier = 6, -- Changed from 4 so I can get slower waves

MaxDistance = 1500, -- Kept the same,

}

local TutorialWave2 = Wave.new(WaveInstance, WaveSettings) -- I included custom settings, however I still want the code to find the bones for me, as they are descendants of the plane

local WaveConnection = TutorialWave2:ConnectRenderStepped()

-- Maybe I want my wave to stop after 10 seconds (for whatever reason)

wait(10)

WaveConnection:Disconnect()

-- And to clean up the wave:

Wave:Refresh()

-- Tada! Your water should have had steep waves for 10 seconds, and then it stopped

Final Result:

https://gyazo.com/e83b77425d1166c05ee6e467f4f248ca

Example 3

In this example, I use a follow part to make the waveā€™s direction look more realistic


local Wave = require(script.Wave) -- Change this to wherever the module may be located

local WaveInstance = workspace:WaitForChild("Wave"):WaitForChild("Plane") -- Change this to wherever your wave's plane is located

-- My custom settings only change the PushPoint, which means all other settings will be default.

local WaveSettings = {

PushPoint = workspace:WaitForChild("PushPoint") -- My push point is called "PushPoint" and is parented under the 'workspace'.

}

local TutorialWave3 = Wave.new(WaveInstance, WaveSettings) -- I included my custom settings with the PushPoint

TutorialWave3:ConnectRenderStepped()

-- I want to destroy the wave after 15 seconds, because it looked at me funny :(

wait(15)

TutorialWave3:Destroy()

-- Nothing to cleanup because we destroyed it!

-- Tada! Your waves should have been pointing away from your PushPoint, and after 15 seconds, been destroyed after calling :Destroy() on the wave.

Final Result:

https://gyazo.com/7c5a7c877fa1ce51fb59d8cb46313780

Final Result With Perlin Noise:
(When inputting an empty Vector2 in the waveā€™s direction)

https://gyazo.com/f8c105a3bbd7317efb2538657d74fea8

Example Place

Linked below is a place that uses the basic wave linked at the top of this post and my module to make slow and calming waves. Press "X" key to toggle fly, and go ahead and see how it works! For this example, I set the direction to (0, 0), which means the perlin noise algorithm will be used instead.

Single plane version:
WaveExample.rbxl (194.5 KB)

Multiplane version: (MUST HAVE GOOD COMPUTER)
WaveExample-4Plane.rbxl (195.4 KB)

Conclusion

Thank you for viewing my module! I hope that it can help you in some way and make you a better developer! If you enjoyed this or like the final results, it would mean a lot to go ahead and press the heart button right underneath this to show your support. If you have any questions or problems with the module, feel free to comment down below and I will help you.

Thanks for 100 likes!

347 Likes

Great tutorial. More efficient that the scripts I made. Iā€™ll link back to this one in my post.

9 Likes

Really cool. I made something very similar to this for my portfolio however it is very cool to see other options for ocean mesh deformation. Wish this was out before I took the time to create a system myself.

Couldnā€™t resist translating it lol.

2 Likes

ZĆ ijiĆ n. Google translate for the win.

Great tutorial, looks pretty good! Iā€™ll definately be using this.

2 Likes

I will check this out later!

Is there any function to get the waveā€™s height from a xz-coordinate?
This would be useful to implement custom floating.

3 Likes

Good idea! I am working on making a function where you can get the CFrame of an object as if it were floating on the wave.

5 Likes

if you look long enough into it it starts looking like some blue fleshy being that moves slowlyā€¦ looks morbid O_O

4 Likes

Is it possible to create tsunamis with this? I really want to test this out!

2 Likes

I suppose it is possible, and change the steepness and the WaveLength to a higher number.

1 Like

UPDATE!

I have now added a feature where before, if you had PushPoint disabled and your Direction was Vector2.new(0,0), it would error because the wave has no idea where to go. Now, instead of erroring, it will come up with a direction on its own using the perlin noise algorithm, to make realistic waves that do not go in any one direction! See this video to get what I mean:

https://gyazo.com/f8c105a3bbd7317efb2538657d74fea8

As you can see, the waves do not go in any one direction, and should be used when you want waves going to no one place. This also includes support for multiple wave planes put next to eachother, as the direction for each point in the wave is decided based on the planeā€™s X and Z position, so positioning a plane next to another plane, you should see a seemless transition when using this.

6 Likes

Simply changing the .Transform property of 2 hundred bones takes about 1 ms, which is a lot of time for such a simple effect, do you have a way to mitigate this performance impact?

3 Likes

Unfortunately, there is no other way to be able to make it faster without lessening the amount of bones, making the waves look less realistic. 1 ms is about the fastest it can go, and luckily, 1 ms is fast enough to not have any noticeable performance impact when rendering the waves.

1 Like

How is 1 ms (1 / 1000 seconds), that long?

5 Likes

Thanks for making this. It will be very helpful for those of us without Blender expertise to be able to experiment with this effect.

I did a quick test of your sample place and it looks cool. I donā€™t think Iā€™ll be able to use it for a flying game as the mesh render distance isnā€™t far enough. At a typical airplane cruising altitude the mesh will not be rendered. Also, I think it might be hard to make an area large enough to pass over so quickly(as in an airplane) and keep up with rendering the next section.

Should make a lot of boat or beach games look awesome though.

1 Like

Iā€™m glad you like my module!

You can change the render distance in the wave settings. And in a future update, I will make it so the characterā€™s Y position does not get counted in the render distance. Also, to make waves render seamlessly, you can put multiple waves next to each other and use the render distance feature to only render waves that are close enough to the plane.

2 Likes

I tried math.huge. Unfortunately it looks like its not a problem with the animation, its the mesh texture render distance. At about 300 stud height, the mesh texture is made flat and all detail is removed. You can still see the wave animation paying along the edges of the wave boundary. Between 250 and 300 stud height the mesh texture reduces in fidelity until at 300+ its completely gone/flat, solid color.

1 Like

Im pretty sure you can fix this by going to the plane mesh and editing a property called RenderFidelity. It decides what happens to the level of detail after a certain distance away from the character.

I checked the sample place and the plane mesh is already set to precise, which is supposed to render the most detail regardless of distance.

This is near 300 height:

this is 300+:

4 Likes

Are you on the highest graphical setting?

1 Like

Oh that might just be the fog from the atmosphere. Thatā€™s not the mesh itself. Mess around with the lighting and fog.

1 Like