Solid modeling is the process of joining parts together in unique ways to form more complex shapes. This includes the boolean operations union, subtraction, and intersection, which are commonly known as Constructive Solid Geometry (CSG). You can perform solid modeling everywhere: in Studio, plugins, even in-game on both the server and client.
In addition to boolean CSG, solid modeling also supports meshes, as long as they are watertight, and operations like sweep and fragment that let you and your players slice, cut, and shatter geometry for unique gameplay interactions.


Watertight geometry
There are three basic elements of meshes:
- Vertex - A single point on the mesh.
- Edge - A line that connects two vertices.
- Face - A surface area between three or more vertices.



Solid modeling operations can only work with watertight geometry; in fact, "solid" and "watertight" are synonymous. In technical terms, a mesh being watertight means that it's closed, manifold, and non-self-intersecting. These terms have strict definitions, but here are some simple rules:
- Each face must have an 'inside' side and an 'outside' side. These are determined by the face's winding order, which is the order of its three vertices.
- Each edge must be shared by exactly two triangles. This means there cannot be any holes in the mesh because the edges around the rim of a hole would only have one triangle.
- Faces cannot pass through other faces.
- Adjacent triangles must agree on which side is the 'outside' side.
- Each vertex must have exactly one fan of adjacent triangles.

The solid modeling system is able to automatically repair specific small issues with a mesh, but in general, API calls will fail if the mesh is not watertight. There's no one-size-fits-all way to repair an existing non-watertight mesh, but there are several Blender plugins which can help, such as 3D Print Toolbox and Mesh Repair Tools. As another option, Meshlab also has very useful tools built-in to try and make the mesh manifold, which is the main requirement for a mesh being watertight.
One way to see that a mesh will be extremely difficult to make watertight is to look at it in Studio from all angles, then try enabling and disabling the mesh's MeshPart.DoubleSided property. If you can see any difference, then the mesh is just a shell and the tools mentioned above won't work because they cannot guess what space is inside versus outside the mesh. However, if all you want is a thin mesh and it isn't important to keep the mesh's dimensions exactly the same, you can use Blender's Solidify modifier to sightly thicken the shell into a watertight mesh.

Solid modeling in Studio
You can perform three basic boolean operations using four tools within the Model tab toolbar.

| Tool | Shortcut | Description |
|---|---|---|
| Union | ShiftCtrlG (Windows) Shift⌘G (Mac) | Join two or more parts together to form a single solid union. |
| Intersect | ShiftCtrlI (Windows) Shift⌘I (Mac) | Intersect overlapping parts into a single solid intersection. |
| Negate | ShiftCtrlN (Windows) Shift⌘N (Mac) | Negate parts, useful for making holes and indentations. |
| Separate | ShiftCtrlU (Windows) Shift⌘U (Mac) | Separate the union or intersection back into its individual parts. |
Union parts
The Union tool joins two or more parts together to form a single solid UnionOperation.


To combine parts together into a union:
- Select all parts that you want to join together.
Intersect parts
The Intersect tool intersects overlapping parts into a single solid IntersectOperation.


To intersect overlapping parts together:
- Select all parts that you want to intersect.
- Click the Intersect button. All of the parts combine into one solid IntersectOperation with the name Intersection.
Negate parts
The Negate tool negates a part so that when it's unioned with another part, the shape of the negated part is subtracted from the other part.


To subtract a part from other overlapping parts:
- Select the part you want to negate from other parts.
- Click Negate. The part becomes tagged as a negated part and a negated symbol appears in the Explorer. The part becomes translucent with a reddish tint to indicate its state.
- Select both the negated part and the parts you want to subtract it from.
- Click Union. The negated part is cut out from the included overlapping parts.
The tag is exposed for scripting, so you can also negate parts by adding the tag rbxNegate from a script or plugin. NegateOperation is no longer used.
Separate unions or intersections
The Separate tool separates a UnionOperation back into its individual parts, essentially serving as an "undo" tool for unions and intersections.
To separate a union or intersection back into individual parts:
- Select the union or intersection.
- Click Separate. The parts separate back into their original form.
In-game solid modeling
You can also perform solid modeling operations while a game is running by using GeometryService functions.
UnionAsync(), IntersectAsync(), and SubtractAsync()
Similar to Studio's built-in basic boolean operation tools, you can use GeometryService functions like UnionAsync(), IntersectAsync(), and SubtractAsync()to perform basic boolean operations while a game is running. For example, the following script uses SubtractAsync()to subtract the volume of one part from another.
local GeometryService = game:GetService("GeometryService")
local mainPart = Instance.new("Part")
local otherPart = Instance.new("Part")
otherPart.Position = Vector3.new(1, 0.5, 1)
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, {otherPart})
end)
if success and newParts then
for _, newPart in pairs(newParts) do
newPart.Parent = workspace
end
end

