How can I transport part of a terrain between maps

I have an old map and a new map and I want to copy part of the old map’s terrain to the new map

Terrain save and load exists, but as far as I can tell, this only copies the entire map and there is no way to select only a designated area to copy/paste

1 Like

Related issues:

Terrain:CopyRegion and :PasteRegion exist, but they use Region3int16 which nobody seems to know how to use, and the only information i could find on it are people affirming that nobody knows how to use it. except this guy from 4 years ago

i made this plugin. it allows you to copy selected areas of terrain to other areas as well as copying selected areas to different experiences. someone will probably comment on this in 4 days revealing that I could’ve done this a much easier way, and I will be sad

local SES = game:GetService("ScriptEditorService")

local toolbar = plugin:CreateToolbar("Plugin Name")

local pluginButton = toolbar:CreateButton(
	"Terrain Copy Paste", --Text that will appear below button
	"Copy and paste terrain", --Text that will appear if you hover your mouse on button
	"rbxassetid://8740888472") --Button icon

local info = DockWidgetPluginGuiInfo.new(
	Enum.InitialDockState.Right, --From what side gui appears
	false, --Widget will be initially enabled
	false, --Don't overdrive previouse enabled state
	200, --default weight
	300, --default height
	150, --minimum weight (optional)
	150 --minimum height (optional)
)

-- Construct UI
local widget = plugin:CreateDockWidgetPluginGui("TestWidget", info)
widget.Title = "TestWidget" --Giving title to our widget gui

local CopyBtn = Instance.new("TextButton")
CopyBtn.BorderSizePixel = 0
CopyBtn.AnchorPoint = Vector2.new(0.5,0)
CopyBtn.Size = UDim2.fromScale(.6, .4)
CopyBtn.Position = UDim2.fromScale(.5, 0.05)
CopyBtn.SizeConstraint = Enum.SizeConstraint.RelativeYY
CopyBtn.Text = "Copy"
CopyBtn.TextScaled = true
CopyBtn.Parent = widget

local PasteBtn = Instance.new("TextButton")
PasteBtn.BorderSizePixel = 0
PasteBtn.AnchorPoint = Vector2.new(0.5,1)
PasteBtn.Size = UDim2.fromScale(.6, .4)
PasteBtn.Position = UDim2.fromScale(.5, 0.95)
PasteBtn.SizeConstraint = Enum.SizeConstraint.RelativeYY
PasteBtn.Text = "Paste"
PasteBtn.TextScaled = true
PasteBtn.Parent = widget

pluginButton.Click:Connect(function()
	widget.Enabled = not widget.Enabled
end)

local function findVolume()
	if not game.Workspace:FindFirstChild("VolumePart") then
		error("You need to create a part named \"VolumePart\" parented to the workspace which represents the area to be copied from or pasted into")
	end
	return game.Workspace:FindFirstChild("VolumePart")
end

local function toString(voxeldata)
	task.wait(.0005)
	local answer = "{\n"
	for key, value in pairs(voxeldata) do
		if key == "Size" then
			answer ..= "   Size = Vector3.new(" .. tostring(value) .. "),\n"
		else
			answer ..= `   [{key}]` .. " = {\n"
			for k, v in pairs(value) do
				answer ..= `      [{k}]` .. " = {\n"
				for kk, vv in pairs(v) do
					answer ..= `         [{kk}]` .. " = " .. tostring(vv) .. ",\n"
				end
				answer ..= "      },\n"
			end
			answer ..= "},\n"
		end
	end
	return answer .. "}"
end

CopyBtn.Activated:Connect(function()
	warn("Creating save data . . . Please wait until done!")
	local Part = findVolume()
	local w = Part.Size/2

	task.wait(0.02)
	local Materials, Occupancies = game.Workspace.Terrain:ReadVoxels(Region3.new(Part.Position - w, Part.Position + w), 4)
	
	local SaveScript = Instance.new("ModuleScript")
	SaveScript.Name = "TerrainData"
	
	local Materials = toString(Materials)
	print("... half way done")
	local Occupancies = toString(Occupancies)
	
	local Source = "local TerrainData = {}"
		.. "\nTerrainData.Materials = " .. Materials
		.. "\nTerrainData.Occupancies = " .. Occupancies
		.. "\nreturn TerrainData"
	SES:UpdateSourceAsync(SaveScript, function(oldContent)
		return Source
	end)
	
	SaveScript.Parent = Part
	warn("Done!")
end)

