Untitled

mail@pastecode.io avatarunknown
lua
a month ago
12 kB
3
Indexable
Never
local TerrainGen = {}

local function Round(num, grid, offset)
	return math.floor(num / grid + (offset or 0.5)) * grid
end
local function Round3D(vec,grid)
	return Vector3.new(Round(vec.x,grid,0),Round(vec.y,grid,0),Round(vec.z,grid,0))
end

local function FillWedge(wedgeCFrame, wedgeSize, material)
	local terrain = workspace.Terrain
	local Zlen, Ylen = wedgeSize.Z, wedgeSize.Y
	local longerSide, shorterSide, isZlonger
	if Zlen > Ylen then
		longerSide, shorterSide, isZlonger = Zlen, Ylen, true
	else
		longerSide, shorterSide, isZlonger = Ylen, Zlen, false
	end

	local closestIntDivisor = math.max(1, math.floor(shorterSide/3))
	local closestQuotient = shorterSide/closestIntDivisor
	local scaledLength = closestQuotient*longerSide/shorterSide    
	local cornerPos = Vector3.new(0, -Ylen, Zlen)/2

	for i = 1, closestIntDivisor - 1 do
		local longest_baselen = (closestIntDivisor-i)*scaledLength
		local size, cf = Vector3.new(math.max(3, wedgeSize.X), closestQuotient, longest_baselen)
		if isZlonger then
			cf = wedgeCFrame:toWorldSpace(CFrame.new(cornerPos) + Vector3.new(0, (i-0.5)*closestQuotient, -longest_baselen/2))
		else
			cf = wedgeCFrame:toWorldSpace(CFrame.Angles(math.pi/2, 0, 0) + cornerPos + Vector3.new(0, longest_baselen/2, -(i-0.5)*closestQuotient))
		end
		terrain:FillBlock(cf, size, material)
	end

	local diagSize = Vector3.new(math.max(3, wedgeSize.X), closestQuotient*scaledLength/math.sqrt(closestQuotient^2 + scaledLength^2), math.sqrt(Zlen^2 + Ylen^2)) --Vector3.new(3, 3, math.sqrt(Zlen^2 + Ylen^2))
	local rv, bv = wedgeCFrame.RightVector, -(Zlen*wedgeCFrame.LookVector - Ylen*wedgeCFrame.UpVector).Unit
	local uv = bv:Cross(rv).Unit
	local diagPos = wedgeCFrame.p - uv*diagSize.Y/2
	local diagCf = CFrame.fromMatrix(diagPos, rv, uv, bv)
	terrain:FillBlock(diagCf, diagSize, material)
end




