实体建模是将零件以独特的方式组合在一起形成更复杂形状的过程。这包括布尔运算中的并集、减法和交集,通常被称为构造实体几何(CSG)。您可以在 Studio、插件中以及游戏内的服务器和客户端上随处进行实体建模。
除了布尔C SG,实体建模还支持网格,只要它们是水密的,并且支持像扫掠和碎片这样的操作,使您和您的玩家可以切片、切割以及粉碎几何体,以实现独特的游戏互动。


水密几何体
网格的基本元素有三种:
- 顶点 - 网格上的一个点。
- 边 - 连接两个顶点的线。
- 面 - 三个或更多顶点之间的表面区域。



实体建模操作只能在水密几何体上工作;实际上,“实体”和“水密”是同义的。从技术上讲,网格是水密的意味着它是封闭的、可流形的且不自相交。这些术语有严格的定义,但以下是一些简单的规则:
- 每个面必须有一个“内部”面和一个“外部”面。这些由面旋转的顺序决定,即三个顶点的顺序。
- 每条边必须恰好被两个三角形共享。这意味着网格中不能有任何孔,因为孔周围的边只有一个三角形。
- 面不能穿过其他面。
- 相邻的三角形必须在“外部”面的一侧达成一致。
- 每个顶点必须有恰好一个相邻三角形的扇形。

实体建模系统能够自动修复网格上的特定小问题,但一般来说,如果网格不是水密的,API 调用将会失败。修复现有的非水密网格没有一刀切的方法,但有几个 Blender 插件可以提供帮助,例如 3D Print Toolbox 和 Mesh Repair Tools。作为另一种选择,Meshlab 也有非常实用的内置工具,可以尝试使网格变得可流形,这也是网格水密的主要要求。
查看网格是否很难制作成水密,可以从各个角度检查它在 Studio 中的情况,然后尝试启用和禁用网格的 MeshPart.DoubleSided 属性。如果您看到任何区别,则该网格只是一个壳体,上述工具将不会工作,因为它们无法判断网格内部和外部的空间。但是,如果您只想要一个薄网格并且不在意保持网格的尺寸完全相同,可以使用 Blender 的 Solidify modifier 稍微加厚壳体以形成水密网格。

Studio 中的实体建模
您可以使用 模型 选项卡工具栏中的四个工具执行三个基本的布尔操作。

| 工具 | 快捷键 | 描述 |
|---|---|---|
| 并集 | ShiftCtrlG (Windows) Shift⌘G (Mac) | 将两个或多个零件结合成一个固体并集。 |
| 交集 | ShiftCtrlI (Windows) Shift⌘I (Mac) | 将重叠的零件交集为一个固体交集。 |
| 否定 | ShiftCtrlN (Windows) Shift⌘N (Mac) | 否定零件,对制作孔和凹陷很有用。 |
| 分离 | ShiftCtrlU (Windows) Shift⌘U (Mac) | 将并集或交集分离回其各个部分。 |
并集零件
并集工具将两个或多个零件结合在一起形成一个单一的固体 UnionOperation。


要将零件组合成一个并集:
- 选择您想要加入的所有零件。
交集零件
交集工具将重叠的零件交集为一个单一的固体 IntersectOperation。


要将重叠的零件进行交集:
- 选择您想要交集的所有零件。
否定零件
否定工具否定一个零件,以便当它与另一个零件联合时,被否定的零件的形状将从另一个零件中减去。


要从其他重叠零件中减去一个零件:
- 选择您想要从其他零件中否定的零件。
- 点击 否定。该零件将标记为否定零件,且在资源管理器中出现否定符号。该零件将变得半透明,呈现红色色调以指示其状态。
- 选择否定零件和您想要从中减去它的零件。
- 点击 并集。该否定零件将从所包含的重叠零件中切除。
该标记对脚本公开,因此您也可以通过脚本或插件添加标记 rbxNegate 来否定零件。NegateOperation 不再使用。
分离并集或交集
分离工具将 UnionOperation 分离回其各个部分,基本上充当并集和交集的“撤消”工具。
要将并集或交集分离回各个零件:
- 选择并集或交集。
- 点击 分离。零件将分离回它们的原始状态。
游戏中实体建模
当游戏运行时,您也可以使用 GeometryService 函数执行实体建模操作。
UnionAsync()、IntersectAsync() 和 SubtractAsync()
与 Studio 中的内置基本布尔操作工具相似,您可以使用 GeometryService 函数,例如 UnionAsync()、IntersectAsync() 和 SubtractAsync() 在游戏运行时执行基本布尔操作。例如,以下脚本使用 SubtractAsync() 将一个零件的体积从另一个中减去。
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

