Katı modelleme, BaseParts parçalarını bir araya getirerek daha karmaşık şekiller oluşturma sürecidir. Bu, genellikle Yapısal Katı Geometri (CSG) olarak bilinen birleşim, kesişim ve çıkarma gibi boolean işlemleri içerir. Katı modellemeyi Studio'da, eklenti ile ve oyun içi olarak hem sunucuda hem de istemcide gerçekleştirebilirsiniz.
Boolean CSG'ye ek olarak, katı modelleme, su geçirmez olduğu sürece ağları da destekler ve oyuncularınızın geometrileri kesip, parçalayarak benzersiz oyun etkileşimleri oluşturmasına olanak tanıyan sweep ve fragment gibi işlemleri de içerir.


Su geçirmez geometri
Katı modelleme işlemleri yalnızca su geçirmez geometri ile çalışabilir. Teknik terimlerle, bir ağın su geçirmez olması, kapalı, manifold ve kendisiyle kesişmeyen bir yapı anlamına gelir. Bu terimlerin kesin tanımları vardır, ancak işte bazı basit kurallar:
- Her yüz "iç" ve "dış" tarafı olmalıdır. Bu, yüzün üç köşesinin sıralama düzeni ile belirlenir.
- Her kenar tam olarak iki üçgen tarafından paylaşılmalıdır. Bu, ağda herhangi bir delik olmaması gerektiği anlamına gelir, çünkü bir deliğin etrafındaki kenarlar yalnızca bir üçgenle sınırlı olacaktır.
- Yüzler diğer yüzlerin içinden geçemez.
- Bitişik üçgenler hangi tarafın "dış" taraf olduğunu kabul etmelidir.
- Her düğüm tam olarak bir fan bitişik üçgenine sahip olmalıdır.
Aşağıdaki örneklerin her biri belirtilen neden için su geçirmez değildir:




Katı modelleme sistemi, bir ağdaki belirli küçük sorunları otomatik olarak onarabilir, 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ğı onarmanın tek tip bir yolu yoktur, ancak 3D Print Toolbox ve Mesh Repair Tools gibi yardımcı olabilecek birkaç Blender eklentisi vardır. Başka bir seçenek olarak, Meshlab da ağı manifold hale getirmeye çalışmak için çok yararlı araçlar içerir; bu, bir ağın su geçirmez olmasının ana gereksinimidir.
Bir ağın su geçirmez hale getirilmesinin son derece zor olacağını görmek için, Studio'da her açıdan bakarak, ağın MeshPart.DoubleSided özelliğini etkinleştirip devre dışı bırakmayı deneyebilirsiniz. Herhangi bir fark görebiliyorsanız, ağ sadece bir kabuk ve yukarıda bahsedilen araçlar çalışmayacaktır çünkü iç ve dış alanı tahmin edemezler. Ancak, eğer tek istediğiniz ince bir ağsa ve ağın boyutlarını tam olarak aynı tutmak önemli değilse, Blender'ın Solidify modifier özelliğini kullanarak kabuğu su geçirmez bir ağa hafifçe 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ç | Kısayol | Açıklama |
|---|---|---|
| Birleşim | ShiftCtrlG (Windows) Shift⌘G (Mac) | İki veya daha fazla parçayı bir araya getirerek tek bir katı birleşim oluşturur. |
| Kesişim | ShiftCtrlI (Windows) Shift⌘I (Mac) | Üst üste binen parçaları tek bir katı kesişim haline getirir. |
| Ayrı | ShiftCtrlU (Windows) Shift⌘U (Mac) | Birleşimi veya kesişimi tekrar bireysel parçalara ayırır. |
| Negate | ShiftCtrlN (Windows) Shift⌘N (Mac) | Parçaları tersine çevirir, delikler ve çukurlar oluşturmak için kullanışlıdır. |
Birleşim
Birleşim aracı, iki veya daha fazla parçayı bir araya getirerek tek bir katı UnionOperation oluşturur.


Parçaları birleştirmek için:
- Bir araya getirmek istediğiniz tüm parçaları seçin.
Kesişim
Kesişim aracı, üst üste binen parçaları tek bir katı IntersectOperation haline getirir.


