Mô hình rắn là quá trình kết hợp các phần lại với nhau theo những cách độc đáo để tạo thành những hình dạng phức tạp hơn. Điều này bao gồm các phép toán boolean hợp nhất, phép trừ, và giao điểm, được biết đến với tên gọi là Hình học rắn xây dựng (CSG). Bạn có thể thực hiện mô hình rắn ở mọi nơi: trong Studio, plugins, thậm chí trong-game cả trên server lẫn client.
Ngoài việc sử dụng CSG boolean, mô hình rắn cũng hỗ trợ lưới, miễn là chúng là đóng nước, và các phép toán như sweep và fragment cho phép bạn và người chơi tạo ra các tương tác gameplay độc đáo bằng cách cắt, xé và vỡ hình học.


Hình học đóng nước
Có ba yếu tố cơ bản của lưới:
- Đỉnh - Một điểm đơn lẻ trên lưới.
- Cạnh - Một đường thẳng kết nối hai đỉnh.
- Mặt - Một khu vực bề mặt giữa ba hoặc nhiều đỉnh.



Các phép toán mô hình rắn chỉ có thể làm việc với hình học đóng nước; thực tế, "rắn" và "đóng nước" là đồng nghĩa. Theo nghĩa kỹ thuật, một lưới được coi là đóng nước nếu nó là khép kín, manh mạch, và không tự giao nhau. Những thuật ngữ này có định nghĩa nghiêm ngặt, nhưng dưới đây là một số quy tắc đơn giản:
- Mỗi mặt phải có một bên 'bên trong' và một bên 'bên ngoài'. Những điều này được xác định bằng thứ tự xoắn của mặt, đó là thứ tự của ba đỉnh của nó.
- Mỗi cạnh phải được chia sẻ bởi chính xác hai tam giác. Điều này có nghĩa là không thể có bất kỳ lỗ nào trong lưới vì các cạnh ở viền của một lỗ chỉ có một tam giác.
- Các mặt không thể đi qua các mặt khác.
- Các tam giác liền kề phải đồng ý về phía nào là 'bên ngoài'.
- Mỗi đỉnh phải có chính xác một quạt của các tam giác liền kề.

Hệ thống mô hình rắn có thể tự động sửa chữa một số vấn đề nhỏ với một lưới nhất định, nhưng nói chung, các cuộc gọi API sẽ thất bại nếu lưới không đóng nước. Không có cách nào phù hợp cho tất cả để sửa chữa một lưới không đóng nước đã tồn tại, nhưng có một số plugin Blender có thể giúp, chẳng hạn như 3D Print Toolbox và Mesh Repair Tools. Một tùy chọn khác, Meshlab cũng có các công cụ rất hữu ích được tích hợp để cố gắng tạo thành một lưới manh mạch, đây là yêu cầu chính để một lưới đóng nước.
Một cách để thấy rằng một lưới sẽ rất khó khăn để làm thành đóng nước là nhìn vào nó trong Studio từ tất cả các góc độ, sau đó cố gắng kích hoạt và vô hiệu hóa thuộc tính MeshPart.DoubleSided của lưới. Nếu bạn có thể thấy bất kỳ sự khác biệt nào, thì lưới chỉ là một lớp vỏ và các công cụ được đề cập phía trên sẽ không hoạt động vì chúng không thể đoán được không gian nằm bên trong và bên ngoài lưới. Tuy nhiên, nếu bạn chỉ muốn một lưới mỏng và không quan trọng việc giữ kích thước của lưới hoàn toàn giống nhau, bạn có thể sử dụng modifiers của Blender để làm dày lớp vỏ một cách nhẹ nhàng thành một lưới đóng nước.

Mô hình rắn trong Studio
Bạn có thể thực hiện ba phép toán boolean cơ bản bằng cách sử dụng bốn công cụ trong thanh công cụ tab Model.