为了进一步演示,下一个代码示例将 mainPart 和 otherParts 数组中的零件的几何体结合在一起,然后销毁参与该操作的原始零件。您可以将对 UnionAsync() 的调用替换为 IntersectAsync() 或 SubtractAsync(),以执行其他布尔操作。
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,
}
-- 在 pcall() 中执行并集操作,因为它是异步的
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- 遍历结果零件以重新父化/重新定位
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- 销毁原始零件
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
只要所有输入是原始零件而不是 MeshParts,联合、交互和减法布尔操作将生成一个 PartOperation,其中包含两件存储的数据:一个称为 CSGTree 的 CSG 操作树和一个用于渲染的网格。
与 BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync() 相比,GeometryService 的布尔函数有所不同:
- 输出是一个实例数组,而不是单个实例。
- 输入零件不需要父类到场景中,允许进行后台操作。
- 所有返回的零件都在主零件的坐标空间中,因此它们的 PVInstance.Origin 位置与主零件相同。这使得网格的顶点在操作之前相对于对象保持在相同位置,但这也意味着返回零件的 (0, 0, 0) 不一定位于其主体的中心。
SweepPartAsync()
GeometryService:SweepPartAsync() 函数创建一个 MeshPart,其形状是输入零件在一组给定的 CFrame 位置中拖动的结果。此函数在进行切片和切割交互操作时非常有用。
输入可以是 Part、PartOperation 或 MeshPart。结果的形状被定义为每对相邻 CFrames 的凸包的并集;如果只提供一个 CFrame,结果将是输入零件的凸包。
为了演示这个函数的工作原理,以下代码示例将一个球体在一组 CFrame 位置中扫掠,创建出一个螺旋形状:
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

示例

此示例使用 GeometryService:SweepPartAsync() 实现了基础功能的剑或激光枪切片机制,其中剑的移动基于玩家的鼠标位置。用户的鼠标动作被记录为 CFrames 列表,SweepPartAsync() 基于此数据构建切片网格,然后从被击中的部分中减去切片网格。
要在 Studio 中运行此示例:
在 ServerScriptService 中创建以下 Script,以执行所有实体建模操作。
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-- 可视化扫掠sweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- 从被击中实例中减去扫掠local 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)在 StarterPlayerScripts 中创建以下 LocalScript 以处理用户输入。
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-- 可视化扫掠sweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- 从被击中实例中减去扫掠local 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)在 ReplicatedStorage 中创建一个名为 DrawCurveEvent 的 RemoteEvent。
FragmentAsync()
GeometryService:FragmentAsync() 和 GeometryService:GenerateFragmentSites() 函数让您可以以自然外观的形状将一个部分打碎成多个小块。GeometryService:FragmentAsync() 使用 voronoi 分解根据传入的点模式将单个零件划分为多个 MeshPart 实例,而 GeometryService:GenerateFragmentSites() 是一个辅助函数,用于生成称为 Voronoi 站点的点,以传递给 FragmentAsync()。
为了演示这些函数如何协同工作,以下代码示例生成 Voronoi 站点以将一个块部分打碎:
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

示例

