Pemodelan padat adalah proses menggabungkan bagian bersama dengan cara unik untuk membentuk bentuk yang lebih kompleks. Ini mencakup operasi boolean gabungan, pengurangan, dan irisan, yang biasa dikenal sebagai Geometri Solid Konstruktif (CSG). Anda dapat melakukan pemodelan padat di mana saja: di Studio, plugin, bahkan dalam permainan di server maupun klien.
Selain CSG boolean, pemodelan padat juga mendukung mesh, selama mereka hermetic, serta operasi seperti sweep dan fragment yang memungkinkan Anda dan pemain Anda untuk memotong, memecah, dan menghancurkan geometri untuk interaksi gameplay yang unik.


Geometri hermetic
Ada tiga elemen dasar dari mesh:
- Vertex - Titik tunggal pada mesh.
- Edge - Garis yang menghubungkan dua vertex.
- Face - Area permukaan antara tiga atau lebih vertex.



Operasi pemodelan padat hanya dapat bekerja dengan geometri hermetic; sebenarnya, "solid" dan "hermetic" adalah sinonim. Dalam istilah teknis, sebuah mesh yang hermetic berarti bahwa itu tertutup, manifold, dan tidak saling memotong. Istilah-istilah ini memiliki definisi yang ketat, tetapi berikut adalah beberapa aturan sederhana:
- Setiap face harus memiliki sisi 'dalam' dan sisi 'luar'. Ini ditentukan oleh urutan winding dari face tersebut, yang merupakan urutan dari tiga vertex-nya.
- Setiap edge harus dibagi oleh tepat dua segitiga. Ini berarti tidak boleh ada lubang dalam mesh karena edge di sekitar tepi lubang hanya akan memiliki satu segitiga.
- Face tidak boleh melewati face lainnya.
- Segitiga yang berdampingan harus sepakat tentang sisi mana yang merupakan sisi 'luar'.
- Setiap vertex harus memiliki tepat satu fan dari segitiga yang berdampingan.

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

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

| Alat | Shortcut | Deskripsi |
|---|---|---|
| Gabungan | ShiftCtrlG (Windows) Shift⌘G (Mac) | Menggabungkan dua bagian atau lebih untuk membentuk satu gabungan padat. |
| Irisan | ShiftCtrlI (Windows) Shift⌘I (Mac) | Beririsan bagian yang saling tumpang tindih menjadi satu irisan padat. |
| Pengurangan | ShiftCtrlN (Windows) Shift⌘N (Mac) | Mengurangkan bagian, berguna untuk membuat lubang dan lekukan. |
| Pisah | ShiftCtrlU (Windows) Shift⌘U (Mac) | Misahkan gabungan atau irisan kembali ke bagian individualnya. |
Gabungan bagian
Alat Gabungan menggabungkan dua atau lebih bagian bersama untuk membentuk UnionOperation padat tunggal.


Untuk menggabungkan bagian-bagian bersama:
- Pilih semua bagian yang ingin digabungkan.
- Klik tombol Gabungan. Semua bagian tersebut digabungkan menjadi satu UnionOperation padat dengan nama Union.
Irisan bagian
Alat Irisan mengiris bagian yang saling tumpang tindih menjadi satu IntersectOperation padat.


Untuk mengiris bagian-bagian yang saling tumpang tindih:
- Pilih semua bagian yang ingin diiris.
- Klik tombol Irisan. Semua bagian tersebut digabungkan menjadi satu IntersectOperation padat dengan nama Irisan.
Pengurangan bagian
Alat Pengurangan mengurangi sebuah bagian sehingga ketika digabungkan dengan bagian lain, bentuk dari bagian yang dikurangi adalah dikurangi dari bagian lainnya.


Untuk mengurangi sebuah bagian dari bagian-bagian yang saling tumpang tindih lainnya:
- Pilih bagian yang ingin Anda kurangi dari bagian lainnya.
- Klik Pengurangan. Bagian tersebut ditandai sebagai bagian yang dikurangi dan simbol negatif muncul di Explorer. Bagian tersebut menjadi transparan dengan warna merah untuk menunjukkan keadaannya.
- Pilih baik bagian yang dikurangi dan bagian-bagian yang ingin Anda kurangi darinya.
- Klik Gabungan. Bagian yang dikurangi dipotong dari bagian-bagian tumpang tindih yang disertakan.
Tag tersebut terbuka untuk scripting, jadi Anda juga dapat mengurangi bagian dengan menambahkan tag rbxNegate dari skrip atau plugin. NegateOperation tidak lagi digunakan.
Pisah gabungan atau irisan
Alat Pisah memisahkan UnionOperation kembali ke bagian individualnya, yang pada dasarnya berfungsi sebagai alat "membatalkan" untuk gabungan dan irisan.
Untuk memisahkan gabungan atau irisan kembali ke bagian individual:
- Pilih gabungan atau irisan.
- Klik Pisah. Bagian-bagian tersebut dipisahkan kembali ke bentuk aslinya.
Pemodelan padat dalam permainan
Anda juga dapat melakukan operasi pemodelan padat saat permainan sedang berjalan dengan menggunakan fungsi GeometryService.
UnionAsync(), IntersectAsync(), dan SubtractAsync()
Mirip dengan alat operasi boolean dasar yang ada di Studio, Anda dapat menggunakan fungsi GeometryService seperti UnionAsync(), IntersectAsync(), dan SubtractAsync() untuk melakukan operasi boolean dasar saat permainan sedang berjalan. 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
end

