Katı modelleme parçaları parçalar bir araya getirerek benzersiz şekiller oluşturma sürecidir. Bu, genellikle Yaratıcı Katı Geometri (CSG) olarak bilinen birleşim, çıkarma ve kesişim gibi boolean işlemlerini içerir. Katı modellemeyi her yerde yapabilirsiniz: Studio'da, eklentilerde, hatta oyun içi hem sunucu hem de istemci üzerinde.
Boolean CSG'nin yanı sıra, katı modelleme ayrıca su geçirmez olduğunda ağları da destekler ve oyuncuların geometrileri dilimlemesine, kesmesine ve parçalarına ayırmasına olanak tanıyan sweep ve fragment gibi işlemleri içerir.


Su geçirmez geometri
Ağların üç temel öğesi vardır:
- Vertex - Ağ üzerindeki tek bir nokta.
- Edge - İki vertici birbirine bağlayan bir çizgi.
- Face - Üç veya daha fazla vertici arasında kalan yüzey alanı.



Katı modelleme işlemleri yalnızca su geçirmez geometrilerle çalışabilir; aslında, "katı" ve "su geçirmez" eş anlamlıdır. Teknik terimlerle, bir ağın su geçirmez olması, kapalı, manifold ve kendisiyle kesişmeyen bir yapıda olduğu anlamına gelir. Bu terimlerin kesin tanımları vardır, ancak burada bazı basit kurallar vardır:
- Her yüzeyin bir 'iç' tarafı ve bir 'dış' tarafı olmalıdır. Bu, yüzeyin üç verticesinin sırası tarafından belirlenir.
- Her kenar yalnızca iki üçgen tarafından paylaşılmalıdır. Bu, ağda delik olmaması gerektiği anlamına gelir, çünkü delik etrafındaki kenarlar yalnızca bir üçgene sahip olacaktır.
- Yüzeyler diğer yüzeylerin içinden geçemez.
- Bitişik üçgenler, hangi tarafın 'dış' taraf olduğunu kabul etmelidir.
- Her vertex'in tam olarak bir üçgen fanına sahip olması gerekir.

Katı modelleme sistemi, bir ağdaki belirli küçük sorunları otomatik olarak düzeltebilir, ancak genel olarak, ağ su geçirmez değilse API çağrıları başarısız olacaktır. Mevcut bir su geçirmez olmayan ağın onarılması için tek tip bir yöntem yoktur, ancak 3D Print Toolbox ve Mesh Repair Tools gibi bunun için yardımcı olabilecek birkaç Blender eklentisi vardır. Başka bir seçenek olarak, Meshlab da ağın manifold hale gelmeye çalışması için yararlı araçlar içerir; bu da bir ağın su geçirmez olmasının ana gerekliliğidir.
Bir ağın su geçirmez hale getirilmesinin son derece zor olduğunu görmek için, Studio'da her açıdan bakarak, ardından ağın MeshPart.DoubleSided özelliğini etkinleştirip devre dışı bırakmayı deneyebilirsiniz. Herhangi bir fark görüyorsanız, o zaman ağ bir kabuktur ve yukarıda belirtilen araçlar çalışmayacaktır çünkü iç ve dış alanın ne olduğunu tahmin edemezler. Ancak, eğer yalnızca ince bir ağ istiyorsanız ve ağın boyutlarının tam olarak aynı kalmasını sağlamak önemli değilse, Blender'ın Solidify modifikatörü kullanılarak kabuğun su geçirmez bir ağ haline getirilmesi için biraz kalınlaştırabilirsiniz.

Studio'da katı modelleme
Model sekmesi araç çubuğunda dört araç kullanarak üç temel boolean işlemi gerçekleştirebilirsiniz.