以下脚本根据位置和半径对零件的一个区域进行打碎。位置通常可以来自于物理碰撞或玩家的射线投射。
GenerateFragmentSites() 生成的站点数组的第一个元素将是所有超出所请求的 radius 的站点的内数组。如果您想对部分“未打碎部分”执行特定操作,可以在遍历 FragmentAsync() 的结果时,检查 fragments[i].Index == 1 来找到该部分。
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("打碎失败:"..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
以下脚本使用第二个零件作为模板,从第一个零件中打碎掉碎片。只有在第二个零件内的 Voronoi 站点会产生单独的碎片。所有其他站点将被合并成一个零件。


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("打碎失败:"..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
以下脚本是一个更小众的用例,但它演示了 GeometryService:FragmentAsync() 返回的索引数据的强大功能。
例如,许多地方包含由多个未联合的块零件组成的建筑。如果手榴弹、炮弹或大锤破坏它,您可能想要将所有墙体零件打碎。该脚本将打碎所有附近的零件,然后将不同零件的碎片联合在一起,以完全隐藏接缝。
这涉及多个 Async 操作,因此可能不适合在游戏中作为用户输入的即时响应使用,例如大锤工具。


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("打碎失败:"..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("联合失败:"..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
以下脚本是 GeometryService:GenerateFragmentSites() 的几乎等效的 Luau 替代方案。如果您想要与 GeometryService:GenerateFragmentSites() 类似的行为,但想进行小幅更改,可以将其用作起点。
它使用抖动网格的点,并确保碎片区域行为良好,而不是完全随机点。
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- 用于调整抖动量
if (origin and not radius) or (radius and not origin) then
warn("必须同时提供原点和半径,或都不提供。")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized 意味着不要破碎整个部分,仅仅一部分。
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
保留约束
如果输入零件有您想要保留的约束或附件,您可以将它们转移到结果零件上。确定哪些输出零件应附加约束可能很麻烦,因此建议使用 GeometryService:CalculateConstraintsToPreserve() 生成一个建议表,您可以遍历并应用。
为了演示,以下代码示例执行减法操作,遍历结果零件以重新父化和重新定位结果零件,然后计算一个约束和附件表,以便保留或删除原始零件。
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,
}
-- 在 pcall() 中执行减法操作,因为它是异步的
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- 遍历结果零件以重新父化/重新定位
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- 计算要保留或删除的约束/附件
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- 根据建议表保留约束/附件
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
-- 销毁原始零件
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
行为细节
如果主零件在操作计算期间移动,您可以将结果零件设置为主零件的更新 CFrame,因为返回的零件处于与主零件相同的坐标空间。
有函数可以替换实例的网格数据,从而更容易在保持主零件的属性、特性、标记和子项的情况下利用操作的几何体。例如 Attachments、Constraints、ParticleEmitters、光源和贴图。这种方法还可以避免完全用另一个 PartOperation 替换原始零件的潜在“闪烁”。
- 如果使用此方法将 PartOperation 作为主零件,并且其他零件都不是 MeshParts,您可以通过 SubstituteGeometry() 来替换其他 PartOperation 的几何体。
- 如果主零件是 MeshPart,则可以使用 MeshPart:ApplyMesh()。
可以在客户端调用这些函数,但有一些限制。首先,必须使用在客户端创建的对象。其次,客户端到服务器没有可用的复制。
实体建模结果考虑
颜色和 UV
实体建模后结果零件的颜色来源于两个地方:面颜色和零件的 Color。
- 如果结果是 PartOperation,它将具有您在 Studio 中选择的第一部分的 Color,但 Studio 默认使用面颜色以保持每个面在操作之前与它的颜色相同。您可以启用其 UsePartColor 属性以覆盖此行为,使整个结果呈现为单一颜色。
UV 处理也取决于结果的类型,有所不同:
- PartOperations 总是具有盒映射的 UV,这意味着每个面将从一个方向(-x、+x、-y、+y、-z、+z 之一)应用材料/纹理/贴图。这可能会拉伸纹理。
- MeshParts 则没有盒映射。使用主零件的网格的 UV。由于 Roblox 目前不支持多材料,因此来自其他零件的面将被赋予 (0, 0) 的 UV。为获得最佳效果,请确保纹理的像素 (0, 0) 具有合理的颜色。
平滑角度
实体建模零件的 SmoothingAngle 属性在相同颜色的相邻表面之间平滑角度。更高的值会产生更平滑的外观,而较低的值则会产生更粗糙的外观,并具有更多的尖锐边缘。
虽然 30 到 70 度之间的值通常会产生良好的结果,但 90 到 180 度之间的值不推荐,因为它们可能在具有尖锐边缘的并集和交集中造成“阴影”效果。


零件简化
如果实体建模操作会导致任何零件有超过 20,000 个三角形,它们将被简化为 20,000 个。如果无法做到这一点,通常在数千个不重叠组件的情况下,操作将产生错误。

