Procedurally generated voxel world

It’s one of those generic Perlin noise terrain generators you often see. Nothing too special.
Chunks are 15x15x15 cubes and each voxel is 4 studs in size. Each chunk is greedy-meshed individually and the entire physical world is client-sided (the actual world data is on the server).

Screenshots

“Bridge”

“Caves”

“Abyss”

“Light”

Try it out yourself: Procedural Perlin - Roblox

As of now, it’s more of an experimental project than a proper game.

16 Likes

I like it! I wonder if you could open source?

This is the down-to-the-bone testing file I used:
It is used to test the 3D Perlin generation + the greedymesher

PerlinGreedymesh.rbxl (36.0 KB)

Of course, don’t expect anything inside of it to be perfect

6 Likes

Yayyyy!!! I’ve been looking for something more simple like this one. This is perfect.

wait i know perlin noise but how did you made it in the sky? and the terrain?? tutorial?

You don’t know what you just forced me to do. Unlimited potential. The power of the sun… in the palm of my hand. Pet- ahem, uh.

Jokes aside, even though this is nothing special, you are probably the first person to release this which is fantastic. I have so many ideas and I can’t wait to explore them all. While it isn’t up to you to help us, would you be able to suggest or assist in creating a system to destroy or place blocks / have them become greedy-meshed?

Bumping up the Density, Haze, and Glare of the Atmosphere
The code responsible for generating the terrain is inside the testing file I posted

What I did is I send over the voxels that the player want to modify. On the server, the edit information gets placed in a queue which processes the changes every 0.1 seconds (in other words, server updates the world a maximum of 10 times a second). On each iteration, the appropriate voxel is modified and then the chunk housing the voxel is re-greedy-meshed and the meshed cuboids are sent to all clients for replication
Although I haven’t done it myself yet, it is suggested to compress the replication information to reduce latency spikes when loading

I mean the voxel sky and terrain
image
sky
image
terrain

That’s still from the Perlin noise terrain generator, just with additional terms for “biomes”

This is great! However, when implementing to a world with 3 studs instead of one, it doesn’t mesh correctly. Heres my edited version of the module (no visuals) and I would like to know how to make the ‘size’ variable come into play.

local module = {}

type voxelGroup = {[number]: any}
type voxelCluster = {[number]: voxelGroup}
type matrix = {[number]: voxelCluster}
type cuboids = {[number]: {Vector3}}

local size = 3

local v3 = Vector3.new

local function find(t: matrix, x: number, z: number, y: number): any
	local _x = t[x]
	if _x then
		local _z = _x[z]
		if _z then
			return _z[y]
		end
	end
	return
end

local function meshRow(t: matrix, sX, eX, z, y, bl): voxelGroup?
	local _r: voxelGroup = {}
	for x = sX, eX do
		local voxel = find(t, x, z, y)
		if (not voxel) or bl[voxel] then return end
		table.insert(_r, voxel)		
	end
	return _r
end

local function meshLayer(t: matrix, sX, sZ, eX, eZ, y, bl): voxelCluster?
	local _r: voxelCluster = {}
	for z = sZ, eZ do
		local row = meshRow(t, sX, eX, z, y, bl)
		if row then
			table.insert(_r, row)
		else return end
	end
	return _r
end

local function greedyChunk(t: matrix, sX, sY, sZ, eX, eY, eZ, bl): (Vector3, Vector3)
	local cX, cY, cZ = sX, sY, sZ

	for x = sX + 1, eX do
		local voxel = find(t, x, sZ, sY)
		if voxel and not bl[voxel] then
			bl[voxel] = true
			cX = x
		else			
			break
		end
	end

	local zAdj = find(t, sX, sZ + 1, sY)
	if zAdj and not bl[zAdj] then
		for z = sZ + 1, eZ do
			local row = meshRow(t, sX, cX, z, sY, bl)
			if row then
				cZ = z
				for _, v in ipairs(row) do
					bl[v] = true
				end
			else
				break
			end
		end
	end

	local yAdj = find(t, sX, sZ, sY + 1)
	if yAdj and not bl[yAdj] then
		for y = sY + 1, eY do
			local layer = meshLayer(t, sX, sZ, cX, cZ, y, bl)
			if layer then
				cY = y
				for _, vg in ipairs(layer) do
					for _, v in ipairs(vg) do
						bl[v] = true
					end
				end
			else				
				break				
			end
		end
	end
	return v3(sX, sY, sZ), v3(cX, cY, cZ)
