SubstituteGeometry appears not to work

SubstituteGeometry used to work fine, but recently (weeks?) it fails to do anything and doesn’t update the original object’s mesh (visually, at least). This is in Studio and I’ve reproduced it via the command bar and a plugin.

When using it I’ll see the Size property of the object update, but not the mesh. Consequently, the Size can become misaligned with the appearance.

Here’s a simple test case that will run in the command bar. I would expect this to result in a sphere being visible but it results in the original cuboid remaining visible. Notably, the triangle count property doesn’t update either.

This creates a Cuboid (unioned with a smaller cuboid inside) and does the same with a Sphere. Then replaces the Cuboid’s geometry with the Sphere union’s. The same result happens with {} passed to UnionAsync - the small cuboid isn’t important, I just wanted to eliminate the risk that a single-part union was a problem.

	local Selection = game:GetService("Selection")
	local ChangeHistoryService = game:GetService("ChangeHistoryService")
	local GeometryService = game:GetService("Geometry")

	ChangeHistoryService:SetWaypoint("Before Test SubstituteGeometry")

	-- Create main part (cuboid/box) - this will be substituted
	local mainPart = Instance.new("Part")
	mainPart.Size = Vector3.new(4, 4, 4)
	mainPart.Anchored = true
	mainPart.CFrame = CFrame.new(0, 10, 0)
	mainPart.Name = "TestMainPart_Cuboid"
	mainPart.Parent = workspace

	-- Create small cuboid to union with main part (to make it a proper union operation)
	local smallCuboid1 = Instance.new("Part")
	smallCuboid1.Size = Vector3.new(0.5, 0.5, 0.5)
	smallCuboid1.Anchored = true
	smallCuboid1.CFrame = CFrame.new(0, 10, 0) -- Same position, will be inside
	smallCuboid1.Name = "TestSmallCuboid1"
	smallCuboid1.Parent = workspace

	-- Create sphere part to union with
	local spherePart = Instance.new("Part")
	spherePart.Shape = Enum.PartType.Ball
	spherePart.Size = Vector3.new(4, 4, 4)
	spherePart.Anchored = true
	spherePart.CFrame = CFrame.new(0, 10, 0)
	spherePart.Name = "TestSpherePart"
	spherePart.Parent = workspace

	-- Create small cuboid to union with sphere part
	local smallCuboid2 = Instance.new("Part")
	smallCuboid2.Size = Vector3.new(0.5, 0.5, 0.5)
	smallCuboid2.Anchored = true
	smallCuboid2.CFrame = CFrame.new(0, 10, 0) -- Same position, will be inside
	smallCuboid2.Name = "TestSmallCuboid2"
	smallCuboid2.Parent = workspace

	warn("[Test SubstituteGeometry] Creating main PartOperation (cuboid union)...")

	-- Create main union (cuboid/box) using part's :UnionAsync() method with small cuboid
	local mainPartOp = mainPart:UnionAsync({smallCuboid1})
	mainPartOp.Name = "TestMainPartOp_Cuboid"
	mainPartOp.Anchored = true
	mainPartOp.CFrame = CFrame.new(0, 10, 0)
	mainPartOp.Parent = workspace
	mainPart:Destroy() -- Clean up original parts
	smallCuboid1:Destroy()

	warn("[Test SubstituteGeometry] Creating substitute PartOperation (sphere union)...")

	-- Create substitute union (sphere) using part's :UnionAsync() method with small cuboid
	local substitutePart = spherePart:UnionAsync({smallCuboid2})
	substitutePart.Name = "TestSubstitutePart_Sphere"
	substitutePart.Anchored = true
	substitutePart.CFrame = mainPartOp.CFrame
	substitutePart.Parent = workspace
	spherePart:Destroy() -- Clean up original parts
	smallCuboid2:Destroy()

	warn("[Test SubstituteGeometry] Attempting SubstituteGeometry (cuboid -> sphere)...")

	local ok, err = pcall(function()
		mainPartOp:SubstituteGeometry(substitutePart)
	end)

	if ok then
		warn("[Test SubstituteGeometry] ✓ SubstituteGeometry call succeeded!")
		warn("[Test SubstituteGeometry] The selected object should now be a SPHERE instead of a CUBOID")
		warn("[Test SubstituteGeometry] Check visually - if it's still a cube, SubstituteGeometry didn't work")

		-- Clean up the substitute part
		substitutePart:Destroy()
		Selection:Set({mainPartOp})
	else
		warn(string.format("[Test SubstituteGeometry] ✗ SubstituteGeometry failed: %s", tostring(err)))
		-- Clean up both
		mainPartOp:Destroy()
		substitutePart:Destroy()
	end

	ChangeHistoryService:SetWaypoint("After Test SubstituteGeometry")

Expected behavior

I would expect that calling SubstituteGeometry would substitute the visual geometry of the union with the one that is passed to it.

Hi @Ed_Win5000 , thank you very much for sharing and especially for the repro steps! We’ll look into it.

Hey, thanks for sharing this. We’ve found a fix for the issue, which we should be able to turn on in the next release cycle. In the meantime, the GeometryService Csg APIs should work properly with substitute geometry.

1 Like

@pho01proof Thanks, on a related note, can you verify that the smoothing is reapplied once substitute geometry is run? Before the above issue broke things, it wasn’t and I had to set it to 0, then back to the angle value again to make it reapply it after the substitution.

It seems there’s a few (not quite) under the hood changes going on here in this area!

Thats a good catch, I guess we should either reapply it or update the smoothing angle to the value that is set in the source part. Will look into it.

2 Likes

This would explain why when I call PartOperation:SubstituteGeometry() with a UnionOperation as its first parameter (valid) it doesn’t update the mesh, in fact I have saved it (being the UnionOperation) as a rbxm file (I have been working with rbxm the past few months for a sandbox game) in both the working and broken states and found that when the UnionOperation is created via PartOperation:SubstituteGeometry() it doesn’t contain “MeshData” (it’s an empty string), a property that as far as I know all UnionOperations should have. (in other words, the render mesh is NOT being updated)

Hey, this should be fixed. Pls let me know if you are still running into this issue.