| Araç | Klavye Kısayolu | Açıklama |
|---|---|---|
| Birleştirme | ShiftCtrlG (Windows) Shift⌘G (Mac) | İki veya daha fazla parçayı birleştirerek tek bir katı birleşim oluşturur. |
| Kesişim | ShiftCtrlI (Windows) Shift⌘I (Mac) | Örtüşen parçaları tek bir katı kesişim haline getirir. |
| Negatif | ShiftCtrlN (Windows) Shift⌘N (Mac) | Parçaları negatif hale getirir; delikler ve çukurlar oluşturmak için kullanışlıdır. |
| Ayrı | ShiftCtrlU (Windows) Shift⌘U (Mac) | Birleşimi veya kesişimi orijinal parçalarına geri ayırır. |
Parçaları Birleştirme
Birleştirme aracı, iki veya daha fazla parçayı birleştirerek tek bir katı UnionOperation oluşturur.


Parçaları birleştirerek bir birleşim oluşturmak için:
- Bir araya getirmek istediğiniz tüm parçaları seçin.
- Birleştirme butonuna tıklayın. Tüm parçalar bir katı UnionOperation haline gelir ve adı Birleşim olur.
Parçaları Kesişim
Kesişim aracı, örtüşen parçaları tek bir katı IntersectOperation haline getirir.


Örtüşen parçaları bir araya getirmek için:
- Kesiştirmek istediğiniz tüm parçaları seçin.
- Kesişim butonuna tıklayın. Tüm parçalar bir katı IntersectOperation haline gelir ve adı Kesişim olur.
Parçaları Negatif Yapma
Negatif aracı, bir parçayı negatif hale getirir, böylece başka bir parçayla birleştiğinde, negatif parçanın şekli diğer parçadan çıkarılır.


Diğer örtüşen parçalardan bir parçayı çıkarmak için:
- Diğer parçalardan çıkarmak istediğiniz parçayı seçin.
- Negatif butonuna tıklayın. Parça negatif bir parça olarak etiketlenir ve Explorer'da negatif sembol görünür. Parça, durumunu göstermek için kırmızımsı bir renk ile saydam hale gelir.
- Hem negatif parçayı hem de ondan çıkarmak istediğiniz parçaları seçin.
- Birleştirme butonuna tıklayın. Negatif parça dahil olan örtüşen parçalardan kesilir.
Etiket, betik yazımı için açılmaktadır, bu nedenle, rbxNegate etiketini bir betik veya eklenti üzerinden ekleyerek parçaları da negatif yapabilirsiniz. NegateOperation artık kullanılmıyor.
Birleşimleri veya Kesişimleri Ayırma
Ayrı aracı, UnionOperation'ı orijinal parçalarına geri ayırır; temelde birleşimler ve kesişimler için bir "geri alma" aracı işlevi görür.
Bir birleşimi veya kesişimi tekrar bireysel parçalara ayırmak için:
- Birleşimi veya kesişimi seçin.
- Ayrı butonuna tıklayın. Parçalar orijinal biçimlerine geri ayrılır.
Oyun içi katı modelleme
Ayrıca, GeometryService fonksiyonlarını kullanarak oyun dururken katı modelleme işlemleri gerçekleştirebilirsiniz.
UnionAsync(), IntersectAsync() ve SubtractAsync()
Studio'daki yerleşik temel boolean işlem araçlarına benzer şekilde, oyun dururken temel boolean işlemleri gerçekleştirmek için GeometryService fonksiyonları olan UnionAsync(), IntersectAsync() ve SubtractAsync() kullanabilirsiniz. Örneğin, aşağıdaki script, bir parçanın hacmini başka bir parçadan çıkarmak için SubtractAsync() kullanmaktadır.
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

