Gerstner waves lerping problem

I have a sea mesh that I’m animating with Gerstner waves but doing these calculations every frame for every vertex causes a lot of lag, so I searched how to reduce it and found this post by @apenzijncoolenleuk1, where he explains you can just “spread” the calculations for every X frames and interpolate between them.

My result after trying to code it was this:

local seaBones = {}
local originPositions = {}
local nextPositions = {}
local lastPositions = {}

local frame = 0
local vertexPos

runService.Heartbeat:Connect(function()
	frame  += 1
	if frame  > 25 then -- (every 25 frames) do...
		print("25")
		frame = 1
		for _, bone in pairs(seaBones) do
			vertexPos = originPositions[bone]
			
			vertexPos += -- Gerstner wave offset (one of this line for each wave)
			
			lastPositions[bone] = bone.Position
			nextPositions[bone] = vertexPos
		end
	end
	
	if typeof(p) == "Vector3" then
		for _, bone in pairs(seaBones) do
			bone.Position = lastPositions[bone]:Lerp(nextPositions[bone], frame/25)
		end
	end
end)

It worked in optimizing my original code, there is zero to no lag now, however, when executing this code there is a main visual bug:

Notice that the middle of the lerps are perfectly smooth, they’re not snapping, however, when changing the target, where to lerp to, the nextPositions[bone] (every 25 frames) the lerp freezes for some time, then goes back to lerping smoothly. (I don’t know if that’s the issue, it’s just a guess)

Before answering:

  • The main issue is the “freezing” of the vertices, which just happens sometimes, you can see if you pay attention to the movement of the mesh in the video

  • Tweens are not an option because as previously explained in the post I linked, they cause a lot of lag

  • I know that there are many other optimization techniques for sea meshes, I plan to implement them after I get this one working.

  • Please read the entire topic and watch the video entirely

  • Help is appreciated and if any additional information is needed please ask me

3 Likes

Make sure you calculate what the target offset will be 25 frames from now. I.e. what it will be at the start of the next tween.

How do I do that, and how does it affect the lerp, making it freeze like in the video? This is what I’m doing to call the Gerstner wave function, I think I’m doing everything correctly as the wave is moving in the correct order:

local function GerstnerWave(windDir, steepnes, length, wave_speed, vertexPosition)
	local direction = Vector2.new(math.cos(windDir * math.pi), math.sin(windDir * math.pi))

	local wave_angularFrequency = 2 * math.pi / length
	local wave_amplitude = steepnes / wave_angularFrequency
	local waveBase = wave_angularFrequency * (direction:Dot(Vector2.new(vertexPosition.X, vertexPosition.Z)) - wave_speed * time())

	local pX = direction.X * (wave_amplitude * math.cos(waveBase))
	local pY = wave_amplitude * math.sin(waveBase)
	local pZ = direction.Y * (wave_amplitude * math.cos(waveBase))

	return Vector3.new(pX, pY, pZ)
end

runService.Heartbeat:Connect(function()
	f += 1
	if f > 25 then
		f = 1
		for _, bone in pairs(seaBones) do
			p = originPositions[bone]
			
			for w=1, waveAmount do
				p += GerstnerWave(
					directions[w],
					steepnesses[w],
					lengths[w],
					script.wave_speed.Value,
					bone.Position
				)
			end
			
			lastPositions[bone] = bone.Position
			nextPositions[bone] = p
		end
	end
	
	if typeof(p) == "Vector3" then
		for _, bone in pairs(seaBones) do
			bone.Position = lastPositions[bone]:Lerp(nextPositions[bone], f/25)
		end
	end
end)

Is there really a way to preview when will be 25 frames from now? The best approximation I can think of is deltaTime * 25 but is just an approximation, the frame intervals are not predictable, adding this approximation to the time in the Gerstner wave function would fix it?

I can’t test it right now because I’m going to school, and it’s full time so don’t worry If I don’t answer you for some hours.

It’s been a while. But yes if I remember correctly there should be a dt in there somewhere. And yes, as you said use a simple multiplier on the current frame time.

