Mô hình rắn

*Nội dung này được dịch bằng AI (Beta) và có thể có lỗi. Để xem trang này bằng tiếng Anh, hãy nhấp vào đây.

Mô hình rắn là quá trình kết hợp BaseParts lại với nhau theo những cách độc đáo để tạo thành các 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, giao nhau, và trừ mà thường được biết đến với tên gọi Hình học Rắn Tạo dựng (CSG). Bạn có thể thực hiện mô hình rắn trong Studio, plugin, và trong trò chơi trên cả máy chủ và máy khách.

Ngoài CSG boolean, mô hình rắn cũng hỗ trợ các lưới, miễn là chúng không thấm nước, và các phép toán như quétphân mảnh cho phép bạn và người chơi của bạn cắt, xé và phá vỡ hình học để tạo ra các tương tác gameplay độc đáo.

MeshPart được tạo ra với SweepPartAsync()
MeshPart bị phá vỡ với FragmentAsync()

Hình học không thấm nước

Các phép toán mô hình rắn chỉ có thể hoạt động với hình học không thấm nước. Về mặt kỹ thuật, một lưới được coi là không thấm nước có nghĩa là nó đã được đóng kín, có mặt, và không tự giao nhau. Những thuật ngữ này có định nghĩa nghiêm ngặt, nhưng đâ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ởi 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 xung quanh 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ề bên nào là bên "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ề.

Mỗi ví dụ sau đây là không thấm nước vì lý do đã nêu:

Hệ thống mô hình rắn có khả năng tự động sửa chữa một số vấn đề nhỏ cụ thể với một lưới, nhưng nói chung, các cuộc gọi API sẽ thất bại nếu lưới không thấm 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 thấm 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 ToolboxMesh Repair Tools. Là một lựa chọn khác, Meshlab cũng có các công cụ rất hữu ích được tích hợp sẵn để cố gắng làm cho lưới có mặt, đây là yêu cầu chính để một lưới không thấm nước.

Một cách để thấy rằng một lưới sẽ rất khó để làm cho không thấm nước là nhìn nó trong Studio từ mọi góc độ, sau đó thử kích hoạt và vô hiệu hóa thuộc tính MeshPart.DoubleSided. 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ập ở trên sẽ không hoạt động vì chúng không thể đoán được không gian bên trong so với bên ngoài lưới. Tuy nhiên, nếu tất cả những gì bạn muốn là 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 Solidify modifier của Blender để làm dày lớp vỏ thành một lưới không thấm nước.

DoubleSided = false

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ụ Mô hình.

Các công cụ mô hình rắn được làm nổi bật trong thanh công cụ của Studio.
Công cụTổ hợp phímMô tả
Hợp nhấtShiftCtrlG (Windows)
ShiftG (Mac)
Kết hợp hai hoặc nhiều phần lại với nhau để tạo thành một hợp nhất rắn duy nhất.
Giao nhauShiftCtrlI (Windows)
ShiftI (Mac)
Giao nhau các phần chồng chéo thành một giao nhau rắn duy nhất.
TáchShiftCtrlU (Windows)
ShiftU (Mac)
Tách hợp nhất hoặc giao nhau trở lại thành các phần riêng lẻ của nó.
TrừShiftCtrlN (Windows)
ShiftN (Mac)
Trừ các phần, hữu ích cho việc tạo lỗ và các chỗ lõm.

Hợp nhất

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.

Một khối và một hình trụ.
Các phần riêng lẻ
Một khối và một hình trụ được kết hợp thành một đối tượng.
Kết quả hợp nhất

Để kết hợp các phần lại với nhau thành một hợp nhất:

  1. Chọn tất cả các phần mà bạn muốn kết hợp lại.
  2. Nhấp vào nút Hợp nhất. Tất cả các phần sẽ kết hợp thành một UnionOperation rắn với tên là Hợp nhất.

Giao nhau

Công cụ Giao nhau giao nhau các phần chồng chéo thành một IntersectOperation rắn duy nhất.

Một khối và một hình trụ.
Các phần riêng lẻ
Một khối và một hình trụ được kết hợp thành một đối tượng.
Kết quả giao nhau

Để giao nhau các phần chồng chéo lại với nhau:

  1. Chọn tất cả các phần mà bạn muốn giao nhau.
  2. Nhấp vào nút Giao nhau. Tất cả các phần sẽ kết hợp thành một IntersectOperation rắn với tên là Giao nhau.

Trừ

Công cụ Trừ trừ một phần để khi nó được hợp nhất với một phần khác, hình dạng của phần bị trừ sẽ được trừ khỏi phần khác.

Một khối và một hình trụ.
Khối và hình trụ bị trừ
Một khối và một hình trụ được kết hợp thành một đối tượng.
Kết quả trừ

Để trừ một phần từ các phần chồng chéo khác:

  1. Chọn phần mà bạn muốn trừ khỏi các phần khác.
  2. Nhấp vào Trừ. Phần sẽ trở nên trong suốt với màu đỏ để chỉ ra trạng thái của nó.
  3. Chọn cả phần bị trừ và các phần mà bạn muốn trừ nó từ.
  4. Nhấp vào Hợp nhất. Phần bị trừ sẽ bị cắt ra khỏi các phần chồng chéo đã bao gồm.