Daha fazla örnek vermek gerekirse, aşağıdaki kod örneği mainPart'ın geometrisini ve otherParts dizisindeki parçaları birleştirir, ardından işlemle ilişkili orijinal parçaları yok eder. UnionAsync() çağrısını IntersectAsync() veya SubtractAsync() ile değiştirerek diğer boolean işlemlerini gerçekleştirebilirsiniz.
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,
}
-- asenkron olduğu için pcall() içinde bir birleşim işlemi gerçekleştirin
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Sonuçlanan parçaları yeniden ebeveyn yapmak / yeniden konumlandırmak için döngü
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Orijinal parçaları yok et
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Girdiğin tüm parçaların MeshParts değil, temel parçalar olması durumunda, birleşim, etkileşim ve çıkarma boolean işlemleri PartOperation ile iki veri parçası içerir: bir CSG işlemi ağacı olan CSGTree ve işlemek için bir ağ.
BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync() ile karşılaştırıldığında, GeometryService boolean fonksiyonları şu şekilde farklılık gösterir:
- Çıktı, tek bir örnek yerine bir dizi örnektir.
- Girdi parçalarının sahneye ebeveynlenmesi gerekmez; arka planda işler yapılabilir.
- SplitApart seçeneği true (varsayılan) olarak ayarlandığında, her ayrı cisim kendi PartOperation/MeshPart içinde dönecektir.
- Tüm döndürülen parçalar ana parçanın koordinat alanındadır, yani PVInstance.Origin konumları ana parçanınkiyle aynıdır. Bu, ağın vertexlerinin nesne ile işlem öncesinden beri aynı konumda kalmasını sağlarken, döndürülen bir parçanın (0, 0, 0) noktasının mutlaka cismisinin merkezinde olmadığı anlamına da gelir.
SweepPartAsync()
GeometryService:SweepPartAsync() fonksiyonu, verilen bir dizi CFrame pozisyonu boyunca çekilen giriş parçasının şeklinde bir MeshPart oluşturur. Bu fonksiyon dilimleme ve kesme etkileşimlerini gerçekleştirmek için çok yararlı olabilir.
Girdi, bir Part, PartOperation veya MeshPart olabilir. Sonuç nesnesinin şekli, her bitişik CFrame çiftinin konveks dış yüzeylerinin birleşimi olarak tanımlanır; yalnızca bir CFrame sağlanıyorsa, sonuç, giriş parçasının konveks dış yüzeyidir.
Bu fonksiyonun nasıl çalıştığını göstermek için, aşağıdaki kod örneği bir topu bir dizi CFrame pozisyonundan geçirerek bir spiral oluşturur:
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

Örnek

Bu örnek, oyuncunun fare pozisyonuna dayalı olarak bir kılıç veya lazer tabancası dilimleme oyun deneyiminde GeometryService:SweepPartAsync() kullanmaktadır. Kullanıcının fare hareketi, CFrames listesini kaydederek kaydedilir, SweepPartAsync() bu verilerden bir dilim ağ oluşturur ve ardından dilim ağ, vurulan parçadan çıkarılır.
Bu örneği Studio'da çalıştırmak için:
Tüm katı modelleme işlemlerini gerçekleştirmek için ServerScriptService içinde aşağıdaki Script'i oluşturun.
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-- Sweep'i görselleştirsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Sweep'i vurulan parçadan çıkarlocal 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)Kullanıcı girdiğini ele almak için StarterPlayerScripts içinde aşağıdaki LocalScript'i oluşturun.
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-- Sweep'i görselleştirsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Sweep'i vurulan parçadan çıkarlocal subtractSuccess, newParts = pcall( function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)ReplicatedStorage içinde DrawCurveEvent adında bir RemoteEvent oluşturun.
FragmentAsync()
GeometryService:FragmentAsync() ve GeometryService:GenerateFragmentSites() fonksiyonları bir parçayı doğal görünümde parçalara ayırmanızı sağlar. GeometryService:FragmentAsync(), bir parçayı, sağlanan nokta desenine göre birden fazla MeshPart örneğine bölmek için Voronoi ayrıştırmasını kullanırken, GeometryService:GenerateFragmentSites() ise FragmentAsync()’e geçirilecek noktaları oluşturan yardımcı bir fonksiyondur.
Bu fonksiyonların nasıl bir arada çalıştığını göstermek için aşağıdaki kod örneği, bir blok parçasını parçalara ayırmak için voronoi siteleri oluşturur:
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

Örnekler