end

function module.greedyMesh(t: matrix, sV3: Vector3, eV3: Vector3): cuboids
	local bl = {}
	local _r = {}
	local sX, sY, sZ = sV3.X, sV3.Y, sV3.Z
	local eX, eY, eZ = eV3.X, eV3.Y, eV3.Z
	for x = sX, eX do
		for z = sZ, eZ do
			for y = sY, eY do
				local voxel = find(t, x, z, y)
				if voxel and not bl[voxel] then
					bl[voxel] = true
					table.insert(_r, {greedyChunk(t, x, y, z, eX, eY, eZ, bl)

					})
				end
			end
		end
		task.wait()
	end
	return _r
end

return module

Edit: the code i use suddenly stopped working idk if its the module or mine

Yeah, I forgot to mention that in that demo the voxel size is completely tied to the greedymesher for the sake of simplicity. You need some kind of “proxy” function that splits the meshing algorithm (which only accepts unit, axis-aligned vectors) from the physical world generation (which should be able to accept any voxel size), instead of trying to resize the voxel within the greedymesher

This is the function that I used for the live game:

local module = {
	blockSize = blockSize, --number
	voxelSize = voxelSize, --Vector3
	
	--note: c2 needs to be "greater" than c1 so that c2 - c1 does not give you a vector with negative components!
	partFromCorners = function(c1: Vector3, c2: Vector3, alt: boolean?): Part
		c1 *= blockSize
		c2 *= blockSize
		local p: Part = if alt then altBlock:Clone() else Instance.new('Part')
		p.Anchored = true
		p.Position = c1:Lerp(c2, .5)
		p.Size = c2 - c1 + voxelSize
		return p
	end,

And just to demonstrate, here’s the world with 1.5 studs voxel size (a non-integer!)

I use that function, however, it doesn’t help with the size issue. One way i found is to edit the module itself and I got some results. It doesn’t greedy mesh correctly but its close. Help would be appreciated.

local module = {}

type voxelGroup = {[number]: any}
type voxelCluster = {[number]: voxelGroup}
type matrix = {[number]: voxelCluster}
type cuboids = {[number]: {Vector3}}

local v3 = Vector3.new

local function find(t: matrix, x: number, z: number, y: number): any
	local _x = t[x]
	if _x then
		local _z = _x[z]
		if _z then
			return _z[y]
		end
	end
	return
end

local function meshRow(t: matrix, sX, eX, z, y, bl): voxelGroup?
	local _r: voxelGroup = {}
	for x = sX, eX do
		local voxel = find(t, x, z, y)
		if (not voxel) or bl[voxel] then return end
		table.insert(_r, voxel)		
	end
	return _r
end

local function meshLayer(t: matrix, sX, sZ, eX, eZ, y, bl): voxelCluster?
	local _r: voxelCluster = {}
	for z = sZ, eZ do
		local row = meshRow(t, sX, eX, z, y, bl)
		if row then
			table.insert(_r, row)
		else return end
	end
	return _r
end

local function greedyChunk(t: matrix, sX, sY, sZ, eX, eY, eZ, bl, size): (Vector3, Vector3)
	local cX, cY, cZ = sX, sY, sZ
	
	for x = sX + size, eX * size, size do
		local voxel = find(t, x, sZ, sY)
		if voxel and not bl[voxel] then
			bl[voxel] = true
			cX = x
		else			
			break
		end
	end
	
	local zAdj = find(t, sX, sZ + size, sY)
	if zAdj and not bl[zAdj] then
		for z = sZ + size, eZ * size, size do
			local row = meshRow(t, sX, cX, z, sY, bl)
			if row then
				cZ = z
				for _, v in ipairs(row) do
					bl[v] = true
				end
			else
				break
			end
		end
	end

	local yAdj = find(t, sX, sZ, sY + size)
	if yAdj and not bl[yAdj] then
		for y = sY + size, eY * size, size do
			local layer = meshLayer(t, sX, sZ, cX, cZ, y, bl)
			if layer then
				cY = y
				for _, vg in ipairs(layer) do
					for _, v in ipairs(vg) do
						bl[v] = true
					end
				end
			else				
				break				
			end
		end
	end

	
	return v3(sX, sY, sZ) / size, v3(cX, cY, cZ) / size
end

function module.greedyMesh(t: matrix, sV3: Vector3, eV3: Vector3, size: number): cuboids
	local bl = {}
	local _r = {}
	local sX, sY, sZ = sV3.X, sV3.Y, sV3.Z
	local eX, eY, eZ = eV3.X, eV3.Y, eV3.Z
	for x = sX, eX do
		for z = sZ, eZ do
			for y = sY, eY do				
				local voxel = find(t, x, z, y)
				if voxel and not bl[voxel] then
					bl[voxel] = true
					local t = {greedyChunk(t, x, y, z, eX, eY, eZ, bl, size)}
					table.insert(t, voxel)
					table.insert(_r, t)
				end
			end
		end
		task.wait()
	end
	return _r
end

return module

gM.greedyMesh(World, region, region, part_size)

As a side note, im also implementing ores but the problem is i dont know how to make the mesher ignore the ores

  1. I don’t think this convo belongs in #help-and-feedback:creations-feedback. You can move it into PMs if you want.

  2. Clearly you didn’t even read what I said. The greedymesher was designed to work with single one-by-one vectors as it’s just an algorithm that takes a nested table (a matrix) as the input. Turning the matrix into a physical world must be done by another function that uses the output OF the greedymesher where it can then freely manipulate the size of the voxels before instantiating the parts.

  3. The function I gave in my previous post uses exactly what’s outputted by the greedyMesh() function. And if you have a look in the demo, the serverscript has this:

for k, v in gM.greedyMesh(world, Vector3.new(-s, -s, -s), Vector3.new(s, s, s)) do
	local p = Instance.new('Part')
	p.Anchored = true
	p.Position = v[1]:Lerp(v[2], .5)
	local disp = v[2] - v[1] + Vector3.one
	p.Size = Vector3.new(math.abs(disp.X), math.abs(disp.Y), math.abs(disp.Z))
	p.Parent = f
	if k % 100 == 0 then task.wait() end
end

You should be able to see some similarities between this and the function I gave. Utilizing the function should be trivial. Everything else you need to know is within the demo file.

So it’s been a month now, and I thought it would be cool for me to showcase some of the changes since then. I’ve been working on it every now and then whenever I felt like it.

Some significant changes include a compression system for world replication. Each chunk looks something like this which is sent via remotes to all clients:

ScreenShot_20220712160718

C1 and C2 are the actual block data. CP and CVP are just pointers telling the algorithm how to decompress it.
As of now, only replication is compressed. I plan on also compressing the actual chunks that are stored on the server (with a buffer system of course).

Full explanation if you're interested

The algorithm is actually pretty simple. It uses LZW compression; upon joining the game, a massive table gets created documenting each possible voxel of chunks (the current chunk size is 15, which means there are 15^3 or 3,375 possible voxels). Base93 is used in identifying the said voxels. I chose base93 because its capacity allows each voxel to be represented with just two characters, making it very efficient. You can see in the video the data transfer rate.

The chunk data is split into two; C1 and C2. C1 is for single voxels that are left out by the greedy-mesher algorithm, thus only 2 characters are needed to represent them. C2 composes the bulk of the chunk; 4 characters are used to represent each cuboid, which are just groups of meshed-up voxels. All of the data are then concatenated together, sent to each client, decompressed by splitting it back up into 2-character/4-character groups, and translated back into voxels with LZW.

The LZW table:

--in ReplicatedStorage
local encode: {[Vector3]: string} = {}
local decode: {[string]: Vector3} = {}
local lzwID: number = 0 --counter actually starts at 1
for x = -chunkQuadrantSize, chunkQuadrantSize do
	for y = -chunkQuadrantSize, chunkQuadrantSize do
		for z = -chunkQuadrantSize, chunkQuadrantSize do
			lzwID += 1
			local hex: string = toBase93(lzwID)
			if lzwID < 93 then
				hex = [[\]]..hex --makes sure that each voxel takes up *exactly* 2 characters
			end
			local voxel: Vector3 = Vector3.new(x, y, z)
			encode[voxel] = hex
			decode[hex] = voxel
		end
	end
end

Another feature added is a garbage collection system for “dead” chunks. On a regular interval, the server iterates through each chunk, and those that are both unedited (not a single voxel has been modified by a player) and inactive (chunk hasn’t been loaded for a while) will get deleted from memory.

Some other minor changes include the lighting/atmosphere, the UI, and sounds.

1 Like