Sampling Terrain Water Height at Any Position

Thanks for the great module. I might do a sweet tutorial about it :smiley:

2 Likes

I’ve built something similar w/ custom Gerstner waves, but was struggling to actually have the boat mimic it. Any chance that you’d share your boat demo?

1 Like

Can’t drop the whole place file, but I can explain how I achieved the effect.

Its got three components: pitch, roll, and height.

Before I explain how I get those values, let me explain how I set up my boats. The first thing to note is the model’s primary part; the boat has a primary part about which the boat should rotate. I just think of it as the closest point to the center of mass with the player in their seat. The primary part is only really used to apply rotations in a relatively realistic manner. To calculate the values I mentioned above, I use a bunch of floating points scattered across the boat I call buoys. There are four important buoys among those, named ‘port,’ ‘starboard,’ ‘stern,’ and ‘bow.’

Here’s how my boat is configured:


The yellow brick is the primary part, the blue parts are bow and stern, and the red parts are port and starboard.

Every frame, I calculate the water wave height at each buoy. The average of all those heights is the y-component of the boat’s primary part. The angle between the height of the port and starboard buoys is the roll, and the angle between the height of the bow and stern is the pitch angle.

Here’s what those calculations look in real time. Pitch angle is in red (because it’s rotated on the X-axis), and roll angle is blue (because its rotated on the Z-axis).

boat - YouTube

here’s how I integrate those numbers into the final CFrame.

local boatY, pitch, roll = getBoatOrientation()

local prim = boat.PrimaryPart
local p = prim.Position
local look = prim.CFrame.LookVector
p = Vector3.new(p.X, boatY, p.Z)
look = Vector3.new(look.X, 0, look.Z)

local cf = CFrame.new(p, p + look)
cf *= CFrame.Angles(pitch, 0, 0)
cf *= CFrame.Angles(0, 0, roll)

boat:SetPrimaryPartCFrame(cf)

Let me know if you have any other questions!

13 Likes

So I’ve been working on the buoys and have been having issues with the angle calculation (working on the roll atm).

function angleBetweenVectors(v1, v2)
    local angle = math.acos(v1.unit:Dot(v2.unit))
    return angle
end

function roll()
	local port = floaters.port.Position
	local starboard = floaters.starboard.Position
	local newPort = Vector3.new(port.X, originHeight[floaters.port] + WaterHeightCalculator.calcWaterHeightOffset(port.X, port.Z), port.Z)
	local newStarboard = Vector3.new(starboard.X, originHeight[floaters.starboard] + WaterHeightCalculator.calcWaterHeightOffset(starboard.X, starboard.Z), starboard.Z)
		
	return angleBetweenVectors(newPort, newStarboard)
end

I stored the origin heights for all the buoys/floaters in an array and then am retrieving it to calculate the vector difference between the new heights. Is this the correct way?

I’ve attached a video of the weird behavior:


Please ignore the sloppy code, I’m just trying to mock up a working prototype.

Really would appreciate any assistance! :slight_smile:

I saw this sort of magic scripting in another boat game, nice to understand how it works now, cheers buddy.

2 Likes

I’m sure you could use some kind of angle between vectors to find the roll, but I found it easier to just use some easy trig. Here’s how I do it:


p is port position, s, is starboard position, Wp is wave height @ p, Ws is wave height @ s. From there you can make a triangle, solve for theta. The sign of your rotation can be determined by math.sign(Ws - Wp). I do the same thing for pitch.

2 Likes

What’s b? If you subtract the port position minus the starboard position, you can’t use math.abs on it? (I believe that’s what the diagram was saying)?

Here’s what I wrote, acting a bit strange.

function roll()
	local port = floaters.port.Position
	local starboard = floaters.starboard.Position
	local portWave = Vector3.new(port.X, originHeight[floaters.port] + WaterHeightCalculator.calcWaterHeightOffset(port.X, port.Z), port.Z)
	local starboardWave = Vector3.new(starboard.X, originHeight[floaters.starboard] + WaterHeightCalculator.calcWaterHeightOffset(starboard.X, starboard.Z), starboard.Z)
	
	local h = portWave.Y - starboardWave.Y
	local b = math.abs(port.Z - starboard.Z)
	local angle = math.atan(h/b)
	
	return angle * math.sign(portWave.Y - starboardWave.Y)
end
3 Likes

Sorry for the confusion. Two lines surrounding a vector indicates the magnitude, not absolute value.

p and s are positions, Ws and Wp are scalars. They’re just the relative height of the wave from sea-level at the x and z of p or s.

Got it. Thanks for the assistance. It seems to be on the wrong side, does the math.sign look right to you?
return angle * math.sign(portWave.Y - starboardWave.Y)

I don’t remember, whatever looks good is probably right. Glad I could help! Let me know if you have any other questions!

1 Like

Weird, works fine without math.sign

1 Like

thats crazy. atan must be taking care of sign then.

2 Likes

Yeah, strange :eyes:
Last question - how’d you do that neat visualizer in that video you sent over? (of the axis)

1 Like

I’ve got a visualizer module I use for this kind of stuff. Visualizers.rbxm (3.4 KB)

2 Likes

Wow, thanks!
Amazing work, appreciate it.

2 Likes

This is game changing! Can’t wait to see what comes in the nearby future. I’m quite surprised this system wasn’t in the game engine to begin with.

4 Likes

I have some quick commission work regarding this. Are you available & do you have a Discord?

1 Like

Thank you so much, this is amazing. I’ve been waiting for so long for somebody to create this, great job!

2 Likes

I remember when seeing this in action first hand - absolutely amazing!
OP’s code helped me A LOT to set the buoyancy of bobbers to properly work using the wave’s heights based on their positions in the world. Here’s a really small gif of it in action in my place using it.

https://gyazo.com/2daccd995793552c663edb4b888c4404

I had a problem with syncing things perfectly at some point though, and as I’ve discussed with OP, setting the timer to update on .RenderStepped or .Heartbeat, instead of .Stepped, seems to have solved the issue I was having. For context, the wave settings are all done client side, which differs from OP’s approach, because if I recall correctly, he does it on the server for this project.

It’s nice actually finding out that you shared this with the whole community @robro786!
Awesome guy :smile:

4 Likes