Aşağıdaki script, bir pozisyon ve yarıçul ile verilen bir parçanın bir alanını parçalar. Pozisyon genellikle fiziksel bir çarpışmadan veya bir oyuncudan gelen bir ışın çiziminden gelir.
GenerateFragmentSites() oluşturduğu site dizisinin ilk elemanı, talep edilen yarıçapın dışında kalan tüm sitelerin iç içe bir dizisi olacaktır. Eğer parçanın geri kalan 'parçalanmamış kısmıyla' özel bir şey yapmak isterseniz, FragmentAsync() sonuçları döngüsünde fragments[i].Index == 1 kontrol ederek o kısmı bulabilirsiniz.
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("Kırma başarısız oldu:"..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
Aşağıdaki script, birinci parçayı, ikinci parça bir şablon olarak kullanarak parçalara ayırır. İkinci parçanın içindeki hiçbir voronoi noktası ayrı parçaların oluşmasına neden olmaz. Tüm diğer noktalar, tek bir parçada birleştirilecektir.


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("Kırma başarısız oldu:"..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
Aşağıdaki script, daha az yaygın bir kullanım senaryosudur, ancak GeometryService:FragmentAsync() tarafından döndürülen indeks verisinin gücünü göstermektedir.
Örneğin, birçok yer, birden fazla birleştirilmemiş blok parçalarından oluşan binalar içerir. Eğer bir el bombası, top mermisi veya balyoz bunlara zarar verirse, tüm duvar parçalarının parçalanmasını istersiniz. Bu script, çevredeki tüm parçaları parçalayarak sonra farklı parçaların fragmentlerini birleştirir ve tamamen dikişleri gizler.
Bu, birden fazla Async işlemi içerir, bu nedenle bu tür bir yanıtı oyun içinde bir kullanıcı girişiyle anında kullanmak uygun olmayabilir, örneğin bir balyoz aracı.


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("Kırma başarısız oldu:"..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("Birleşme başarısız oldu:"..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
Aşağıdaki script, GeometryService:GenerateFragmentSites() için neredeyse aynı bir Luau yerine geçiştir. GeometryService:GenerateFragmentSites() benzeri bir davranış aldığınızdan emin olmak ancak ufak ufak değişiklikler istiyorsanız, bunu başlangıç noktası olarak kullanabilirsiniz.
Bu, noktaların oldukları yere jitterlı bir ızgara kullanır ve parça + yerleştirdiğimiz alanın iyi davrandığından emin olur, rastgele noktalar kadar değil.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Jitter miktarını ayarlamak için kullanın
if (origin and not radius) or (radius and not origin) then
warn("Ya her ikisi de ya da hiçbiri sağlanmalıdır.")
return {}
end
local isLocalized = (radius ~= nil) -- yerel değil, tüm parçayı parçalamak yerine sadece bir bölüm.
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
Kısıtlamaları koruma
Bir girdi parçasının korumak istediğiniz kısıtlamaları veya bağlantıları varsa, bunları sonuç parçalarına aktarabilirsiniz. Hangi çıktı parçasına bir kısıtlamanın ilişkilendirileceğini anlamak zor olabilir, bu nedenle GeometryService:CalculateConstraintsToPreserve() kullanarak önermek için bir öneri tablosu oluşturmak en iyi seçenektir.
Aşağıda, bir çıkarma işlemi gerçekleştiren, sonuç parçalarını yeniden ebeveyn yapan ve yeniden konumlandıran, ardından tüm orijinal parçaları yok etmeden önce koruyacak veya bırakacak kısıtlamaların ve bağlantıların bir tablosunu hesaplayan kod örneği bulunmaktadır.
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,
}
-- asenkron olduğu için pcall() içinde bir çıkarma işlemi gerçekleştirin
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Sonuçlanan parçaları yeniden ebeveyn yapmak / yeniden konumlandırmak için döngü
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Koruyacak veya bırakacak kısıtlamaları / bağlantıları hesaplayın
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Önerilen tabloya dayalı olarak kısıtlamaları / bağlantıları koruyun
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
-- Orijinal parçaları yok et
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Davranış detayları
Ana parça, işlem hesaplanırken hareket ediyorsa, döndürülen parçaları güncellenmiş CFrame değerine ayarlayabilirsiniz çünkü döndürülen parçalar ana parça ile aynı koordinat alanında bulunmaktadır.
Bir nesnenin özelliklerini, niteliklerini, etiketlerini ve çocuklarını korurken, parça verisini değiştirmenizi sağlayan fonksiyonlar bulunmaktadır. Bu yaklaşım, orijinal PartOperation nesnesini tamamen değiştirmek yerine, orijinal nesnedeki özelliklerin korunmasını da sağlar, bu da "titreme" veya "flicker" olasılığını azaltır.
- Eğer bu yöntemi ana parça olarak bir PartOperation ile kullanıyorsanız ve diğer parçaların hiçbiri MeshParts değilse, başka bir PartOperation geometrisini SubstituteGeometry() ile değiştirebilirsiniz.
- Ana parça bir MeshPart ise, MeshPart:ApplyMesh() kullanarak değişiklik yapabilirsiniz.
Bu fonksiyonları istemcide çağırmak mümkündür, ancak bazı kısıtlamalarla birlikte. Öncelikle, bunların istemcide oluşturulmuş nesnelerle yapılması gerekir. İkincisi, istemciden sunucuya yeniden çoğaltma yoktur.
Ana parçadan aşağıdaki özellikler sonucu PartOperations veya MeshParts üzerinde uygulanır:
- Çarpışma: BasePart.AudioCanCollide, BasePart.CanCollide
Katı modelleme sonuçlarıyla ilgili hususlar
Renkler ve UV'ler
Solid modelleme ile elde edilen parçaların renkleri iki yerden gelir: yüzey renkleri ve parçanın Color özelliği.
- Eğer sonuç bir PartOperation ise, Studio'da ilk seçtiğiniz parçanın Color özelliğini alacaktır, ancak Studio, her yüzeyin, işlemden önceki rengiyle aynı olmasını sağlamak için yüzey renklerini varsayılan olarak kullanır. Bu davranışı geçersiz kılmak ve tüm sonucu tek bir renkte yapmak için UsePartColor özelliğini etkinleştirebilirsiniz.
- Eğer sonuç bir MeshPart ise, Color özelliği beyaz olur ve yüzey renkleri her zaman görünür. Oluşan parça(lar)ın rengini, Color özelliklerini değiştirecek şekilde ayarlayabilirsiniz, ancak bu, yüzey renkleriyle birlikte karışacaktır (çarpılacaktır). Bu da rengi tintler, ancak yüzey renklerini tamamen geçersiz kılmaz. Eğer çıktı üzerindeki renk üzerinde tam kontrol istiyorsanız, girdi parçalarını önce beyaz yapmanız en iyisidir.
UV'ler de sonucu türüne bağlı olarak farklı bir şekilde işlenir:
- PartOperations, her yüzeyin bir yön için malzeme/kaplama/decal tarafından uygulandığı kutu haritalama UV'lerine (boxmapped UVs) sahiptir. Bu, dokuların uzanmasına neden olabilir.
- MeshParts, kutu haritalı değildir. Ana parçanın ağının UV'leri kullanılır. Roblox, çoklu malzeme desteğine sahip olmadığından, diğer parçalardan gelen yüzeylerin UV'leri (0, 0) değerini alır. En iyi sonuçlar için, dokunun piksel (0, 0) konumunun makul bir rengi olduğundan emin olun.
Pürüzsüzlük açısı
Bir katı modelleme ile oluşturulan parçanın SmoothingAngle özelliği, aynı renkteki bitişik yüzeylerin arasındaki açıyı pürüzsüzleştirir. Daha yüksek bir değer, daha pürüzsüz bir görünüm sağlarken, daha düşük bir değer, daha keskin kenarlarla daha pürüzlü bir görünüm oluşturur.
30 ile 70 derece arasındaki değerler genellikle iyi sonuç verir, 90 ile 180 arasındaki değerler tavsiye edilmez; çünkü bu, birleşimler ve kesişimler üzerinde keskin kenarlarla "gölgeleme" etkisi yaratabilir.


Parça basitleştirme
Eğer bir katı modelleme işlemi 20,000'den fazla üçgene sahip bir parça ile sonuçlanıyorsa, bunlar 20,000'e basitleştirilir. Bunu gerçekleştiremiyorsa, genellikle binlerce örtüşmeyen parçanın olduğu durumlarda işlem hata alır.

