實體建模是將部件以獨特的方式結合在一起以形成更複雜的形狀。這包括布林運算 聯集、減去 和 交集,這些通常稱為構造實體幾何(CSG)。您可以在 Studio、插件,甚至 遊戲中 在伺服器和客戶端上執行實體建模。
除了布林 CSG,實體建模還支持網格,只要它們是防水的,以及像 掃描 和 碎片 的運算,這使您和您的玩家可以切割、切割和粉碎幾何體以獲得獨特的遊戲互動。


防水幾何
網格有三個基本元素:
- 頂點 - 網格上的單一點。
- 邊緣 - 連接兩個頂點的線。
- 面 - 三個或更多頂點之間的表面區域。



實體建模運算只能在 防水的 幾何上進行;事實上,“實體”和“防水”是同義詞。在技術術語中,網格防水意味著它是封閉的、可流形的,並且不自交。這些術語有嚴格的定義,但這裡有一些簡單的規則:
- 每個面必須有一個“內部”面和一個“外部”面。這由面向的繞行順序決定,即它的三個頂點的順序。
- 每個邊緣必須由恰好兩個三角形共享。這意味著網格中不能有任何洞,因為洞的邊緣周圍的邊緣只有一個三角形。
- 面不能穿過其他面。
- 相鄰的三角形必須就哪一側是“外部”面達成一致。
- 每個頂點必須有恰好一個相鄰三角形的扇形。

實體建模系統能自動修復網格中的特定小問題,但一般而言,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() 函數創建一個形狀為輸入部件穿過給定一組 CFrame 位置的 MeshPart。此函數對於執行切割和切割交互非常有用。
輸入可以是 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() 生成的站點數組的第一個元素將是一個內部數組,包括所有超出指定 半徑 的站點。如果您想要對部件的剩餘“未粉碎部分”執行特定操作,則可以在循環遍歷 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 默認會使用面顏色保持每個面與操作之前相同的顏色。您可以在 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。如果無法做到,通常在成千上萬個非重疊組件的情況下,該操作將導致錯誤。