To further demonstrate, the next code sample combines the geometry of mainPart and the parts in the otherParts array, then it destroys the original parts involved in the operation. You can replace the call to UnionAsync() with IntersectAsync() or SubtractAsync() to perform the other boolean operations.
local GeometryService = game:GetService("GeometryService")
local mainPart = workspace.BlueBlock
local otherParts = { workspace.PurpleCylinder }
local options = {
CollisionFidelity = Enum.CollisionFidelity.Default,
RenderFidelity = Enum.RenderFidelity.Automatic,
SplitApart = false,
}
-- Perform union operation in pcall() since it's asynchronous
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop through resulting parts to reparent/reposition
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Destroy original parts
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
As long as all the inputs are composed of primitive parts rather than MeshParts, the union, interact, and subtract boolean operations will produce a PartOperation with two pieces of stored data: a tree of CSG operations known as a CSGTree, and a mesh for rendering.
Compared to BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), the GeometryService boolean functions differ as follows:
- The output is an array of instances rather than a single instance.
- The input parts do not need to be parented to the scene, allowing for background operations.
- When the SplitApart option is set to true (default), each distinct body will be returned in its own PartOperation/MeshPart.
- All the returned parts are in the coordinate space of the main part, so their PVInstance.Origin positions are the same as the main part's. This keeps the vertices of the mesh in the same position relative to the object as before the operation, but it does also mean the (0, 0, 0) of a returned part is not necessarily at the center of its body.
SweepPartAsync()
The GeometryService:SweepPartAsync() function creates a MeshPart which has the shape of the input part dragged through a given set of CFrame positions. This function can be very useful for performing slicing and cutting interactions.
The input can be a Part, PartOperation, or MeshPart. The result's shape is defined as the union of the convex hulls of each adjacent pair of CFrames; if only a single CFrame is provided, the result will be a convex hull of the input part.
To demonstrate how this function works, the following code sample sweeps a ball through a set of CFrame positions to create a spiral:
local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Shape = Enum.PartType.Ball
local cframeList = {}
for i = 1, 50 do
local rotation = CFrame.Angles(0, i * 0.5, 0)
local position = Vector3.new(0, i * 0.1, -1)
table.insert(cframeList, rotation * CFrame.new(position))
end
local success, sweptPart = pcall( function()
return GeometryService:SweepPartAsync(inputPart, cframeList)
end)
if success and sweptPart then
sweptPart.Parent = workspace
end

Example

