Perlin noise without height map

Hi, i want to know how to make something like the one in the picture.
I want to make terrain generation without height maps.
Example:
image
Brown - dirt
Green - grass
Light yellow - sand
Light grey - stone

My results



Code:

local TerrainGeneratorService = {}
TerrainGeneratorService.__index = TerrainGeneratorService

local MAP_SIZE = Vector3.new(6144, 0, 6144)
local RESOLUTION = 4
local CHUNK_SIZE = 256

local MATERIAL_LAYERS = {
    {
        name = "Grass",
        noiseScale = 0.00005,
        threshold = 0.7,
        material = Enum.Material.Grass,
        occupancy = 0.9,
        priority = 2
    },
    {
        name = "Sand",
        noiseScale = 0.00007,
        threshold = 0.6,
        material = Enum.Material.Sand,
        occupancy = 0.8,
        priority = 2
    },
	{
		name = "Stone",
		noiseScale = 0.0001,
		threshold = 0.5,
		material = Enum.Material.Rock,
		occupancy = 0.8,
		priority = 2
	},
    {
        name = "Ground",
        noiseScale = 0.0001,
        threshold = 0.4,
        material = Enum.Material.Ground,
        occupancy = 0.5,
        priority = 3
    }
}

table.sort(MATERIAL_LAYERS, function(a, b)
    return a.priority < b.priority
end)

function TerrainGeneratorService.new()
	local self = setmetatable({}, TerrainGeneratorService)
	
	self.services = {
		Workspace = game:GetService("Workspace"),
		UserInputService = game:GetService("UserInputService")
	}
	
	self.terrain = self.services.Workspace.Terrain
	self:ConnectEvents()
	self:Generate()
	return self
end

function TerrainGeneratorService:ConnectEvents()
	self.services.UserInputService.InputBegan:Connect(function(input, gameProcessed)
		if input.UserInputType == Enum.UserInputType.Keyboard and not gameProcessed then
			if input.KeyCode == Enum.KeyCode.N then
				self:Generate()
			end
		end
	end)
end

function TerrainGeneratorService:GetMaterial(worldX, worldZ, seed)
	local selectedMaterial = MATERIAL_LAYERS[1].material
    local selectedOccupancy = MATERIAL_LAYERS[1].occupancy

    for _, layer in ipairs(MATERIAL_LAYERS) do
        local nx = (worldX + seed) * layer.noiseScale
        local nz = (worldZ + seed) * layer.noiseScale

        local noiseValue = math.noise(nx, seed, nz)
        noiseValue = (noiseValue + 1) / 2

        if noiseValue >= layer.threshold then
            selectedMaterial = layer.material
            selectedOccupancy = layer.occupancy
        end
    end

    return selectedMaterial, selectedOccupancy
end

function TerrainGeneratorService:Generate()
    local seed = math.random(0, os.time())
    math.randomseed(seed)

    self.terrain:Clear()
    for x = -MAP_SIZE.X, MAP_SIZE.X, CHUNK_SIZE do
        for z = -MAP_SIZE.Z, MAP_SIZE.Z, CHUNK_SIZE do
            local voxelsPerChunk = CHUNK_SIZE / RESOLUTION
            local materials = {}
            local occupancies = {}

            for i = 1, voxelsPerChunk do
                materials[i] = {}
                occupancies[i] = {}
                materials[i][1] = {}
                occupancies[i][1] = {}

                for j = 1, voxelsPerChunk do
                    local worldX = x + (i - 1) * RESOLUTION
                    local worldZ = z + (j - 1) * RESOLUTION

                    local material, occupancy = self:GetMaterial(worldX, worldZ, seed)

                    materials[i][1][j] = material
                    occupancies[i][1][j] = occupancy
                end
            end

            local minPos = Vector3.new(x, 0, z)
            local maxPos = Vector3.new(x + CHUNK_SIZE, RESOLUTION, z + CHUNK_SIZE)
            local region = Region3.new(minPos, maxPos):ExpandToGrid(RESOLUTION)

            self.terrain:WriteVoxels(region, RESOLUTION, materials, occupancies)
        end
    end
end

return TerrainGeneratorService