Union Operation Is Labeled as PreciseConvexDecomposition but has invisible collision

There is a bug with unions or at least these unions that makes precise geometry collision not work properly. For example
I’ve been using the UnionAsync method during runtime to generate floors for my dungeons that have square holes for the stairs.
I’ve written a script that merges overlapping parts, after destroying those that are inside a larger box or are duplicates.

This is a snippet from my code were I pass Enum.CollisionFidelity.PreciseConvexDecomposition.

	if stairs==nil then 
				union=newPart:UnionAsync(payload,Enum.CollisionFidelity.PreciseConvexDecomposition)
			else 
				union=newPart:SubtractAsync(payload,Enum.CollisionFidelity.PreciseConvexDecomposition)
			end

I’ve put the bugged floor union inside this file.

BuggedFloorUnion.rbxm (14.0 KB)

The section that is not working is this void right here.


This is a common bug, but there’s nothing I can do on my end.

“Precise” is a bit of a misnomer. It is still an approximation and does not always line up with the render mesh. The inconsistencies become more pronounced with very large meshes like the one you have.

Suggestions to help work around this:

  • Use viewport setting “Collision fidelity” to identify when the approximated physics geometry does not match the render geometry
    image

  • Reduce the size of large unions or meshes by breaking them into smaller chunks. The details will be more consistent and have better approximations.

The union is not very larg, it is Vector3.new(240,4,160) in size the amount of polygons this union contains is less than 100. Also, I am generating these unions during runtime and cannot resolve the actual engine bug that is going on. But what I can try is to increase the size of the holes to Vector3.new(8,8,8) instead of Vector3.new(6,8,6).

I wouldn’t say that this is a bug but a misunderstanding. An “Exact” enum might be a good feature request.

I do think that the most effective approach to resolve the problems that you are having is to split up the floor into smaller pieces before doing the negation of the holes.

No I’m pretty sure PreciseConvexDecomposition means that when you have consistent holes in a flate brick that you can pass through them. Look I’m not building a castle here with floor unions. I’ve generated a simple flat brick union then I add holes to it. This object is simple it works for most of the holes then it randomly doesn’t work. My algorithm to merge overlapping floor unions is super efficient. It does it in two main steps. It’s like a chain of checking a part and the parts that it overlaps with those and the parts they overlap with. While not doing it unnessarily. I’d be happy to share the source code if you think that’s useful enough to use.


local CollectionService = game:GetService("CollectionService")
local num=0
-- Function to create a new part and handle unions
local function getinterest(types)
	if types==true or types==nil then
		local newpayload={}
		for i,v in CollectionService:GetTagged("floorunion") do 
			if v:HasTag("omit") then
			else 
				table.insert(newpayload,v)
			end
		end
		return newpayload
	else 
		return CollectionService:GetTagged(types)
		--return workspace:GetChildren()
	end
end
local materials={
	Enum.Material.WoodPlanks,Enum.Material.Asphalt,Enum.Material.Limestone,Enum.Material.Concrete,Enum.Material.CrackedLava,Enum.Material.Cobblestone,Enum.Material.Pavement,Enum.Material.Glacier,Enum.Material.Marble,Enum.Material.Metal,Enum.Material.Brick,Enum.Material.CeramicTiles,
}
local materialColors = {
	[Enum.Material.WoodPlanks] = Color3.fromRGB(139, 69, 19), -- Brown
	[Enum.Material.Asphalt] = Color3.fromRGB(50, 50, 50), -- Dark Gray
	[Enum.Material.Limestone] = Color3.fromRGB(200, 200, 169), -- Light Beige
	[Enum.Material.Concrete] = Color3.fromRGB(128, 128, 128), -- Gray
	[Enum.Material.CrackedLava] = Color3.fromRGB(207, 16, 32), -- Red
	[Enum.Material.Cobblestone] = Color3.fromRGB(112, 128, 144), -- Slate Gray
	[Enum.Material.Pavement] = Color3.fromRGB(105, 105, 105), -- Dim Gray
	[Enum.Material.Glacier] = Color3.fromRGB(173, 216, 230), -- Light Blue
	[Enum.Material.Marble] = Color3.fromRGB(255, 255, 255), -- White
	[Enum.Material.Metal] = Color3.fromRGB(169, 169, 169), -- Silver
	[Enum.Material.Brick] = Color3.fromRGB(178, 34, 34), -- Brick Red
	[Enum.Material.CeramicTiles] = Color3.fromRGB(255, 255, 255), -- White
}
--materialColors[Enum.Material.WoodPlanks].
local function applyNoiseToColor(color)
	local function clamp(value, min, max)
		if value < min then
			return min
		elseif value > max then
			return max
		else
			return value
		end
	end

	local r = clamp(color.R * 255 + math.random(-25, 25), 0, 255)
	local g = clamp(color.G * 255 + math.random(-25, 25), 0, 255)
	local b = clamp(color.B * 255 + math.random(-25, 25), 0, 255)

	return Color3.fromRGB(r, g, b)