This examples uses GeometryService:SweepPartAsync() to achieve a sword or laser gun slice gameplay feature, where the movement of the sword is based on the player's mouse position. The user's mouse movement is recorded as a list of CFrames, SweepPartAsync() builds a slice mesh from this data, then the slice mesh is subtracted from the part which was hit.
To get this example running in Studio:
Create the following Script in ServerScriptService to perform all of the solid modeling operations.
local ReplicatedStorage = game:GetService("ReplicatedStorage")local GeometryService = game:GetService("GeometryService")local DrawCurveEvent = ReplicatedStorage:WaitForChild("DrawCurveEvent")DrawCurveEvent.OnServerEvent:Connect(function(player, cframeList, hitInstance)local blade = Instance.new("Part")blade.Size = Vector3.new(0.2, 0.2, 15.0)local success, sweptPart = pcall( function()return GeometryService:SweepPartAsync(blade, cframeList)end)if success and sweptPart then-- Visualize the sweepsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Subtract the sweep from the hit instancelocal subtractSuccess, newParts = pcall( function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)Create the following LocalScript in StarterPlayerScripts to handle user input.
local ReplicatedStorage = game:GetService("ReplicatedStorage")local GeometryService = game:GetService("GeometryService")local DrawCurveEvent = ReplicatedStorage:WaitForChild("DrawCurveEvent")DrawCurveEvent.OnServerEvent:Connect(function(player, cframeList, hitInstance)local blade = Instance.new("Part")blade.Size = Vector3.new(0.2, 0.2, 15.0)local success, sweptPart = pcall( function()return GeometryService:SweepPartAsync(blade, cframeList)end)if success and sweptPart then-- Visualize the sweepsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Subtract the sweep from the hit instancelocal subtractSuccess, newParts = pcall( function()r eturn GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)Create a RemoteEvent in ReplicatedStorage named DrawCurveEvent.
FragmentAsync()
The GeometryService:FragmentAsync() and GeometryService:GenerateFragmentSites() functions let you shatter a part into pieces with natural-looking shapes. GeometryService:FragmentAsync() uses voronoi decomposition to divide a single part into multiple MeshPart instances according to the pattern of points passed in, while GeometryService:GenerateFragmentSites() is a helper function which generates points known as voronoi sites to pass into FragmentAsync().
To demonstrate how these functions work together, the following code sample generates voronoi sites to fragment a block part:
local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Position = Vector3.new(0, 0.7, 20)
local sites = GeometryService:GenerateFragmentSites(inputPart)
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(inputPart, sites)
end)
if success and fragments then
for _, item in fragments do
local instance = item.Instance
instance.Parent = workspace
end
end

Examples

The following script fragments an area of a part, given by a position and radius. The position might commonly come from a physical collision or a raycast from a player.
The first element of the site array which GenerateFragmentSites() generates will be an inner array of all the sites which are outside the requested radius. If you want to do something specific to the remaining 'unshattered portion' of the part, you can find that portion by checking fragments[i].Index == 1 when looping over the results of FragmentAsync().
local GeometryService = game:GetService("GeometryService")
function fragmentAtPosition(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("Failed to Fracture:"..tostring(fragments))
return
end
local decals = {}
for _,child in pairs(part:GetChildren()) do
if child:IsA("Decal") or child:IsA("SurfaceAppearance") then
table.insert(decals,child)
end
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
if fragment == nil then
continue
end
for _,d in pairs(decals) do
local d2 = d:Clone()
d2.Parent = fragment
end
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
end
if #fragments ~= 0 then
part:Destroy()
end
return fragments
end
The following script breaks fragments off the first part within a shape defined by using a second part as a stencil. Only voronoi sites within the second part will result in separate pieces. All other sites will have their cells combined into a single part.


local GeometryService = game:GetService("GeometryService")
function fragmentWithinStencil(player, part)
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Include
overlapParams.FilterDescendantsInstances = {workspace.Stencil}
overlapParams.RespectCanCollide = false
local sensor = Instance.new("Part")
sensor.Size = Vector3.new(0.01, 0.01, 0.01)
local allSites = GeometryService:GenerateFragmentSites(part, {SiteSpacing = 0.9})
local fragmentSites = {}
local mainPartSites = {}
for _, site in ipairs(allSites) do
sensor.CFrame = CFrame.new(site)
local partsFound = workspace:GetPartsInPart(sensor, overlapParams)
if #partsFound > 0 then
table.insert(fragmentSites, site)
else
table.insert(mainPartSites, site)
end
end
local sortedSites = fragmentSites
table.insert(sortedSites, mainPartSites)
workspace.Stencil:Destroy()
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, sortedSites, {SplitApart = false})
end)
if not success then
warn("Failed to Fracture:"..tostring(fragments))
return
end
local decals = {}
for _,child in pairs(part:GetChildren()) do
if child:IsA("Decal") or child:IsA("SurfaceAppearance") then
table.insert(decals,child)
end
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
if fragment == nil then
continue
end
for _,d in pairs(decals) do
local d2 = d:Clone()
d2.Parent = fragment
end
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
end
if #fragments ~= 0 then
part:Destroy()
end
return fragments
end
The following script is a much more niche use case, but it demonstrates the power of the index data which is returned from GeometryService:FragmentAsync().
For example, many places contain buildings formed from multiple non-unioned block parts. If a grenade, cannonball, or sledgehammer were to damage it, you would want all of the wall parts to be fragmented. This script fragments all of the nearby parts, then unions the fragments of different parts together to completely hide the seams.
This involves multiple Async operations, so it may not be suitable for use in-game as an instant response to user input, such as a sledgehammer tool.


