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

[Update] November 9th, 2023

Hello Creators!

Constructive solid geometry (CSG) is a foundational system in Roblox and is central to our long term vision for Geometry on the platform. While meshes are relatively new to the platform, CSG has been a key modeling tool within Studio and a key gameplay enabler for engaging interactions within experiences.

Today, we are thrilled to announce a whole slate of new additions to the in-experience CSG engine that opens up a new range of possibilities for your experiences while supercharging existing ones.

New capabilities:

  • New GeometryService API to easily access all current (and future) CSG Async Operations from your scripts
  • New CSG Async APIs that can return multiple parts instead of just one
  • New CalculateConstraintsToPreserve() API to help you decide which constraints to keep after CSG operations
  • New PartOperation:SubstituteGeometry() API to swap out the geometry of one PartOperation with another (now without any flicker!)

Performance improvements:

  • New “warm start” engine that speeds up in-experience CSG operations through caching
  • Incremental re-meshing through localization techniques to greatly improve performance

Let’s dive into these new APIs and capabilities! Fair warning: there’s some pretty cool stuff in here so we might get a bit technical!

Enabling the beta

To enable these new capabilities and improvements simply go to File > Beta Features and enable the In-experience CSG improvements beta checkbox:

Usage

New GeometryService APIs

GeometryService | Documentation - Roblox Creator Hub

Today, UnionAsync(), SubtractAsync() and IntersectAsync() are APIs on the BasePart object (See BasePart | Documentation - Roblox Creator Hub) This required scripts to call the operation on the source part and had certain limitations like requiring one of the parts to be parented in the scene and only allowing one returned part

Now, these APIs can be accessed through the new GeometryService which removes those limitations and makes it much easier to access

You can use the following code to get a reference to the GeometryService and then access the in-experience CSG APIs:

local geometryService = game:GetService("GeometryService")
geometryService:UnionAsync()
geometryService:SubtractAsync()
geometryService:IntersectAsync()

These new APIs behave very similarly to the old versions of the APIs with a few key improvements:

  • The input parts don’t need to be parented to the scene (this allows for background operations)
  • With the new APIs, you can set options.SplitApart = true and the API will return references to multiple parts in a list which you can then iterate over in a script.
  • These APIs can now also be called from client-side only (local) scripts with certain caveats:
    • They can only be called on primitives or on PartOperations that were created on client
    • No replication back to server or other clients
  • Objects are not automatically re-centered after the operation. This means each of the new parts will have the same CFrame as the original part. This has 2 consequences:
    • If an object keeps moving (e.g. due to physics) while the CSG operation is executing, you can just set the CFrame to the new CFrame
    • (0,0,0) is not the geometric center of the part. (If you would like to get the geometric center of the part, you can use BasePart:ExtentsCFrame)

Here’s a quick snippet showing the SubtractAsync() function being used between a tool and an existing model.

local geometryService = game:GetService("GeometryService")
local opts = {
    SplitApart = true;
    CollisionFidelity = Enum.CollisionFidelity.Default;
}

local success, returns = pcall(geometryService.SubtractAsync, geometryService,game.Workspace.Model2.Top, {game.Workspace.Tool2}, opts)

if not success then
    print("Got an error in CSG: "..tostring(returns))
    return
end

for _,part in pairs(returns) do
    part.Parent = game.Workspace.Model2
end
game.Workspace.Model2.Top.Parent=nil

And here’s what it looks like in the experience! Note: You can find this and other working place files in the Example places section below

New SubstituteGeometry() API

SubstituteGeometry | Documentation - Roblox Creator Hub

The SubstituteGeometry() API allows you to substitute both the rendered and collision geometry of one part with that of another. This allows the part to maintain all its children and consistent references in the code (e.g. queuing operations, lights) and also removes the rendering flicker that used to occur with the previous in-experience CSG APIs.

Note: Since SubstituteGeometry() swaps out the geometry for the original part with the new parts, all Constraints and Attachments are preserved automatically.

If you don’t want some of the children / attachments / constraints to be preserved, you will need to drop those instances manually (see CalculateConstraintsToPreserve() API below for an easy way to do this).

This code snippet shows how you can use the SubstituteGeometry() API:

newparts = geometryService:subtractAsync(originalPart, tools, options)
newparts[1].CFrame=part.CFrame
originalPart:SubstituteGeometry(newparts[1])