| Công cụ | Phím tắt | Mô tả |
|---|---|---|
| Hợp nhất | ShiftCtrlG (Windows) Shift⌘G (Mac) | Kết hợp hai hoặc nhiều phần lại với nhau để tạo thành một sự hợp nhất rắn duy nhất. |
| Giao nhau | ShiftCtrlI (Windows) Shift⌘I (Mac) | Giao nhau các phần chồng lấn thành một giao điểm rắn duy nhất. |
| Phép trừ | ShiftCtrlN (Windows) Shift⌘N (Mac) | Thực hiện phép trừ các phần, hữu ích để tạo lỗ và các vết lõm. |
| Tách riêng | ShiftCtrlU (Windows) Shift⌘U (Mac) | Tách sự hợp nhất hoặc giao điểm trở lại thành các phần riêng lẻ của nó. |
Hợp nhất các phần
Công cụ Hợp nhất kết hợp hai hoặc nhiều phần lại với nhau để tạo thành một UnionOperation rắn duy nhất.


Để kết hợp các phần lại với nhau thành một sự hợp nhất:
- Chọn tất cả các phần mà bạn muốn kết hợp lại.
Giao nhau các phần
Công cụ Giao nhau giao nhau các phần chồng lấn thành một IntersectOperation rắn duy nhất.


Để giao nhau các phần chồng lấn lại với nhau:
- Chọn tất cả các phần mà bạn muốn giao nhau.
- Nhấn nút Giao nhau. Tất cả các phần sẽ được kết hợp thành một IntersectOperation rắn với tên Giao điểm.
Phép trừ các phần
Công cụ Phép trừ thực hiện phép trừ một phần để khi nó hợp nhất với một phần khác, hình dạng của phần bị phủ định sẽ được trừ bỏ từ phần kia.


Để thực hiện phép trừ một phần từ các phần chồng lấn khác:
- Chọn phần mà bạn muốn phủ định từ các phần khác.
- Nhấn Phép trừ. Phần trở thành phần bị phủ định và một biểu tượng phủ định xuất hiện trong Explorer. Phần trở nên trong suốt với màu đỏ để chỉ ra trạng thái của nó.
- Chọn cả phần bị phủ định và các phần bạn muốn trừ nó khỏi.
- Nhấn Hợp nhất. Phần bị phủ định sẽ được cắt ra khỏi các phần chồng lấn đã bao gồm.
Thẻ này được công khai cho scripting, vì vậy bạn cũng có thể phủ định các phần bằng cách thêm thẻ rbxNegate từ một script hoặc plugin. NegateOperation không còn được sử dụng nữa.
Tách riêng các hợp nhất hoặc giao điểm
Công cụ Tách riêng tách một UnionOperation trở lại thành các phần riêng lẻ của nó, về cơ bản hoạt động như một công cụ "hoàn tác" cho các hợp nhất và giao điểm.
Để tách một hợp nhất hoặc giao điểm trở lại thành các phần riêng:
- Chọn hợp nhất hoặc giao điểm.
- Nhấn Tách riêng. Các phần sẽ tách ra trở lại thành dạng nguyên thủy của chúng.
Mô hình rắn trong game
Bạn cũng có thể thực hiện các phép toán mô hình rắn trong khi game đang chạy bằng cách sử dụng các hàm GeometryService.
UnionAsync(), IntersectAsync(), và SubtractAsync()
Tương tự như các công cụ phép toán boolean cơ bản tích hợp sẵn trong Studio, bạn có thể sử dụng các hàm GeometryService như UnionAsync(), IntersectAsync(), và SubtractAsync() để thực hiện các phép toán boolean cơ bản trong khi game đang chạy. Ví dụ, đoạn mã sau sử dụng SubtractAsync() để trừ thể tích của một phần từ một phần khác.
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