PasteBtn.Activated:Connect(function()
	local Part = findVolume()
	local w = Part.Size/2
	local SaveData = require(Part:FindFirstChild("TerrainData"))
	
	-- Read existing voxels
	local Materials, Occupancies = game.Workspace.Terrain:ReadVoxels(Region3.new(Part.Position - w, Part.Position + w), 4)
	
	SaveData.Materials.Size = Materials.Size
	SaveData.Occupancies.SIze = Occupancies.Size
	
	-- Ensure we have enough material and occupancies data
	local newSize = Materials.Size
	for x = 1, newSize.X do
		local row = SaveData.Materials[x]
		if row then
			for y = 1, newSize.Y do
				local col = SaveData.Materials[x][y]
				if col then
					for y = 1, newSize.Z do
						if not col[y] then
							col[y] = Enum.Material.Air
						end
					end
				else
					-- No SaveData.Materials[x][y]
					SaveData.Materials[x][y] = table.create(newSize.Z, Enum.Material.Air)
				end
			end
		else
			-- No SaveData.Materials[x]
			SaveData.Materials[x] = {}
			for y = 1, newSize.Y do
				SaveData.Materials[x][y] = table.create(newSize.Z, Enum.Material.Air)
			end
		end
	end
	
	local newSize = Occupancies.Size
	for x = 1, newSize.X do
		local row = SaveData.Occupancies[x]
		if row then
			for y = 1, newSize.Y do
				local col = SaveData.Occupancies[x][y]
				if col then
					for y = 1, newSize.Z do
						if not col[y] then
							col[y] = 0
						end
					end
				else
					-- No SaveData.Materials[x][y]
					SaveData.Occupancies[x][y] = table.create(newSize.Z, 0)
				end
			end
		else
			-- No SaveData.Materials[x]
			SaveData.Occupancies[x] = {}
			for y = 1, newSize.Y do
				SaveData.Occupancies[x][y] = table.create(newSize.Z, 0)
			end
		end
	end
	
	-- Ensure we don't have too many data
	if #SaveData.Materials > newSize.X then
		for x = #SaveData.Materials, newSize.X+1, -1 do
			table.remove(SaveData.Materials, x)
		end
	end
	for x = 1, #SaveData.Materials do
		local row = SaveData.Materials[x]
		if #row > newSize.Y then
			for y = #row, newSize.Y + 1, -1 do
				table.remove(row, y)
			end
		end
		for y = 1, #row do
			local column = row[y]
			if #column > newSize.Z then
				for z = #column, newSize.Z + 1, -1 do
					table.remove(column, z)
				end
			end
		end
	end
	
	if #SaveData.Occupancies > newSize.X then
		for x = #SaveData.Occupancies, newSize.X+1, -1 do
			table.remove(SaveData.Occupancies, x)
		end
	end
	for x = 1, #SaveData.Occupancies do
		local row = SaveData.Occupancies[x]
		if #row > newSize.Y then
			for y = #row, newSize.Y + 1, -1 do
				table.remove(row, y)
			end
		end
		for y = 1, #row do
			local column = row[y]
			if #column > newSize.Z then
				for z = #column, newSize.Z + 1, -1 do
					table.remove(column, z)
				end
			end
		end
	end
	
	game.Workspace.Terrain:WriteVoxels(Region3.new(Part.Position - w, Part.Position + w), 4, SaveData.Materials, SaveData.Occupancies)
end)

To use:

  1. Turn that code into a plugin. If you don’t know how: Create a script inside your experience. Copy and paste the above code into the script. Right click and select “Save as Local Plugin” and save. It will now appear as a plugin in your Plugins tab.
  2. Create a part in the world named “VolumePart”. This represents the area of terrain that you are going to copy; place/size it accordingly.
  3. Press the “Copy” button in the plugin. It will begin copying the terrain data. Don’t touch anything while it’s copying, but you’re free to tab out. WARNING: Large areas copy quite slowly because I don’t actually know what I’m doing
  4. The Paste button pastes the terrain into the VolumePart. You can move the VolumePart to paste the terrain somewhere else, or copy/paste the VolumePart to a new experience to paste the terrain there (the volume part has become parented to a module with the terrain data)
  5. profit