Üst üste binen parçaları kesiştirmek için:
- Kesiştirmek istediğiniz tüm parçaları seçin.
- Kesişim butonuna tıklayın. Tüm parçalar, Kesişim adıyla tek bir katı IntersectOperation haline gelir.
Negate
Negate aracı, bir parçayı tersine çevirir, böylece bir başka parçayla birleştiğinde, tersine çevrilen parçanın şekli diğer parçadan çıkarılır.


Diğer üst üste binen parçalardan bir parçayı çıkarmak için:
- Diğer parçalardan çıkarmak istediğiniz parçayı seçin.
- Negate butonuna tıklayın. Parça, durumunu belirtmek için kırmızımsı bir tonla yarı saydam hale gelir.
- Hem tersine çevrilen parçayı hem de ondan çıkarmak istediğiniz parçaları seçin.
- Birleşim butonuna tıklayın. Tersine çevrilen parça, dahil edilen üst üste binen parçalardan kesilir.
Ayrı
Ayrı aracı, bir UnionOperation'ı tekrar bireysel parçalara ayırır ve esasen birleşimler ve kesişimler için bir "geri alma" aracı olarak hizmet eder.
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 formlarına geri döner.
Oyun içi katı modelleme
Ayrıca, bir oyun çalışırken veya GeometryService yöntemleri aracılığıyla bir eklenti ile katı modelleme işlemleri gerçekleştirebilirsiniz.
Temel işlemler
Studio'nun yerleşik işlem araçlarına benzer şekilde, GeometryService yöntemlerini kullanarak UnionAsync(), IntersectAsync() ve SubtractAsync() gibi temel boolean işlemleri gerçekleştirebilirsiniz. Örneğin, aşağıdaki script, bir parçanın hacmini diğerinden çıkarmak için SubtractAsync() kullanı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
endDaha fazla göstermek için, bir sonraki kod örneği mainPart ve otherParts dizisindeki parçaların geometrisini birleştirir, ardından işlemde yer alan 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
}
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Sonuçlanan parçaları yeniden ebeveynleştirip yeniden konumlandır
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
endParça negasyonu için motor düzeyinde bir yöntem yoktur, ancak rbxNegate etiketini bir script veya eklenti içinde ekleyerek Studio'nun Negate araç çubuğu butonuna eşdeğer bir negasyon gerçekleştirebilirsiniz.
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync() ile karşılaştırıldığında, GeometryService yöntemleri şu şekilde farklılık gösterir:
- Çıktı, tek bir örnek yerine bir dizi örnek içerir.
- Giriş parçalarının sahneye bağlı olması gerekmez, arka planda işlemler yapılabilir.
- SplitApart seçeneği true (varsayılan) olarak ayarlandığında, her ayrı cisim kendi PartOperation/MeshPart olarak dönecektir.
- Tüm dönen parçalar ana parçanın koordinat alanında olduğundan, PVInstance.Origin konumları ana parçanın konumlarıyla aynı olacaktır. Bu, ağın köşelerinin işlemden önceki nesneye göre aynı konumda kalmasını sağlar, ancak dönen bir parçanın (0, 0, 0) konumunun mutlaka gövdesinin merkezinde olmadığı anlamına gelir.
Sweep part
SweepPartAsync() yöntemi, belirli bir CFrame konumları seti boyunca sürüklenen giriş parçasının şeklini alan bir MeshPart oluşturur. Bu yöntem, kesme ve kesme etkileşimleri gerçekleştirmek için çok yararlı olabilir.
Giriş, bir Part, PartOperation veya MeshPart olabilir. Sonucun şekli, her bitişik CFrames çiftinin konveks kabuğunun birleşimi olarak tanımlanır; yalnızca tek bir CFrame sağlanırsa, sonuç giriş parçasının konveks kabuğu olacaktır.
Bu yöntemin nasıl çalıştığını göstermek için, aşağıdaki kod örneği bir topu bir dizi CFrame konumu boyunca sürükler ve 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

