實體建模是將 BaseParts 以獨特的方式結合在一起以形成更複雜的形狀的過程。這包括常被稱為建構實體幾何(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) | 將重疊的零件交集成一個實體交集。 |
| 分離 | ShiftCtrlU (Windows) Shift⌘U (Mac) | 將聯集或交集分離回其各個零件。 |
| 減法 | ShiftCtrlN (Windows) Shift⌘N (Mac) | 對零件進行減法,對於製作孔和凹陷非常有用。 |
聯集
聯集工具將兩個或更多零件結合在一起形成一個實體 UnionOperation。


要將零件結合成一個聯集:
- 選擇所有您想要結合的零件。
交集
交集工具將重疊的零件交集成一個實體 IntersectOperation。


要將重疊的零件交集在一起:
- 選擇所有您想要交集的零件。
減法
減法工具對一個零件進行減法,使其在與另一個零件 聯集 時,減去該零件的形狀。


要從其他重疊的零件中減去一個零件:
- 選擇您想要從其他零件中減去的零件。
- 點擊 減法。該零件變得半透明,並帶有紅色陰影以指示其狀態。
- 選擇減去的零件和您想要從中減去的零件。
- 點擊 聯集。減去的零件將從所包含的重疊零件中切出。
分離
分離工具將 UnionOperation 分離回其各個零件,基本上作為聯集和交集的「撤銷」工具。
要將聯集或交集分離回各個零件:
- 選擇聯集或交集。
- 點擊 分離。零件將分離回其原始形狀。
遊戲內實體建模
您也可以在遊戲運行時或通過 GeometryService 方法從 插件 執行實體建模操作。
核心操作
類似於 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
}
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雖然沒有引擎級方法可以進行零件 減法,但您可以在腳本或 插件 中添加 rbxNegate 標籤,以執行相當於 Studio 的 減法 工具欄按鈕的減法。
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")與 BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync() 相比,GeometryService 方法的區別如下:
- 輸出是一個實例的數組,而不是單個實例。
- 輸入零件不需要被父級到場景,允許在背景中操作。
- 所有返回的零件都在主零件的坐標空間中,因此它們的 PVInstance.Origin 位置與主零件相同。這保持了網格的頂點相對於物體的位置與操作之前相同,但這也意味著返回零件的 (0, 0, 0) 不一定位於其主體的中心。
掃描零件
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

此示例使用 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() 和 GenerateFragmentSites() 方法讓您將一個零件粉碎成自然形狀的碎片。FragmentAsync() 使用 Voronoi 分解將單個零件根據傳遞的點模式劃分為多個 MeshPart 實例,而 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")
local 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")
local 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以下腳本是一個更具針對性的用例,但它展示了從 FragmentAsync() 返回的索引數據的強大功能。
例如,許多地方包含由多個未聯合的方塊零件組成的建築。如果手榴彈、大砲彈或大錘損壞它,您會希望所有牆面零件都被碎片化。此腳本碎片化所有附近的零件,然後將不同零件的碎片聯合在一起以完全隱藏接縫。
這涉及多個 Async() 操作,因此可能不適合用於遊戲中作為對用戶輸入的即時響應,例如大錘工具。


local GeometryService = game:GetService("GeometryService")
local 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以下腳本是 GenerateFragmentSites() 的幾乎相同的 Luau 替代品。如果您想要類似於 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保留約束
如果輸入零件具有您想要保留的約束或附件,您可以將它們轉移到結果零件上。確定約束應該附加到哪個輸出零件可能很繁瑣,因此建議使用 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,因為返回的零件與主零件在同一坐標空間中。
如果實體建模操作會導致任何零件的三角形數量超過 20,000,則它們將被簡化為 20,000。如果無法做到,通常在有數千個不重疊的組件的情況下,操作將導致錯誤。
有方法可以替換實例的網格數據,使其更容易利用操作的幾何形狀,同時保持主零件的屬性、屬性、標籤和子物件,例如 Attachments、Constraints、ParticleEmitters、光源物件和貼圖。這種方法還可以避免完全用另一個 PartOperation 替換原始的潛在「閃爍」。
- 如果主零件是 MeshPart,則可以使用 MeshPart:ApplyMesh()。
可以在客戶端調用這些方法,但有一些限制。首先,必須使用 在客戶端創建 的物件。其次,客戶端到伺服器之間沒有複製。
顏色和 UV
實體建模後結果零件的顏色來自兩個地方:面顏色和零件的 Color。
- 如果結果是 PartOperation,則它將具有您在 Studio 中選擇的第一個零件的 Color,但 Studio 默認使用面顏色以保持每個面與操作之前的顏色相同。您可以在 Studio 中啟用其 UsePartColor 屬性以覆蓋此行為,使整個結果為單一顏色。
UV 也根據結果的類型以不同方式處理:
- MeshParts 不進行盒映射。主零件的網格的 UV 被使用。由於 Roblox 目前不支持多材料,因此來自其他零件的面將被賦予 (0, 0) 的 UV。為了獲得最佳效果,確保紋理的像素 (0, 0) 具有合理的顏色。
平滑角度
實體建模零件的 SmoothingAngle 屬性平滑相同顏色的相鄰表面之間的角度。較高的值會產生更平滑的外觀,而較低的值則會產生更粗糙的外觀,邊緣更尖銳。
雖然 30 到 70 度之間的值通常會產生良好的結果,但不建議使用 90 到 180 度之間的值,因為它們可能會在具有尖銳邊緣的聯集和交集中造成「陰影」效果。