Để minh họa thêm, mẫu mã tiếp theo kết hợp hình học của mainPart và các phần trong mảng otherParts, sau đó nó tiêu diệt các phần gốc tham gia vào phép toán. Bạn có thể thay thế cuộc gọi đến UnionAsync() bằng IntersectAsync() hoặc SubtractAsync() để thực hiện các phép toán boolean khác.
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,
}
-- Thực hiện phép toán hợp nhất trong pcall() vì nó là không đồng bộ
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Lặp lại qua các phần kết quả để tái bố trí lại
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Phá hủy các phần gốc
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Chừng nào tất cả các đầu vào được cấu thành từ các phần cơ bản thay vì MeshParts, các phép toán hợp nhất, giao nhau và trừ sẽ tạo ra một PartOperation với hai mẩu dữ liệu được lưu trữ: một cây các phép toán CSG được gọi là CSGTree, và một lưới để render.
So với BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), các hàm boolean của GeometryService khác biệt như sau:
- Đầu ra là một mảng các thể hiện thay vì một thể hiện đơn lẻ.
- Các phần đầu vào không cần phải được cha vào cảnh, cho phép các phép toán nền.
- Khi tùy chọn SplitApart được thiết lập là true (mặc định), mỗi thân hình duy nhất sẽ được trả về trong PartOperation/MeshPart riêng của nó.
- Tất cả các phần được trả về đều nằm trong không gian tọa độ của phần chính, vì vậy các vị trí PVInstance.Origin của chúng giống với phần chính. Điều này giữ cho các đỉnh của lưới ở cùng vị trí liên quan đến đối tượng như trước phép toán, nhưng cũng có nghĩa là (0, 0, 0) của một phần trả về không nhất thiết phải ở giữa cơ thể của nó.
SweepPartAsync()
Hàm GeometryService:SweepPartAsync() tạo ra một MeshPart có hình dạng của phần đầu vào kéo qua một tập hợp các vị trí CFrame nhất định. Hàm này có thể rất hữu ích để thực hiện các tương tác cắt và xé.
Đầu vào có thể là một Part, PartOperation, hoặc MeshPart. Hình dạng của kết quả được xác định là sự hợp nhất của các vỏ lồi của mỗi cặp CFrames liền kề; nếu chỉ một CFrame được cung cấp, kết quả sẽ là một vỏ lồi của phần đầu vào.
Để minh họa cách hàm này hoạt động, đoạn mã sau kéo một quả bóng qua một tập hợp các vị trí CFrame để tạo thành một hình xoáy:
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

Ví dụ

Ví dụ này sử dụng GeometryService:SweepPartAsync() để đạt được một tính năng gameplay cắt bằng kiếm hoặc súng laser, trong đó chuyển động của thanh kiếm dựa trên vị trí chuột của người chơi. Chuyển động chuột của người dùng được ghi lại dưới dạng một danh sách các CFrames, SweepPartAsync() xây dựng một lưới cắt từ dữ liệu này, sau đó lưới cắt sẽ được trừ khỏi phần mà nó va chạm.
Để chạy ví dụ này trong Studio:
Tạo một Script trong ServerScriptService để thực hiện tất cả các phép toán mô hình rắn.
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-- Hiển thị lưới cắtsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Trừ lưới cắt khỏi thể hiện bị va chạmlocal 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)Tạo một LocalScript trong StarterPlayerScripts để xử lý input từ người dùng.
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-- Hiển thị lưới cắtsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Trừ lưới cắt khỏi thể hiện bị va chạmlocal 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)Tạo một RemoteEvent trong ReplicatedStorage có tên DrawCurveEvent.
FragmentAsync()
Các hàm GeometryService:FragmentAsync() và GeometryService:GenerateFragmentSites() cho phép bạn phá vỡ một phần thành các mảnh với hình dạng tự nhiên. GeometryService:FragmentAsync() sử dụng phân rã voronoi để chia một phần đơn thành nhiều thể hiện MeshPart theo mẫu các điểm được truyền vào, trong khi GeometryService:GenerateFragmentSites() là một hàm trợ giúp tạo ra các điểm được gọi là các trang web voronoi để truyền vào FragmentAsync().
Để minh họa cách các hàm này hoạt động cùng nhau, đoạn mã sau tạo ra các trang web voronoi để phân khối phần:
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

Ví dụ

