Infinite ocean help

Hi

I am trying to create an ocean using skinned meshes. I have it working now, but it only works for a square that is 1000 by 1000 studs wide:

How can I make the ocean infinite? I have tried setting up new tiles whenever the player walks to far away, but that causes some lag and I have get problems trying to animate these tiles.

I am sure it can be done as @tyridge77 did it here: Seamless, endless, and performant custom water using skinned meshes and PBR. I just don’t know how.

I’m using Gerstner waves for the animation:

Is there some way I can move the sea along with the player without messing up the animation?

Any help is greatly appreciated.
Thanks.

4 Likes

This is what I have done (my current project involves a skinned mesh sea as well):

  1. Move the skinned mesh to the X and Z coordinates of the player’s camera
  2. Recalculate the height of each bone

I do this on RunService:BindToRenderstep('SeaUpdate', Enum.RenderPriority.Camera.Value + 1, function() Sea:Update() end)

2 Likes

Hi

Is the new height instantly calculated and applied to all bones?

I have around 4000 bones inside the sea plane, and updating those every frame causes a large lag spike. I spread the work over a few frames and smoothly tween them into place over those frames.

Do you notice lag spikes to? Or how did you handle them?

Thanks for the reply.

1 Like

I have 1920 bones in total to handle each frame. The sea is performant and I am still getting 60 frames per second (and so is a friend of mine who plays on his laptop). 4000 bones seems like a lot, but there is only one way to find out I guess. You can probably optimize the mesh and use Luau if you need more performance.

1 Like

With around 2000 bones, how large is your sea? I will probably try lowering the amount of bones, but if I go to low, I get ugly looking waves with low resolution.

1 Like

This is what mine looks like.


I should admit that I am not very handy when it comes to using Blender (I… used a ROBLOX texture lol…). My waves look a bit ugly when they are too dense. I think that you can probably get away with it if the ugly parts are too far from the camera.

I am planning on dividing my circlular skinned mesh into quadrants so that I can give each one a size of 2048x2048. My sea is currently a circular mesh with a size of 2048x2048.

1 Like

I tried updating the mesh position every frame, and updating the bone offsets every frame. And I get this result:

As you can see the texture seems to be scrolling over the wave. Do you have this issue with your waves? I would like the texture to be still instead of moving. I’m not sure this is possible if I move the mesh along with the player…

So the mesh would have to be stationary. But if it doesn’t move, it can’t be infinite :disappointed_relieved:.
I’m not sure how I could resolve this. Maybe using copies of the sea that get placed around it once the player moves to far? I have already tried that, but to no success…

If anyone could provide a scripting example of such an infinite tiles system, I would be very happy. Or if you come up with another possible solution, please do share it!

1 Like

I had this issue too. This is how I fixed it (keep in mind that I am currently using a ROBLOX texture):

local function updatePlane(x, z)
	plane.CFrame = CFrame.new(x, 0, z)

	-- Use mod to ensure that the offsets have a fixed domain of [0, STUDS_PER_TILE]
	-- This prevents them from reaching outrageously big numbers
	local xOffset = -x % STUDS_PER_TILE
	local zOffset = -z % STUDS_PER_TILE
	for _, texture in pairs(textures) do
		texture.OffsetStudsU = xOffset
		texture.OffsetStudsV = zOffset
	end
end

As you can see, I am moving the texture in a grid that is based on the size of each texture pattern. I do not know if this is possible for your sea as well.

1 Like

Thanks for the help.
This sadly doesn’t seem to be possible currently with SurfaceAppearance objects.

1 Like

Maybe you can move the entire plane instead of the texture. You can then move it on a grid that is based on the size of each texture pattern.

1 Like

I’m not sure what you mean.
Could you explain more?

The idea is basically the same as my solution with the ROBLOX texture (where I make sure that the texture stays in a grid). You can also apply this idea to the entire plane to achieve the same effect.

local function updatePlane(x, z)
	plane.CFrame = CFrame.new(
		x - x % STUD_PER_TILE,
		0,
		z - z % STUD_PER_TILE
	)
end
1 Like

Okay, so I would need to have a repeating texture?

Yes, for this solution you would need one. I am trying to think of something prettier though.

1 Like

My problem was different actually. The texture that I used moved with my sea, which made it look as if there was no speed at all. I am not sure what you want it to look like.

1 Like

I want the texture to look stationary. The waves are the only movement. Just like the video I placed in my first post.

1 Like

I did some research, but I could not find any valuable information. @tyridge77 did not really elaborate on whether he uses chunks or dynamic offset. I find it a bit of a shame that he did not open source the water modules.

A chunk manager would work even if you do not have a repeating texture. You can then optimize the calculation process by only updating bones that are visible. Also, you can recycle all of your chunks.

1 Like

By chunk manager, do you mean clones of the ocean that get placed around the player’s character?
I tried creating something like that:

local DEBOUNCE_TIME = 3
local CHANGE_RADIUS = 10