Untuk lebih menunjukkan, contoh kode berikut menggabungkan geometri dari mainPart dan bagian-bagian dalam array otherParts, kemudian menghancurkan bagian-bagian asli yang terlibat dalam operasi tersebut. 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,
}
-- Melakukan operasi gabungan dalam pcall() karena asinkron
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop melalui bagian hasil untuk mereparent/mereposisi
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
end
Selama semua input terdiri dari bagian primitif daripada MeshParts, operasi gabungan, potong, dan pengurangan boolean akan menghasilkan PartOperation dengan dua potongan data yang disimpan: pohon operasi CSG yang dikenal sebagai CSGTree, dan mesh untuk rendering.
Jika dibandingkan dengan BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), fungsi boolean GeometryService berbeda sebagai berikut:
- Output adalah array dari instance, daripada satu instance tunggal.
- Bagian input tidak perlu diparent ke dalam scene, memungkinkan untuk operasi latar belakang.
- Ketika opsi SplitApart diatur ke true (default), setiap body yang berbeda akan dikembalikan dalam PartOperation/MeshPart-nya masing-masing.
- Semua bagian yang dikembalikan berada dalam ruang koordinat dari bagian utama, sehingga posisi PVInstance.Origin mereka adalah sama dengan bagian utama. Ini menjaga vertex dari mesh dalam posisi yang sama relatif terhadap objek seperti sebelum operasi, tetapi ini juga berarti (0, 0, 0) dari bagian yang dikembalikan tidak selalu berada di pusat bodynya.
SweepPartAsync()
Fungsi GeometryService:SweepPartAsync() menciptakan MeshPart yang memiliki bentuk dari bagian input yang ditarik melalui serangkaian posisi CFrame tertentu. Fungsi ini bisa sangat berguna untuk melakukan interaksi pemotongan dan slicing.
Input bisa berupa Part, PartOperation, atau MeshPart. Bentuk hasilnya didefinisikan sebagai gabungan dari cangkang konveks dari setiap pasangan CFrames; jika hanya satu CFrame yang diberikan, hasilnya akan menjadi cangkang konveks dari bagian input.
Untuk menunjukkan bagaimana fungsi ini bekerja, contoh kode berikut menyapu bola melalui satu set posisi CFrame untuk menciptakan 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

