How to make Part/Boat float and tilt on GerstnerWaves?

Hello, I am trying to make a part float and tilt on Gerstner waves, but I have no idea how I will start.
My idea is that it will work something like this: Block floating on Gerstner waves (Roblox) - YouTube)

The Gerstner Wave system I am using basically uses Mesh Deformation and Gerstner Formula.
Pls leave suggestions i will leave it open source so everyone can Make it.

First i have a local script in the StarterPlayerScripts. The script is basically the settings for the Gerstner formula module.

-- For this one, I chose to make slow and calming waves
local Wave = require(script.Wave)

local Settings = {
	WaveLength = 100,
	Direction = Vector2.new(0,0), -- It is 0, 0, so it will use the perlin noise algorithm to make random waves for me
	--PushPoint = Plane, -- Alternative to Direction, the wave's direction will always push away from the part.
	Steepness = 0.1,
	TimeModifier = 3,
	MaxDistance = 1500,
}

local Wave1 = Wave.new(workspace.Wave1.Plane,Settings)



Wave1:ConnectRenderStepped()

This is the Module Script:

local Wave = {}
Wave.__index = Wave

local newCFrame = CFrame.new
local IdentityCFrame = newCFrame()
local EmptyVector2 = Vector2.new()
local math_noise = math.noise
local random = math.random
local setseed = math.randomseed

local Stepped = game:GetService("RunService").RenderStepped
local Player = game:GetService("Players").LocalPlayer

local default = {
	WaveLength = 85,
	Gravity = 1.5,
	Direction = Vector2.new(1,0), -- Must be a Vector2
	FollowPoint = nil, -- Alternative to Direction, the wave's direction will always push away from the part or Vector3.
	Steepness = 1,
	TimeModifier = 4,
	MaxDistance = 1500,
}

local function Gerstner(Position: Vector3,Wavelength: number,Direction: Vector2,Steepness: number,Gravity: number,Time: number)
	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(Position.X,Position.Z)) - c * Time
	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

local function CreateSettings(s: table,o: table)
	o = o or {}
	s = s or default
	local new = {
		WaveLength = s.WaveLength or o.WaveLength or default.WaveLength,
		Gravity = s.Gravity or o.Gravity or default.Gravity,
		Direction = s.Direction or o.Direction or default.Direction,
		PushPoint = s.PushPoint or o.PushPoint or default.PushPoint,
		Steepness = s.Steepness or o.Steepness or default.Steepness,
		TimeModifier = s.TimeModifier or o.TimeModifier or default.TimeModifier,
		MaxDistance = s.MaxDistance or o.MaxDistance or default.MaxDistance,
	}
	return new
end

local function GetDirection(Settings,WorldPos)
	local Direction = Settings.Direction
	local PushPoint = Settings.PushPoint

	if PushPoint then
		local PartPos = nil

		if PushPoint:IsA("Attachment") then
			PartPos = PushPoint.WorldPosition
		elseif PushPoint:IsA("BasePart") then
			PartPos = PushPoint.Position
		else
			warn("Invalid class for FollowPart, must be BasePart or Attachment")
			return
		end

		Direction = (PartPos-WorldPos).Unit
		Direction = Vector2.new(Direction.X,Direction.Z)
	end
	
	return Direction
end

function Wave.new(instance: instance, waveSettings: table | nil, bones: table | nil)
	-- Get bones on our own
	if bones == nil then
		bones = {}
		for _,v in pairs(instance:GetDescendants()) do
			if v:IsA("Bone") then
				table.insert(bones,v)
			end
		end
	end
	
	local Time = os.time()
	
	return setmetatable({
		_instance = instance,
		_bones = bones,
		_time = 0,
		_connections = {},
		_noise = {},
		_settings = CreateSettings(waveSettings)
	},Wave)
end

function Wave:Update()
	for _,v in pairs(self._bones) do
		local WorldPos = v.WorldPosition
		local Settings = self._settings
		local Direction = Settings.Direction
		
		if Direction == EmptyVector2 then
			-- Use Perlin Noise
			local Noise = self._noise[v]
			local NoiseX = Noise and self._noise[v].X
			local NoiseZ = Noise and self._noise[v].Z
			local NoiseModifier = 1 -- If you want more of a consistent direction, change this number to something bigger
			
			if not Noise then
				self._noise[v] = {}
				-- Uses perlin noise to generate smooth transitions between random directions in the waves
				NoiseX = math_noise(WorldPos.X/NoiseModifier,WorldPos.Z/NoiseModifier,1)
				NoiseZ = math_noise(WorldPos.X/NoiseModifier,WorldPos.Z/NoiseModifier,0)
				
				self._noise[v].X = NoiseX
				self._noise[v].Z = NoiseZ
			end
			
			Direction = Vector2.new(NoiseX,NoiseZ)
		else
			Direction = GetDirection(Settings,WorldPos)
		end
		
		v.Transform = newCFrame(Gerstner(WorldPos,Settings.WaveLength,Direction,Settings.Steepness,Settings.Gravity,self._time))
	end