Bu örnek, SweepPartAsync() kullanarak bir kılıç veya lazer tabancası kesme oyun özelliği elde eder; burada kılıcın hareketi oyuncunun fare pozisyonuna dayanır. Kullanıcının fare hareketi, CFrames listesini kaydeder, SweepPartAsync() bu verilerden bir kesme ağı oluşturur, ardından kesme 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ı girişini işlemek 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.
Fragment
FragmentAsync() ve GenerateFragmentSites() yöntemleri, bir parçayı doğal görünümlü parçalara ayırmanıza olanak tanır. FragmentAsync() belirli bir noktalar dizisine göre bir parçayı birden fazla MeshPart örneğine ayırmak için voronoi ayrıştırmasını kullanırken, GenerateFragmentSites() ise FragmentAsync()'ye geçmek için kullanılacak voronoi noktalarını oluşturan yardımcı bir yöntemdir.
Aşağıdaki kod örneği, bir blok parçasını parçalara ayırmak için voronoi noktaları 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

Aşağıdaki script, bir parçanın belirli bir konum ve yarıçap ile bir alanını parçalara ayırır. Konum genellikle bir fiziksel çarpışmadan veya bir oyuncudan gelen bir ışın atışından gelir.
GenerateFragmentSites() tarafından oluşturulan site dizisinin ilk elemanı, istenen yarıçapın dışındaki tüm noktaların iç içe bir dizisi olacaktır. Parçanın kalan 'kırılmamış kısmı' ile ilgili özel bir şey yapmak istiyorsanız, FragmentAsync() sonuçlarını döngüye alırken fragments[i].Index == 1 kontrolü yaparak o kısmı bulabilirsiniz.
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("Kırılma 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
endAşağıdaki script, bir stencil kullanarak bir şekil içinde ilk parçadan parçalar kırar. Sadece ikinci parçanın içindeki voronoi noktaları ayrı parçalara neden olur. Diğer tüm noktalar, hücreleri tek bir parça haline getirilerek birleştirilecektir.


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("Kırılma 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
endAşağıdaki script, FragmentAsync()'den dönen indeks verilerinin gücünü gösteren daha niş bir kullanım durumudur.
Örneğin, birçok yer, birden fazla birleşmemiş blok parçalarından oluşan binalar içerir. Eğer bir el bombası, top veya balyoz bunlara zarar verirse, tüm duvar parçalarının parçalanmasını istersiniz. Bu script, tüm yakın parçaları kırar, ardından farklı parçaların parçalarını birleştirerek dikişleri tamamen gizler.
Bu, birden fazla Async() işlemi içerdiğinden, kullanıcı girişine anında yanıt olarak oyun içinde kullanmak için uygun olmayabilir, örneğin bir balyoz aracı.


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("Kırılma 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
endAşağıdaki script, GenerateFragmentSites() için neredeyse aynı Luau yerine geçer. GenerateFragmentSites() ile benzer bir davranış istiyorsanız ancak küçük değişiklikler yapmak istiyorsanız, bunu başlangıç noktası olarak kullanabilirsiniz.
Jitterlı bir nokta ızgarası kullanır ve parçalanmış alanın iyi davrandığından emin olur, tamamen rastgele noktalardan farklı olarak.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Jitter miktarını ayarlamak için bunu kullanın
if (origin and not radius) or (radius and not origin) then
warn("Ya hem orijinal ve yarıçap sağlanmalıdır ya da hiçbiri.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized, tüm parçayı kırmak yerine sadece bir kısmını kırmak anlamına gelir.
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
endKısıtlamaları koruma
Bir giriş parçasının korumak istediğiniz kısıtlamaları veya eklemeleri varsa, bunları sonuçlanan parçalara aktarabilirsiniz. Hangi çıktı parçasına bir kısıtlamanın eklenmesi gerektiğini belirlemek zor olabilir, bu nedenle CalculateConstraintsToPreserve() kullanarak, döngüye alıp uygulayabileceğiniz öneriler içeren bir tablo oluşturmanız önerilir.
Göstermek için, aşağıdaki kod örneği bir çıkarma işlemi gerçekleştirir, sonuçlanan parçaları yeniden ebeveynleştirip yeniden konumlandırır, ardından koruyacak veya bırakacak kısıtlamalar ve eklemeler için bir tablo hesaplar.
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 çıkarma işlemini gerçekleştir
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Sonuçlanan parçaları yeniden ebeveynleştirip yeniden konumlandır
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ı/eklemeleri hesapla
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Önerilen tabloya göre kısıtlamaları/eklemeleri koru
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
endKatı modelleme sonuçları
Davranış detayları
Ana parça işlem sırasında hareket ediyorsa, sonuçlanan parçaları ana parçanın güncellenmiş CFrame değerine ayarlayabilirsiniz, çünkü dönen parçalar ana parçayla aynı koordinat alanındadır.
Bir katı modelleme işlemi, 20.000'den fazla üçgen içeren herhangi bir parça oluşturursa, bu parçalar 20.000'e basitleştirilecektir. Bunu yapmak mümkün değilse, genellikle binlerce üst üste gelmeyen bileşen içeren bir durumda, işlem bir hata ile sonuçlanır.
Bir örneğin mesh verilerini değiştirmek için yöntemler vardır; bu, ana parçanın özelliklerini, niteliklerini, etiketlerini ve çocuklarını korurken işlemin geometrisini kullanmayı kolaylaştırır. Bu yaklaşım, orijinal PartOperation'ı tamamen değiştirmekten kaynaklanabilecek "titreme" sorununu da aşar.
- Bu yöntemi ana parça olarak bir PartOperation kullanıyorsanız ve diğer parçaların hiçbiri MeshParts değilse, başka bir PartOperation'ın geometrisini SubstituteGeometry() ile değiştirebilirsiniz.
- Ana parça bir MeshPart ise, MeshPart:ApplyMesh() kullanabilirsiniz.
Bu yöntemleri istemci üzerinde çağırmak mümkündür, ancak bazı sınırlamalar vardır. Öncelikle, bunlar yalnızca istemcide oluşturulan nesnelerle yapılmalıdır. İkincisi, istemciden sunucuya replikasyon mevcut değildir.
Ana parçadan aşağıdaki özellikler, sonuçlanan PartOperations veya MeshParts'a uygulanır:
Renkler ve UV'ler
Katı modelleme sonrasında elde edilen parça(ların) renkleri iki yerden gelir: yüz renkleri ve parçanın Color değeri.
- Sonuç bir PartOperation ise, Studio'da seçtiğiniz ilk parçanın Color değerine sahip olacaktır, ancak Studio, her yüzün işlemden önceki aynı renkte kalmasını sağlamak için varsayılan olarak yüz renklerini kullanır. Bu davranışı geçersiz kılmak için UsePartColor özelliğini Studio'da etkinleştirebilirsiniz.
- Sonuç bir MeshPart ise, Color beyaz olacak ve yüz renkleri her zaman görünür olacaktır. Sonuç parça(ların) rengini ayarlamak için Color değerini değiştirerek tonunu ayarlayabilirsiniz, ancak bu, yüz renkleriyle çarpılacaktır. Bu, sonucu renklendirmek yerine yüz renklerini tamamen geçersiz kılar. Çıktının rengini tamamen kontrol etmek istiyorsanız, girişlerinizi önce beyaz yapmanız en iyisidir.
UV'ler de sonuç türüne bağlı olarak farklı şekilde işlenir:
- PartOperations her zaman kutu haritalı UV'lere sahiptir, bu da her yüzün bir yönden (bir -x, +x, -y, +y, -z, +z) malzeme/tekstür/dekal alacağı anlamına gelir. Bu, tekstürleri gerilebilir.
- MeshParts kutu haritalı değildir. Ana parçanın mesh'inin UV'leri kullanılır. Roblox şu anda çoklu malzeme desteğine sahip olmadığından, diğer parçalardan gelen yüzlerin UV'leri (0, 0) değerine sahip olur. En iyi sonuçlar için, tekstürünüzün piksel (0, 0) değerinin makul bir renge sahip olduğundan emin olun.
Yumuşatma açısı
Bir katı modelleme parçasının SmoothingAngle özelliği, aynı renkteki bitişik yüzler arasındaki açıları yumuşatır. Daha yüksek bir değer daha pürüzsüz bir görünüm üretirken, daha düşük bir değer daha keskin kenarlarla daha pürüzlü bir görünüm üretir.
30 ile 70 derece arasındaki bir değer genellikle iyi bir sonuç verir, ancak 90 ile 180 arasındaki değerler önerilmez çünkü keskin kenarlarla birleşimlerde "gölgeleme" etkisi yaratabilir.