Contoh ini menggunakan GeometryService:SweepPartAsync() untuk mencapai fitur gameplay potongan 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 padat.
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 sweepsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Kurangi sweep 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 sweepsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Kurangi sweep dari instance yang terkenalocal subtractSuccess, newParts = pcall( function()r eturn 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.
FragmentAsync()
Fungsi GeometryService:FragmentAsync() dan GeometryService:GenerateFragmentSites() memungkinkan Anda menghancurkan sebuah bagian menjadi potongan-potongan dengan bentuk yang tampak alami. GeometryService:FragmentAsync() menggunakan dekomposisi voronoi untuk membagi satu bagian menjadi beberapa instance MeshPart sesuai dengan pola titik yang diberikan, sementara GeometryService:GenerateFragmentSites() adalah fungsi pembantu yang menghasilkan titik yang dikenal sebagai situs voronoi untuk diteruskan ke FragmentAsync().
Untuk menunjukkan bagaimana fungsi-fungsi ini bekerja bersama, contoh kode berikut menghasilkan situs voronoi untuk memecah 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

Contoh

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


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("Gagal fracturing:"..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
Skrip berikut adalah kasus penggunaan yang jauh lebih niche, tetapi menunjukkan kekuatan data indeks yang dikembalikan dari GeometryService:FragmentAsync().
Misalnya, banyak tempat mengandung bangunan yang terbentuk dari beberapa bagian blok yang tidak tergabung. Jika sebuah granat, bola meriam, atau palu godam untuk merusaknya, Anda ingin semua bagian dinding terfragmentasi. Skrip ini memfragmentasi semua bagian yang ada di dekatnya, kemudian menggabungkan fragmen dari bagian yang berbeda bersama-sama untuk sepenuhnya menyembunyikan sambungan.
Ini melibatkan beberapa operasi Async, sehingga mungkin tidak cocok untuk digunakan dalam permainan sebagai respons instan terhadap input pengguna, seperti alat palu godam.


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("Gagal fracturing:"..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 Union:"..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
Skrip berikut adalah pengganti Luau yang hampir identik dengan GeometryService:GenerateFragmentSites(). Jika Anda menginginkan perilaku yang mirip dengan GeometryService:GenerateFragmentSites() tetapi ingin melakukan sedikit perubahan, Anda dapat menggunakan ini sebagai titik awal.
Ini menggunakan grid titik jittered, dan menjamin bahwa area yang terfragmentasi bersifat 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 mengatur jumlah jitter
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 memfragmentasi 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
end
Mempertahankan batasan
Jika sebuah bagian input memiliki batasan atau lampiran yang ingin Anda pertahankan, Anda dapat mentransfernya ke bagian hasil. Sulit untuk mengetahui bagian output mana yang seharusnya dihubungkan oleh batasan, jadi disarankan untuk menggunakan GeometryService:CalculateConstraintsToPreserve() untuk menghasilkan tabel rekomendasi yang bisa Anda loop dan terapkan.
Untuk menunjukkan, contoh kode berikut melakukan operasi pengurangan, melingkari hasil bagian untuk mereparent dan mereposisi bagian hasil kemudian menghitung tabel batasan dan lampiran untuk dipertahankan atau dihapus 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,
}
-- Melakukan pengurangan dalam pcall() karena asinkron
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop melalui bagian hasil untuk mereparent/mereposisi
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 dihapus
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
end
Rincian perilaku
Jika bagian utama bergerak selama perhitungan operasi, Anda dapat mengatur bagian yang dihasilkan ke CFrame terbaru dari bagian utama, karena bagian yang dikembalikan berada dalam ruang koordinat yang sama dengan bagian utama.
Ada fungsi untuk menukar data mesh dari sebuah instance, memudahkan untuk memanfaatkan geometri operasi sambil mempertahankan properti, atribut, tag, dan anak-anak dari bagian utama, seperti Lampiran, Batasan, PengemisiPartikel, objek lampu, dan decal. Pendekatan ini juga menghindari "kedip" potensial saat sepenuhnya mengganti PartOperation asli dengan yang lain.
- Jika menggunakan metode ini dengan PartOperation sebagai bagian utama dan tidak ada bagian lainnya yang merupakan MeshParts, Anda dapat mengganti geometri dari PartOperation lain melalui SubstituteGeometry().
- Jika bagian utama adalah MeshPart, Anda dapat menggunakan MeshPart:ApplyMesh().
Memanggil fungsi-fungsi ini di klien adalah mungkin, tetapi dengan beberapa batasan. Pertama, itu harus dilakukan pada objek yang DIBUAT di klien. Kedua, tidak ada replikasi yang tersedia dari klien ke server.
Properti berikut dari bagian utama diterapkan pada hasil PartOperations atau MeshParts:
Pertimbangan hasil pemodelan padat
Warna dan UV
Warna dari bagian hasil setelah pemodelan padat berasal dari dua tempat: warna wajah dan Color dari bagian tersebut.
- Jika hasil 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 dengan warna yang sama seperti sebelumnya operasi. Anda dapat mengaktifkan properti UsePartColor untuk menyalahi perilaku ini dan membuat hasil keseluruhan menjadi warna tunggal.
- Jika hasil adalah MeshPart, Color-nya akan berwarna putih, dan warna wajah akan selalu terlihat. Anda dapat menyesuaikan warna hasil dengan mengubah Color mereka, tetapi itu akan dicampur (dimultiply) dengan warna wajah. Ini memberi warna pada hasil daripada sepenuhnya menutupi warna wajah. Jika Anda ingin kontrol penuh atas warna output, yang terbaik adalah membuat input berwarna putih terlebih dahulu.
UV juga ditangani secara berbeda tergantung 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 mendukung multi-material, UV dari wajah yang berasal dari bagian lain mendapatkan UV (0, 0). Untuk hasil terbaik, pastikan piksel (0, 0) dari tekstur Anda memiliki warna yang layak.
Sudut pelunakan
Properti SmoothingAngle dari bagian yang dipodelkan padat melunakkan sudut antara permukaan yang berdampingan 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 sudut 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 gabungan dan irisan dengan sudut tajam.


Penyederhanaan bagian
Jika operasi pemodelan padat mengakibatkan 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.

