How would I spawn grass using perlin noise?

I’m trying to make voxel-based generation. But I’m stumped on trying to generate grass using perlin noise.

Here’s the code:

local TERRAIN_GENERATION = {}

--// SERVICES
local S_RUN = game:GetService("RunService")
local S_RS = game:GetService("ReplicatedStorage")

--// CONSTANTS
local BLOCK_SIZE = 3

local GRID_SIZE = 48 -- blocks
local MAX_HEIGHT = 25
local MIN_HEIGHT = -25

local FREQUENCY = 10
local SCALE = 200
local AMPLITUDE = 8

local STONE_LEVEL = -10

--[PRIVATE]--

--// Generates blocks on surface level
local function generate_surface(seed)

	local blocks = {}
	local surface_y = {}

	for x = 1, GRID_SIZE, 1 do

		blocks[x] = {}
		surface_y[x] = {}

		for z = 1, GRID_SIZE, 1 do

			blocks[x][z] = {}

			local y = math.floor(
				math.noise((x / SCALE) * FREQUENCY, (z / SCALE) * FREQUENCY, seed) * AMPLITUDE
			)

			local block = S_RS.ITEMS.BLOCKS["Grass Block"]:Clone()
			block.Parent = workspace.MAP
			block.Position = Vector3.new(x * BLOCK_SIZE, y * BLOCK_SIZE, z * BLOCK_SIZE)

			surface_y[x][z] = y
			blocks[x][z][y] = block
		end
	end
	
	local values_returned = {blocks, surface_y}

	return values_returned
end

local function generate_grass(blocks: {}, surface_y: {})
	
	local grass = S_RS.ITEMS.BLOCKS.Grass
	local grass_size = grass.PrimaryPart.Size
	
	local width_difference = (BLOCK_SIZE - grass_size.X) / 2 * 100
	local y_difference = (BLOCK_SIZE - grass_size.Y)
	
	for x, x_blocks in blocks do
		
		for z, z_blocks in x_blocks do
			
			-- 25% of a surface block having grass
			if math.random(100) < 25 then
				
				local grass = grass:Clone()
				grass.Parent = workspace.MAP
				
				local y_level = surface_y[x][z] + 1
				
				
				grass:PivotTo(CFrame.new(
					(x * BLOCK_SIZE + math.random(-width_difference, width_difference) / 100 + math.random(0.1, 0.2)),
					(y_level * 3 - y_difference / 2),
					(z * BLOCK_SIZE + math.random(-width_difference, width_difference) / 100) + math.random(0.1, 0.2))
				)
				
				blocks[x][z][y_level] = "Grass"
				
			end
			
		end
		
	end
	
end

local function generate_below_surface(blocks: {}, surface_y: {})
	
	for x, x_blocks in blocks do
		
		for z, z_blocks in x_blocks do
			
			local surface_level = surface_y[x][z]
			
			for y = MAX_HEIGHT, MIN_HEIGHT, -1 do
				
				if y >= surface_level then continue end
				
				local block = S_RS.ITEMS.BLOCKS["Dirt"]:Clone()
				block.Parent = workspace.MAP
				block.Position = Vector3.new(x * BLOCK_SIZE, y * BLOCK_SIZE, z * BLOCK_SIZE)
				
				blocks[x][z][y] = block
				
				--if y < math.random(STONE_LEVEL - 2, STONE_LEVEL + 2) then
					--local block = S_RS.ITEMS["Stone"]:Clone()
					--block.Parent = workspace.MAP
					--block.Position = Vector3.new(x * BLOCK_SIZE, y * BLOCK_SIZE, z * BLOCK_SIZE)

					--blocks[x][z][y] = block
				--end
				
			end
		end
	end
end

--[PUBLIC]--

function TERRAIN_GENERATION.generate(seed)
	local surface_blocks = generate_surface(seed)
	local grass = generate_grass(surface_blocks[1], surface_blocks[2])
	--local below_surface_blocks = generate_below_surface(surface_blocks[1], surface_blocks[2])
end

return TERRAIN_GENERATION

Any help is greatly appreciated.

1 Like

you have the right idea of iterating over each block in the blocks data structure and deciding whether to generate grass on it based on a random chance. but, you can enhance this process by using Perlin noise to determine the density or distribution of grass across the terrain.

To generate grass using Perlin noise, you can modify your generate_grass

local function generate_grass(blocks, surface_y)
    local grass = S_RS.ITEMS.BLOCKS.Grass
    local grass_size = grass.PrimaryPart.Size

    local width_difference = (BLOCK_SIZE - grass_size.X) / 2 * 100
    local y_difference = (BLOCK_SIZE - grass_size.Y)

    local perlin = math.noise

    for x, x_blocks in ipairs(blocks) do
        for z, z_blocks in ipairs(x_blocks) do
            local density = perlin(x / SCALE, z / SCALE, 0) -- Calculate Perlin noise density
            
            local y_level = surface_y[x][z] + 1
            
            -- Adjust density threshold as needed (e.g., 0.5)
            if density > 0.5 then
                local grass_instance = grass:Clone()
                grass_instance.Parent = workspace.MAP

                grass_instance.PivotTo = CFrame.new(
                    x * BLOCK_SIZE + math.random(-width_difference, width_difference) / 100,
                    y_level * BLOCK_SIZE - y_difference / 2,
                    z * BLOCK_SIZE + math.random(-width_difference, width_difference) / 100
                )
                
                blocks[x][z][y_level] = grass_instance
            end
        end
    end
end

This is exactly what I needed.

Obviously, it looks off; but thats just a matter of changing the density threshold.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.