Improving my procedural terrain generation function

I’m not sure if this is the right category, please let me know if it was. Other than that, let’s get started;

  • What does the code do and what are you not satisfied with?

My code takes 3 numbers (coordinates in this case), and returns a material enum and a value (occupancy) to place in the terrain instance, I’m not satisfied with the basic noise method I wrote here, I would like some proper cliffs, and overhangs, I know this is possible with 3D noise, but that doesn’t satisfy me enough.


  • What potential improvements have you considered?

I’ve considered making the procedural terrain generator more realistic, with cliffs, overhangs, and caves.


  • How (specifically) do you want to improve the code?

This is not really anything about code improvement, this is just about improving the looks of the terrain using code. I would want to improve the code, as mentioned above:

I’ve considered making the procedural terrain generator more realistic, with cliffs, overhangs, and caves


Reference code:

-- This is just a reference code, I did something a little better in the main code.
-- I'm still not satisfied with the main code either.
local function basic(pos) -- Assume this is the function I currently have.
    local mat, occ = Enum.Material.Air, 0
    -- The material, and occupancy values to be set in terrain
    
    local noise = math.noise(pos.x * 0.1, pos.y * 0.1, pos.z * 0.1)
    local ny = (noise * 16) - pos.y
    local diff = math.clamp(ny, 0, 1) -- Using diff to clamp the value for the occupancy value
    if diff > 0 then -- If there is any value to occupy, then set the material to grass.
        mat, occ = Enum.Material.Grass, diff
    end
    
    return mat, occ
end

-- After that, just iterate in all directions (x, y, z) in for loops
-- and assign the values to the terrain using WriteVoxels.

-- Note: The function above can generate in negative coordinates,
-- due to non-normalized noise

It would be fun to have a community invent many kinds of procedural terrain methods. :huh:

6 Likes

Could we have some screenshots to work with?

My favorite way to get overhangs is to use something called domain warping. In short, it warps the input coordinates to the noise function, so the output of it doesn’t resemble smooth, boring hills so much.

Cool art with noise

World gen with domain warping

One trick is to use noise to warp the noise.

Here's a function I've used in many proc-gen projects to do domain-warping
function coordinateTwirl(twirlNoise, x, y, z, power)
	local power = power or 1
	local twirlX, twirlY, twirlZ = 
		twirlNoise(x, y, z) * power,
		twirlNoise(x+10, y, z) * power,
		twirlNoise(x, y, z-50) * power
	
	return x + twirlX, y + twirlY, z + twirlZ
end

return coordinateTwirl

It takes a noise function, 3 coordinates and an optional multiplier that controls (in concert with the noise function) how heavily the output is distorted.

Here's a fractral noise generator factory I usually use along with it:
local r = Random.new(os.time())
local e = 2.718281828459 --the mathematical constant
local RANDOM_SEED_AMPLITUDE = 10e5

function fractalNoiseFactory(args)
	--"args" keys: {seed, amplitude, frequency, octaves, lacunarity, persistence}
	--Generates a fractal 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 = args.seed or (e * r:NextInteger(-RANDOM_SEED_AMPLITUDE, RANDOM_SEED_AMPLITUDE)) -- * e because noise is weird at integer coordinates, and e is most irrational
	local amplitude = args.amplitude or 1
	local frequency = args.frequency or 4
	local period = 1/frequency
	local octaves = args.octaves or 5
	local lacunarity = args.lacunarity or 1.75
	local persistence = args.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 fractalNoiseFactory

It returns a function that uses multiple layers of math.noise to create fractal noise (proper Perlin noise). The default parameters are pretty reasonable, so to get started you probably just need to tweak the amplitude and frequency.

Here's how you can use those:
local basicNoise = fractalNoiseFactory({amplitude = 16, frequency = 1/32, octaves = 2})
local twirlNoise = fractalNoiseFactory({frequency = 1/8, octaves = 1})

local function basic(pos)
    ...
    local x, y, z = coordinateTwirl(twirlNoise, pos.X, pos.Y, pos.Z, 4)
    local noise = basicNoise(x, y, z)
    ...
end

end

EDIT: Examples of the kinds of results you can get in this old post. Check out the picture gallery :smile: :+1:

3 Likes

Yes!

I’ll set up a sample project with this function above, and iterate it through with xyz for-loops, and then writing it into terrain by using WriteVoxels.


Sample Project Code
local terrain = workspace.Terrain

local worldSize = Vector3.new(64, 64, 64)

function voxel(pos)
	local mat, occ = Enum.Material.Air, 0
	-- The material, and occupancy values to be set in terrain
	
	local noise = math.noise(pos.x * 0.1, pos.y * 0.1, pos.z * 0.1)
	local ny = (noise * 5) - pos.y
	local diff = math.clamp(ny, 0, 1) -- Using diff to clamp the value for the occupancy value
	if diff > 0 then -- If there is any value to occupy, then set the material to grass.
		mat, occ = Enum.Material.Grass, diff
	end
	
	return mat, occ
end

