Pemodelan solid adalah proses menggabungkan BaseParts bersama dengan cara yang unik untuk membentuk bentuk yang lebih kompleks. Ini termasuk operasi boolean union, intersection, dan subtraction yang dikenal sebagai Geometri Solid Konstruktif (CSG). Anda dapat melakukan pemodelan solid di Studio, plugin, dan dalam permainan baik di server maupun klien.
Selain CSG boolean, pemodelan solid juga mendukung mesh, selama mereka kedap air, dan operasi seperti sweep dan fragment yang memungkinkan Anda dan pemain Anda memotong, memecah, dan menghancurkan geometri untuk interaksi gameplay yang unik.


Geometri Kedap Air
Operasi pemodelan solid hanya dapat bekerja dengan geometri kedap air. Dalam istilah teknis, mesh yang kedap air berarti bahwa ia tertutup, manifold, dan tidak saling berpotongan. Istilah-istilah ini memiliki definisi yang ketat, tetapi berikut adalah beberapa aturan sederhana:
- Setiap wajah harus memiliki sisi "dalam" dan sisi "luar". Ini ditentukan oleh urutan winding wajah, yaitu urutan dari tiga verteksnya.
- Setiap tepi harus dibagi oleh tepat dua segitiga. Ini berarti tidak boleh ada lubang di mesh karena tepi di sekitar tepi lubang hanya akan memiliki satu segitiga.
- Wajah tidak boleh melewati wajah lain.
- Segitiga yang berdekatan harus sepakat tentang sisi mana yang merupakan sisi "luar".
- Setiap verteks harus memiliki tepat satu kipas segitiga yang berdekatan.
Setiap contoh berikut adalah tidak kedap air karena alasan yang dicatat:




Sistem pemodelan solid dapat secara otomatis memperbaiki masalah kecil tertentu dengan mesh, tetapi secara umum, panggilan API akan gagal jika mesh tidak kedap air. Tidak ada cara satu ukuran untuk semua untuk memperbaiki mesh yang tidak kedap air yang ada, tetapi ada beberapa plugin Blender yang dapat membantu, seperti 3D Print Toolbox dan Mesh Repair Tools. Sebagai opsi lain, Meshlab juga memiliki alat yang sangat berguna yang terintegrasi untuk mencoba dan membuat mesh menjadi manifold, yang merupakan persyaratan utama agar mesh menjadi kedap air.
Salah satu cara untuk melihat bahwa mesh akan sangat sulit dibuat kedap air adalah dengan melihatnya di Studio dari semua sudut, kemudian coba aktifkan dan nonaktifkan properti MeshPart.DoubleSided. Jika Anda dapat melihat perbedaan, maka mesh tersebut hanyalah sebuah shell dan alat yang disebutkan di atas tidak akan berfungsi karena mereka tidak dapat menebak ruang mana yang ada di dalam versus di luar mesh. Namun, jika yang Anda inginkan hanyalah mesh tipis dan tidak penting untuk menjaga dimensi mesh tetap persis sama, Anda dapat menggunakan Solidify modifier di Blender untuk sedikit menebalkan shell menjadi mesh yang kedap air.


Pemodelan solid di Studio
Anda dapat melakukan tiga operasi boolean dasar menggunakan empat alat dalam toolbar tab Model.

| Alat | Shortcut | Deskripsi |
|---|---|---|
| Union | ShiftCtrlG (Windows) Shift⌘G (Mac) | Gabungkan dua atau lebih bagian bersama untuk membentuk satu union solid. |
| Intersect | ShiftCtrlI (Windows) Shift⌘I (Mac) | Potong bagian yang tumpang tindih menjadi satu intersection solid. |
| Separate | ShiftCtrlU (Windows) Shift⌘U (Mac) | Pisahkan union atau intersection kembali ke bagian individualnya. |
| Negate | ShiftCtrlN (Windows) Shift⌘N (Mac) | Negasikan bagian, berguna untuk membuat lubang dan lekukan. |
Union
Alat Union menggabungkan dua atau lebih bagian bersama untuk membentuk satu UnionOperation solid.


Untuk menggabungkan bagian bersama menjadi sebuah union:
- Pilih semua bagian yang ingin Anda gabungkan.
Intersect
Alat Intersect memotong bagian yang tumpang tindih menjadi satu IntersectOperation solid.


