Making realistic waves using skinned meshes and mesh deformation!
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.