Making A Mesh Deformation Ocean

I considered writing my own version of a guide like this, but instead, I’ll just post a few tips:

  1. If you are using textures rather than a SurfaceAppearance, you can lock the water plane to the position of the camera and make it appear to move properly by scrolling the UV offset of the texture.

  2. Use the same formula that you used to generate the water to detect surface collisions by sampling the wave at the target position rather than trying to detect collisions with the actual wave mesh.

Here is a Gerstner wave function you can use to generate your waves:

--GerstnerWave(Vector3 SamplePosition, Float Wavelength, Vector2 Direction, Float Steepness, Float Gravity, Float SampleTick)
function GerstnerWave(SamplePosition,Wavelength,Direction,Steepness,Gravity,SampleTick)
	local k = (2 * math.pi) / Wavelength
	local a = Steepness/k
	local d = Direction.Unit
	local c = math.sqrt(Gravity / k)
	local f = k * d:Dot(Vector2.new(SamplePosition.X,SamplePosition.Z)) - c * SampleTick
	local cosF = math.cos(f)

	--Displacement Vectors
	local dX = (d.X * (a * cosF))
	local dY = a * math.sin(f)
	local dZ = ( d.Y * (a * cosF))
	return Vector3.new(dX,dY,dZ)
end
  1. The wavelength of your smallest wave must be larger than the distance between vertices on your water mesh, otherwise, it will look pretty strange.

  2. You can add the displacement from multiple Gerstner waves to get a more convincing final displacement for the water.

29 Likes

OMG IVE BEEN LOOKING FOR SOMETHING LIKE THIS FOR SO LONG!!! Yay

3 Likes

I can’t tell you how much this is amazing timing for me. Due to that being a script imported from blender, would it possible for this to become a model?

4 Likes

Yeah, I could make the ocean mesh a model and link it in the original post.

2 Likes

Hi, I’m having difficulties trying to get this function to work. Could you provide a code snippet, or explain what type each variable has to be?
Thank you.

2 Likes

GerstnerWave(Vector3 SamplePosition, Float Wavelength, Vector2 Direction, Float Steepness, Float Gravity, Float SampleTick)

SamplePosition is the position at which you are checking the wave displacement (E.G. the world position of a given bone in a water mesh)

Wavelength, Direction, Steepness, and Gravity are all pretty self-explanatory, but it does take some tinkering to find numbers that look nice.

SampleTick is the time you are sampling the wave at, should be something that you can sync between clients if possible. Waves sampled at the same SampleTick with the same arguments will always return the same results (deterministic).

Here are some example arguments that I’ve found look pretty nice:

	local Wave1 = GerstnerWave(SamplePosition,80,Vector2.new(1,0),.05,1.5,SampleTick)
	local Wave2 = GerstnerWave(SamplePosition,90,Vector2.new(0,.3),.07,1.5,SampleTick)
	local Wave3 = GerstnerWave(SamplePosition,100,Vector2.new(1,1),.05,1.5,SampleTick)
	local TotalDisplacement = Wave1+Wave2+Wave3 -- This is your final wave displacement
4 Likes

Hello again, I am struggling to to import the FBX into Studio. Would you mind exporting this as a Roblox model?

Thanks

https://www.roblox.com/library/6580905134/FBXImportGeneric

try that out

4 Likes

Wow, this can help alot I appreciate it!

Thank you for the clarification.
I’m currently doing this inside of a loop to update all the bones.

for _, bone in pairs(plane:GetChildren()) do
	if bone:IsA("Bone") then
		local SamplePosition = bone.Position
		local SampleTick = tick()

		local Wave1 = GerstnerWave(SamplePosition, 80, Vector2.new(1, 0), .05, 1.5, SampleTick)
		local Wave2 = GerstnerWave(SamplePosition, 90, Vector2.new(0, .3), .07, 1.5, SampleTick)
		local Wave3 = GerstnerWave(SamplePosition, 100, Vector2.new(1, 1), .05, 1.5, SampleTick)
		local TotalDisplacement = Wave1 + Wave2 + Wave3 -- This is your final wave displacement
		bone.Position = TotalDisplacement
	end
end

It doesn’t work however. Is there another way to set a bone’s position?

You’ll want to sample the wave at the WorldPosition of the bone instead of the Position. That could be the cause of your issue.

2 Likes

Something like this?

for _, bone in pairs(plane:GetChildren()) do
	if bone:IsA("Bone") then
		local SamplePosition = bone.WorldPosition
		local SampleTick = tick()

		local Wave1 = GerstnerWave(SamplePosition, 80, Vector2.new(1, 0), .05, 1.5, SampleTick)
		local Wave2 = GerstnerWave(SamplePosition, 90, Vector2.new(0, .3), .07, 1.5, SampleTick)
		local Wave3 = GerstnerWave(SamplePosition, 100, Vector2.new(1, 1), .05, 1.5, SampleTick)
		local TotalDisplacement = Wave1 + Wave2 + Wave3
		bone.Position = TotalDisplacement
	end
end

This doesn’t seem to work either…

You also need to cache the initial position of the bone, the GerstnerWave function is returning a displacement that is meant to be added to a position.

Something like:

bone.Position = OriginalPosition+TotalDisplacement
1 Like

just to clarify, would this be the final product?

for _, bone in pairs(plane:GetChildren()) do
	if bone:IsA("Bone") then
		local SamplePosition = bone.WorldPosition
		local SampleTick = tick()
		
		local Wave1 = GerstnerWave(SamplePosition, 80, Vector2.new(1, 0), .05, 1.5, SampleTick)
		local Wave2 = GerstnerWave(SamplePosition, 90, Vector2.new(0, .3), .07, 1.5, SampleTick)
		local Wave3 = GerstnerWave(SamplePosition, 100, Vector2.new(1, 1), .05, 1.5, SampleTick)
		local TotalDisplacement = Wave1 + Wave2 + Wave3
		local OriginalPosition = bone.Position
		bone.Position = OriginalPosition+TotalDisplacement
	end
end

No, if you do that you are changing the OriginalPosition of each bone each step. You’ll want to store (cache) the original position of each bone in a table at the start and get it from there. You could probably also use Bone.Transform, but I haven’t experimented with that yet.

what do you mean by this? and how would I acheive it?

1 Like

Here’s how I do it:

game:GetService("RunService").Heartbeat:Connect(function()
	local char = LocalPlayer.Character
	if char and char:FindFirstChild("HumanoidRootPart") then
		for _, bone in pairs(plane:GetChildren()) do
			if bone:IsA("Bone") then
				if origPosTable[bone] then
					if (char.HumanoidRootPart.Position - bone.WorldPosition).Magnitude <= maxDistance then
						local SamplePosition = bone.WorldPosition

						local Wave1 = GerstnerWave(SamplePosition, 150, Vector2.new(1, 0), .05, 1, speedCounter)
						local Wave2 = GerstnerWave(SamplePosition, 175, Vector2.new(0, .3), .2, 1.25, speedCounter)
						local Wave3 = GerstnerWave(SamplePosition, 200, Vector2.new(1, 1), .1, 1.5, speedCounter)
						local TotalDisplacement = Wave1 + Wave2 + Wave3
						bone.Position = origPosTable[bone] + TotalDisplacement
					end
				end
			end
		end
	end
end)

The origPosTable contains every bone’s position. This is initialized at the start of the script.

2 Likes

is this a local script? I see localplayer… so

the char and humanoid root part is not necessary in the script?

Yes, I believe it is. LocalPlayer can only be used on the client.