local GeometryService = game:GetService("GeometryService")
function fragmentCrossPart(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local fragmentsSorted = {}
for i = 1, #allSites do
fragmentsSorted[i] = {}
end
local partsFound = workspace:GetPartBoundsInRadius(contactPoint, radius)
for i, part in ipairs(partsFound) do
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("Failed to Fracture:"..tostring(fragments))
return
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
local siteIndex = fragments[i].Index
if fragment == nil or siteIndex == nil then
continue
end
table.insert(fragmentsSorted[siteIndex], fragment)
end
end
for i = 1, #fragmentsSorted do
local fragmentList = fragmentsSorted[i]
if #fragmentList == 0 then
continue
end
if #fragmentList == 1 then
local fragment = fragmentList[1]
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
continue
end
if i == #allSites then
for j = 1, #fragmentList do
local fragment = fragmentList[j]
fragment.Parent = part.Parent
fragment.Anchored = true
end
continue
end
local mainPart = fragmentList[1]
local otherParts = {}
for j = 2, #fragmentList do
table.insert(otherParts, fragmentList[j])
end
local success, results = pcall( function()
return GeometryService:UnionAsync(mainPart, otherParts)
end)
if not success then
warn("Failed to Union:"..tostring(results))
return
end
for j = 1, #results do
results[j].Parent = part.Parent
results[j].Anchored = false
results[j]:SetNetworkOwner(player)
end
end
for i, part in ipairs(partsFound) do
part:Destroy()
end
end
The following script is a nearly identical Luau replacement for GeometryService:GenerateFragmentSites(). If you want similar behavior to GeometryService:GenerateFragmentSites() but want to make slight changes, you can use this as a starting point.
It uses a jittered grid of points, and guarantees that the fragmented area is well behaved, unlike fully random points.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Use this to adjust the amount of jitter
if (origin and not radius) or (radius and not origin) then
warn("Either both origin and radius should be provided, or neither.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized means don't fracture the whole part, just a section.
local partCFrame = part.ExtentsCFrame
local gridDimensions: Vector3
local localGridCenter: Vector3
local spacing
if siteSpacing then
spacing = siteSpacing
elseif isLocalized then
spacing = radius * 0.5
else
local partSize = part.Size
local volume = partSize.X * partSize.Y * partSize.Z
spacing = (volume / 5) ^ (1/3)
end
if isLocalized then
local localOrigin = partCFrame:PointToObjectSpace(origin)
local gridSize = math.ceil(radius * 2 / spacing) + 3
gridDimensions = Vector3.new(gridSize, gridSize, gridSize)
localGridCenter = localOrigin
else
local partSize: Vector3 = part.Size
local xCount = math.ceil(partSize.X / spacing)
local yCount = math.ceil(partSize.Y / spacing)
local zCount = math.ceil(partSize.Z / spacing)
gridDimensions = Vector3.new(xCount, yCount, zCount)
localGridCenter = Vector3.zero
end
local totalGridSize = gridDimensions * spacing
local halfCell = Vector3.new(spacing, spacing, spacing) * 0.5
local localStartOffset = localGridCenter - (totalGridSize * 0.5) + halfCell
local innerJitter = spacing * 0.5 * RANDOMNESS_MULTIPLIER
local outerJitter = math.min(spacing * 0.5 * 0.866, innerJitter)
local sitesFlatList = {}
for x = 0, gridDimensions.X - 1 do
for y = 0, gridDimensions.Y - 1 do
for z = 0, gridDimensions.Z - 1 do
local isOuterShell =
x == 0 or x == gridDimensions.X - 1 or
y == 0 or y == gridDimensions.Y - 1 or
z == 0 or z == gridDimensions.Z - 1
local jitterAmount = if (isOuterShell and isLocalized) then outerJitter else innerJitter
local jitterOffset = Vector3.new(
(math.random() - 0.5) * 2 * jitterAmount,
(math.random() - 0.5) * 2 * jitterAmount,
(math.random() - 0.5) * 2 * jitterAmount
)
local offsetInGrid = Vector3.new(x, y, z) * spacing
table.insert(sitesFlatList, localStartOffset + offsetInGrid + jitterOffset)
end
end
end
local sitesListFinal = {}
if isLocalized then
local mainPartSites = {}
for _, localSite in ipairs(sitesFlatList) do
local worldSite = partCFrame * localSite
local distance = (worldSite - origin).Magnitude
if distance < radius then
table.insert(sitesListFinal, worldSite)
else
table.insert(mainPartSites, worldSite)
end
end
table.insert(sitesListFinal, 1, mainPartSites)
else
for _, localSite in ipairs(sitesFlatList) do
local worldSite = partCFrame * localSite
table.insert(sitesListFinal, worldSite)
end
end
return sitesListFinal
end
Preserve constraints
If an input part has constraints or attachments that you want to preserve, you can transfer them over to the resulting parts. It can be tedious to figure out which output part a constraint should be attatched to, so it's recommended to use GeometryService:CalculateConstraintsToPreserve() to generate a table of recommendations which you can loop over and apply.
To demonstrate, the following code sample performs a subtract operation, loops through the resulting parts to reparent and reposition the resulting parts, then calculates a table of constraints and attachments to either preserve or drop before destroying all original parts.
local GeometryService = game:GetService("GeometryService")
local mainPart = workspace.PurpleBlock
local otherParts = { workspace.BlueBlock }
local options = {
CollisionFidelity = Enum.CollisionFidelity.Default,
RenderFidelity = Enum.RenderFidelity.Automatic,
SplitApart = true,
}
local constraintOptions = {
tolerance = 0.1,
weldConstraintPreserve = Enum.WeldConstraintPreserve.All,
dropAttachmentsWithoutConstraints = false,
}
-- Perform subtract operation in pcall() since it's asynchronous
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop through resulting parts to reparent/reposition
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Calculate constraints/attachments to either preserve or drop
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Preserve constraints/attachments based on recommended table
for _, item in pairs(recommendedTable) do
if item.Attachment then
item.Attachment.Parent = item.AttachmentParent
if item.Constraint then
item.Constraint.Parent = item.ConstraintParent
end
elseif item.NoCollisionConstraint then
local newNoCollision = Instance.new("NoCollisionConstraint")
newNoCollision.Part0 = item.NoCollisionPart0
newNoCollision.Part1 = item.NoCollisionPart1
newNoCollision.Parent = item.NoCollisionParent
elseif item.WeldConstraint then
local newWeldConstraint = Instance.new("WeldConstraint")
newWeldConstraint.Part0 = item.WeldConstraintPart0
newWeldConstraint.Part1 = item.WeldConstraintPart1
newWeldConstraint.Parent = item.WeldConstraintParent
end
end
-- Destroy original parts
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Behavior details
If the main part is moving during the calculation of the operation, you can set the resulting parts to the updated CFrame of the main part, since the returned parts are in the same coordinate space as the main part.
There are functions to swap out the mesh data of an instance, making it easier to utilize the geometry of the operation while maintaining properties, attributes, tags, and children of the main part, such as Attachments, Constraints, ParticleEmitters, light objects, and decals. This approach also circumvents the potential "flicker" of completely replacing the original PartOperation with another.
- If using this method with a PartOperation as the main part and none of the other parts are MeshParts, you can substitute in the geometry of another PartOperation via SubstituteGeometry().
- If the main part is a MeshPart, you can use MeshPart:ApplyMesh().
It's possible to call these functions on the client, but with some limitations. First, it must be done with objects created on the client. Secondly, there is no replication available from client to the server.
The following properties from the main part are applied to the resulting PartOperations or MeshParts:
- Collision: BasePart.AudioCanCollide, BasePart.CanCollide
Solid modeling results considerations
Colors and UVs
The colors of the resulting part(s) after solid modeling come from two places: the face colors and the part's Color.
- If the result is a PartOperation, it will have the Color of the first part you selected in Studio, but Studio uses the face colors by default to keep each face the same color as it was before the operation. You can enable its UsePartColor property in Studio to override this behavior and make the entire result a single color.
- If the result is a MeshPart, its Color will be white, and the face colors will always show through. You can adjust the tint of the resulting part(s) by changing their Color, but it will be mixed (multiplied) with the face colors. This tints the result rather than overriding the face colors completely. If you want complete control over the color of the output, it's best to make the inputs white first.
UVs are also handled differently depending of the type of result:
- PartOperations always have boxmapped UVs, which means each face will have the material/texture/decal from one direction (one of -x, +x, -y, +y, -z, +z) applied to it. This can stretch textures.
- MeshParts are not boxmapped. The UVs of the main part's mesh are used. Since Roblox does not currently have multi-material support, the UVs of faces originating from the other parts are given UVs of (0, 0). For best results, ensure pixel (0, 0) of your texture has a reasonable color.
Smoothing angle
A solid modeled part's SmoothingAngle property smooths angles between adjacent surfaces of the same color. A higher value produces a smoother appearance while a lower value produces a rougher appearance with more sharp edges.
While a value between 30 and 70 degrees usually produces a good result, values between 90 and 180 are not recommended as they may cause a "shadowing" effect on unions and intersections with sharp edges.


Part simplification
If a solid modeling operation would result in any parts with more than 20,000 triangles, they will be simplified to 20,000. If that cannot be done, usually in a case with thousands of non-overlapping components, the operation results in an error.