function world()
	terrain:Clear()
	
	local mx, ox = {}, {} -- Material table
	
	for x = 1, worldSize.x, 1 do
		local my, oy = {}, {}
		
		for y = 1, worldSize.y, 1 do
			local mz, oz = {}, {}
			
			for z = 1, worldSize.z, 1 do
				local pos = Vector3.new(x, y, z) - (worldSize * 0.5)
				local m, o = voxel(pos)
				
				mz[z], oz[z] = m, o
			end
			
			my[y], oy[y] = mz, oz
		end
		
		mx[x], ox[x] = my, oy
		
		wait()
	end
	
	terrain:WriteVoxels(
		Region3.new(
			(-worldSize * 0.5) * 4,
			(worldSize * 0.5) * 4
		), 4, mx, ox
	)
end

world()

Image:


Would it be a good idea if I made a thread where various number of people made unique voxel functions that generates noise, random terrain, etc.?

2 Likes

This is what I got from copying you code over to the sample project. (lol)
I’ll do more tweaking, don’t worry.

Sample Project Code
local worldSize = Vector3.new(64, 64, 64)

local terrain = workspace.Terrain

local fractalNoiseFactory = require(script.FractalNF)
local coordinateTwirl = require(script.CoordTwirl)

local basicNoise = fractalNoiseFactory({amplitude = 2, frequency = 1/16, octaves = 2})
local twirlNoise = fractalNoiseFactory({frequency = 0.125, octaves = 1})

function voxel(pos)
	local mat, occ = Enum.Material.Air, 0
	-- The material, and occupancy values to be set in terrain
	
	local noise = coordinateTwirl(
		twirlNoise,
		pos.x * 0.5,
		pos.y * 0.5,
		pos.z * 0.5,
		4
	)
	local ny = (noise * 5) - pos.y
	local diff = math.clamp(ny, 0, 1) -- Using diff to clamp the value for the occupancy value
	if diff > 0 then -- If there is any value to occupy, then set the material to grass.
		mat, occ = Enum.Material.Grass, diff
	end
	
	return mat, occ
end

function world()
	terrain:Clear()
	
	local mx, ox = {}, {} -- Material table
	
	for x = 1, worldSize.x, 1 do
		local my, oy = {}, {}
		
		for y = 1, worldSize.y, 1 do
			local mz, oz = {}, {}
			
			for z = 1, worldSize.z, 1 do
				local pos = Vector3.new(x, y, z) - (worldSize * 0.5)
				local m, o = voxel(pos)
				
				mz[z], oz[z] = m, o
			end
			
			my[y], oy[y] = mz, oz
		end
		
		mx[x], ox[x] = my, oy
		
		wait()
	end
	
	terrain:WriteVoxels(
		Region3.new(
			(-worldSize * 0.5) * 4,
			(worldSize * 0.5) * 4
		), 4, mx, ox
	)
end

world()


It looks like the noise occupancy is not being correctly calculated :confused:

It seems that the terrain is getting like close to 1 occupancy per voxel, is that intentional?


They’re all “cubes”!


This doesn’t seem right…
Floating points?


I finally got some interesting shapes out of it, but somehow it became vertical.


Perhaps making a voxel function to return material, and occupancy would be appreciated!
Of course, you can use multiple functions before the main voxel function.

2 Likes

Interesting, this type of terrain generation, especially the last picture that @FrontHD provided, reminds me a lot of the Farlands in Minecraft. I can definitely see Fractals generating some really neat terrain. My suggestion is just to experiment around with different voxel functions till you see what works.

1 Like

Yes, this is very similar to far lands, distorted noise in a certain axis.
I’ve managed to make a few “biomes” or themes in the voxel function, since then.


Attempts

Rocky:

Arctic:

Oasis:

Hellish preset:

Mountains:

Desert:

image

Island:

Yoshi Island: (Somehow, I don’t know but it was just pure luck.)


I’m curious to see if any one of you could suggest a theme, biome, or function.

3 Likes

I’d love to see a little cave cut into the side of a mountain with like an ocean, like a little hideaway

Waterfall? River? …

Edit: I see, like a hidden place where there’s a pool inside.

This is procedurally generated, so it might be really hard to do stuff like that.

That’s fair, great job on all this by the way, it has a lot of potential!

1 Like

I think it might be valuable to use the Convert Part to Terrain Feature that is now in the Studio Beta Features.

Then you can model and your overhangs with blocks, cylinders etc, and then programmatically vary them as you place them.

Something like this:

local function convertItemAndChildrenToTerrain(props)
local parent = props.parent
local ignoreKids = props.ignoreKids
local canCollide = props.canCollide or false

local material = props.material or Enum.Material.Sand
local function convert(part)
    if part:IsA('BasePart') and part.CanCollide == true then
        if part:IsA('WedgePart') then
            game.Workspace.Terrain:FillWedge(part.CFrame, part.Size, material)
        elseif part.Shape == Enum.PartType.Ball then
            game.Workspace.Terrain:FillBall(part.CFrame, part.Size, material)
        elseif part.Shape == Enum.PartType.Cylinder then
            local height = part.Size.X
            local radius = part.Size.Z / 2
            local newCFrame = part.CFrame * CFrame.Angles(0, 0, math.rad(90))
            game.Workspace.Terrain:FillCylinder(newCFrame, height, radius, material)
        else
            game.Workspace.Terrain:FillBlock(part.CFrame, part.Size, material)
        end
        part.Transparency = 1
        part.CanCollide = canCollide
    end
end

convert(parent)
if not ignoreKids then
    local children = parent:GetDescendants()
    for i, item in ipairs(children) do
        convert(item)
    end
end
end
2 Likes