Previous behavior without SubstituteGeometry() and flicker removal

New behavior with SubstituteGeometry() and flicker removal

New CalculateConstraintsToPreserve() API

CalculateConstraintsToPreserve | Documentation - Roblox Creator Hub

While SubstituteGeometry() preserves all constraints automatically, there are many cases where you might want to selectively preserve certain constraints based on your scenario.

For example, a common use-case might be to ‘cut’ an existing part into two resulting parts; Previously, if the original part had any constraints on it, any CSG operation would simply discard all those constraints. Now, if you use SubstituteGeometry(), all constraints, attachments and children would be preserved. So how do you selectively keep only the relevant constraints/attachments?

The CalculateConstraintsToPreserve() API solves this problem by looking at the original and the resulting parts (or PartOperations) and returning a table of all constraints and attachments along with a recommended parent for each one. If the recommended parent returned from the API is nil the recommendation is for that constraint/attachment to be dropped, otherwise, the recommended parent is most likely where the respective constraint/attachment should be under.

CalculateConstraintsToPreserve() works great in tandem with Async operations (UnionAsync(), SubtractAsync(), IntersectAsync()) by allowing you to reason over the constraints to be preserved for all resulting parts.

This code snippet below shows the simplest use of this API. It uses a helper function from the constraintsModule library (see: Example place files included below) to automatically apply the recommendations returned from the API

local newParts = geometryService:subtractAsync(originalPart, tools, options)
for _,ipart in pairs(newParts) do
    ipart.Parent = part.Parent
    ipart.CFrame = part.CFrame
    ipart.Anchored = part.Anchored
end

local recommendedTable = geometryService:CalculateConstraintsToPreserve(originalPart, newparts, constraintOptions)
constraintsModule.preserveConstraints(constraintsTable)

With this new API, a really fun scenario like this destructible rope bridge becomes much easier to accomplish:

The CalculateConstraintsToPreserve() also works great in tandem with the above SubstituteGeometry() API by providing recommendations of constraints/attachments to drop.

You, as the creator, then have the option to iterate through the returned table of constraints to make the final decision on each one based on your specific scenario.

This code snippet shows how to perform a ‘cut’ using SubtractAsync(), followed by using the CalculateConstraintsToPreserve() API to calculate recommended constraints / attachments to be dropped, then uses the SubstituteGeometry() API to swap the part and finally uses a helper function to drop the recommended constraints.

newparts = geometryService:subtractAsync(originalPart, tools, options)
newparts[1].CFrame=part.CFrame
local recommendedTable = geometryService:CalculateConstraintsToPreserve(originalPart, newparts, constraintOptions)
originalPart:SubstituteGeometry(newparts[1])
constraintsModule.dropConstraints(recommendedTable)

Note: The constraintsModule library is a set of helper functions that work well with the new APIs. You can find this module in any of the example place files below. If you would rather not use the constraintsModule You can use the following code snippets directly to either preserve or drop attachments and constraints based on your scenario

Drop Constraints / Attachments

The following code snippet shows how you can iterate through the table returned by the CalculateConstraintsToPreserve() API, check if the recommendation was to drop the constraint/attachment and then set the specific constraint/attachment’s parent to nil to drop it.

function dropConstraints(constraintsTable)
    for _, item in pairs(constraintsTable) do
        if (item.Attachment) then
            if item.ConstraintParent == nil then
                item.Constraint.Parent = nil
            end
            if item.AttachmentParent == nil then
                item.Attachment.Parent = nil
            end
        end
    end
end

Keep Constraints / Attachments

The following code snippet shows how you can iterate through the table returned by the CalculateConstraintsToPreserve() API, check if the recommendation was to keep the constraint/attachment and then set the specific constraint/attachment’s parent to the recommended parent.


function preserveConstraints(constraintsTable)
    for _, item in pairs(constraintsTable) do
        if (item.Attachment) then
            item.Constraint.Parent = item.ConstraintParent
            item.Attachment.Parent = item.AttachmentParent
        end
    end
End

Performance

All these new capabilities make the CSG system much more versatile but with all this great power comes great responsibility! To that end, we are introducing two new features that should greatly improve the snappiness of the CSG system especially when coupled with the above APIs

Warm start

CSG uses complex algorithms and computations to generate new geometry. As you perform more and more operations on an individual part, these computations can get exponentially more complicated causing the CSG system to get bogged down after a while.

