Problem with perlin noise map generation

I am currently working on a mining game which generates a completely new quarry (mining area) each time you join a new server. And so far the generation works. The only problem/doubt i currently have is “how do i make it so i can add multiple block layers?” because let’s be honest, who wants a mine that is only 1 block deep.

does anyone know a way of adding more depth?

i did evaluate the possibility of making blocks to generate around the mined area in order to make that infinite depth look. the thing is that i want to add a cave system too and i have 0 clue on how i can add more depth using perlin noise and at the same time generate that caves.

this is the current module script i’m using

local Perlin = {}
local PerlinBlock = script.PerlinBlock

local MapSize = 250
local TerrainParts = workspace.TerrainParts
local Colors = {
    [0.8] = Color3.fromRGB(104, 104, 104), 
	[0.75] = Color3.fromRGB(104, 104, 104), 
	[0.5] = Color3.fromRGB(104, 104, 104), 
	[-100] = Color3.fromRGB(104, 104, 104), 
	[0.25] = Color3.fromRGB(104, 104, 104), 
	[0.10] = Color3.fromRGB(104, 104, 104), 
	[0] = Color3.fromRGB(104, 104, 104), 
}

local function GetColor(y : number) : Color3
    local closetKey = -1

    -- Loop through colors and find the one that it is greater than but also less than the next one.
    for key, color in pairs(Colors) do
        if y >= key then
            if key > closetKey then
                closetKey = key
            end
        end
    end

    return Colors[closetKey]
end

local function InverseLerp(value : number, start : number, endingValue : number) : number
    return (value - start) / (endingValue - start)
end

local function SmoothNumber(baseY : number, threshold : number) : number
    if baseY <= threshold then
        return 0
    else
        local lerpedValue = InverseLerp(baseY, threshold, 1)
        return lerpedValue
    end
end

function Perlin.Generate(Scale : number, Frequency : number, Amplitude : number, ChunkSize : number, SmoothingFactor : number, Seed : number)
    -- Remove all previous blocks for new generation.
    for _, object in pairs(TerrainParts:GetChildren()) do
        object:Destroy()
    end

    -- Generate random offsets.
    local random = Random.new(Seed)
    local xOffset = random:NextNumber(-100000, 100000)
    local zOffset = random:NextNumber(-100000, 100000)

    -- Loop through each chunk.
    for x = 0, MapSize do
        for z = 0, MapSize do
            -- Change sizes of sample X and Z.
            local sampleX = x / Scale * Frequency + xOffset
            local sampleZ = z / Scale * Frequency + zOffset

            -- Generate the perlin noise?!?!
            local baseY = math.clamp((math.noise(sampleX, sampleZ) + 0.5), 0, 1)
            local preservedBaseY = baseY
            
            baseY = SmoothNumber(baseY, SmoothingFactor)
            
            local y = baseY * Amplitude

            -- Get the correct CFrame.
            local BlockCFrame = CFrame.new(x * ChunkSize, y * ChunkSize, z * ChunkSize)

            -- Create a new part for the terrain.
            local clone = PerlinBlock:Clone()
            clone.CFrame = BlockCFrame
            clone.Color = GetColor(preservedBaseY)
            clone.Size = Vector3.new(ChunkSize, y * 2 + 1, ChunkSize) 
            clone.Parent = TerrainParts   

        end
        task.wait()
    end
end

return Perlin

Ah, it looks like you’re using code meant for just terrain (using 2d math.noise as a height map) and not for filling a 3D block.

You need to add a loop for the Y dimension and add Y into the math.noise call so the values make sense when changing along Y as well as X and Z.

It could be something like:

for x = 0, MapSize do
    for z = 0, MapSize do
        for y = 0, MapSize do 
             local sampleX = x / Scale * Frequency + xOffset
             local sampleZ = z / Scale * Frequency + zOffset
             local sampleY = y / Scale * Frequency + yOffset

            -- no more baseY or any of that since we are filling a 3D space, not generating a height map
     
            local val = math.noise(sampleX, sampleY, sampleZ) + 0.5
            local BlockCFrame = CFrame.new(x * ChunkSize, y * ChunkSize, z * ChunkSize)
            ...
            // get color based on the val
            GetColor(val)
        end
    end
end

1 Like

that seemed pretty useful!

but still i do have a problem. I want my terrain generation to work like minecraft, allow me to explain.

i want to fill the entire distance from the “surface” layer down to the very bottom (some sort of max depth factor) so i can generate caves correctly.

still i gotta say that your idea seemed pretty useful for me, i understood in a deeper manner how perlin noise works.


i do want to keep my terrain with geography too, if i add your code segment, the terrain will flatten and turn into a baseplate (with the desired depth though)

So in that case you’re going to want to do almost two passes probably, one to generate the terrain height map, like the 3D one you had before, and then a 3D one to fill in a space below, that checks against the height map to see if it should fill in that voxel or not.

Making a MC-style voxel world larger than just a chunk or two is not currently very feasible in Roblox without pulling out every greedy merging, lazy generation, part pooling, and custom streaming optimization trick in the book. You can’t just naïvely generate chunks where every voxel is a cube-shaped Part, without very quickly hitting memory, performance, and replication/streaming limits. IMHO, it’s not even worth trying if your goal is anything more than a experiment or showcase.

I’m not saying it’s totally impossible, it’s just very painful to implement and requires learning about all kinds of data compression and spatial partitioning/subdivision techniques. Someday, maybe Roblox will give us 1-bit-occupancy voxel terrain, but I’m not holding my breath waiting for it.

1 Like