Bring real world creation to your experience with Constructive Solid Geometry (CSG) improvements [Beta]

I like this update

2 Likes

I have a question about performance. Let’s say that i generate 5 cube parts that i randomly place in a radius. I want to use them to make a hole in the part using substract but also create debris using intersect. So here is the question: would unioning the parts before doing any substract and intersect operations increase performance? Or would it actually make it worse? Or maybe there wouldn’t be any difference?

Hi everyone. Happy New year

Couple of updates:

  1. @zagusan We have fix for this coming soon (knock on wood).
  2. Following this bug fix, we should be ready for an open beta relatively quickly.
  3. @BubasGaming Many simple tools is much better for the API than unioned parts. Quick behind the curtain: When you do the union, you generate a convex decomposition, mesh and a network call. Additionally, when your tools are complex (ie, not primitives), they currently have to be recomputed in the same coordinate space as the main part. In other words. It is MUCH better to pass simple tools to the API. That being said, if there is a request for a combined subtract/intersect operation, it might be worth taking a look at it

BelgianBikeGuy

1 Like

Wow cool feature, I remember that baseParts also had it but it’s only server sided so that’s a no-no from me. I found a pretty unique (imo) way of using it as a sort of a “void explosion”


I am pretty sure I have done something wrong since it still sometimes flickers while somewhere in documentation it said it shouldn’t flick. And it’s also slow for the Suburban template (although I don’t know what’s inside houses tbh, maybe it’s too much to handle). Not to mention sometimes it throws an error -4 and -6

Source code
local TweenService = game:GetService('TweenService')
local GeometryService = game:GetService('GeometryService')
local ContextActionService = game:GetService('ContextActionService')

local mouse = game:GetService('Players').LocalPlayer:GetMouse()

local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Include
overlapParams.FilterDescendantsInstances = {workspace.GameMap} -- a random folder so we are only interested in static props/buildings

local radius = 25

local options = {
	CollisionFidelity = Enum.CollisionFidelity.Default,
	RenderFidelity = Enum.RenderFidelity.Automatic,
	SplitApart = false
}

local function createBall(position)
	local part = Instance.new('Part')
	part.Shape = Enum.PartType.Ball
	part.Anchored = true
	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false
	part.Size = Vector3.one * radius * 2
	part.Position = position
	part.Color = Color3.fromRGB(75, 6, 131)
	part.TopSurface = Enum.SurfaceType.Smooth
	part.BottomSurface = Enum.SurfaceType.Smooth
	part.CastShadow = false
	part.Material = Enum.Material.ForceField
	return part
end

local function fire(action, userInputState: UserInputState, inputObject: InputObject)
	if userInputState ~= Enum.UserInputState.Begin then return end
	local mouseCFrame = mouse.Hit

	local partsInRadius = workspace:GetPartBoundsInRadius(mouseCFrame.Position, radius, overlapParams)
	
	local resultPartsTable = {}
	
	local negateBall = createBall(mouseCFrame.Position)
	negateBall.Parent = workspace.Temp -- works as an 'explosion' effect
	TweenService:Create(negateBall, TweenInfo.new(1.25), {Transparency = 1}):Play()
	
	for i, part in partsInRadius do
		local resultParts = GeometryService:SubtractAsync(part, {negateBall}, options)
		for i, resultPart in resultParts do
			table.insert(resultPartsTable, resultPart)
			resultPart.Parent = workspace.Temp
		end
		
		part.Transparency = 1 -- making original parts invisible
	end
	
	task.wait(2)
	negateBall:Destroy() -- this part is invisible at this state
	
	for i, part in partsInRadius do
		TweenService:Create(part, TweenInfo.new(3), {Transparency = 0}):Play() -- making original parts visible again
	end
	
	task.wait(3.2)
	
	for i, part in resultPartsTable do
		part:Destroy() -- destroying generated parts
	end
end

ContextActionService:BindAction('fire', fire, true, Enum.UserInputType.MouseButton1)

Edit: just saw a guy in this topic that uses new api to destroy a map, guess it’s not-so-original now

4 Likes

Hi that’s me lol

They changed that a while back, now you can also use CSG on the client, but the results don’t replicate to other users

That’s a pretty cool use of the technology. I’d like to see it turned into a game mechanic

Ok, so void explosions are a super neat idea.

Error -4 and -6 mean that they’re either an unknown primitive or a broken CSG object from the catalog. We are aware that the errors are very cryptic (and annoying honestly) and we have are planning on making them more user readable (with more information so you guys know how you should react to them).

Additionally, I’m kinda interested in the many little parts case. Do you have a simple repro (it would save me a decent amount of time :slight_smile: ) @MrSuperKrut

Thanks
~BelgianBikeGuy

1 Like

I have made multiple improvements to this code, most importantly if part is fully within the radius then instead of us performing a SubtractAsync operation we just hide this part, saving a lot of time. Nonetheless here is the source code