With “warm start”, the CSG system now “pre-caches” all previous CSG operations so new ones don’t bog down the system as much. When you enable the beta using the above instructions, warm start will be automatically enabled for your experience and you should enjoy much quicker compounded CSG operations.

Without “Warm-start”

With “Warm-start”

Incremental re-meshing

Alongside this new beta, the CSG system has also incorporated some new localization algorithms to greatly improve the performance of operations especially in situations where multiple CSG operations need to be completed in the same physical area.

Take a look at this side-by-side comparison of a sculpting experience before and after this improvement.

Example places

Since a lot of these new APIs work together in powerful ways, here are a few example place files that showcase how these APIs can be used together to achieve some exciting new scenarios.

Example #1: Getting Started

GettingStarted.rbxl (64.9 KB)

Instructions:

  • Hit Run (F8) to watch the pieces get subtracted from.

Notes:

  • This is a minimal example with a script called CSGExample under ServerScriptService that simply runs the operations sequentially.
  • In sequential order the operations are:
    • Operation with no splitting (similar to previous CSG)
    • Substituting the geometry in a PartOperation (allows children to be preserved, no flicker on complex objects being substituted and pointers to the object are not invalidated)
    • Operation with objects being split apart
Example #2: Simple Tools

SimpleTools.rbxl (247.9 KB)

SimpleTools

Instructions:

  • Hit Play to spawn in the workshop
  • Subtractor tool [1 key] lets you remove material and Adder [2 key] lets you add material
  • Controls will show up on the side (can be seen in gif)
    • The first set of options lets you control the shape
    • Split apart determines if objects will break apart as a result of the operation (different objects in datamodel)
    • Substitute will make the part be replaced if there is only one returned object and the source was a PartOperation

Notes:

  • Most everything can be found under ReplicatedStorage.
    • CSGTool is the template the tools Subtract and Union use
    • CSGModule is the wrapper function for the CSG Async call
    • Constraints are some helper methods for the constraints transfer API
  • If you want to add new objects and have the tools recognize them as interactable for CSG, make sure to tag them as “breakable” using the collection system
Example #3: The Workshop

Workshop.rbxl (358.6 KB)

Instructions:

  • Hit Play to spawn in the workshop
  • Equip the “Shop” tool by clicking on the button or by hitting the 2 key
  • You can use the Change tool button to swap between the Internal Lathe, Band Saw and External Lathe tools
  • Use the sliders to control the tool and make various cuts into the stock part that is already loaded
  • At any point, you can also equip the “Laser Gun” tool by using the 3 key
  • This tool allows you to make free-form edits to the stock part by clicking
  • Once you are done, use the band saw tool to cut off your part from the stock part
  • Finally, use the wrench tool (1 key) to attach your new part to the car.
  • Drive away in your new car!