function TerrainGen:Do(replaceMaterials, triangles,fillMaterial)
	-- Variables storing initial configurations
	local triangleChunks = {}
	local triangleChunkSize = 16
	local surfaceThickness = 20
	local minX, minY, minZ = math.huge, math.huge, math.huge
	local maxX, maxY, maxZ = -math.huge, -math.huge, -math.huge
	local startTime = os.clock()
	
	-- Constants for the calculation
	local runDuration = 0.05
	local runUntil = os.clock() + runDuration
				
	local t0 = os.clock()
	-- Looping over each triangle for calculation
	for _,tri in triangles do
		local triMinX, triMinY, triMinZ = math.huge, math.huge, math.huge
		local triMaxX, triMaxY, triMaxZ = -math.huge, -math.huge, -math.huge

		for _,vert in tri do
			-- Skipping iterations for instances of Color3 or EnumItem
			if typeof(vert) == "Color3" or typeof(vert) == "EnumItem" then
				continue
			end
			-- Finding minimum and maximum coordinates of triangles
			triMinX = math.min(triMinX, vert.X)
			triMinY = math.min(triMinY, vert.Y)
			triMinZ = math.min(triMinZ, vert.Z)
			triMaxX = math.max(triMaxX, vert.X)
			triMaxY = math.max(triMaxY, vert.Y)
			triMaxZ = math.max(triMaxZ, vert.Z)
		end
			
			
		-- Calculating chunk minimum and maximum
		local chunkMin = Round3D(Vector3.new(triMinX, 0, triMinZ), triangleChunkSize)
		local chunkMax = Round3D(Vector3.new(triMaxX, 0, triMaxZ), triangleChunkSize)

		-- Triangle vertices
		local a, b, c = tri[1], tri[2], tri[3]

		-- Inverse determinant for intersecting lines within the triangle
		local invDet = 1 / ((b.Z - c.Z) * (a.X - c.X) + (c.X - b.X) * (a.Z - c.Z))
		-- Triangle center and radius calculation
		local center = (a + b + c) / 3
		tri.invDet = invDet
		tri.center = center
		tri.radius = math.max((a - center).Magnitude, (b - center).Magnitude, (c - center).Magnitude) + surfaceThickness + 3.464
			
			
		-- Iterating over x and z coordinates to update chunks
		for x = chunkMin.X, chunkMax.X, triangleChunkSize do
			for z = chunkMin.Z, chunkMax.Z, triangleChunkSize do
				local key = Vector3.new(x, 0, z)
				local chunk = triangleChunks[key]
				-- Creating a new chunk if not existing
				if not chunk then
					chunk = {}
					triangleChunks[key] = chunk
				end
				-- Inserting triangle into chunk
				table.insert(chunk, tri)
			end
		end

		-- Updating minimum and maximum coordinates
		minX = math.min(triMinX, minX)
		minY = math.min(triMinY, minY)
		minZ = math.min(triMinZ, minZ)
		maxX = math.max(triMaxX, maxX)
		maxY = math.max(triMaxY, maxY)
		maxZ = math.max(triMaxZ, maxZ)
	end
		
	print(string.format("phase %f took %.2f seconds",1,os.clock()-t0))
	
	
	-- Updating the range of the terrain
	local min = Round3D(Vector3.new(minX, minY, minZ), 4)
	local max = Round3D(Vector3.new(maxX, maxY, maxZ), 4)


	-- Table storing 'hit' calculations
	local hits = {}

	-- Defining bulk size
	local bulkSizeStuds = 64 
	local bulkSize = Vector3.new(bulkSizeStuds / 4, (max.Y - min.Y) / 4 + 1, bulkSizeStuds / 4)

	local materials, occupancy
	
	local t0 = os.clock()
	-- Checking if materials need replacement
	if replaceMaterials then
		materials = table.create(bulkSize.X)
		occupancy = table.create(bulkSize.Y)
		-- Creating 3D array for material and occupancy values
		for x = 1, bulkSize.X do
			local matsX = table.create(bulkSize.Y)
			local occuX = table.create(bulkSize.Y)

			materials[x] = matsX
			occupancy[x] = occuX

			for y = 1, bulkSize.Y do
				matsX[y] = table.create(bulkSize.Z, fillMaterial)
				occuX[y] = table.create(bulkSize.Z, 0)
			end
		end
	end
	print(string.format("phase %f took %.2f seconds",2,os.clock()-t0))
	
	t0 = os.clock()
	-- Reading through existing voxels and altering them as per data
	for x0 = min.X, max.X, bulkSizeStuds do
		for z0 = min.Z, max.Z, bulkSizeStuds do
			local region = Region3.new(
				Vector3.new(x0, min.Y, z0),
				Vector3.new(x0 + bulkSizeStuds, max.Y + 4, z0 + bulkSizeStuds)
			)

			-- Getting existing voxels if not replacing Materials
			if not replaceMaterials then
				materials, occupancy = workspace.Terrain:ReadVoxels(region, 4)
			end

			for x1 = 0, bulkSizeStuds - 1, 4 do
				local occuX = occupancy[x1 / 4 + 1]
				local matsX = materials[x1 / 4 + 1]

				for z1 = 0, bulkSizeStuds - 1, 4 do
					local bulkZ = z1 / 4 + 1

					local point = Vector3.new(x0 + x1 + 2, 0, z0 + z1 + 2)
					local chunk = triangleChunks[Round3D(Vector3.new(point.X, 0, point.Z), triangleChunkSize)]

					if replaceMaterials then
						for i = 1, bulkSize.Y do
							matsX[i][bulkZ] = fillMaterial
						end
					end

					-- Calculations for managing chunk heights, bulkCursor and occupancy within the chunk
					-- Carries out a number of computations within each chunk
					if chunk then
						for _,tri in chunk do
							local a, b, c, invDet = tri[1], tri[2], tri[3], tri.invDet
							local v = ((b.Z - c.Z) * (point.X - c.X) + (c.X - b.X) * (point.Z - c.Z)) * invDet

							-- Checks and calculates combination of v and w for intersection within triangle
							if v >= 0 and v <= 1 then
								local w = ((c.Z - a.Z) * (point.X - c.X) + (a.X - c.X) * (point.Z - c.Z)) * invDet

								if w >= 0 and w <= 1 and v + w <= 1 then
									tri.hitHeight = a.Y * v + b.Y * w + c.Y * (1 - v - w)
									tri.hitSolid = (b - a):Cross(c - a).Y > 0
									table.insert(hits, tri)
								end
							end

							-- Calculations for managing materials and occupancy in the cases of triangular intersection and areas within the triangular radius
							local center = tri.center
							local radius = tri.radius

							local dist = ((center.X - point.X)^2 + (center.Z - point.Z)^2)^0.5

							-- More checks and calculations in each chunk - these ones maintain material continuity around the triangle
							if dist < radius then
								local height = radius * (1 - (dist / radius)^2)^0.5
								local material = tri[4]

								local from = math.max(min.Y, center.Y - height)
								local to = math.min(max.Y, center.Y + height)

								for y = from - from % 4, to, 4 do
									matsX[(y - min.Y) / 4 + 1][bulkZ] = material
								end
							end
						end

						-- Sorts the hits by ascending height
						table.sort(hits, function(a, b) return a.hitHeight < b.hitHeight end)
					end

					local height = max.Y
					local solid = false

					local bulkCursor = bulkSize.Y
					local skipNext = false
					local solid = false

					-- Calculations in the chunk that inform the occupancy and material arrays
					for i = #hits, 0, -1 do
						if skipNext then skipNext = false continue end
						local tri = hits[i]

						if tri and tri.hitSolid == solid then
							if not solid and i > 1 and hits[i - 1].hitSolid and hits[i - 1].hitHeight > tri.hitHeight - 1 then
								skipNext = true
							end
							continue
						end

						local newHeight = if i == 0 then min.Y else tri.hitHeight
						local cursor = (newHeight - newHeight % 4 - min.Y) / 4 + 1

						if solid then
							for i = cursor, bulkCursor do
								occuX[i][bulkZ] = 1
							end
						else
							if replaceMaterials then
								for i = cursor, bulkCursor do
									occuX[i][bulkZ] = 0
								end
							end
						end

						bulkCursor = cursor - 1
						solid = not solid
					end

					-- Clearing the hits array for the next chunk
					table.clear(hits)
				end
			end

			-- Writing the prepared voxel data to the workspace terrain, making the calculated changes
			workspace.Terrain:WriteVoxels(region, 4, materials, occupancy)

			-- The terrain will only be written every 0.05 seconds - if the time exceeds this, we will let non-GUI threads run
			if os.clock() >= runUntil then
				task.wait()
				runUntil = os.clock() + runDuration
			end
		end
	end
	print(string.format("phase %f took %.2f seconds",3,os.clock()-t0))
	
	
	-- We've finished the voxel adjustments, now we can print a message saying the adjustments have been made
	local midTime1 = os.clock()

	-- Repeating the process for writing the modified voxel data to the workspace terrain, this time handling the rendering of each individual triangle
	runUntil = os.clock() + runDuration

	for _,tri in triangles do
		local a, b, c,material = unpack(tri)

		-- The following code block comes from another function, and it finds parameters to render each triangle - these parameters are derived from the triangle vertices
						
		local ba = b - a
		local ca = c - a

		local dir, mag = ca.Unit, ca.Magnitude
		local dist = dir:Dot(ba)

		if dist < 0 then
			a, b = b, a

			ba, ca = b - a, c - a
			dir, mag = ca.Unit, ca.Magnitude
			dist = dir:Dot(ba)
		elseif dist > mag then
			b, c = c, b

			ba, ca = ca, ba
			dir, mag = ca.Unit, ca.Magnitude
			dist = dir:Dot(ba)
		end

		local perp = ca:Cross(ba).Unit
		local up = perp:Cross(dir)
		local h = up:Dot(ba)

		local cframe1 = CFrame.fromMatrix((a + b) / 2, -perp, up)
		local size1 = Vector3.new(0, h, dist)

		local cframe2 = CFrame.fromMatrix((b + c) / 2, perp, up)
		local size2 = Vector3.new(0, h, mag - dist)
		--

		-- If any interface of the triangles has size, fill it with solid parts according its material
		local padding = 16
		if size1.Z > 0.001 then FillWedge(cframe1, size1 + Vector3.one * padding, material) end
		if size2.Z > 0.001 then FillWedge(cframe2, size2 + Vector3.one * padding, material) end

		-- Again, write to the terrain every 0.05 seconds
		if os.clock() >= runUntil then
			task.wait()
			runUntil = os.clock() + runDuration
		end
	end

	-- All operations have completed - print out a message detailing how long each phase took.
	local endTime = os.clock()

	print(string.format("DONE IN %.2f SECONDS (FILL: %.2fs, TRIANGLES: %.2fs)", endTime - startTime, midTime1 - startTime, endTime - midTime1))
end

return TerrainGen