alternatively, you could find out how region3int16s work. and please tell me

ive never made a plugin before. i followed this guy’s guide

made it better. instructions print to console when you press copy

local SES = game:GetService("ScriptEditorService")

local toolbar = plugin:CreateToolbar("Plugin Name")

local pluginButton = toolbar:CreateButton(
	"Terrain Copy Paste", --Text that will appear below button
	"Copy and paste terrain", --Text that will appear if you hover your mouse on button
	"rbxassetid://8740888472") --Button icon

local info = DockWidgetPluginGuiInfo.new(
	Enum.InitialDockState.Right, --From what side gui appears
	false, --Widget will be initially enabled
	false, --Don't overdrive previouse enabled state
	200, --default weight
	300, --default height
	150, --minimum weight (optional)
	150 --minimum height (optional)
)

-- Construct UI
local widget = plugin:CreateDockWidgetPluginGui("TestWidget", info)
widget.Title = "TestWidget" --Giving title to our widget gui

local CopyBtn = Instance.new("TextButton")
CopyBtn.BorderSizePixel = 0
CopyBtn.AnchorPoint = Vector2.new(0.5,0)
CopyBtn.Size = UDim2.fromScale(.6, .4)
CopyBtn.Position = UDim2.fromScale(.5, 0.05)
CopyBtn.SizeConstraint = Enum.SizeConstraint.RelativeYY
CopyBtn.Text = "Copy"
CopyBtn.TextScaled = true
CopyBtn.Parent = widget

local PasteBtn = Instance.new("TextButton")
PasteBtn.BorderSizePixel = 0
PasteBtn.AnchorPoint = Vector2.new(0.5,1)
PasteBtn.Size = UDim2.fromScale(.6, .4)
PasteBtn.Position = UDim2.fromScale(.5, 0.95)
PasteBtn.SizeConstraint = Enum.SizeConstraint.RelativeYY
PasteBtn.Text = "Paste"
PasteBtn.TextScaled = true
PasteBtn.Parent = widget

pluginButton.Click:Connect(function()
	widget.Enabled = not widget.Enabled
end)

local function findVolume()
	if not game.Workspace:FindFirstChild("VolumePart") then
		error("You need to create a part named \"VolumePart\" parented to the workspace which represents the area to be copied from or pasted into")
	end
	return game.Workspace:FindFirstChild("VolumePart")
end

--[[local function toString(voxeldata)
	task.wait(.0005)
	local answer = "{\n"
	for key, value in pairs(voxeldata) do
		if key == "Size" then
			answer ..= "   Size = Vector3.new(" .. tostring(value) .. "),\n"
		else
			answer ..= `   [{key}]` .. " = {\n"
			for k, v in pairs(value) do
				answer ..= `      [{k}]` .. " = {\n"
				for kk, vv in pairs(v) do
					answer ..= `         [{kk}]` .. " = " .. tostring(vv) .. ",\n"
				end
				answer ..= "      },\n"
			end
			answer ..= "},\n"
		end
	end
	return answer .. "}"
end]]

local http = game:GetService("HttpService")
local function toString(voxelData)
	for x, row in pairs(voxelData) do
		if x ~= "Size" then
			for y, column in pairs (row) do
				for z, entry in pairs(column) do
					if typeof(entry) == "EnumItem" then
						column[z] = entry.Name
					end
				end
			end
		end
	end
	return http:JSONEncode(voxelData)
end

local function createSaveForVolume(Part)
	local w = Part.Size/2

	task.wait(0.02)
	local Materials, Occupancies = game.Workspace.Terrain:ReadVoxels(Region3.new(Part.Position - w, Part.Position + w), 4)

	local SaveScript = Instance.new("ModuleScript")
	SaveScript.Name = "TerrainData"

	local Materials = toString(Materials)
	local Occupancies = toString(Occupancies)

	local Source = "local TerrainData = {}"
		.. "\nTerrainData.Materials = " .. "`" .. Materials .. "`"
		.. `\nTerrainData.Occupancies = "{Occupancies}"`
		.. "\nreturn TerrainData"
	SES:UpdateSourceAsync(SaveScript, function(oldContent)
		return Source
	end)

	SaveScript.Parent = Part