end

local amnt=#materials
local debris=game:GetService("Debris")
--local globalpayload={}
local function areObjectsOverlapping(object1,key)
	-- Get the CFrame and size of the first object
	local cframe1 = object1.CFrame
	local size1 = object1.Size
	-- Create OverlapParams
	local overlapParams = OverlapParams.new()
	overlapParams.FilterType = Enum.RaycastFilterType.Include
	overlapParams.FilterDescendantsInstances = getinterest(key or true)
	if key=="stairunion" then
		overlapParams.BruteForceAllSlow=true
	end
	
	local overlappingParts = workspace:GetPartBoundsInBox(object1.CFrame,object1.Size+Vector3.new(5,2,5), overlapParams)	
	local payload={}
	if key=="stairunion" then
		--print(overlappingParts)
		local maxr=math.max(object1.Size.X,object1.Size.Z)
		for i,v in overlappingParts do
			if math.abs(v.Position.Y-object1.Position.Y)<2 then
				if (v.Position-object1.Position).Magnitude<maxr then
					table.insert(payload,v)
				end
			end
		end
		return payload
	end
	--local long=math.max(object1.Size.X,object1.Size.Z)*.5
	for i,v in overlappingParts do 
		if (v.Position.Y==object1.Position.Y) and v~=object1 then
			if  v:IsA("BasePart") and (v.Size.X<=object1.Size.X and v.Size.Z<=object1.Size.Z) and v.Position==object1.Position  then--or not v:IsDescendantOf(workspace) then
				v:RemoveTag("floorunion")
				v.Parent=nil
				v:Destroy()				
			else--if (v.Position-object1.Position).Magnitude<(math.max(v.Size.X,v.Size.Z)*.5)+long then
			table.insert(payload,v)
			end
		end
	end
	return payload
end
function module.createFloorUnion(cframe, size,texture,noterrain)
	-- Create the new part
	local newPart = Instance.new("Part")
	newPart.CFrame = cframe
	newPart.Size = size
	newPart.Anchored = true
	newPart.Parent = workspace
	newPart.Name="floorunion"..num
	newPart:AddTag("terraincheck")
	num+=1
	areObjectsOverlapping(newPart)
	--local newPart=getpayload(newPart)
	local number=num
		CollectionService:AddTag(newPart, "floorunion")
		
		newPart.Name="floorunion"..number
		--num+=1
	newPart.Material=materials[math.random(1,amnt)]
	if typeof(texture)=="table" then texture=texture[1] end
	
		if texture then texture:Clone().Parent=newPart end
		newPart.Color=applyNoiseToColor(materialColors[newPart.Material])--Color3.fromRGB(r,g,b)
		newPart.Material=materials[math.random(1,amnt)]
		newPart.CastShadow=false
local v=newPart		
--if noterrain==nil or noterrain==false then
--			if v:HasTag("terraincheck") then
--				v:RemoveTag("terraincheck")	
--				workspace.Terrain:FillBlock(v.CFrame:ToWorldSpace(CFrame.new(0,10,0)),Vector3.new(v.Size.X+4,24,v.Size.Z+4),Enum.Material.Air)	
--				v:AddTag("terraincheck2")
--			elseif v:HasTag("terraincheck2") then
--				v:RemoveTag("terraincheck2")
--				v:AddTag("terraincheck3")
--			elseif v:HasTag("terraincheck3") then
--				v:RemoveTag("terraincheck3")
--				workspace.Terrain:FillBlock(v.CFrame:ToWorldSpace(CFrame.new(0,10,0)),Vector3.new(v.Size.X+4,24,v.Size.Z+4),Enum.Material.Air)
--			end
--		end

	--	end
