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!