Sampling Gerstner Wave; position between two bones

I’m working on a mesh deformation based water system; pretty common these days, right? Well, one of the issues I’m having is the fact that sampling a position between two bones in a mesh deformed plane, wont return the proper sample as to what the wave height is at that position.

Here’s an example video.

You can see that the green dot doesn’t follow the wave properly. Any ideas?

function Water:CalculateBoneDisplacement(Position, Wavelength, Direction, Steepness, Gravity, Offset)
	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 * (tick() + (Offset or 0))
	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

function Water:CalculateDisplacementFromOrigin(Origin)
	local DisplacementA = self:CalculateBoneDisplacement(Origin, 15, Vector2.new(1, 0), 0.3, 1.3)
	local DisplacementB = self:CalculateBoneDisplacement(Origin, 12, Vector2.new(2, .4), 0.4, 1.5)
	local DisplacementC = self:CalculateBoneDisplacement(Origin, 12, Vector2.new(0, 1), 0.3, 1.5)

	local Displacement = DisplacementA + DisplacementB + DisplacementC
	return Displacement
end
4 Likes

Could you give a little more info? Where are you calling these methods? Where are your bones in this video?

If the square is near the grid (red dots), it works as expected, however if it’s in the center of the grid (four points), it breaks.

Well, if you’re just calling CalculateDisplacementFromOrigin to get the green cube’s position, it wouldn’t work exactly because there’s a flat triangle stretching between the red dots, not a continuous wave.

At the very least, you’ll need to determine which triangle the green cube is “sitting on”, and then linearly interpolate between the three vertices to find the final position.

The exact way you determine which triangle you’re on depends on how your mesh is laid out:

+---+---+---+---+---+
|  /|  /|  /|  /|  /|
| / | / | / | / | / |
|/  |/  |/  |/  |/  |
+---+---+---+---+---+
|  /|  /|  /|  /|  /|
| / | / | / | / | / |
|/  |/  |/  |/  |/  |
+---+---+---+---+---+

Or this:

+---+---+---+---+---+
|\  |\  |\  |\  |\  |
| \ | \ | \ | \ | \ |
|  \|  \|  \|  \|  \|
+---+---+---+---+---+
|\  |\  |\  |\  |\  |
| \ | \ | \ | \ | \ |
|  \|  \|  \|  \|  \|
+---+---+---+---+---+

… or maybe this:

+---+---+---+---+---+
|  /|  /|  /|  /|  /|
| / | / | / | / | / |
|/  |/  |/  |/  |/  |
+---+---+---+---+---+
|\  |\  |\  |\  |\  |
| \ | \ | \ | \ | \ |
|  \|  \|  \|  \|  \|
+---+---+---+---+---+

To be fair though, I’m not sure that’s the issue. Can you share the code where you’re setting the positions of the bones and cube?

You also might be interested in this thread, which also relates to gerstner waves: Where do I even start with a Gerstner wave floating system? (although I don’t remember what the issue was, or if it was ever solved :slight_smile: )

2 Likes

The thread seemed to move away from Gernstner waves towards the end, and I’d rather tackle the issue heads on, it did give some insight though. (Not going to lie, mainly the parts in which you were the one to comment, was the only thing of real value)


This is the setup of the bones, where the centered (blue circle) is elevated.

This is the plane set-up;

After triangulation, it looks like the following;

Which makes sense, you can see I drew the outline of the same sequence we’re getting in studio.

So all in all, it looks like this

+---+---+---+---+---+
|\  |\  |\  |\  |\  |
| \ | \ | \ | \ | \ |
|  \|  \|  \|  \|  \|
+---+---+---+---+---+
|\  |\  |\  |\  |\  |
| \ | \ | \ | \ | \ |
|  \|  \|  \|  \|  \|
+---+---+---+---+---+

This is the code for positioning my bones; excuse the lack of comments- I’ll add them all later on.

function Water:CalculateBoneDisplacement(Position, Wavelength, Direction, Steepness, Gravity, Offset)
	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 * (tick() + (Offset or 0))
	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

function Water:CalculateDisplacementFromOrigin(Origin)
	local DisplacementA = self:CalculateBoneDisplacement(Origin, 15, Vector2.new(1, 0), 0.3, 1.3)
	local DisplacementB = self:CalculateBoneDisplacement(Origin, 12, Vector2.new(2, .4), 0.4, 1.5)
	local DisplacementC = self:CalculateBoneDisplacement(Origin, 12, Vector2.new(0, 1), 0.3, 1.5)

	local Displacement = DisplacementA + DisplacementB + DisplacementC
	return Displacement
end

function Water:UpdateSingularBone(Bone)
	local CachedBone = Bones[Bone] or {Origin = Bone.WorldPosition};
	local Origin = CachedBone.Origin
	
	local Displacement = self:CalculateDisplacementFromOrigin(Origin)	
	Bone.WorldPosition = Origin + Vector3.new(0, Displacement.Y, 0)
	
	Bones[Bone] = CachedBone
end

function Water:Update()
	for _, Bone in pairs(self.Plane:GetChildren()) do
		if not Bone:IsA("Bone") then continue end
		
		self:UpdateSingularBone(Bone)
	end
	
	--local Wave1 = self:Wave(Origin, script.A.Length.Value, Vector2.new(1, 0), script.A.Steepness.Value, script.A.Gravity.Value, tick()) -- 15 | .8
end

Where the RunService loop handles the rest, including the positioning of the part.