local debounce = 0
local partTable = {}
local updateInProgress = false
-- Standard configuration (indexes of table):
-- 1 = TopLeft
-- 2 = TopMiddle
-- 3 = TopRight
-- 4 = MiddleLeft
-- 5 = MiddleMiddle (reference)
-- 6 = MiddleRight
-- 7 = BottomLeft
-- 8 = BottomMiddle
-- 9 = BottomRight
local folder = Instance.new("Folder")
folder.Name = "SeaParts"
folder.Parent = workspace

-- Return positions around referencePosition (only for squares)
local function positionsAroundReference(refPart)
	local refPos = refPart.Position
	local length = refPart.Size.X
	return {
		[1] = refPos + Vector3.new(-length, 0, length),
		[2] = refPos + Vector3.new(0, 0, length),
		[3] = refPos + Vector3.new(length, 0, length),
		[4] = refPos + Vector3.new(-length, 0, 0),
		[5] = refPos,
		[6] = refPos + Vector3.new(length, 0, 0),
		[7] = refPos + Vector3.new(-length, 0, -length),
		[8] = refPos + Vector3.new(0, 0, -length),
		[9] = refPos + Vector3.new(length, 0, -length),
	}
end

-- Create a part at the specified position
local function createPart(pos)
	local part = Instance.new("Part")
	part.Size = Vector3.new(25, 1, 25)
	part.Anchored = true
	part.CanCollide = true
	part.Position = pos
	part.Parent = folder

	CHANGE_RADIUS = part.Size.X / 2

	-- local part = workspace.Ocean.Plane:Clone()
	-- part.Position = pos
	-- part.Parent = workspace
	return part
end

local function updatePartTable(sourcePart)
	if updateInProgress then
		return
	end
	-- Check debounce
	if debounce <= 0 then
		debounce = DEBOUNCE_TIME
	else
		return
	end
	print("Update")
	updateInProgress = true

	-- Get new positions
	local newPositions = positionsAroundReference(sourcePart)

	local newTable = {}
	for index, newPos in pairs(newPositions) do
		if index == 5 then
			-- Use sourcePart in the middle
			newTable[index] = sourcePart
			newTable[index].Color = Color3.fromRGB(255, 0, 0)
		else
			for _, part in pairs(folder:GetChildren()) do
				if (part.Position - newPos).Magnitude <= 0.25 then
					-- This already created part has the position we want!
					newTable[index] = part
				end
			end

			if not newTable[index] then
				-- Create a new part
				newTable[index] = createPart(newPos)
			end
		end
	end

	-- Remove unused parts
	-- for _, v in pairs(folder:GetChildren()) do
	-- 	local found = table.find(newTable, v)
	-- 	if not found then
	-- 		v:Destroy()
	-- 	end
	-- end

	-- Update partsTable
	for i, v in pairs(newTable) do
		partTable[i] = v
	end

	updateInProgress = false
end

local sourcePart = createPart(Vector3.new(0, 200, 0))

updatePartTable(sourcePart)

game:GetService("RunService").Heartbeat:Connect(function(dt)
	debounce -= dt

	local char = game:GetService("Players").LocalPlayer.Character
	if char then
		local rootPart = char:FindFirstChild("HumanoidRootPart")
		if rootPart and partTable[5] then
			-- Vector pointing from middle part to HumanoidRootPart (Vector2)
			local distance = (Vector2.new(rootPart.Position.X, rootPart.Position.Z) - Vector2.new(
				partTable[5].Position.X,
				partTable[5].Position.Z
			)).Magnitude

			if distance > CHANGE_RADIUS then
				-- Player has walked further than max distance --> update parts
				local closestPart
				local closestDistance = math.huge
				for i, part in pairs(partTable) do
					if i ~= 5 then -- Don't use middle part
						local newDistance = (Vector2.new(part.Position.X, part.Position.Z) - Vector2.new(
							rootPart.Position.X,
							rootPart.Position.Z
						)).Magnitude
						if newDistance < closestDistance then -- If part is closer (by a margin)
							closestPart = part
							closestDistance = newDistance
						end
					end
				end
				updatePartTable(closestPart)
			end
		end
	end
end)

This should be ran inside a LocalScript, and should work in an empty place.

Now, if I use the ocean mesh instead of the parts, it works nicely. But the animation part is where I struggle.

Mainly this question:

  • How can I get the bones within a radius of the player efficiently?

Simply looping through all the bones (of all the different meshes) every frame, causes lag. As there are 4000 * 9 = 36000 bones the client has to check the distance from their HumanoidRootPart of.

Does there exist an efficient way of getting all bones within a radius from a point?

1 Like

So, it’s been a while.
I’ve stopped working on this project because I can’t figure it out. I hate to bump topics, but can someone help?
I would greatly appreciate it. Thanks.

I would love to help but it seems you’ve privated your github project link so I cannot see how you’ve implemented the ocean.