实体建模

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

实体建模是将零件以独特的方式组合在一起形成更复杂形状的过程。这包括布尔运算中的并集减法交集,通常被称为构造实体几何(CSG)。您可以在 Studio、插件中以及游戏内的服务器和客户端上随处进行实体建模。

除了布尔C SG,实体建模还支持网格,只要它们是水密的,并且支持像扫掠碎片这样的操作,使您和您的玩家可以切片、切割以及粉碎几何体,以实现独特的游戏互动。

使用 SweepPartAsync() 创建的网格对象
使用 FragmentAsync() 打破的网格对象

水密几何体

网格的基本元素有三种:

  • 顶点 - 网格上的一个点。
  • - 连接两个顶点的线。
  • - 三个或更多顶点之间的表面区域。
一个立方体网格上的单个活动顶点。
顶点
一个立方体网格上的单个活动边。
一个立方体网格上的单个活动面。

实体建模操作只能在水密几何体上工作;实际上,“实体”和“水密”是同义的。从技术上讲,网格是水密的意味着它是封闭的、可流形的且不自相交。这些术语有严格的定义,但以下是一些简单的规则:

  • 每个面必须有一个“内部”面和一个“外部”面。这些由面旋转的顺序决定,即三个顶点的顺序。
  • 每条边必须恰好被两个三角形共享。这意味着网格中不能有任何孔,因为孔周围的边只有一个三角形。
  • 面不能穿过其他面。
  • 相邻的三角形必须在“外部”面的一侧达成一致。
  • 每个顶点必须有恰好一个相邻三角形的扇形。
非水密几何体的示例

实体建模系统能够自动修复网格上的特定小问题,但一般来说,如果网格不是水密的,API 调用将会失败。修复现有的非水密网格没有一刀切的方法,但有几个 Blender 插件可以提供帮助,例如 3D Print ToolboxMesh Repair Tools。作为另一种选择,Meshlab 也有非常实用的内置工具,可以尝试使网格变得可流形,这也是网格水密的主要要求。

查看网格是否很难制作成水密,可以从各个角度检查它在 Studio 中的情况,然后尝试启用和禁用网格的 MeshPart.DoubleSided 属性。如果您看到任何区别,则该网格只是一个壳体,上述工具将不会工作,因为它们无法判断网格内部和外部的空间。但是,如果您只想要一个薄网格并且不在意保持网格的尺寸完全相同,可以使用 Blender 的 Solidify modifier 稍微加厚壳体以形成水密网格。

启用双面时看起来不同的网格示例。
这是一个壳体网格,自动修复方法将无法对其生效。

Studio 中的实体建模

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

Studio 工具栏中突出显示的实体建模工具。
工具快捷键描述
并集ShiftCtrlG (Windows)
ShiftG (Mac)
将两个或多个零件结合成一个固体并集。
交集ShiftCtrlI (Windows)
ShiftI (Mac)
将重叠的零件交集为一个固体交集。
否定ShiftCtrlN (Windows)
ShiftN (Mac)
否定零件,对制作孔和凹陷很有用。
分离ShiftCtrlU (Windows)
ShiftU (Mac)
将并集或交集分离回其各个部分。

并集零件

并集工具将两个或多个零件结合在一起形成一个单一的固体 UnionOperation

一个方块和一个圆柱体对比。
单独零件
一个方块和一个圆柱体合并成一个对象。
并集结果

要将零件组合成一个并集:

  1. 选择您想要加入的所有零件。
  2. 点击 并集 按钮。所有零件将合并成一个名为 并集 的固体 UnionOperation

交集零件

交集工具将重叠的零件交集为一个单一的固体 IntersectOperation

一个方块和一个圆柱体。
单独零件
一个方块和一个圆柱体合并成一个对象。
交集结果

要将重叠的零件进行交集:

  1. 选择您想要交集的所有零件。
  2. 点击 交集 按钮。所有零件将合并成一个名为 交集 的固体 IntersectOperation

否定零件

否定工具否定一个零件,以便当它与另一个零件联合时,被否定的零件的形状将从另一个零件中减去

一个方块和一个圆柱体。
方块和否定的圆柱体
一个方块和一个圆柱体合并成一个对象。
减去的结果

要从其他重叠零件中减去一个零件:

  1. 选择您想要从其他零件中否定的零件。
  2. 点击 否定。该零件将标记为否定零件,且在资源管理器中出现否定符号。该零件将变得半透明,呈现红色色调以指示其状态。
  3. 选择否定零件和您想要从中减去它的零件。
  4. 点击 并集。该否定零件将从所包含的重叠零件中切除。

该标记对脚本公开,因此您也可以通过脚本或插件添加标记 rbxNegate 来否定零件。NegateOperation 不再使用。

分离并集或交集

分离工具将 UnionOperation 分离回其各个部分,基本上充当并集和交集的“撤消”工具。

要将并集或交集分离回各个零件:

  1. 选择并集或交集。
  2. 点击 分离。零件将分离回它们的原始状态。

游戏中实体建模

当游戏运行时,您也可以使用 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
一个从另一个区块中减去的区块。

为了进一步演示,下一个代码示例将 mainPartotherParts 数组中的零件的几何体结合在一起,然后销毁参与该操作的原始零件。您可以将对 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 的布尔函数有所不同:

  • 输出是一个实例数组,而不是单个实例。
  • 输入零件不需要父类到场景中,允许进行后台操作。
  • SplitApart 选项设置为 true(默认值)时,每个不同的主体将以其自己的 PartOperation/MeshPart 返回。
  • 所有返回的零件都在主零件的坐标空间中,因此它们的 PVInstance.Origin 位置与主零件相同。这使得网格的顶点在操作之前相对于对象保持在相同位置,但这也意味着返回零件的 (0, 0, 0) 不一定位于其主体的中心。

SweepPartAsync()

GeometryService:SweepPartAsync() 函数创建一个 MeshPart,其形状是输入零件在一组给定的 CFrame 位置中拖动的结果。此函数在进行切片和切割交互操作时非常有用。

输入可以是 PartPartOperationMeshPart。结果的形状被定义为每对相邻 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 中运行此示例:

  1. 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 = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- 从被击中实例中减去扫掠
    local subtractSuccess, newParts = pcall(function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  2. 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 = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- 从被击中实例中减去扫掠
    local subtractSuccess, newParts = pcall(function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  3. ReplicatedStorage 中创建一个名为 DrawCurveEventRemoteEvent

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 站点会产生单独的碎片。所有其他站点将被合并成一个零件。

形状为 Roblox Studio 标志的模板部分
零件(深灰色)和模板部分
模板形状的碎片结果
碎片脚本结果

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

行为细节

实体建模结果考虑

颜色和 UV

实体建模后结果零件的颜色来源于两个地方:面颜色和零件的 Color

  • 如果结果是 PartOperation,它将具有您在 Studio 中选择的第一部分的 Color,但 Studio 默认使用面颜色以保持每个面在操作之前与它的颜色相同。您可以启用其 UsePartColor 属性以覆盖此行为,使整个结果呈现为单一颜色。
  • 如果结果是 MeshPart,它的 Color 将是白色,并且面颜色将始终显示出来。您可以通过更改结果零件的 Color 来调整其色调,但其颜色会与面颜色混合(相乘)。这改变了结果的色调,而不是完全覆盖面颜色。如果您想完全控制输出的颜色,最好先将输入调整为白色。

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 个。如果无法做到这一点,通常在数千个不重叠组件的情况下,操作将产生错误。

状态良好的 MeshPart
简化前
网格质量明显降低的 MeshPart
简化后
©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。