function Water:Start()
	Distributor = self:GetDistributor()
	ServiceProvider = Distributor:GetServiceProvider()
	
	CollectionService = ServiceProvider:GetService("CollectionService")
	RunService = ServiceProvider:GetService("RunService")
	
	local Planes = CollectionService:GetTagged("Water")
	for _, Plane in pairs(Planes) do
		table.insert(Cache, Water.new(Plane))
	end
	
	local CheckOrigin = workspace.CheckThis.Position

	RunService.RenderStepped:Connect(function()
		local CheckDisplacement = self:CalculateDisplacementFromOrigin(CheckOrigin)
		workspace.CheckThis.Position = CheckOrigin + Vector3.new(0, CheckDisplacement.Y, 0)
		
		for _, Water in pairs(Cache) do
			Water:Update()
		end
	end)
end
3 Likes

Beautiful reply and context :slight_smile:

Here’s me spilling my thoughts:

So yes, the

		local CheckDisplacement = self:CalculateDisplacementFromOrigin(CheckOrigin)
		workspace.CheckThis.Position = CheckOrigin + Vector3.new(0, CheckDisplacement.Y, 0)

part should probably not use CalculateDisplacementFromOrigin, and should instead use a new method, GetRealWaveHeight or something, which takes into account bones’ current positions and triangles and things.

GetRealWaveHeight(pos) would:

  1. Figure out the three bones which make up the triangle on which our cube sits
  2. Interpolate between those three bones using pos to get an exact location on the plane they describe

Each of those should probably be it’s own method. (2) is probably easy, some quick googling would figure it out. Something something barycentric coordinates or bilinear interpolation or something.

(1) seems trickier. As a first pass, I would brute force it and iterate over all the triangles in the mesh. Write some function IsInsideTriangle2D(pos, tri1, tri2, tri2) and call it on each one until you find the three bones you need.

To do this, you may need to organize your bones a little more than you have, e.g. with a data structure that defines all the triangles (i.e., a mesh). Also might be useful to store the bones in a grid structure where you can look them up by grid index.

You will eventually get fancier and restrict your search set to only those triangles which could possibly be overlapping by taking into account e.g. how far a vertex could possibly stretch based on your wave function. But that should come after you’ve got it actually working :slight_smile:

4 Likes

I also don’t know how exactly you’re going from blender to studio, but it might be worth it to write a parser for your obj file that spits out lua code that defines all the triangles or something.

I think I wrote something like that a while ago I’ll see if I can find it.

Also: triangulate your mesh before your export it. If blender is exporting quads, roblox would be doing the triangulation itself, and then you have no control over it.

1 Like

So! I have separated all of my plane in to quads, from there I separate the quads in to triangles. I took this approach because optimization wise, it seems a lot better to loop through quads (four triangles), then it would be to loop through each triangle itself (three each, a quad consists technically of 2 triangles, 6 vertices total would need to be looped)

Nonetheless, that’s working as expected.

I have successfully managed to add red dots at the vertices (bones) which are correlating to the triangle that the square is in.

The next issue however, is the best way to find out the height of a two dimensional point on a three dimensional triangle. I’ve done some research, and I’ve come to this point-

function PlaneTriangulation:FindNormalOfTriangle(Points)
	local VectorU = Points[2] - Points[1]
	local VectorV = Points[3] - Points[1]
	
	local X = (VectorU.Y * VectorV.Z) - (VectorU.Z * VectorV.Y)
	local Y = (VectorU.Z * VectorV.X) - (VectorU.X * VectorV.Z)
	local Z = (VectorU.X * VectorV.Y) - (VectorU.Y * VectorV.X)
	
	return Vector3.new(X, Y, Z)
end

function PlaneTriangulation:GetHeightOfPositionInTriangle(Triangle, Position)
	local X, Z = Position.X, Position.Z
	
	local TriangleNormal = self:FindNormalOfTriangle(Triangle)
end

I understand I need the normal of the ‘triangle’ (or three vectors), however my Normal ends up being a relatively strange return value;
(0, 102.515625, 0.00125…)
(Don’t all normals need to add up to one, or whatever)

Which I don’t imagine to be the proper normal, I’m not really sure how normals work though, admittedly.

So my main question, is how would I go about getting the height of a triangle point, if given an X & Z

1 Like

You may also be interested in this old thread:

Specifically this function:

-- projects a point vertically onto the plane
-- vec: point to project
-- p: point on the plane
-- n: unit normal of plane
local function ProjectVertically(vec, p, n)
	local off = vec - p
	local y = -(n.X*off.X + n.Z*off.Z)/n.Y
	return p + Vector3.new(off.X, y, off.Z)
end

As for finding the normal, your research is very correct—you’re computing the cross product of the vectors forming two sides of the triangle. Roblox has built in the cross product function for us so you don’t need to manually do all that:

-- projects pos vertically onto the plane formed by a, b, and c
local function ProjectToPlane(pos, a, b, c)
	local ab, bc = b - a, c - b
	local n = ab:Cross(bc).Unit -- normal vector of plane
	
	if n.Y < 0 then n = -n end -- points up

	return ProjectVertically(pos, a, n)
end

(The .Unit makes it “add up to one, or whatever”)

4 Likes

:heart:

Thank you so much for a help, I appreciate it a ton! I can’t thank you enough.
Everything is working aside from some minor mesh clipping with the water, but that’s mainly because the boat is extremely thin on the bottom, I’ll just tell modeler to make it bigger, tbh.

Thank you, thank you, thank you!

2 Likes

Eyyy check that out, awesome effect! It might also help the clipping if you sampled 3 points along the boat’s hull (like one at the tip and two in each back corner) and then set the boat’s actual CFrame based on that or something. Either way, good luck with the rest of your game :slight_smile:

Can you tell us how did you manage to make the boat float like that so beautifully? What were the steps you used to make it buoyant according to the results you got from the ProjectVertically function?

1 Like