end



local processing=script.Signal

local function mergepayload(payload,newPart,texture,stairs)
	if #payload>=1 and newPart and newPart.Parent then
		--	print(payload)
		pcall(function() texture.Parent=nil end)
		if stairs==nil then
		for i,v in payload do 
			if v.Parent==nil or newPart.Position.Y~=v.Position.Y then
				table.remove(payload,i)
			end
		end	
		end
		if #payload>=1 then
		local union 
			if stairs==nil then 
				union=newPart:UnionAsync(payload,Enum.CollisionFidelity.PreciseConvexDecomposition)
			else 
				union=newPart:SubtractAsync(payload,Enum.CollisionFidelity.PreciseConvexDecomposition)
			end
			
		union.Parent=workspace
		if texture then
			pcall(function() texture.Parent=union end)
		end
		--	union.UsePa
		if stairs==nil then
		for i,v in payload do 
			v:RemoveTag("floorunion")
			v.Parent=nil
			v:Destroy()
			debris:AddItem(v,.01)	
		end 
		else 
			for i,v in payload do 
				debris:AddItem(v,15)	
			end 
		end
		--CollectionService:AddTag(union, "floorunion")
		union.Name=newPart.Name
		union.Material=materials[math.random(1,amnt)]
		union.Color=applyNoiseToColor(materialColors[union.Material])--Color3.fromRGB(r,g,b)
		union.UsePartColor=true
		newPart:RemoveTag("floorunion")
		newPart.Parent=nil		
		newPart:Destroy() debris:AddItem(newPart,0.01)	
		--if not stairs then
		union:AddTag("floorunion")
		--end
		--print("Merged")
		else 
		newPart:AddTag("floorunion")	
		end
	else 
		
	end
