Voxel generation

Welp, you got me started down a really deep rabbit hole… Here’s what I found :stuck_out_tongue:

There’s no reason that you can’t use both 2D noise for a heightmap and use 3D noise to determine which cells are solid and which are air. The height map can influence the 3D “density map”. This is probably the best way of making sure you have no floating islands that aren’t connected to the rest of the terrain while still allowing overhangs. At least that’s my preferred method. To get the overhangs you need to somehow offset the input coordinates to your heightmap function. I like to “twirl” them with coherent noise. Yes, this means you’ll use coherent noise to generate the coordinates that you sample coherent noise at. Weird.

Here's a Module for generating fractal (or Perlin?) noise. It's great for making more interesting terrain, with details at different scales.
local r = Random.new(os.time())

function fractalNoiseGenerator(seed, amplitude, frequency, octaves, lacunarity, persistence)
	--Generates a noise generator using cohrent noise (math.noise)
	--Uses "sensible" default values. 
	--Check out this glossary: http://libnoise.sourceforge.net/glossary/index.html
	
	local noise = math.noise
	local seed = seed or (r:NextInteger(-10e3, 10e3) * math.pi) -- * pi because math.noise is weird at integer coordinates, and pi is irrational
	local amplitude = amplitude or 1
	local frequency = frequency or 4
	local period = 1/frequency
	local octaves = octaves or 3
	local lacunarity = lacunarity or 1.75
	local persistence = persistence or 0.8
	
	return function(x, y, z)
		local v = 0
		local a = amplitude
		local f = frequency
		
		for o = 1, octaves do
			v = v + noise(seed + x*f, seed + y*f, seed + z*f) * a
			a = a * persistence
			f = f * lacunarity	
		end
		
		return v
	end
end

return fractalNoiseGenerator
Here's a script for actually generating the terrain (requires the Fractal noise thing)
local FractalNoise = require(game.ServerStorage.FractalNoise)
local heightmapNoise = FractalNoise(nil, 0.6, 2, 2, nil, nil) --These are just magic numbers
local swirlNoise = FractalNoise(nil, .3, 5, 2) --These are just magic numbers

function mapSet(map, x, y, z, value)
	map[x] = map[x] or {}
	map[x][y] = map[x][y] or {}
	map[x][y][z] = value
end

function mapGet(map, x, y, z)
	if map[x] then
		if map[x][y] then
			return map[x][y][z]
		end
	end
end

function twirlCoordinates(x, y, z, power)
	local power = power or 1
	local tX, tY, tZ = 	
		swirlNoise(x, y, z),
		swirlNoise(x+1000, y, z), --Don't want the *same* twirl on each axis
		swirlNoise(x, y+1000, z)
		
	return x + tX * power, y + tY * power, z + tZ * power
end

function heightMap(x, z)
	return heightmapNoise(x, 0, z)
end

function density(x, y, z)
	--If you twirl with power 0, you'll just get a plain heightmap
	local tX, tY, tZ = twirlCoordinates(x, y, z, 1)
	tZ = tZ / (1 + y)
	tX = tX / (1 + y)
	local densityOffset = 0.5 + heightMap(tX, tZ) - y --Add 0.5 density so that there's a guaranteed bottom layer
	return densityOffset
end

function generateTerrain(mapSize)
	local densityMap = {}
	for x = 1, mapSize do
		for y = 1, mapSize/2 do
			for z = 1, mapSize do
				mapSet(densityMap, x, y, z, density(x/mapSize, y/(mapSize/2), z/mapSize))
			end
		end
	end
	
	for x = 1, mapSize do
		for y = 1, mapSize/2 do
			for z = 1, mapSize do
				local d = mapGet(densityMap, x, y, z)
				if d >= 0 then
					local block = script.Block:Clone()
					block.CFrame = CFrame.new(x, y, z)
					block.Color = Color3.fromHSV(((y/mapSize)%1), .75, 1 - (y/mapSize))
					block.Parent = game.Workspace
				end
			end
		end
	end
end

generateTerrain(64)
Here's a simplified terrain generation script, to see how 3D noise can be used to make a "height map".
local FractalNoise = require(game.ServerStorage.FractalNoise)
local heightmapNoise = FractalNoise(nil, 0.6, 2, 2, nil, nil) --These are just magic numbers

function mapSet(map, x, y, z, value)
	map[x] = map[x] or {}
	map[x][y] = map[x][y] or {}
	map[x][y][z] = value
end

function mapGet(map, x, y, z)
	if map[x] then
		if map[x][y] then
			return map[x][y][z]
		end
	end
end

function heightMap(x, z)
	return heightmapNoise(x, 0, z)
end

function density(x, y, z)
	local densityOffset = 0.5 + heightMap(x, z) - y --Add 0.5 density so that there's a guaranteed bottom layer
	return densityOffset
end

function generateTerrain(mapSize)
	local densityMap = {}
	for x = 1, mapSize do
		for y = 1, mapSize/2 do
			for z = 1, mapSize do
				mapSet(densityMap, x, y, z, density(x/mapSize, y/(mapSize/2), z/mapSize))
			end
		end
	end
	
	for x = 1, mapSize do
		for y = 1, mapSize/2 do
			for z = 1, mapSize do
				local d = mapGet(densityMap, x, y, z)
				if d >= 0 then
					local block = script.Block:Clone()
					block.CFrame = CFrame.new(x, y, z)
					block.Color = Color3.fromHSV(((y/mapSize)%1), .75, 1 - (y/mapSize))
					block.Parent = game.Workspace
				end
			end
		end
	end
end

generateTerrain(64)
Here is a (huge) picture gallery of stuff I found out when playing around :P

Toothpaste series

:tooth:
Basic examples of combining height maps and twirled coordinates. Nice overhangs, no floaties.

“Fire” series

Because I picked some cool colors and the shapes look like licking flames \m/. Examples of extreme amounts of twirl (the “bases” of the “mountains” are horizontally far away from their “peaks”).

Rainbow series

:rainbow: :rainbow: :rainbow: :rainbow: :rainbow:

Experiment with twirling more at the top, and less and less towards the bottom. Extreme twirl can generate really nice caves, but reducing twirl near the bottom of the world prevents them from going too deep.

Various

This one colors blocks depending on the “density” at their position, instead of their height. I don’t have anything clever to say about this :

Here’s a 2D version of many of the same concepts. Instead of using coherent noise for the height, a simple sine curve is used. The X coordinate is still twirled, giving it overhangs. Using a sine curve instead of a noisy heightmap gives a better understanding of what exactly “twirling” the coordinates does (there are regular peaks and valleys, with consistent heights and depths. The noise is only used for “surface level” features). Also it looks like fire :fire: :fire: :fire:

Annotation%202019-08-28%20021740

Here’s a cartoony version of the same thing. The sine of the y coordinate is used for twirling the input to math.noise(), which is kind of the opposite of the other flames. Any coherent function can be used to “twirl” the coordinates. I’m really interested in what other examples of smoothly varying functions I can find.

Just a heads up you can still totally get floaties, but only if you twirl too hard. You’ll have to tune your specific parameters to your needs.
Hope this rambly post is some help, and let me know if you have questions :slight_smile:

45 Likes