[OPEN SOURCE] Part2Terrain - A part conversion and loading tool

Hello everyone!

While working for my game Blacksite: Code Red, which heavily relies on procedural generation, I ran into a problem. I wanted to have terrain in my game, but finding a good way to load terrain within this system while still having it be flexible enough that I could freely put terrain wherever was a challenge. I searched online and on the forums to try and find a solution but really couldn’t find anything on it outside of converting terrain to parts. As a result I wrote a script and came up with a good way to do it and made it into my first ever plugin that I would like to share with you.

The Approach

Here’s the idea I had: take the camera’s position and create a region around it and take data from the terrain and put it into blocks inside of the roblox voxel grid. Reading from those parts, we can reconstruct the terrain.

The Code

Here’s the code behind the plugin:

local toolbar = plugin:CreateToolbar("Terrain/Part Conversion Kit")
local buttonParts = toolbar:CreateButton("Convert to Parts", "Convert nearby terrain to parts (from camera)", "rbxassetid://132237535373001")
local buttonTerrain = toolbar:CreateButton("Convert to Terrain", "Convert all parts back to terrain", "rbxassetid://88999484386098")

local CollectionService = game:GetService("CollectionService")
local terrain = workspace.Terrain

local resolution = 4
local radius = 250

-- Materials table for FillBlock conversion
local materials = {
	["Enum.Material.Air"] = Enum.Material.Air,
	["Enum.Material.Asphalt"] = Enum.Material.Asphalt,
	["Enum.Material.Basalt"] = Enum.Material.Basalt,
	["Enum.Material.Brick"] = Enum.Material.Brick,
	["Enum.Material.Cobblestone"] = Enum.Material.Cobblestone,
	["Enum.Material.Concrete"] = Enum.Material.Concrete,
	["Enum.Material.CrackedLava"] = Enum.Material.CrackedLava,
	["Enum.Material.Glacier"] = Enum.Material.Glacier,
	["Enum.Material.Grass"] = Enum.Material.Grass,
	["Enum.Material.Ground"] = Enum.Material.Ground,
	["Enum.Material.Ice"] = Enum.Material.Ice,
	["Enum.Material.LeafyGrass"] = Enum.Material.LeafyGrass,
	["Enum.Material.Limestone"] = Enum.Material.Limestone,
	["Enum.Material.Mud"] = Enum.Material.Mud,
	["Enum.Material.Pavement"] = Enum.Material.Pavement,
	["Enum.Material.Rock"] = Enum.Material.Rock,
	["Enum.Material.Salt"] = Enum.Material.Salt,
	["Enum.Material.Sand"] = Enum.Material.Sand,
	["Enum.Material.Sandstone"] = Enum.Material.Sandstone,
	["Enum.Material.Slate"] = Enum.Material.Slate,
	["Enum.Material.Snow"] = Enum.Material.Snow,
	["Enum.Material.WoodPlanks"] = Enum.Material.WoodPlanks,
}

-- Convert terrain to parts around camera
local function convertToParts()
	local camera = workspace.CurrentCamera
	if not camera then
		warn("No camera found.")
		return
	end

	local center = camera.CFrame.Position
	local radiusVec = Vector3.new(1, 1, 1) * 250  
	local regionStart = center - radiusVec
	local regionEnd = center + radiusVec
	local region = Region3.new(regionStart, regionEnd):ExpandToGrid(resolution)

	local materialsGrid, occupancies = workspace.Terrain:ReadVoxels(region, resolution)
	local regionSize = region.Size
	local origin = region.CFrame.Position - (regionSize / 2)

	for x = 1, materialsGrid.Size.X do
		for y = 1, materialsGrid.Size.Y do
			for z = 1, materialsGrid.Size.Z do
				local occ = occupancies[x][y][z]
				if occ > 0 then
					local mat = materialsGrid[x][y][z]

					local part = Instance.new("Part")
					part.Size = Vector3.new(resolution, resolution, resolution)
					part.Position = origin + Vector3.new(
						(x - 0.5) * resolution,
						(y - 0.5) * resolution,
						(z - 0.5) * resolution
					)
					part.Anchored = true
					part.Material = Enum.Material.SmoothPlastic
					part.Color = workspace.Terrain:GetMaterialColor(mat)

					CollectionService:AddTag(part, "TerrainPart")
					part:SetAttribute("Material", tostring(mat))
					part:SetAttribute("Occupancy", occ)

					part.Name = "TerrainPart"
					part.Parent = workspace
				end
			end
		end
	end
	print("Converted to parts")
end

-- Convert parts back to terrain
local function convertToTerrain()
	local parts = CollectionService:GetTagged("TerrainPart")
	if #parts == 0 then
		warn("No terrain parts found.")
		return
	end

	local minPos, maxPos = parts[1].Position, parts[1].Position
	for _, part in ipairs(parts) do
		local pos = part.Position
		minPos = Vector3.new(
			math.min(minPos.X, pos.X),
			math.min(minPos.Y, pos.Y),
			math.min(minPos.Z, pos.Z)
		)
		maxPos = Vector3.new(
			math.max(maxPos.X, pos.X),
			math.max(maxPos.Y, pos.Y),
			math.max(maxPos.Z, pos.Z)
		)
	end

	local region = Region3.new(minPos, maxPos):ExpandToGrid(resolution)
	local origin = region.CFrame.Position - region.Size / 2

	local materialsGrid, occupanciesGrid = terrain:ReadVoxels(region, resolution)

	local sizeX = materialsGrid.Size.X
	local sizeY = materialsGrid.Size.Y
	local sizeZ = materialsGrid.Size.Z

	for _, part in ipairs(parts) do
		local pos = part.Position
		local offset = pos - origin
		local x = math.floor(offset.X / resolution) + 1
		local y = math.floor(offset.Y / resolution) + 1
		local z = math.floor(offset.Z / resolution) + 1

		if x >= 1 and x <= sizeX and y >= 1 and y <= sizeY and z >= 1 and z <= sizeZ then
			local materialName = part:GetAttribute("Material")
			local occupancy = part:GetAttribute("Occupancy")
			if materialName and occupancy then
				local material = materials[materialName]
				materialsGrid[x][y][z] = material
				occupanciesGrid[x][y][z] = occupancy
			end
		end
		part:Destroy()
	end

	terrain:WriteVoxels(region, resolution, materialsGrid, occupanciesGrid)
	print("Terrain reconstruction complete.")
end

buttonParts.Click:Connect(convertToParts)
buttonTerrain.Click:Connect(convertToTerrain)

To use in your games just use the convertToTerrain() function.

Here’s the plugin:

Enjoy!

2 Likes