Source code
local WAIT_FOR_GENERATING_PARTS = false -- if true, the script wont hide parts until it generates all parts using geometryService
local IGNORE_UNANCHORED_PARTS = true -- setting to true to any of these will make all choosen parts ignored
local IGNORE_UNION_PARTS = false
local IGNORE_MESH_PARTS = false
local EXPLOSION_RADIUS = 50

local tweenService = game:GetService('TweenService')
local geometryService = game:GetService('GeometryService')

local mouse = game:GetService('Players').LocalPlayer:GetMouse() -- i know it's deprecated but it will do for this
local userInputService = game:GetService('UserInputService')
local actionService = game:GetService('ContextActionService')

local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams.FilterDescendantsInstances = {workspace.Animation} -- a random folder so we ignore newly created parts and characters

local options = {
	CollisionFidelity = Enum.CollisionFidelity.Default,
	RenderFidelity = Enum.RenderFidelity.Automatic,
	SplitApart = false
}

local function isPartFullyInRadius(part: BasePart, position: Vector3) -- this function returns true if all corners of a given part are within a radius
	-- position is a position of a mouse
	local cframes = {
		part.CFrame * CFrame.new(part.Size.X / 2, part.Size.Y / 2, part.Size.Z / 2),
		part.CFrame * CFrame.new(part.Size.X / 2, part.Size.Y / 2, -part.Size.Z / 2),
		part.CFrame * CFrame.new(part.Size.X / 2, -part.Size.Y / 2, -part.Size.Z / 2),
		part.CFrame * CFrame.new(-part.Size.X / 2, -part.Size.Y / 2, -part.Size.Z / 2),
		part.CFrame * CFrame.new(part.Size.X / 2, -part.Size.Y / 2, part.Size.Z / 2),
		part.CFrame * CFrame.new(-part.Size.X / 2, -part.Size.Y / 2, part.Size.Z / 2),
		part.CFrame * CFrame.new(-part.Size.X / 2, part.Size.Y / 2, part.Size.Z / 2),
		part.CFrame * CFrame.new(-part.Size.X / 2, part.Size.Y / 2, -part.Size.Z / 2),
	}

	for i, cframe: CFrame in cframes do
		if (cframe.Position - position).Magnitude >= EXPLOSION_RADIUS then -- one of the corners are outside of radius - returning false
			return false
		end
	end
	return true
end

local function createBall(position)
	local part = Instance.new('Part')
	part.Shape = Enum.PartType.Ball
	part.Anchored = true
	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false
	part.Size = Vector3.one * EXPLOSION_RADIUS * 2
	part.Position = position
	part.Color = Color3.fromRGB(75, 6, 131)
	part.TopSurface = Enum.SurfaceType.Smooth
	part.BottomSurface = Enum.SurfaceType.Smooth
	part.CastShadow = false
	part.Material = Enum.Material.ForceField
	return part
end