Tách

Công cụ Tách tách một UnionOperation trở lại thành các phần riêng lẻ của nó, về cơ bản phục vụ như một công cụ "hoàn tác" cho các hợp nhất và giao nhau.

Để tách một hợp nhất hoặc giao nhau trở lại thành các phần riêng lẻ:

  1. Chọn hợp nhất hoặc giao nhau.
  2. Nhấp vào Tách. Các phần sẽ tách trở lại thành hình dạng ban đầu của chúng.

Mô hình rắn trong trò chơi

Bạn cũng có thể thực hiện các phép toán mô hình rắn trong khi trò chơi đang chạy hoặc từ một plugin thông qua các phương thức GeometryService.

Các phép toán cốt lõi

Tương tự như các công cụ hoạt động tích hợp sẵn của Studio, bạn có thể sử dụng các phương thức GeometryService như UnionAsync(), IntersectAsync(), và SubtractAsync() để thực hiện các phép toán boolean cơ bản. 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.

Trừ 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, đoạn 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ó phá hủy các phần gốc liên quan đến 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.

Kết Hợp Hình Học Của Nhiều Phần
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
-- Lặp qua các phần kết quả để thay đổi cha/đặt lại vị trí
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

Trong khi không có phương thức cấp engine cho việc trừ phần, bạn có thể thêm thẻ rbxNegate trong một đoạn mã hoặc plugin để thực hiện việc trừ tương đương với nút trên thanh công cụ Trừ của Studio.

Trừ Phần Qua Đoạn Mã
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")

So với BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), các phương thức GeometryService khác nhau như sau:

  • Đầu ra là một mảng các thể hiện thay vì một thể hiện duy nhất.
  • Các phần đầu vào không cần phải được gán vào cảnh, cho phép thực hiện các phép toán nền.
  • Khi tùy chọn SplitApart được đặt thành true (mặc định), mỗi cơ thể riêng biệt sẽ được trả về trong PartOperation/MeshPart của riêng nó.
  • Tất cả các phần được trả về đều ở 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 như của phần chính. Điều này giữ cho các đỉnh của lưới ở cùng vị trí tương đối với đối tượng như trước khi thực hiện phép toán, nhưng cũng có nghĩa là (0, 0, 0) của một phần được trả về không nhất thiết phải ở trung tâm của cơ thể của nó.

Quét phần

Phương thức SweepPartAsync() tạo ra một MeshPart có hình dạng của phần đầu vào được kéo qua một tập hợp các vị trí CFrame. Phương thức này có thể rất hữu ích cho việc 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à hợp nhất của các hình chóp lồi của mỗi cặp CFrames liền kề; nếu chỉ cung cấp một CFrame duy nhất, kết quả sẽ là một hình chóp lồi của phần đầu vào.

Để minh họa cách phương thức này hoạt động, đoạn mã sau quét một quả bóng qua một tập hợp các vị trí CFrame để tạo ra một hình xoắn ốc:

Quét Phần Dọc Theo Hình Xoắn Ốc
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
Một hình xoắn ốc được tạo ra bằng cách quét một hình cầu.
Một hình dạng cong trong suốt bị trừ khỏi một khối.

Ví dụ này sử dụng SweepPartAsync() để đạt được tính năng gameplay cắt bằng kiếm hoặc súng laser, nơi chuyển động của 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 được trừ khỏi phần bị va chạm.

Để chạy ví dụ này trong Studio:

  1. Tạo Script sau 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ị quét
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Trừ quét khỏi thể hiện bị va chạm
    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. Tạo LocalScript sau trong StarterPlayerScripts để xử lý đầu vào của 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ị quét
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Trừ quét khỏi thể hiện bị va chạm
    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. Tạo một RemoteEvent trong ReplicatedStorage có tên là DrawCurveEvent.

Phân mảnh

Các phương thức FragmentAsync()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. FragmentAsync() sử dụng phân rã voronoi để chia một phần duy nhất thành nhiều thể hiện MeshPart theo mẫu các điểm được truyền vào, trong khi GenerateFragmentSites() là một phương thức trợ giúp tạo ra các điểm được gọi là các vị trí voronoi để truyền vào FragmentAsync().

Đoạn mã sau tạo ra các vị trí voronoi để phân mảnh một phần khối:

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
Một khối bị đập vỡ thành các mảnh.
Một khối với góc bị phân mảnh thành các mảnh.

Đoạn mã sau 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í có thể thường đến từ một va chạm vật lý hoặc một raycast từ một người chơi.