end
local function mergeobjects()
	local array=getinterest(true)
	local payload={}
	
	local function recursiveoverlap(i,t)
		local overlaps=areObjectsOverlapping(t) 
		if #overlaps>0 then
			for r,v in overlaps do 
				--if v:HasTag("floorunion" then
			if v:HasTag("floorunion") then
				v:RemoveTag("floorunion")
				--recursiveoverlap(i,v)
				table.insert(payload[i].Parts,v)
			end
			end
			for r,v in overlaps do 
				--v:RemoveTag("floorunion")
				recursiveoverlap(i,v)
				--table.insert(payload[i].Parts,v)
			end
		end
	end
	
	for i,v in array do 
		if v:HasTag("floorunion") and v.Size.X<1024 and v.Size.Z<1024 then
		v:RemoveTag("floorunion") 
		if payload[i]==nil then
			payload[i]={Union=v,Parts={}}
		end
		for r,t in areObjectsOverlapping(v)  do 
			if i~=r and t:HasTag("floorunion") then
			t:RemoveTag("floorunion")
			table.insert(payload[i].Parts,t)
			recursiveoverlap(i,t)
			end
		end	
		if #payload[i].Parts==0 then
			payload[i]=nil
		end
		
		v:AddTag("floorunion") 
		end
	end
	return payload
end
local function mergestairunion()
	local array=getinterest(true)
	local payload={}
	local function recursiveoverlap(i,t)
		local overlaps=areObjectsOverlapping(t,"stairunion") 
		if #overlaps>0 then
			for r,v in overlaps do 

				--v:RemoveTag("floorunion")
				--recursiveoverlap(i,v)
				table.insert(payload[i].Parts,v)
			end
			--for r,v in overlaps do 
			--	--v:RemoveTag("floorunion")
			--	recursiveoverlap(i,v)
			----table.insert(payload[i].Parts,v)
			--end
		end
	end

	for i,v in array do 
		if payload[i]==nil then
				payload[i]={Union=v,Parts={}}
			end
			for r,t in areObjectsOverlapping(v,"stairunion")  do 
				table.insert(payload[i].Parts,t)
				--recursiveoverlap(i,t)
			end
			if #payload[i].Parts==0 then
				payload[i]=nil
			end
	end
	return payload
end
local function MergeAll()
	local payload=mergeobjects()
	for i,v in payload do 		
	mergepayload(v.Parts,v.Union,v.Union:FindFirstChildOfClass("Texture"))
	end
	local payload=mergestairunion()
	
	for i,v in payload do 
	mergepayload(v.Parts,v.Union,v.Union:FindFirstChildOfClass("Texture"),true)
	end
end
function module.ClearTerrain()
		for i,v in getinterest() do 
		if v:HasTag("terraincheck") then
			
			v:RemoveTag("terraincheck")	
			workspace.Terrain:FillBlock(v.CFrame:ToWorldSpace(CFrame.new(0,10,0)),Vector3.new(v.Size.X+4,24,v.Size.Z+4),Enum.Material.Air)	
			v:AddTag("terraincheck2")
		elseif v:HasTag("terraincheck2") then
			v:RemoveTag("terraincheck2")
			workspace.Terrain:FillBlock(v.CFrame:ToWorldSpace(CFrame.new(0,10,0)),Vector3.new(v.Size.X+4,24,v.Size.Z+4),Enum.Material.Air)
			v:AddTag("terraincheck3")
		elseif v:HasTag("terraincheck3") then
			v:RemoveTag("terraincheck3")
			v:AddTag("terraincheck4")
			workspace.Terrain:FillBlock(v.CFrame:ToWorldSpace(CFrame.new(0,10,0)),Vector3.new(v.Size.X+4,24,v.Size.Z+4),Enum.Material.Air)
		elseif v:HasTag("terraincheck4") then
			v:RemoveTag("terraincheck4")
			workspace.Terrain:FillBlock(v.CFrame:ToWorldSpace(CFrame.new(0,10,0)),Vector3.new(v.Size.X+4,24,v.Size.Z+4),Enum.Material.Air)
		end
	end	
end
function module.mergeStairUnions()
MergeAll()
processing.Value=false
end```

I found the announcement post for the feature and it does state the nature of it’s exactness.

New Studio Feature - CollisionFidelity.PreciseConvexDecomposition (Enabled Globally):

I suggest that you break the floor into multiple chunked tiles because it will likely improve the quality of the approximations quicker than increasing the hole size. Splitting the floor into 4 unions will likely double the collision fidelity. And you could repeat this process if you need more. Also, increasing the hole sizes sounds like it would be a compromise to the artistic integrity, but chunking the floor is just a technical detail.

There is no chunking the floor. The object has no cylindrical features. It’s just a floor now please stop posting on this and let someone who’s job it is deal with an actual bug. I appreciate your attempt to help but you’re just wasting, my energy at this point. This is a bug with an object that only has 100 polygons It’s not cylinderical.

Would like to reiterate that this bug has nothing to do with Cylindrical tunnels or any complicated shape. The object is a simple flat plane with square holes with about 100 polygons that shouldn’t have any issues with precise convex decomposition. The union was made in two steps, one to create the union shape and a second to subtract the geometry. It’s actually perfect and it took a lot of effort to properly code in this algorithm to be optimized. Initially I was using terrain to do these floors procedurally, but a more performant and consistent solution would be the union API and I don’t understand why the API should be bugged, I have provided the problem union operation and it is preciseconvexdecomposition and in wire frame rendering it shows a perfectly optimized plane of about 100 polygons with the given holes,so based on that information alone why would it not be precise?
BuggedFloorUnion.rbxm (14.0 KB)

Again, “Precise” is a misnomer that led you to believe that the physics geometry is exact or more accurate of an approximation than you expect.

My suggested solutions to your problem are from my experience. The defects in the approximations are proportional to the scale of the details to the size of the mesh.

I’ve used precise convex decomposition for all sorts of things and again THIS is not normal behavior. I would like to hear feedback from ROBLOX staff and not this again.