end

function Wave:Refresh()
	for _,v in pairs(self._bones) do
		v.Transform = IdentityCFrame
	end
end

function Wave:UpdateSettings(waveSettings)
	self._settings = CreateSettings(waveSettings,self._settings)
end

function Wave:ConnectRenderStepped()
	local Connection = Stepped:Connect(function()
		if not game:IsLoaded() then return end
		local Character = Player.Character
		local Settings = self._settings
		if not Character or (Character.PrimaryPart.Position-self._instance.Position).Magnitude < Settings.MaxDistance then
			local Time = (DateTime.now().UnixTimestampMillis/1000)/Settings.TimeModifier
			self._time = Time
			self:Update()
		else
			self:Refresh()
		end
	end)
	table.insert(self._connections,Connection)
	return Connection
end

function Wave:Destroy()
	self._instance = nil
	for _,v in pairs(self._connections) do
		pcall(function()
			v:Disconnect()
		end)
	end
	self._bones = {}
	self._settings = {}
	self = nil
	-- Basically makes the wave impossible to use
end

return Wave
1 Like

you will need to create a GetHeight function, very complicated but many tutorials around to help you out

Could i not try to get the magnitude of the nearest bone to front or back and tilt it that direction ?

One question does it work with raycasting or not

not that wont work with these waves, you need a get height function

raycasting wont work either

1 Like

Hi! I saw your message you sent and am willing to set you on the right track.

So, lets lay out the problem at hand. When you make your waves, in reality what you’re doing is moving particular bones, which translate to moving points on a triangle.

Let’s draw this out and see what it looks like.

What this picture is showcasing, is a plane which has points; which are your bones.
When you move one of the bones, (in this case I’m moving the green points labeled) up or down, the correlating triangle moves up and down as well.

So ideally, if there is a new point, we’ll call it Z, we need to find where Z is on our plane of points, and then find the correlating quadrant (four points) in which is lays on. From there we can find the triangle (three points within the quadrant) in which we can then calculate what the height of the point on the triangle is.

Alright, this image is getting complicated. So let me explain, the purple/dark blue lines represent the quadrant in which our orange point, (z for simplicity, keep in mind this isn’t a coordinate but just the point name / reference) lies in. From there we find the triangle in which it belongs to, ie; which half it falls on. And now we have the three points in which we need to calculate the normal offset (the height offset)

I’m not going to go over the code for this; because it’s important you actually learn and tackle how to do it yourself, this is really good stepping stone in to optimization alongside learning how to organize your mathematics in to chunks, etc…

However, I’ll answer any questions you have- without spoon feeding!

I believe my explanation was relatively good, but I’m open for more help if needed!

5 Likes

Hey again, i am starting to understand the logic now. The information i got by you was very good and useful, but i dont have any idea how i would detect the bones. I tried using magnitude but didnt work. But when i can detect the nearest bone to the part i guess i should group them to a group of 4 to make a quadrant. Then seperate the quadrant to 2 triangels… Right? Answer when you feel like, No need to hurry. Thanks for everything.

So i have sorted my bones into columns and one column is creating a line of 22 bones.
This function is what is doing it

local function Gridify(objects, cols: number)
table.sort(objects, function(a, b) return a.Position.Z < b.Position.Z end)

local grid = {}	

-- then, split up into groups
for row = 1, #objects / cols do
	-- the first object in the row
	local lowerIndex = 1 + (row - 1) * cols

	-- the last object in the row
	local upperIndex = row * cols

	-- copy this row into its own table..
	local thisRow = {}
	table.move(objects, lowerIndex, upperIndex, 1, thisRow)

	-- ... so we can sort it based on position
	table.sort(thisRow, function(a, b) return a.Position.X < b.Position.X end)
	grid[row] = thisRow
end

return grid

end
local grid = Gridify(Bones, 22)
print(grid)

So now i wonder how i would sort these columns into quadrants

I know you’re working on formulas and stuff, but can’t you just use a 1x1x1 unanchored part, and weld other things to it (keeping them cancollide off)? You can use a bodyvelocity or something to prevent the part from being pulled by the waves.

it is a quick solution but it is not what what i am looking for

Oh, okay! I’ll probably go now since I don’t have much else to contribute to this topic.