Untuk memotong bagian yang tumpang tindih bersama:
- Pilih semua bagian yang ingin Anda potong.
- Klik tombol Intersect. Semua bagian akan bergabung menjadi satu solid IntersectOperation dengan nama Intersection.
Negate
Alat Negate menegasikan sebuah bagian sehingga ketika digabungkan dengan bagian lain, bentuk bagian yang dinyatakan negatif akan dikurangi dari bagian lainnya.


Untuk mengurangi sebuah bagian dari bagian yang tumpang tindih lainnya:
- Pilih bagian yang ingin Anda negasikan dari bagian lainnya.
- Klik Negate. Bagian tersebut menjadi transparan dengan nuansa merah untuk menunjukkan statusnya.
- Pilih baik bagian yang dinyatakan negatif dan bagian yang ingin Anda kurangi darinya.
- Klik Union. Bagian yang dinyatakan negatif dipotong dari bagian yang tumpang tindih yang disertakan.
Separate
Alat Separate memisahkan UnionOperation kembali ke bagian individualnya, pada dasarnya berfungsi sebagai alat "undo" untuk union dan intersection.
Untuk memisahkan union atau intersection kembali ke bagian individual:
- Pilih union atau intersection.
- Klik Separate. Bagian-bagian tersebut akan terpisah kembali ke bentuk aslinya.
Pemodelan solid dalam permainan
Anda juga dapat melakukan operasi pemodelan solid saat permainan sedang berjalan atau dari plugin melalui metode GeometryService.
Operasi inti
Mirip dengan alat operasi bawaan Studio, Anda dapat menggunakan metode GeometryService seperti UnionAsync(), IntersectAsync(), dan SubtractAsync() untuk melakukan operasi boolean dasar. Misalnya, skrip berikut menggunakan SubtractAsync() untuk mengurangi volume satu bagian dari bagian lainnya.
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
endUntuk lebih menunjukkan, contoh kode berikut menggabungkan geometri dari mainPart dan bagian dalam array otherParts, kemudian menghancurkan bagian asli yang terlibat dalam operasi. Anda dapat mengganti panggilan ke UnionAsync() dengan IntersectAsync() atau SubtractAsync() untuk melakukan operasi boolean lainnya.
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
-- Loop melalui bagian hasil untuk mengubah induk/posisi
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Hancurkan bagian asli
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
endMeskipun tidak ada metode tingkat mesin untuk negasi bagian, Anda dapat menambahkan tag rbxNegate dalam skrip atau plugin untuk melakukan negasi yang setara dengan tombol toolbar Negate di Studio.
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")Dibandingkan dengan BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), metode GeometryService berbeda sebagai berikut:
- Output adalah array dari instance daripada satu instance tunggal.
- Bagian input tidak perlu diparent ke scene, memungkinkan untuk operasi latar belakang.
- Ketika opsi SplitApart diatur ke true (default), setiap tubuh yang berbeda akan dikembalikan dalam PartOperation/MeshPart masing-masing.
- Semua bagian yang dikembalikan berada dalam ruang koordinat bagian utama, sehingga posisi PVInstance.Origin mereka sama dengan bagian utama. Ini menjaga verteks mesh berada di posisi yang sama relatif terhadap objek seperti sebelum operasi, tetapi juga berarti bahwa (0, 0, 0) dari bagian yang dikembalikan tidak selalu berada di pusat tubuhnya.
Sweep part
Metode SweepPartAsync() membuat MeshPart yang memiliki bentuk bagian input yang diseret melalui serangkaian posisi CFrame. Metode ini bisa sangat berguna untuk melakukan interaksi pemotongan dan pemecahan.
Inputnya bisa berupa Part, PartOperation, atau MeshPart. Bentuk hasilnya didefinisikan sebagai union dari hull cembung dari setiap pasangan CFrames yang berdekatan; jika hanya satu CFrame yang diberikan, hasilnya akan menjadi hull cembung dari bagian input.
Untuk menunjukkan bagaimana metode ini bekerja, contoh kode berikut menyapu bola melalui serangkaian posisi CFrame untuk membuat spiral:
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