Đoạn mã sau đây phân mảnh một khu vực của một phần, được xác định bởi một vị trí và bán kính. Vị trí này có thể đến từ va chạm vật lý hoặc raycast từ một người chơi.
Phần tử đầu tiên của mảng trang web mà GenerateFragmentSites() tạo ra sẽ là một mảng con của tất cả các trang web nằm ngoài bán kính yêu cầu. Nếu bạn muốn thực hiện một cái gì đó cụ thể với phần 'không bị vỡ' còn lại của phần, bạn có thể tìm thấy phần đó bằng cách kiểm tra fragments[i].Index == 1 khi lặp qua các kết quả của 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("Thất bại trong việc phá vỡ:"..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
Đoạn mã sau đây phá vỡ các mảnh ra khỏi phần đầu tiên trong một hình dạng được xác định bằng cách sử dụng một phần thứ hai như một khuôn mẫu. Chỉ các trang web voronoi trong phần thứ hai sẽ tạo ra các mảnh riêng. Tất cả các trang web khác sẽ có các tế bào của chúng hợp nhất thành một phần duy nhất.


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("Thất bại trong việc phá vỡ:"..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
Đoạn mã sau đây là một trường hợp sử dụng niche nhiều hơn, nhưng nó chứng minh sức mạnh của dữ liệu chỉ số được trả về từ GeometryService:FragmentAsync().
Ví dụ, nhiều nơi chứa các tòa nhà được hình thành từ nhiều phần khối không hợp nhất. Nếu một quả lựu đạn, viên đạn pháo, hoặc búa tạ làm hỏng nó, bạn sẽ muốn tất cả các phần tường bị phân mảnh. Đoạn mã này phân mảnh tất cả các phần gần đó, sau đó hợp nhất các mảnh của các phần khác nhau để hoàn toàn ẩn các mối nối.
Điều này liên quan đến nhiều phép toán Async, vì vậy nó có thể không phù hợp để sử dụng trong game như một phản hồi ngay lập tức cho input của người dùng, chẳng hạn như công cụ búa tạ.


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("Thấy thất bại trong việc phá vỡ:"..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("Thất bại trong việc hợp nhất:"..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
Đoạn mã sau đây là một sự thay thế Luau gần giống cho GeometryService:GenerateFragmentSites(). Nếu bạn muốn có hành vi tương tự như GeometryService:GenerateFragmentSites() nhưng muốn thực hiện một vài thay đổi nhẹ, bạn có thể sử dụng điều này như một điểm khởi đầu.
Nó sử dụng một lưới điểm jittered và đảm bảo rằng khu vực phân mảnh được xử lý tốt, không giống như các điểm hoàn toàn ngẫu nhiên.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Sử dụng điều này để điều chỉnh mức độ jitter
if (origin and not radius) or (radius and not origin) then
warn("Hoặc cả origin và radius nên được cung cấp, hoặc không cái nào.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized có nghĩa là đừng phân mảnh toàn bộ phần, chỉ một phần.
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
Bảo tồn các ràng buộc
Nếu một phần đầu vào có các ràng buộc hoặc các gắn kết mà bạn muốn bảo tồn, bạn có thể chuyển các ràng buộc đó sang các phần kết quả. Có thể sẽ rất khó khăn để xác định phần đầu ra nào thì một ràng buộc nên được gắn vào, vì vậy tốt nhất là sử dụng GeometryService:CalculateConstraintsToPreserve() để tạo ra một bảng các khuyến nghị mà bạn có thể lặp qua và áp dụng.
Để minh họa, đoạn mã sau đây thực hiện một phép trừ, lặp qua các phần kết quả để tái bố trí lại và vị trí của chúng, sau đó tính toán bảng các ràng buộc và các gắn kết để bảo tồn hoặc bỏ qua trước khi tiêu diệt tất cả các phần gốc.
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,
}
-- Thực hiện phép trừ trong pcall() vì nó là không đồng bộ
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Lặp qua các phần kết quả để tái bố trí lại
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Tính toán các ràng buộc/gắn kết để bảo tồn hoặc bỏ qua
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Bảo tồn các ràng buộc/gắn kết dựa trên bảng khuyến nghị
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
-- Phá hủy các phần gốc
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Chi tiết hành vi
Nếu phần chính đang di chuyển trong khi tính toán phép toán, bạn có thể đặt các phần kết quả vào CFrame đã cập nhật của phần chính, vì các phần trả về nằm trong cùng không gian tọa độ với phần chính.
Có các hàm để hoán đổi dữ liệu lưới của một thể hiện, giúp dễ dàng sử dụng hình học của phép toán trong khi vẫn duy trì các thuộc tính, thuộc tính, thẻ, và con của phần chính, chẳng hạn như Attachments, Constraints, ParticleEmitters, các đối tượng ánh sáng, và các decal. Cách tiếp cận này cũng giúp tránh "nhấp nháy" có thể xảy ra khi hoàn toàn thay thế PartOperation gốc bằng một cái khác.
- Nếu sử dụng phương pháp này với một PartOperation làm phần chính và không có phần nào khác là MeshParts, bạn có thể thay thế hình học của một PartOperation khác thông qua SubstituteGeometry().
- Nếu phần chính là một MeshPart, bạn có thể sử dụng MeshPart:ApplyMesh().
Có thể gọi các hàm này trên client, nhưng với một số giới hạn. Đầu tiên, nó phải được thực hiện với các đối tượng được tạo trên client. Thứ hai, không có sự nhân bản nào từ client đến server.
Các thuộc tính sau từ phần chính được áp dụng cho các PartOperations hoặc MeshParts kết quả:
- Va chạm: BasePart.AudioCanCollide, BasePart.CanCollide
Các cân nhắc về kết quả mô hình rắn
Màu sắc và UV
Màu sắc của các phần mà bạn đã tạo ra sau khi mô hình rắn có nguồn gốc từ hai nơi: màu sắc bề mặt và màu sắc Color của phần.
- Nếu kết quả là một PartOperation, nó sẽ có Color của phần đầu tiên bạn chọn trong Studio, nhưng Studio sử dụng màu sắc bề mặt theo mặc định để giữ cho mỗi mặt có cùng màu như trước khi thực hiện phép toán. Bạn có thể kích hoạt thuộc tính UsePartColor của nó trong Studio để ghi đè hành vi này và làm cho toàn bộ kết quả có một màu duy nhất.
- Nếu kết quả là một MeshPart, Color của nó sẽ là màu trắng, và màu sắc bề mặt sẽ luôn hiện lên. Bạn có thể điều chỉnh màu sắc của các phần mà bạn đã tạo ra bằng cách thay đổi Color của chúng, nhưng nó sẽ được trộn (nhân) với màu sắc bề mặt. Điều này tôn vinh kết quả thay vì ghi đè hoàn toàn màu sắc bề mặt. Nếu bạn muốn kiểm soát hoàn toàn màu sắc của đầu ra, tốt nhất hãy làm cho các đầu vào có màu trắng trước.
UV cũng được xử lý khác nhau tùy thuộc vào loại kết quả:
- PartOperations luôn có UV được bọc hộp, có nghĩa là mỗi mặt sẽ có vật liệu/texture/decal từ một hướng (một trong -x, +x, -y, +y, -z, +z) áp dụng lên nó. Điều này có thể làm căng các texture.
- MeshParts không được bọc hộp. UV của lưới phần chính được sử dụng. Do Roblox hiện không có hỗ trợ cho nhiều vật liệu, UV của các mặt phát sinh từ các phần khác được cấp UV bằng (0, 0). Để có kết quả tốt nhất, hãy đảm bảo pixel (0, 0) của texture của bạn có một màu hợp lý.
Góc làm mịn
Thuộc tính SmoothingAngle của một phần được mô hình hóa rắn làm mịn các góc giữa các bề mặt liền kề cùng màu. Giá trị lớn hơn tạo ra một vẻ ngoài mịn màng hơn trong khi giá trị nhỏ hơn tạo ra một vẻ ngoài thô hơn với nhiều cạnh sắc nét hơn.
Trong khi giá trị giữa 30 và 70 độ thường tạo ra kết quả tốt, các giá trị giữa 90 và 180 không được khuyến nghị vì chúng có thể gây ra hiệu ứng "bóng tối" trên các hợp nhất và giao điểm có cạnh sắc.


Đơn giản hóa phần
Nếu một phép toán mô hình rắn sẽ dẫn đến bất kỳ phần nào có hơn 20,000 tam giác, chúng sẽ được đơn giản hóa xuống còn 20,000. Nếu điều đó không thể thực hiện được, thường là trong một trường hợp có hàng ngàn thành phần không chồng lấn, phép toán sẽ dẫn đến một lỗi.