end

CopyBtn.Activated:Connect(function()
	if not game.Workspace:FindFirstChild("TerrainCopyVolumes") then
		warn("You need to set up your Workspace"
			.."\n1. Create a folder in the Workspace named \"TerrainCopyVolumes\". This has now been created for you"
			.."\n2. Create Parts and parent those Parts to TerrainCopyVolumes. These parts represent the space(s) being copied."
			.."\n    Leave the parts in their original orientation. This does not support rotated parts."
			.."\n    Be careful not to make them too big! Trying to copy massive spaces can lag out your machine or even crash Studio."
			.."\n    If you're experimenting with large volumes, save your file first!"
			.."\n    If you need to copy a massive area, divide the area into multiple parts parented to TerrainCopyVolumes"
			.."\n3. Move the parts (still within the folder) to wherever you want to paste the copied terrain to"
			.."\n    To paste the terrain in a different experience, copy the TerrainCopyVolumes folder, paste it to the new experience, and then use the Paste button in the plugin"
			.."\n    To paste to a different area in the same map, just move the parts and click paste"
			.."\n4. Peace and love on planet earth"
		)
		
		local F = Instance.new("Folder")
		F.Name = "TerrainCopyVolumes"
		F.Parent = game.Workspace
		print("Folder: ", F)
		return
	end
	
	warn("Creating save data . . . Please wait until done!")
	local count = #game.Workspace.TerrainCopyVolumes:GetChildren()
	
	if count == 0 then
		warn("No parts were found..."
			.."\n- Did you make sure the parts you created are parented to TerrainCopyVolumes?"
			.."\n- If so, do you accidentally have multiple folders named TerrainCopyVolumes?"
		)
		return
	end
	
	for i, c in pairs(game.Workspace.TerrainCopyVolumes:GetChildren()) do
		createSaveForVolume(c)
		print(`... finished {i}/{count}`)
	end
	warn("Done! Peace and love on planet earth")
end)

local function loadSaveDataOnPart(Part)
	local w = Part.Size/2
	local SaveData = require(Part:FindFirstChild("TerrainData"))

	-- Read existing voxels
	local Materials, Occupancies = game.Workspace.Terrain:ReadVoxels(Region3.new(Part.Position - w, Part.Position + w), 4)
	
	local SaveMaterials = http:JSONDecode(SaveData.Materials)
	local SaveOccupancies = http:JSONDecode(SaveData.Occupancies)
	
	SaveMaterials.Size = Materials.Size
	SaveOccupancies.Size = Occupancies.Size
	
	-- Ensure we have enough material and occupancies data
	local newSize = Materials.Size
	for x = 1, newSize.X do
		local row = SaveMaterials[x]
		if row then
			for y = 1, newSize.Y do
				local col = SaveMaterials[x][y]
				if col then
					for y = 1, newSize.Z do
						if col[y] then
							col[y] = Enum.Material[col[y]]
						else
							col[y] = Enum.Material.Air
						end
					end
				else
					-- No SaveMaterials[x][y]
					SaveMaterials[x][y] = table.create(newSize.Z, Enum.Material.Air)
				end
			end
		else
			-- No SaveMaterials[x]
			SaveMaterials[x] = {}
			for y = 1, newSize.Y do
				SaveMaterials[x][y] = table.create(newSize.Z, Enum.Material.Air)
			end
		end
	end

	local newSize = Occupancies.Size
	for x = 1, newSize.X do
		local row = SaveOccupancies[x]
		if row then
			for y = 1, newSize.Y do
				local col = SaveOccupancies[x][y]
				if col then
					for y = 1, newSize.Z do
						if not col[y] then
							col[y] = 0
						end
					end
				else
					-- No SaveMaterials[x][y]
					SaveOccupancies[x][y] = table.create(newSize.Z, 0)
				end
			end
		else
			-- No SaveMaterials[x]
			SaveOccupancies[x] = {}
			for y = 1, newSize.Y do
				SaveOccupancies[x][y] = table.create(newSize.Z, 0)
			end
		end
	end

	-- Ensure we don't have too many data
	if #SaveMaterials > newSize.X then
		for x = #SaveMaterials, newSize.X+1, -1 do
			table.remove(SaveMaterials, x)
		end
	end
	for x = 1, #SaveMaterials do
		local row = SaveMaterials[x]
		if #row > newSize.Y then
			for y = #row, newSize.Y + 1, -1 do
				table.remove(row, y)
			end
		end
		for y = 1, #row do
			local column = row[y]
			if #column > newSize.Z then
				for z = #column, newSize.Z + 1, -1 do
					table.remove(column, z)
				end
			end
		end
	end

	if #SaveOccupancies > newSize.X then
		for x = #SaveOccupancies, newSize.X+1, -1 do
			table.remove(SaveOccupancies, x)
		end
	end
	for x = 1, #SaveOccupancies do
		local row = SaveOccupancies[x]
		if #row > newSize.Y then
			for y = #row, newSize.Y + 1, -1 do
				table.remove(row, y)
			end
		end
		for y = 1, #row do
			local column = row[y]
			if #column > newSize.Z then
				for z = #column, newSize.Z + 1, -1 do
					table.remove(column, z)
				end
			end
		end
	end
	
	game.Workspace.Terrain:WriteVoxels(Region3.new(Part.Position - w, Part.Position + w), 4, SaveMaterials, SaveOccupancies)