Contoh ini menggunakan SweepPartAsync() untuk mencapai fitur gameplay pemotongan pedang atau pistol laser, di mana gerakan pedang didasarkan pada posisi mouse pemain. Gerakan mouse pengguna dicatat sebagai daftar CFrames, SweepPartAsync() membangun mesh potongan dari data ini, kemudian mesh potongan tersebut dikurangi dari bagian yang terkena.
Untuk menjalankan contoh ini di Studio:
Buat Script berikut di ServerScriptService untuk melakukan semua operasi pemodelan solid.
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-- Visualisasikan sapuansweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Kurangi sapuan dari instance yang terkenalocal 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)Buat LocalScript berikut di StarterPlayerScripts untuk menangani input pengguna.
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-- Visualisasikan sapuansweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Kurangi sapuan dari instance yang terkenalocal 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)Buat RemoteEvent di ReplicatedStorage bernama DrawCurveEvent.
Fragment
Metode FragmentAsync() dan GenerateFragmentSites() memungkinkan Anda menghancurkan sebuah bagian menjadi potongan-potongan dengan bentuk yang terlihat alami. FragmentAsync() menggunakan dekomposisi voronoi untuk membagi satu bagian menjadi beberapa instance MeshPart sesuai dengan pola titik yang diberikan, sementara GenerateFragmentSites() adalah metode pembantu yang menghasilkan titik yang dikenal sebagai situs voronoi untuk diteruskan ke FragmentAsync().
Contoh kode berikut menghasilkan situs voronoi untuk memfragmentasi sebuah bagian blok:
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

