[Update] April 23, 2024
[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 onePartOperation
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!
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
- They can only be called on primitives or on
- 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
underServerScriptService
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)
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)
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