Remove All Water in Terrain

Made a fun little script because somebody I know wanted a way to get rid of all of the water in the game. It works fast enough, although due to size constraints it can take a fair amount of time for large areas of terrain (using Roblox’s terrain generation tool at a size of 512 takes around 2 minutes to remove all water from, using a size of 3000x2000x3000 studs). Only requires setting the (approximate) center of terrain and an approximate size the terrain encompasses. So it’s technically not all water in the game, but you can set the size of the area you want to modify and go from there.

It wasn’t too complicated, but the Terrain API isn’t really helpful for doing stuff like this.

image

Could also easily modify the script so that, rather than replacing water with air, it replaces any material you want with another material. With a little bit more work, could make it to, say, lower the level of water by 1 or something like that.

Code
local Terrain = workspace.Terrain
local processBlockSize = 640
local WATER = Enum.Material.Water
local AIR = Enum.Material.Air
local floor = math.floor

function isInRegion3(region, point)
	--function by buildthomas
	--https://devforum.roblox.com/t/see-if-vector3-is-in-region3/118457/7
    local relative = (point - region.CFrame.p) / region.Size
    return -0.5 <= relative.X and relative.X <= 0.5
       and -0.5 <= relative.Y and relative.Y <= 0.5
       and -0.5 <= relative.Z and relative.Z <= 0.5
end

local function round(num)
	return math.floor(num + .5)
end

local function getAlignedPosition(pos)
	--Function from https://wiki.roblox.com/index.php?title=Smooth_terrain#Flood_Fill
	local x = round(pos.X)
	x = x - x%4 + 2
	local y = round(pos.Y)
	y = y - y%4 + 2
	local z = round(pos.Z)
	z = z - z%4 + 2
	
	return Vector3.new(x,y,z)
end

local function comma_value(n) --http://lua-users.org/wiki/FormattingNumbers
	local left,num,right = string.match(n,'^([^%d]*%d)(%d*)(.-)$')
	return left..(num:reverse():gsub('(%d%d%d)','%1,'):reverse())..right
end