Skrip berikut memfragmentasi area dari sebuah bagian, yang diberikan oleh posisi dan radius. Posisi tersebut mungkin biasanya berasal dari tabrakan fisik atau raycast dari pemain.
Elemen pertama dari array situs yang dihasilkan oleh GenerateFragmentSites() akan menjadi array dalam dari semua situs yang berada di luar radius yang diminta. Jika Anda ingin melakukan sesuatu yang spesifik pada 'bagian yang tidak hancur' dari bagian tersebut, Anda dapat menemukan bagian tersebut dengan memeriksa fragments[i].Index == 1 saat melakukan loop melalui hasil dari 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("Gagal Memecah:"..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
endSkrip berikut memecah potongan dari bagian pertama dalam bentuk yang didefinisikan dengan menggunakan bagian kedua sebagai stensil. Hanya situs voronoi dalam bagian kedua yang akan menghasilkan potongan terpisah. Semua situs lainnya akan memiliki sel-sel mereka digabungkan menjadi satu bagian.


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("Gagal Memecah:"..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
endSkrip berikut adalah kasus penggunaan yang jauh lebih niche, tetapi menunjukkan kekuatan data indeks yang dikembalikan dari FragmentAsync().
Misalnya, banyak tempat mengandung bangunan yang dibentuk dari beberapa bagian blok yang tidak disatukan. Jika sebuah granat, bola meriam, atau palu godam merusak itu, Anda ingin semua bagian dinding terfragmentasi. Skrip ini memfragmentasi semua bagian terdekat, kemudian menggabungkan potongan dari bagian yang berbeda untuk sepenuhnya menyembunyikan sambungan.
Ini melibatkan beberapa operasi Async(), jadi mungkin tidak cocok untuk digunakan dalam permainan sebagai respons instan terhadap input pengguna, seperti alat palu godam.


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("Gagal Memecah:"..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("Gagal Menggabungkan:"..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
endSkrip berikut adalah pengganti Luau yang hampir identik untuk GenerateFragmentSites(). Jika Anda menginginkan perilaku serupa dengan GenerateFragmentSites() tetapi ingin melakukan sedikit perubahan, Anda dapat menggunakan ini sebagai titik awal.
Ini menggunakan grid titik yang bergetar, dan menjamin bahwa area yang terfragmentasi berperilaku baik, tidak seperti titik yang sepenuhnya acak.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Gunakan ini untuk menyesuaikan jumlah getaran
if (origin and not radius) or (radius and not origin) then
warn("Baik origin maupun radius harus disediakan, atau tidak ada.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized berarti jangan memecah seluruh bagian, hanya bagian tertentu.
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
endMempertahankan batasan
Jika sebuah bagian input memiliki batasan atau lampiran yang ingin Anda pertahankan, Anda dapat mentransfernya ke bagian hasil. Ini bisa merepotkan untuk mengetahui bagian output mana yang seharusnya dilampirkan batasan, jadi disarankan untuk menggunakan CalculateConstraintsToPreserve() untuk menghasilkan tabel rekomendasi yang dapat Anda loop dan terapkan.
Untuk menunjukkan, contoh kode berikut melakukan operasi pengurangan, melakukan loop melalui bagian hasil untuk mengubah induk dan posisi bagian hasil, kemudian menghitung tabel batasan dan lampiran untuk dipertahankan atau dibuang sebelum menghancurkan semua bagian asli.
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,
}
-- Lakukan operasi pengurangan dalam pcall() karena bersifat asinkron
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop melalui bagian hasil untuk mengubah induk/posisi
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Hitung batasan/lampiran untuk dipertahankan atau dibuang
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Pertahankan batasan/lampiran berdasarkan tabel rekomendasi
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
-- Hancurkan bagian asli
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
endHasil pemodelan solid
Detail perilaku
Jika bagian utama bergerak selama perhitungan operasi, Anda dapat mengatur bagian yang dihasilkan ke CFrame yang diperbarui dari bagian utama, karena bagian yang dikembalikan berada dalam ruang koordinat yang sama dengan bagian utama.
Jika operasi pemodelan solid akan menghasilkan bagian dengan lebih dari 20.000 segitiga, mereka akan disederhanakan menjadi 20.000. Jika itu tidak dapat dilakukan, biasanya dalam kasus dengan ribuan komponen yang tidak tumpang tindih, operasi akan menghasilkan kesalahan.
Ada metode untuk menukar data mesh dari sebuah instance, membuatnya lebih mudah untuk memanfaatkan geometri dari operasi sambil mempertahankan properti, atribut, tag, dan anak dari bagian utama, seperti Attachments, Constraints, ParticleEmitters, objek cahaya, dan decal. Pendekatan ini juga menghindari potensi "kedip" dari sepenuhnya mengganti PartOperation asli dengan yang lain.
- Jika menggunakan metode ini dengan PartOperation sebagai bagian utama dan tidak ada bagian lain yang merupakan MeshParts, Anda dapat mengganti geometri dari PartOperation lain melalui SubstituteGeometry().
- Jika bagian utama adalah MeshPart, Anda dapat menggunakan MeshPart:ApplyMesh().
Dimungkinkan untuk memanggil metode ini di klien, tetapi dengan beberapa batasan. Pertama, itu harus dilakukan dengan objek yang dibuat di klien. Kedua, tidak ada replikasi yang tersedia dari klien ke server.
Properti berikut dari bagian utama diterapkan pada PartOperations atau MeshParts yang dihasilkan:
Warna dan UV
Warna dari bagian yang dihasilkan setelah pemodelan solid berasal dari dua tempat: warna wajah dan Color bagian tersebut.
- Jika hasilnya adalah PartOperation, itu akan memiliki Color dari bagian pertama yang Anda pilih di Studio, tetapi Studio menggunakan warna wajah secara default untuk menjaga setiap wajah tetap berwarna sama seperti sebelumnya operasi. Anda dapat mengaktifkan properti UsePartColor di Studio untuk menimpa perilaku ini dan membuat seluruh hasil menjadi satu warna.
- Jika hasilnya adalah MeshPart, Color-nya akan berwarna putih, dan warna wajah akan selalu terlihat. Anda dapat menyesuaikan nuansa dari bagian yang dihasilkan dengan mengubah Color mereka, tetapi itu akan dicampur (dikalikan) dengan warna wajah. Ini memberi nuansa pada hasil daripada sepenuhnya menimpa warna wajah. Jika Anda ingin kontrol penuh atas warna output, sebaiknya buat input menjadi putih terlebih dahulu.
UV juga ditangani secara berbeda tergantung pada jenis hasil:
- PartOperations selalu memiliki UV yang dipetakan kotak, yang berarti setiap wajah akan memiliki material/tekstur/decal dari satu arah (salah satu dari -x, +x, -y, +y, -z, +z) diterapkan padanya. Ini dapat meregangkan tekstur.
- MeshParts tidak dipetakan kotak. UV dari mesh bagian utama digunakan. Karena Roblox saat ini tidak memiliki dukungan multi-material, UV dari wajah yang berasal dari bagian lain diberikan UV (0, 0). Untuk hasil terbaik, pastikan piksel (0, 0) dari tekstur Anda memiliki warna yang wajar.
Sudut Penghalusan
Properti SmoothingAngle dari bagian yang dimodelkan solid menghaluskan sudut antara permukaan yang berdekatan dengan warna yang sama. Nilai yang lebih tinggi menghasilkan penampilan yang lebih halus sementara nilai yang lebih rendah menghasilkan penampilan yang lebih kasar dengan lebih banyak tepi tajam.
Sementara nilai antara 30 dan 70 derajat biasanya menghasilkan hasil yang baik, nilai antara 90 dan 180 tidak disarankan karena dapat menyebabkan efek "bayangan" pada union dan intersection dengan tepi tajam.