local function fire(action, userInputState: UserInputState, inputObject: InputObject)
	if userInputState ~= Enum.UserInputState.Begin then return end
	
	local startTime = os.clock()
	local generatingTotalTime = 0
	
	local mouseCFrame = mouse.Hit

	local partsInRadius = workspace:GetPartBoundsInRadius(mouseCFrame.Position, EXPLOSION_RADIUS, overlapParams)
	local originalPartsTransparency = {} -- [index] = Transparency. Some objects could be semi-transparent so we are saving their original Transparency property here

	local resultPartsTable = {}

	local negateBall = createBall(mouseCFrame.Position)
	negateBall.Parent = workspace.Animation
	tweenService:Create(negateBall, TweenInfo.new(1.25), {Transparency = 1}):Play()
	
	for i, part in partsInRadius do
		originalPartsTransparency[i] = part.Transparency
		if (part.Anchored == false and IGNORE_UNANCHORED_PARTS) or (part:IsA('UnionOperation') and IGNORE_UNION_PARTS) or ((part:IsA('MeshPart') or part:FindFirstChildWhichIsA('SpecialMesh', false)) and IGNORE_MESH_PARTS) then
			continue
			
		elseif not isPartFullyInRadius(part, mouseCFrame.Position) then -- if part isn't fully within a radius - perform a subtractAsync operation
			
			local generatingStartTime = os.clock()
			local success, resultParts = pcall(function()
				return geometryService:SubtractAsync(part, {negateBall}, options)
			end)
			
			if not success then
				warn('Error for', part ,`. Error: {resultParts}`)
			else
				for i, resultPart in resultParts do
					table.insert(resultPartsTable, resultPart)
					if WAIT_FOR_GENERATING_PARTS == false then
						resultPart.Parent = workspace.Animation
					end
				end
			end
			
			generatingTotalTime += os.clock() - generatingStartTime
		end
		
		if WAIT_FOR_GENERATING_PARTS == false then
			part.Transparency = 1
		end
	end
	
	if WAIT_FOR_GENERATING_PARTS then -- instead of hiding parts during generating new parts, it can hide parts right here after all parts were generated
		for i, part in partsInRadius do
			part.Transparency = 1
		end
		for i, part in resultPartsTable do
			part.Parent = workspace.Animation
		end
	end
	
	print('Generating:' .. generatingTotalTime .. ' s., total: ' .. os.clock() - startTime)
	print('Parts skipped: ' .. #partsInRadius - #resultPartsTable .. ', generated: ' .. #resultPartsTable .. ', total: ' .. #partsInRadius)
	task.wait(2.5)

	negateBall:Destroy()
	
	local lastTween
	for i, part in partsInRadius do
		local targetTransparency = originalPartsTransparency[i]
		lastTween = tweenService:Create(part, TweenInfo.new(3), {Transparency = targetTransparency})
		lastTween:Play()
	end

	lastTween.Completed:Wait()

	for i, part in resultPartsTable do
		part:Destroy()
	end
end

actionService:BindAction('fire', fire, true, Enum.KeyCode.Q)

destructionTest.rbxl (1.3 MB)

In the output you can see something like Generating: x s., total: y s.. It just tells you the amount of seconds it took for a script to use SubtractAsync and a total time it took to go trough the function (excluding the last part where we just using TweenService). Usually they are almost the same.
In addition it also prints Parts skipped: x, generated: y, total: x + y. Parts skipped mean how many parts were ignored with the SOMETHING_IGNORE property and not used in the SubtractAsync due to part being fully within the explosion radius; generated is how many parts were involved into generating new parts.

It also handles the SubtractAsync errors providing the following: Error for Instance . Error: errorMessage. Instance is clickable and will choose a model that caused an error

This project uses default Suburban template with the “Animation” folder created and has 2 scripts:
StarterPlayer.StarterPlayerScripts.LocalScript is the code above;
StarterPlayer.StarterCharacterScripts.Script is a script that just sets character’s parent to “Animation” folder inside of workspace

To use an “explosion” effect, press Q on your keyboard and it will use your mouse as an explosion position

And sorry if this code isn’t not very readable, too much complicated and has some grammar errors

Also forgot to mention that spamming “explosions” will break the world since it will remember the wrong Transparency value, and it just not gonna work if you use it in the same position

Edit 2: after playing with a code a little bit I achieved low generating times using task.spawn shenanigans
Before using task.spawn:


After:

What I am trying to say is it would be cool to have an ability to do it with just a single SubtractAsync call instead of using this method since I believe people behind the engine know more that most devs.

3 Likes

Sorry if I misread your code, but why are you using SubtractAsync on every part individually instead of using a list of parts in a single SubtractAsync call?

Because a first argument of a SubtractAsync must be an Instance aka the part you want to modify. Only one part can be modified in a single call. The second argument is the “negative parts” array. In the script i am taking multiple parts from the world to modify their appearence using one sphere (referenced as explosion) hence that’s why i am making multiple calls

1 Like

Instead of bringing new APIs to an existing method - Why not work more on the actual CSG methodology?

I’m unsure as to why CSG v3 does not have the same smoothing abilities as CSG v1 - SmoothingAngle is completely useless in comparison to the smoothing that CSG v1 offered.

2 Likes

When will this be out of bata?

Any news on when this will be in our hands or if there will be availability to early enroll a game? This is the last hope for a big project I’m working on to be released, so some knowledge would be very helpful

3 Likes

fix the unions please… i already filled a bug report and nobody is answering this is urgent. image

1 Like

Hi @TimTsuki, we were able to reproduce and fixed the issue. Please recheck. If your issue persists, please provide us with your model for further investigation.

2 Likes

omg let me check, i will refresh the studio and see, i’ve made a bug report but nobody answers!! https://devforum.roblox.com/t/important-textures-show-incorrectly-on-the-geometry-of-the-new-unions/2819657/2?u=timtsuki

welp, i’m not sure if i’ve got the Update on my studio yet… the issue persists.
Heres a example model:
bugged unions.rbxm (162.5 KB)
i think my studio haven’t updated yet, you can see my unions thought, there some examples of an union + a Texture / decal

hello again, @meshadapt
i forgot to mention that the new broken unions
image
have this triangular fissures and micro fissures (which they can only be seen when selecting) the bigger fissures can be seen when using unions that involves spheres. i use solid modelling a lot and is breaking my progress :coffin:

2 Likes

@TimTsuki this seems to be a different issue. Could you provide us the model that can reproduce the problem?

1 Like

NEVERMIND! it is already solved, it was fixed when you fixed the wrong displayed textures!! (my studio didn’t seem to update at the time) Thank you so much @meshadapt !

2 Likes

Does anyone know when this will be available for external use?