Join the RoClicks Discord
RoClicks

ZFightSweep.luau

Luau, 144 lines from Building overview

Download ZFightSweep.luau
--!strict
-- ZFightSweep: find (and optionally fix) faces that will z-fight inside a model or a built scene.
--
-- Two box faces fight when their outward normals agree, their planes are < TH apart, and their rectangles
-- overlap by more than MARGIN on both in-plane axes. Edge contact is fine. Only Block parts are checked
-- (MeshParts, unions, balls and SpecialMesh parts don't have box faces).
-- A pair is "visible" when the faces differ in colour/material/variant, or either carries a stud
-- MaterialVariant (texture origin is per part, so even identical studded parts disagree).
--
-- Fix: grow the part with the SMALLER face outward along that face normal until the planes are EPS apart
-- (its opposite face stays put). Run sweeps in edit mode; long loops on a playtest client can kill it.
--
--   local pairs_ = ZFightSweep.sweep(model)            -- read only
--   local fixed = ZFightSweep.fix(model)               -- returns count fixed
--
-- This is an O(n^2)-per-bin sketch: fine for templates (hundreds of parts) and scenes up to ~20k parts
-- with the grid binning. For bigger worlds, sweep per chunk.

local ZFightSweep = {}
ZFightSweep.TH = 0.005 -- planes closer than this fight
ZFightSweep.EPS = 0.01 -- fixed planes end up this far apart
ZFightSweep.MARGIN = 0.004 -- overlap must exceed this to count
ZFightSweep.BIN = 0.05 -- plane-offset bin size

type Face = {
	part: BasePart,
	n: Vector3, -- outward world normal
	c: Vector3, -- centre
	u: Vector3, v: Vector3, -- in-plane axes
	hu: number, hv: number, -- half sizes
	axis: number, sign: number,
	area: number,
}

local function eligible(p: BasePart): boolean
	if p.Transparency >= 0.98 then return false end
	if not p:IsA("Part") or p.Shape ~= Enum.PartType.Block then return false end
	if p:FindFirstChildWhichIsA("DataModelMesh") then return false end
	return true
end

local function faces(p: BasePart, out: { Face })
	local cf, s = p.CFrame, p.Size
	local ax = { cf.RightVector, cf.UpVector, -cf.LookVector }
	local half = { s.X / 2, s.Y / 2, s.Z / 2 }
	for a = 1, 3 do
		local b, c = a % 3 + 1, (a + 1) % 3 + 1
		for _, sg in { 1, -1 } do
			table.insert(out, {
				part = p, n = ax[a] * sg, c = cf.Position + ax[a] * sg * half[a],
				u = ax[b], v = ax[c], hu = half[b], hv = half[c],
				axis = a, sign = sg, area = 4 * half[b] * half[c],
			})
		end
	end
end

local function looksDifferent(a: BasePart, b: BasePart): boolean
	return a.Color ~= b.Color or a.Material ~= b.Material or a.MaterialVariant ~= b.MaterialVariant
		or a.MaterialVariant ~= "" or b.MaterialVariant ~= ""
end

-- overlap of face b's rectangle projected on face a's axes (both directions must exceed MARGIN)
local function overlaps(a: Face, b: Face): boolean
	local d = b.c - a.c
	local bu = math.abs(b.u:Dot(a.u)) * b.hu + math.abs(b.v:Dot(a.u)) * b.hv
	local bv = math.abs(b.u:Dot(a.v)) * b.hu + math.abs(b.v:Dot(a.v)) * b.hv
	local ou = a.hu + bu - math.abs(d:Dot(a.u))
	local ov = a.hv + bv - math.abs(d:Dot(a.v))
	return ou > ZFightSweep.MARGIN and ov > ZFightSweep.MARGIN
end

export type Pair = { a: Face, b: Face, sep: number, visible: boolean }

function ZFightSweep.sweep(root: Instance): { Pair }
	local all: { Face } = {}
	for _, d in root:GetDescendants() do
		if d:IsA("BasePart") and eligible(d) then
			faces(d, all)
		end
	end
	-- bin by (rounded normal, plane offset) so only candidates are compared
	local bins: { [string]: { Face } } = {}
	for _, f in all do
		local n = f.n
		local key = string.format("%d,%d,%d|%d", math.round(n.X * 100), math.round(n.Y * 100),
			math.round(n.Z * 100), math.floor(f.c:Dot(n) / ZFightSweep.BIN))
		local list = bins[key]
		if not list then list = {}; bins[key] = list end
		table.insert(list, f)
	end
	local found: { Pair } = {}
	local seen: { [string]: boolean } = {}
	for key, list in bins do
		-- also compare against the neighbouring offset bin (planes straddling a bin edge)
		local head, off = key:match("^(.-)|(%-?%d+)$")
		local nxt = bins[`{head}|{tonumber(off) :: number + 1}`] or {}
		local cands = table.clone(list)
		table.move(nxt, 1, #nxt, #cands + 1, cands)
		for i, a in list do
			for j, b in cands do
				if (j > i or j > #list) and a.part ~= b.part and a.n:Dot(b.n) > 0.9995 then
					local sep = math.abs((b.c - a.c):Dot(a.n))
					if sep < ZFightSweep.TH and overlaps(a, b) then
						local id = `{a.part:GetDebugId()}{a.axis}{a.sign}/{b.part:GetDebugId()}{b.axis}{b.sign}`
						if not seen[id] then
							seen[id] = true
							table.insert(found, { a = a, b = b, sep = sep, visible = looksDifferent(a.part, b.part) })
						end
					end
				end
			end
		end
	end
	return found
end

-- Grow the smaller face's part outward so the planes end EPS apart. Jointed (unanchored) parts are skipped:
-- moving them would break weld offsets; report and fix those by hand.
function ZFightSweep.fix(root: Instance): number
	local fixed = 0
	for _, pr in ZFightSweep.sweep(root) do
		if pr.visible then
			local f = if pr.a.area <= pr.b.area then pr.a else pr.b
			local other = if f == pr.a then pr.b else pr.a
			local p = f.part
			if p.Anchored then
				-- distance f's plane must move outward to sit EPS beyond the other plane
				local grow = ZFightSweep.EPS - (f.c - other.c):Dot(f.n)
				if grow > 0 then
					local axisSize = { p.Size.X, p.Size.Y, p.Size.Z }
					axisSize[f.axis] += grow
					p.Size = Vector3.new(axisSize[1], axisSize[2], axisSize[3])
					p.CFrame = p.CFrame + f.n * (grow / 2)
					fixed += 1
				end
			end
		end
	end
	return fixed
end

return ZFightSweep