Additionally what you can try is a smaller interval. Once every 25 frames is like once every 0.4s at 60 fps. Might be too big of an interval. Remember you’re trading accuracy/smoothness (in a limited quantity) for performance.

I think your time() should be adjusted to time()+25*dt.

I tried before, it just makes the mesh freeze more often, that’s why I made it 25:

I tried it right now, am I doing something wrong?

local function GerstnerWave(windDir, steepnes, length, wave_speed, vertexPosition, timeOffset)
	local direction = Vector2.new(math.cos(windDir * math.pi), math.sin(windDir * math.pi))
	
	local wave_angularFrequency = 2 * math.pi / length
	local wave_amplitude = steepnes / wave_angularFrequency
	local waveBase = wave_angularFrequency * (direction:Dot(Vector2.new(vertexPosition.X, vertexPosition.Z)) - wave_speed * (time()+timeOffset))
-- Time offset is at the end of the line above this one

	local pX = direction.X * (wave_amplitude * math.cos(waveBase))
	local pY = wave_amplitude * math.sin(waveBase)
	local pZ = direction.Y * (wave_amplitude * math.cos(waveBase))

	return Vector3.new(pX, pY, pZ)
end

runService.Heartbeat:Connect(function(dt)
	f += 1
	if f > 15 then
		f = 1
		for _, bone in pairs(seaBones) do
			p = originPositions[bone]
			
			for w=1, waveAmount do
				p += GerstnerWave(
					directions[w],
					steepnesses[w],
					lengths[w],
					script.wave_speed.Value,
					bone.Position,
					25*dt -- Time offset
				)
			end
			
			lastPositions[bone] = bone.Position
			nextPositions[bone] = p
		end
	end
	
	if typeof(p) == "Vector3" then
		for _, bone in pairs(seaBones) do
			bone.Position = lastPositions[bone]:Lerp(nextPositions[bone], f/15)
		end
	end
end)

The result:

I think this problem is something related to the way I’m lerping the vertices, because I made a lerp test on a baseplate, and parts don’t freeze when doing lerps in sequence:

However I don’t know what I’m doing different with the lerping in the sea mesh, I even made this red part lerp every X frames:

local runService = game:GetService("RunService")

local parts = {
	p1 = workspace.Part,
	p2 = workspace.Part2,
	p3 = workspace.Part3,
	p4 = workspace.Part4,
	p5 = workspace.Part5
}

local f = 0
local p = 1

runService.Heartbeat:Connect(function()
	f += 1
	if f > 100 then
		f = 1
		
		if p == 5 then
			p = 2
		else
			p += 1
		end
	end
	
	if p ~= 5 then
		parts.p1.Position = parts["p" .. p].Position:Lerp(parts["p" .. p+1].Position, f/100)
	else
		parts.p1.Position = parts["p" .. p].Position:Lerp(parts.p2.Position, f/100)
	end
end)

lerpTest.rbxl (45.6 KB)

‘f’ is different in both. Probably not the issue, but worth a try.

Maybe the bone loop + calculation just takes too long. I suggest checking the script profiler in studio. You can see it in the view tab.

What you can try is for your test tweening many parts (same amount as you have bones). For a stress test.

This explanation is the one that makes more sense to me, I really can’t see anything wrong with the code, and the mesh only freezes when heartbeat reaches X frames (when the code runs the bone loop + wave calculaton), it freezes more often the smaller X is.

Thank you, I’ll check that out when I get home.

Alright
Just realized the script profiler will probably not be super helpful since both parts are in the same script. I suggest timing that part of the code:

local t = workspace:GetServerTimeNow()

...

print(workspace:GetServerTimeNow() - t)

Or use the microprofiler debug tags (harder to use in my opinion).

1 Like


That’s 100% the issue, huge lag spikes when the bone loop runs, with most of the update time being taken by it. Just to be sure I’m not doing anything dumb: I added the bone loop tag like this:

runService.Heartbeat:Connect(function()
	f += 1
	if f > 25 then
		f = 1
		debug.profilebegin("bone loop")
		for _, bone in pairs(seaBones) do
			p = originPositions[bone]
			
			for w=1, waveAmount do
				p += GerstnerWave(
					directions[w],
					steepnesses[w],
					lengths[w],
					script.wave_speed.Value,
					bone.Position
				)
			end
			
			lastPositions[bone] = bone.Position
			nextPositions[bone] = p
		end
		debug.profileend()
	end
	
	if typeof(p) == "Vector3" then
		for _, bone in pairs(seaBones) do
			bone.Position = lastPositions[bone]:Lerp(nextPositions[bone], f/25)
		end
	end
end)

However now that I know the problem how do I fix it? I’m using a sea mesh of around 4000 vertices, I tried one with around 2000 vertices and the results weren’t much better, the lag spikes are still there, just smaller.

What is lagging the bone loop so much? The only thing I can think of is having to access the tables: directions, steepnesses, and lengths for every Gerstner wave I add. I was testing it and the only amount of waves I had no lag was 1, having 2 or more already made that “freezing” effect on the mesh.

Would removing the

for w=1, waveAmount do

and manually coding each wave change something?

I know it could be my computer but I’m confident it’s not, I have played Roblox games that had beautiful sea meshes (or even the Roblox terrain water), and even though with high graphics I could experience some lag, the sea never froze like my mesh is doing, the same applies to other games out of Roblox.

Btw I used the microprofiler because I’m more familiar with it, I don’t even know how you can measure lag with your method, it’s easier for me with the visualization microprofiler offers, the bars, colors, spikes, etc.

The timing method would just measure time a piece of code takes, and print it out. If you want 60 fps, then frame time is 1/60s or roughly 17ms. If your code takes up half of that or more, you’ll probably not reach a solid 60 fps. Rendering, etc also has to be done in a frame.

The trick is to divide the work. For example: let’s say you want to update all bone tweens every second. Divide all bones in 4 groups (can be arbitrary division, but make sure it’s done only once). Now divide your second in 4 (0.25s). Loop every 0.25s, the first iteration will update the first quarter of bones, second the second quarter, etc.

Like that you’re only doing 1/4th of the expensive calculation. Essentially you’re spreading it over a second (over multiple frames).

Update time and number of groups should be experimented with. 4 is probably not large enough of a divider. The beauty of this method is that it can be easily expanded to have another divider. Make sure to profile so you don’t get opposite returns due to overhead.

1 Like

It’s great to use interpolation for the other 24 frames, but you still create the big frame spike when you end up doing the big calculation on the 25th frame.

The trick here is you literally spread the work across the frames: instead of doing very little work for 24 frames then doing a whole bunch on the 25th, try to adjust your code so that you do a bit of work on each of the 25 frames. This way, you never have a frame freeze because of trying to do too much work on it,

So basically split your seaBones table into 24 sections, do the calculations for one of the 24 sections each frame and save it to a temp table, then swap the temp table in for the real table being used for updates/lerping on the 25th frame.


Also, very cool project, I’m a fan!

1 Like

Damn, I literally wrote the solution in the first paragraph and just couldn’t interpret Apenz1’s post that I linked, my English must be really rusty :sweat_smile: :sweat_smile:

That makes a lot of sense, I’ll try it later because I’m going to school again, and if it works (probably will) I’ll come back here and mark the solution, thanks both of you!

Thank you, I appreciate it! I’m planning to make it a plugin if I optimize it enough, I made a little project with Roblox’s terrain water and I don’t want anybody to go through that, it’s terrible: the waves are small, simple and their wave clock is hidden from developers! The only thing in terrain water that is great is its appearance, beautiful water but hard to deal with.

1 Like

It worked! The bone loop now takes around 2 - 4 ms every frame instead of 80 ms every 25 frames:
image

I’m fighting another issue now but I’m confident I can get around it, I’m not as stuck as I was with that lag problem, just too tired right now

Thanks to both of you! I’m marking Apenz1’s answer as the solution because he’s been trying to help me since I created the topic and gave the same answer as you, in a different way, but first, so I’m sorry BendsSpace, thank you for your help, and I’m glad you like the project!

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.