Phần đầu tiên của mảng vị trí mà GenerateFragmentSites() tạo ra sẽ là một mảng con của tất cả các vị trí nằm ngoài bán kính yêu cầu. Nếu bạn muốn làm điều 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 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")
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("Không thể phân mảnh:"..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 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 làm khuôn mẫu. Chỉ các vị trí voronoi nằm trong phần thứ hai sẽ tạo ra các mảnh riêng biệt. Tất cả các vị trí khác sẽ có các ô của chúng được kết hợp thành một phần duy nhất.

Khuôn mẫu có hình dạng của logo Roblox Studio
Phần (màu xám đậm) và phần khuôn mẫu
Kết quả của một phân mảnh theo hình dạng của một khuôn mẫu
Kết quả của đoạn mã phân mảnh
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("Không thể phân mảnh:"..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 là một trường hợp sử dụng rất đặc biệt, nhưng nó thể hiện sức mạnh của dữ liệu chỉ mục được trả về từ 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 được hợp nhất. Nếu một quả lựu đạn, viên đạn đại bác, hoặc búa tạ làm hỏng nó, bạn sẽ muốn tất cả các phần tường được 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 lại với nhau để hoàn toàn ẩn các đường nối.

Điều này liên quan đến nhiều phép toán Async(), vì vậy có thể không phù hợp để sử dụng trong trò chơi như một phản hồi ngay lập tức với đầu vào của người dùng, chẳng hạn như một công cụ búa tạ.

Một hàng khối
Một hàng khối
Một hàng khối bị phân mảnh
Mỗi mảnh có thể xuất phát từ nhiều phần đầu vào khác nhau
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("Không thể phân mảnh:"..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("Không thể 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 là một thay thế Luau gần như giống hệt cho GenerateFragmentSites(). Nếu bạn muốn hành vi tương tự như GenerateFragmentSites() nhưng muốn thực hiện một số thay đổi nhỏ, bạn có thể sử dụng điều này làm điểm khởi đầu.

Nó sử dụng một lưới điểm bị rung, và đảm bảo rằng khu vực phân mảnh hoạt động 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 độ rung
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ó cái nào.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized có nghĩa là khô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 gắn kết mà bạn muốn bảo tồn, bạn có thể chuyển chúng sang các phần kết quả. Việc xác định phần nào trong số các phần đầu ra mà một ràng buộc nên được gán vào có thể rất tẻ nhạt, vì vậy nên sử dụng 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 thực hiện một phép toán trừ, lặp qua các phần kết quả để thay đổi cha và đặt lại vị trí các phần kết quả, sau đó tính toán một bảng các ràng buộc và gắn kết để bảo tồn hoặc bỏ trước khi phá hủy 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 toán trừ trong pcall() vì nó là bất đồ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ả để thay đổi cha/đặt lại vị trí
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ỏ
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

Kết quả mô hình rắn

Chi tiết hành vi

  • Các phần gốc vẫn nguyên vẹn sau một phép toán thành công, và các phần được trả về không có Parent được thiết lập. Trong hầu hết các trường hợp, nên gán các phần được trả về vào cùng một nơi với phần chính, sau đó Destroy() các phần gốc.

  • Nếu phần chính đang di chuyển trong quá trình 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 được trả về ở cùng không gian tọa độ với phần chính.

  • 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 trường hợp có hàng ngàn thành phần không chồng chéo, phép toán sẽ dẫn đến một lỗi.

  • Có các phương thức để thay thế 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à decal. Cách tiếp cận này cũng tránh được hiện tượng "nhấp nháy" tiềm năng khi hoàn toàn thay thế PartOperation gốc bằng một cái khác.

  • Có thể gọi các phương thức này trên máy khách, nhưng với một số hạn chế. Đầu tiên, nó phải được thực hiện với các đối tượng được tạo trên máy khách. Thứ hai, không có sự sao chép nào từ máy khách đến máy chủ.

Màu sắc và UV

Màu sắc của các phần kết quả sau khi mô hình rắn đến từ hai nơi: màu mặt và 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 mà bạn chọn trong Studio, nhưng Studio sử dụng màu 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 mặt sẽ luôn hiển thị. Bạn có thể điều chỉnh màu sắc của các phần kết quả bằng cách thay đổi Color của chúng, nhưng nó sẽ được trộn (nhân) với màu mặt. Điều này làm cho kết quả có màu sắc hơn là ghi đè hoàn toàn lên màu 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 là 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 ánh xạ 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 các -x, +x, -y, +y, -z, +z) được áp dụng cho nó. Điều này có thể làm căng các texture.
  • MeshParts không được ánh xạ hộp. UV của lưới phần chính được sử dụng. Vì Roblox hiện không hỗ trợ nhiều vật liệu, UV của các mặt xuất phát từ các phần khác được gán UV là (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àu sắc hợp lý.

Góc làm mịn

Thuộc tính SmoothingAngle của một phần được mô hình rắn làm mịn các góc giữa các bề mặt liền kề cùng màu. Một giá trị cao hơn tạo ra một vẻ ngoài mịn màng hơn trong khi một giá trị thấp 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 một giá trị giữa 30 và 70 độ thường tạo ra một 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" trên các hợp nhất và giao nhau với các cạnh sắc nét.

©2026 Roblox Corporation. Roblox, logo Roblox và Powering Imagination là các nhãn hiệu đã đăng ký và chưa đăng ký của chúng tôi tại Hoa Kỳ và các quốc gia khác.