end

PasteBtn.Activated:Connect(function()
	for i, c in pairs(game.Workspace.TerrainCopyVolumes:GetChildren()) do
		loadSaveDataOnPart(c)
	end
end)

I don’t get the point of this post, it seems you have achieved what you are looking for and you are just showcasing your plugin?

You can read more on Region3int16 here which has very useful hyperlinks: Region3int16 | Documentation - Roblox Creator Hub

If you want to transfer Terrain between experiences, you can export the TerrainRegion from Terrain:CopyRegion() to your desktop and import + load it to your other game.

I had a problem, fixed it, and am sharing how I fixed it in case anyone struggles with someone similar

I did try that, but the nature of translating an area in the workspace to Region3int16s is unclear. When I tried to use :CopyRegion and :PasteRegion using Region3int16s that I calculated based off a part I sized/positioned in the workspace to try to copy/paste that area, nothing happened. I tested with all sorts of different numbers, but nothing happened. I can’t figure out how to use Region3int16s to define an area that I want to copy and paste into. I appreciate your feedback

I see. Those Terrain region methods operates on a 4x4x4 resolution grid, so you would have to convert your numbers to that grid.

--!strict

local Terrain = workspace.Terrain

local function copyRegion(position: Vector3, size: Vector3): TerrainRegion
	local minVector3: Vector3 = position - (size / 2)
	local maxVector3: Vector3 = position + (size / 2)
	
	local minVector3int16: Vector3int16 = Vector3int16.new(
		math.floor(minVector3.X / 4),
		math.floor(minVector3.Y / 4),
		math.floor(minVector3.Z / 4)
	)
	
	local maxVector3int16: Vector3int16 = Vector3int16.new(
		math.floor(maxVector3.X / 4),
		math.floor(maxVector3.Y / 4),
		math.floor(maxVector3.Z / 4)
	)
	
	local region3int16: Region3int16 = Region3int16.new(minVector3int16, maxVector3int16)
	
	return Terrain:CopyRegion(region3int16)
end

local function pasteRegion(terrainRegion: TerrainRegion, corner: Vector3, pasteEmptyCells: boolean): ()
	Terrain:PasteRegion(terrainRegion, Vector3int16.new(
		math.floor(corner.X / 4),
		math.floor(corner.Y / 4),
		math.floor(corner.Z / 4)
	), pasteEmptyCells)
end

local myRegion: TerrainRegion = copyRegion(workspace.Region.Position, workspace.Region.Size)

task.wait(1)

Terrain:Clear()

task.wait(1)

pasteRegion(myRegion, workspace.Region.Position - (workspace.Region.Size / 2), false)