local function removeWater(voxelPos, worldSize)
	local functionStart = tick()
	voxelPos = getAlignedPosition(voxelPos)
	local material = nil
	local occupancy = nil
	
	local boundaryRegion = Region3.new(voxelPos-worldSize/2, voxelPos+worldSize/2)
	boundaryRegion:ExpandToGrid(4)
	local minPoint = boundaryRegion.CFrame.p-boundaryRegion.Size/2
	print("Starting")
	local partsProcessed = 0
	
	local regions = {} --Stores all of the regions to read/write to
	
	local start = getAlignedPosition(minPoint)
	local xOffset = 0
	local yOffset = 0
	local zOffset = 0
	local done = false
	repeat
		local startPos = start+Vector3.new(xOffset,yOffset,zOffset)
		local size = boundaryRegion.CFrame.p+boundaryRegion.Size/2-startPos
		local x,y,z = size.X, size.Y, size.Z
		if x > processBlockSize then
			x = processBlockSize
		end
		if y > processBlockSize then
			y = processBlockSize
		end
		if z > processBlockSize then
			z = processBlockSize
		end
		size = Vector3.new(x,y,z)
		local region = Region3.new(startPos, startPos+size)
		region = region:ExpandToGrid(4)
		table.insert(regions, region)
		zOffset = zOffset + processBlockSize
		if zOffset >= boundaryRegion.Size.Z then
			zOffset = 0
			yOffset = yOffset + processBlockSize
		end
		if yOffset >= boundaryRegion.Size.Y then
			yOffset = 0
			xOffset = xOffset + processBlockSize
		end
		if xOffset >= boundaryRegion.Size.X then
			done = true
		end
	until done
	
	print("Writing to ", #regions, " regions!")
	print("Removing water...")
	local totalRemoved = 0
	local totalProcessed = 0
	local totalVolume = worldSize.X*worldSize.Y*worldSize.Z
	local changed,materials,occupancy,size,matsX,occX,matsY,occY,p
	for index, region in pairs(regions) do
		--[[
		--Uncomment this if you want parts to display where each chunk is
		p = Instance.new("Part")
		p.Anchored = true
		p.CanCollide = false
		p.Transparency = 0.9
		--p.BrickColor = BrickColor.new(index%255)
		p.Parent = workspace
		p.Size = region.Size
		p.CFrame = region.CFrame
		]]
		changed = 0
		materials, occupancy = Terrain:ReadVoxels(region, 4)
		size = materials.Size
		for x = 1, size.X do
			matsX = materials[x] --Creating variables to reduce amount of searching
			occX = occupancy[x]
			for y = 1, size.Y do
				matsY = matsX[y]
				occY = occX[y]
				for z = 1, size.Z do
					totalProcessed = totalProcessed + 1
					if matsY[z] == WATER then
						matsY[z] = AIR
						occY[z] = 0
						changed = changed + 1
					end
				end
			end
			--[[
			--Use this and comment out the wait below if you want constant progress updates
			if x%10 == 0 then
				print((floor((index-1+x/size.X)/#regions*1000+0.5)/10).."% complete!")
				wait(0)
			end
			]]
		end
		
		if changed > 0 then --No need to write if there is no changes
			Terrain:WriteVoxels(region, 4, materials, occupancy)
		end
		--Comment the following block out if you use the above wait ability.
		if index%4 == 0 then
			print((floor(index/#regions*1000+0.5)/10).."% complete!")
			wait(0)
		end
		totalRemoved = totalRemoved + changed
	end
	
	print("Total time elapsed: ", (tick()-functionStart), " seconds!")
	print("Total water blocks removed: ", totalRemoved)
	print("Total cells processed: ", comma_value(totalProcessed))
	print("Region/Chunks: ", #regions)
	print("Total Volume: ", totalVolume, "cubic studs")
	print("Done processing")
end

--Use this function as pass it the origin the removing should take place, and the size
--of the area to check for water.
--removeWater(origin, size)
removeWater(Vector3.new(0,0,0), Vector3.new(3000,2000,3000))

Also attaching the place file used (and hey, rounds to 666 KB, wasn’t even planning for that). I was planning on making it a plugin, but I felt it’d take too much work learning how to use plugins and implement the various settings (origin, size, which material to which, etc). But if anybody was willing, that’d be sweet.

clearTerrainWater.rbxl (665.7 KB)

@Elmuowo
I don’t suppose you still need something like this? :stuck_out_tongue:

34 Likes

I was looking for this exact thing. Thank you very much for sharing this.

I feel like there might be a way to use multiple threads in order to speed up this process. Region limitations can be overcome with multiple regions, can’t they?

You might find this of use:

I work with terrain daily, I wrote myself this script to swap water and air. All you gotta do is scale a part to the size of the area you want to clear, and then paste this in the command bar (with variable PartToFill set to the part you’re using)

It pretty much works instantly so it will probably save you a lot of time. I use this when working around coastlines and stuff where water would get in the way

local PartToFill = game.Workspace.Part -- Change this to your part

function PartToRegion3(part)
	local r = Region3.new(part.Position - (part.Size*.5),part.Position + (part.Size*.5))
	return r:ExpandToGrid(4)
end

function Sponge(region)
	local materials,occupancy = workspace.Terrain:ReadVoxels(region,4)
	for x = 1,materials.Size.X do
		for y = 1,materials.Size.Y do
			for z = 1,materials.Size.Z do
				if materials[x][y][z] == Enum.Material.Water then
					materials[x][y][z] = Enum.Material.Air
				end
			end
		end
	end
	workspace.Terrain:WriteVoxels(region,4,materials,occupancy)
end

Sponge(PartToRegion3(PartToFill))
16 Likes

Makes me feel like there should be a C++ function where you can just supply a parse function and C++ will do the all the heavy lifting.

Very cool my dude.

I’m tempted to invert this script to replace all air with water. :thinking:

5 Likes

wouldnt this destroy your game

Not if you’re a fish

8 Likes

3 years late… but I’ll take it!!

2 Likes

Just what I needed. Big thanks.