Notes:

  • Take a look at the following scripts in Explorer to see how the new CSG APIs are used
    • ServerScriptService/CSGToolScript for the overall script that sets up the various CSG tools
    • ReplicatedStorage/CSGModule for the core usage of the new CSG APIs
    • StarterPack/* for the various tool scripts
  • When you use the band saw to cut off your part, this uses the new Multiple Return functionality
  • The wrench is a little finicky and is more of a demonstration. Left click will duplicate the part and attach to the front axle. Right click will attach to the rear axle.
Example #4: Laser Tanks!

TankDestruction.rbxl (1.5 MB)

Tanks2-3

Instructions:

  • Jump into either of the two tanks (the M1 or the Prism tank). Note: It is easiest to enter the prism tank from the front and M1 tank from the rear.
  • Both of the tanks use the following controls:
    • WASD for movement,
    • Q/E for turret rotation
    • R/F for moving the turret up/down.
    • Left-click to Fire
    • Hold Shift while firing to allow the laser to penetrate (Prism tank only)

Notes:

  • Have fun
  • The tanks themselves are CSG-able. If you end up breaking a thread, amusement might ensue.

Known Issues

If you face any of these issues and have a consistent way to reproduce it, please let us know by responding to this post so we can track down the issue.

  • Rarely, the wrong part may be returned when using the splitApart option
  • If the off-center origin of the original part isn’t in view of the camera, it might be culled from the frame. → We are actively working on a fix for this
  • These new GeometryService Async APIs are not interchangeable with the older versions of the APIs (e.g. CFrames might be different for objects in the same place) → We are working on reconciling the differences so they are eventually interchangeable.

Coming soon

Our long-term vision for CSG on the Roblox platform is that it evolves into a universally versatile tool in your “creator tool belt” that can be used on anything in your experience (from primitive parts, all the way to meshes and even terrain at some point!). At the same time, we want to ensure that all “matter” in your experience reacts realistically to physical phenomena like forces and aerodynamics. With those two ideas in mind, keep an eye out for even more improvements to the CSG system in the coming months.

In the meantime, please let us know if you hit any bugs or unexpected issues with anything from today’s release or if you have any feedback on the API surface and how they work together.

Happy CSG-ing,

@TravelerUniverse, @BelgianBikeGuy, @syntezoid and @FGmm_r2 on behalf of the entire Geometry team at Roblox

338 Likes

This topic was automatically opened after 10 minutes.

This is a banger update. But… but… no SeparateAsync? It would also be nice to be able to create NegateOperations to allow the user to scale a negate area visually.

31 Likes

Wow!!! I didn’t think these improvements were even possible. Serious trade-offs for working with meshparts now if you’re building a highly physical experience.

12 Likes

(lol ignore this joke post i put)

9 Likes

image
There is a small mistake there where you go 2, 2, 4 which should be 2, 3, 4

13 Likes

So when are we getting client support for CSG?

9 Likes

it’s included in this release. Maybe it’s not prominent enough. There are a couple of restrictions though:
Under the New GeometryService APIs section where it explains difference with old APIs

  • These APIs can now also be called from client-side only (local) scripts with certain caveats:
    • They can only be called on primitives or on PartOperations that were created on client
    • No replication back to server or other clients
16 Likes

Okay, but when will we be able to regenerate MeshPart collisions so I don’t have to deal with THIS? (This is a fence)
Real collision: (PreciseConvexDecomposition)


Expected collision:

7 Likes

Is this using CSG? I haven’t seen that behavior with PreciseConvexDecomposition. Feel free to share an rbxl.

8 Likes

This is just a normal MeshPart that was uploaded via an external program (Visual Studio 2019). Furthermore, upon analysis, the CollisionFidelity was ACTUALLY Default, and PreciseConvexDecomposition actually fixed the collisions. HOWEVER, this is not the first time I have encountered this. I will provide a different RBXL file in which changing it to PreciseConvexDecomposition DOES NOT fix the collision.
BadCollisions.rbxl (307.0 KB)

2 Likes

Are there any plans for SeparateAsync?

5 Likes

Ohh yeah my bad! I completely missed that!

Thank you so much for this, it’ll be extremely useful!

4 Likes

Internally, we are figuring out how to provide a separate feature in experience. It’s a little tricky due to a myriad of issues.

In other words: It’s being discussed

13 Likes

This change is amazing!
Before, it was a terrible idea to ever use things like this simply due to their poor performance and behaviour, but this is truly a great.
I can already think of many use cases.

2 Likes

This is AMAZING, Honestly BEST Year for ROBLOX for implementing features we NEVER thought where coming to ROBLOX, Thank you so much ROBLOX For implementing this feature!

5 Likes

Lets hope the next update is fixing physics in ROBLOX since it isn’t the best and lags most of the time

4 Likes

Actually really nice update, appreciate it a lot.
However i do have to ask. Would there be any chance of the CSG system supporting the ability to carve and apply materials from other parts in the future? Currently we can only transfer and carve around colors and being able to carve materials would make them way more useful.
Any chance of the triangle limit being lifted aswell? Any sort of real dynamic destructible geometry could eventually hit that magic triangle limit that would cause geometry to simply just heavily degrade in quality.
And this one is a little unreasonable thing for me to ask but is there any chance of having the 131072 stud union range limit?

3 Likes

Is it recommended to use unions without hesitation regarding performance issues? Some have said that unions can be laggy in the final result because they merge multiple parts into a single instance. Is this still the case with the newer versions of CSG? Can I rely on unions when all these features are fully implemented? I am mainly concerned about performance issues with unions.

4 Likes

This is all really cool and stuff that I’m looking to use so, the timing was spot on. However, I’m curious as to what this entails. In the broken-bridge video, it was hard to tell if the performance was great or not. Are there any perf tests or ?

Additionally, are there any gotchas?

4 Likes