Modelowanie bryłowe to proces łączenia części w unikalny sposób, aby utworzyć bardziej złożone kształty. Obejmuje to operacje boolowskie suma, odejmowanie i przecięcie, znane też jako konstruktywna geometria bryłowa (CSG). Możesz wykonywać modelowanie bryłowe wszędzie: w Studio, wtyczkach, a nawet w doświadczeniach zarówno na serwerze, jak i kliencie.
Oprócz boolowskiego CSG, modelowanie bryłowe wspiera także siatki, o ile są one wodoszczelne, oraz operacje takie jak sweep i fragment, które pozwalają Tobie i Twoim graczom kroić, ciąć i łamać geometrię w unikalnych interakcjach rozgrywki.


Wodoszczelna geometria
Istnieją trzy podstawowe elementy siatek:
- Wierzchołek - Pojedynczy punkt na siatce.
- Krawędź - Linia łącząca dwa wierzchołki.
- Powierzchnia - Powierzchnia między trzema lub więcej wierzchołkami.



Operacje modelowania bryłowego mogą działać tylko na geometriach wodoszczelnych; w rzeczywistości, "bryła" i "wodoszczelna" są synonimami. W terminologii technicznej, geometria siatki jest wodoszczelna, gdy jest zamknięta, manifeast i nie przecina się sama. Terminy te mają ścisłe definicje, ale oto kilka prostych zasad:
- Każda powierzchnia musi mieć stronę 'wewnętrzną' i 'zewnętrzną'. Są one ustalane przez kolejność wierzchołków powierzchni, która jest kolejnością trzech jej wierzchołków.
- Każda krawędź musi być współdzielona przez dokładnie dwa trójkąty. To oznacza, że nie może być żadnych otworów w siatce, ponieważ krawędzie wokół krawędzi otworu miałyby tylko jeden trójkąt.
- Powierzchnie nie mogą przechodzić przez inne powierzchnie.
- Sąsiednie trójkąty muszą uzgodnić, która strona jest 'zewnętrzną' stroną.
- Każdy wierzchołek musi mieć dokładnie jeden wachlarz sąsiednich trójkątów.

System modelowania bryłowego jest w stanie automatycznie naprawić niektóre drobne problemy z siatką, ale w ogólnym przypadku, wywołania API zakończą się niepowodzeniem, jeśli siatka nie jest wodoszczelna. Nie ma uniwersalnej metody naprawy istniejącej siatki niena wodoszczelnej, ale istnieje kilka wtyczek do Blendera, które mogą pomóc, takich jak 3D Print Toolbox i Mesh Repair Tools. Jako dodatkową opcję, Meshlab również ma bardzo pomocne narzędzia wbudowane, aby spróbować uczynić siatkę manifeast, co jest głównym wymaganiem dla siatki wodoszczelnej.
Jednym ze sposobów sprawdzenia, czy siatka będzie niezwykle trudna do wodoszczelnienia, jest oglądanie jej w Studio z różnych kątów, a następnie próba włączenia i wyłączenia właściwości MeshPart.DoubleSided siatki. Jeśli widzisz jakąkolwiek różnicę, to siatka jest tylko powłoką, a narzędzia wymienione powyżej nie działają, ponieważ nie mogą odgadnąć, jaka przestrzeń znajduje się w środku a jaka na zewnątrz siatki. Jednak jeśli zależy Ci tylko na cienkiej siatce i nie jest ważne zachowanie dokładnych rozmiarów siatki, możesz użyć modifikatora Solidify Blendera, aby nieznacznie pogrubić powłokę w wodoszczelną siatkę.

Modelowanie bryłowe w Studio
Możesz wykonać trzy podstawowe operacje boolowskie używając czterech narzędzi w pasku narzędzi zakładki Model.

| Narzędzie | Skrót | Opis |
|---|---|---|
| Suma | ShiftCtrlG (Windows) Shift⌘G (Mac) | Połącz dwie lub więcej części w jedną bryłę. |
| Przecięcie | ShiftCtrlI (Windows) Shift⌘I (Mac) | Przecięcie nakładających się części w jeden bryłę. |
| Nadaj | ShiftCtrlN (Windows) Shift⌘N (Mac) | Nadaj częściom, przydatne do tworzenia otworów i wklęsłości. |
| Oddziel | ShiftCtrlU (Windows) Shift⌘U (Mac) | Oddziel sumę lub przecięcie z powrotem na pojedyncze części. |
Suma części
Narzędzie Suma łączy dwie lub więcej części w jedną solidną UnionOperation.


Aby połączyć części w sumę:
- Wybierz wszystkie części, które chcesz połączyć.
Przecięcie części
Narzędzie Przecięcie przecina nakładające się części w jedną solidną IntersectOperation.


Aby przeciąć nakładające się części:
- Wybierz wszystkie części, które chcesz przeciąć.
- Kliknij przycisk Przecięcie. Wszystkie części łączą się w jedną bryłę IntersectOperation o nazwie Przecięcie.
Nadaj części
Narzędzie Nadaj nadaje część tak, aby gdy jest połączona z inną częścią, kształt nadanej części jest odejmowany od drugiej części.


Aby odjąć część od innych nakładających się części:
- Wybierz część, którą chcesz odjąć od innych części.
- Kliknij Nadaj. Część zostaje oznaczona jako nadana, a w Eksploratorze pojawia się symbol negacji. Część staje się przezroczysta z czerwonawym odcieniem aby wskazać jej stan.
- Wybierz zarówno nadaną część, jak i części, od których chcesz ją odjąć.
- Kliknij Suma. Nadana część zostaje wycięta z uwzględnionych nakładających się części.
Etykieta jest udostępniana do skryptów, dzięki czemu możesz również nadać części, dodając etykietę rbxNegate z poziomu skryptu lub wtyczki. NegateOperation nie jest już używane.
Oddziel sumy lub przecięcia
Narzędzie Oddziel oddziela UnionOperation do jego pojedynczych części, pełniąc zasadniczo rolę narzędzia "cofnij" dla sum i przecięć.
Aby oddzielić sumę lub przecięcie z powrotem na pojedyncze części:
- Wybierz sumę lub przecięcie.
- Kliknij Oddziel. Części wracają do swojej oryginalnej formy.
Modelowanie bryłowe w doświadczeniach
Możesz także wykonywać operacje modelowania bryłowego, gdy doświadczenie jest uruchomione, korzystając z funkcji GeometryService.
UnionAsync(), IntersectAsync() i SubtractAsync()
Podobnie jak w wbudowanych narzędziach do podstawowych operacji boolowskich w Studio, możesz użyć funkcji GeometryService, takich jak UnionAsync(), IntersectAsync(), i SubtractAsync(), aby wykonać podstawowe operacje boolowskie podczas działania doświadczenia. Na przykład, poniższy skrypt używa SubtractAsync() do odjęcia objętości jednej części od drugiej.
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

Aby dodatkowo zaprezentować, poniższy próbka kodu łączy geometrię mainPart i części w tablicy otherParts, a następnie niszczy oryginalne części zaangażowane w operację. Możesz zastąpić wywołanie UnionAsync() wywołaniem IntersectAsync() lub SubtractAsync(), aby wykonać inne operacje boolowskie.
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,
}
-- Wykonaj operację sumy w pcall(), ponieważ jest asynchroniczna
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Pętla przez wynikowe części, aby ponownie je rodzic/przesunąć
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Zniszcz oryginalne części
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Tak długo, jak wszystkie wejścia składają się z prymitywnych części, a nie z MeshParts, operacje suma, przecięcie i odejmowanie bryłowe wytworzą PartOperation z dwiema częściami przechowywanymi: drzewem CSG znanym jako CSGTree oraz siatką do renderowania.
W porównaniu z BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), funkcje boolowskie GeometryService różnią się następująco:
- Wyjście to tablica instancji, a nie jedna instancja.
- Części wejściowe nie muszą być rodzicowane do sceny, co pozwala na operacje w tle.
- Gdy opcja SplitApart ustawiona jest na true (domyślnie), każda odrębna bryła będzie zwracana w swoim własnym PartOperation/MeshPart.
- Wszystkie zwrócone części znajdują się w przestrzeni współrzędnych głównej części, więc ich PVInstance.Origin są takie same, jak głównej części. Utrzymuje to wierzchołki siatki w tej samej pozycji w stosunku do obiektu, jak przed operacją, ale oznacza to również, że (0, 0, 0) zwróconej części niekoniecznie znajduje się w centrum jej bryły.
SweepPartAsync()
Funkcja GeometryService:SweepPartAsync() tworzy MeshPart, który ma kształt przesuwanego obiektu przez dany zestaw pozycji CFrame. Ta funkcja może być bardzo użyteczna przy wykonywaniu interakcji związanych z krojeniem i cięciem.
Wejście może być Part, PartOperation lub MeshPart. Kształt rezultatu jest definiowany jako suma wypukłych brył każdej sąsiedniej pary CFrames; jeśli dostarczone jest tylko jedno CFrame, wynik będzie wypukłą bryłą jednostkową.
Aby zademonstrować, jak działa ta funkcja, poniższy skrypt przesuwa kulę przez zestaw pozycji CFrame, aby stworzyć 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

Przykład

Ten przykład wykorzystuje GeometryService:SweepPartAsync() do osiągnięcia funkcji krojenia w grze z użyciem miecza lub lasera, gdzie ruch miecza bazuje na pozycji myszy gracza. Ruch myszy użytkownika jest rejestrowany jako lista CFrames, SweepPartAsync() buduje zasoby siatki z tych danych, a następnie zasoby siatki są odejmowane od części, która została trafiona.
Aby uruchomić ten przykład w Studio:
Utwórz poniższy Script w ServerScriptService, aby wykonywać wszystkie operacje modelowania bryłowego.
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-- Wizualizuj przesunięciesweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Odejmij przesunięcie od trafionej instancjilocal 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)Utwórz następujący LocalScript w StarterPlayerScripts, aby obsługiwać dane wejściowe.
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-- Wizualizuj przesunięciesweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Odejmij przesunięcie od trafionej instancjilocal 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)Utwórz RemoteEvent w ReplicatedStorage o nazwie DrawCurveEvent.
FragmentAsync()
Funkcje GeometryService:FragmentAsync() i GeometryService:GenerateFragmentSites() pozwalają Ci zadać część w kawałki o naturalnym kształcie. GeometryService:FragmentAsync() wykorzystuje dekompozycję voronoi, aby podzielić jedną część na wiele instancji MeshPart zgodnie z wzorcem punktów przekazanych jako argument, podczas gdy GeometryService:GenerateFragmentSites() to funkcja pomocnicza, która generuje punkty znane jako witryny voronoi, które można przekazać do FragmentAsync().
Aby zademonstrować, jak te funkcje współpracują, poniższy skrypt generuje witryny voronoi do fragmentacji części bloku:
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

Przykłady

Poniższy skrypt fragmentuje obszar części, określony przez pozycję i promień. Pozycja może pochodzić z kolizji fizycznej lub rzutowania przez gracza.
Pierwszym elementem tablicy witryn, który generuje GenerateFragmentSites(), będzie wewnętrzna tablica wszystkich witryn, które znajdują się poza żądanym promieniem. Jeśli chcesz zrobić coś specyficznego dla pozostałej "nienaruszonej część" części, możesz znaleźć tę część, sprawdzając fragments[i].Index == 1, gdy pętla przechodzi przez wyniki 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("Nie udało się złamać:"..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
Poniższy skrypt łamie fragmenty o pierwszą część w kształcie zdefiniowanym przez użycie drugiej części jako szablon. Tylko witryny voronoi w obrębie drugiej części będą skutkować oddzielnymi kawałkami. Wszystkie inne witryny będą miały swoje komórki połączone w jedną część.


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("Nie udało się złamać:"..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
Poniższy skrypt to znacznie bardziej niszowe zastosowanie, ale demonstruje moc danych indeksu, które są zwracane z GeometryService:FragmentAsync().
Na przykład, wiele miejsc zawiera budynki złożone z wielu niepołączonych części blokowych. Jeśli granat, kula lub młot miałyby uszkodzić to, chciałbyś, aby wszystkie części ścian były fragmentowane. Ten skrypt fragmentuje wszystkie pobliskie części, a następnie łączy fragmenty różnych części razem, aby całkowicie ukryć szwy.
To wymaga wielu operacji Async, więc może nie być odpowiednie do użycia w doświadczeniu jako natychmiastowa odpowiedź na dane wejściowe użytkownika, takie jak narzędzie młota.


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("Nie udało się złamać:"..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("Nie udało się połączyć:"..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
Poniższy skrypt to prawie identyczny odpowiednik Luau dla GeometryService:GenerateFragmentSites(). Jeśli chcesz uzyskać podobne zachowanie jak GeometryService:GenerateFragmentSites(), ale chcesz dokonać niewielkich zmian, możesz użyć tego jako punktu wyjścia.
Używa jittered grid of points i gwarantuje, że fragmentowany obszar zachowuje się dobrze, w przeciwieństwie do punktów całkowicie losowych.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Użyj tego do dostosowania ilości drgań
if (origin and not radius) or (radius and not origin) then
warn("O ile powinno być podane zarówno położenie, jak i promień, lub żadne.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized oznacza, aby nie łamać całej części, tylko sekcję.
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
Zachowanie ograniczeń
Jeśli wejściowa część ma ograniczenia lub załączniki, które chcesz zachować, możesz przenieść je do wynikowych części. Może to być trudne do ustalenia, do której części powinna być podłączona, więc zaleca się użycie GeometryService:CalculateConstraintsToPreserve(), aby wygenerować tabelę rekomendacji, którą możesz przeglądać i stosować.
Aby zademonstrować, poniższy skrypt wykonuje operację odejmowania, przegląda wynikowe części, aby ponownie je rodzicować i przestawiać, a następnie oblicza tabelę ograniczeń i załączników do zachowania lub odrzucenia przed zniszczeniem wszystkich oryginalnych części.
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,
}
-- Wykonaj operację odejmowania w pcall(), ponieważ jest asynchroniczna
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Pętla przez wynikowe części, aby ponownie je rodzic/przesunąć
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Oblicz ograniczenia/załączniki, które należy zachować lub odrzucić
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Zachowaj ograniczenia/załączniki w oparciu o rekomendowaną tabelę
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
-- Zniszcz oryginalne części
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Szczegóły zachowania
Jeśli główna część porusza się podczas obliczania operacji, możesz ustawić wynikowe części na zaktualizowane CFrame głównej części, ponieważ zwrócone części są w tej samej przestrzeni współrzędnych co główna część.
Istnieją funkcje, które pozwalają wymienić dane siatki instancji, co ułatwia wykorzystanie geometrii operacji, zachowując właściwości, atrybuty, tagi i dzieci głównej części, takie jak Attachments, Constraints, ParticleEmitters, obiekty świetlne i tekstury. To podejście także omija potencjalny "migotanie" całkowitego zastąpienia oryginalnej PartOperation inną.
- Jeśli używasz tej metody z PartOperation jako główną częścią, a żadne z innych części nie jest MeshParts, możesz zastąpić geometrię innej PartOperation poprzez SubstituteGeometry().
- Jeśli główna część jest MeshPart, możesz użyć MeshPart:ApplyMesh().
Możliwe jest wywoływanie tych funkcji po stronie klienta, ale z pewnymi ograniczeniami. Po pierwsze, musi to być zrobione z obiektami utworzonymi po stronie klienta. Po drugie, brak jest replikacji z klienta do serwera.
Następujące właściwości z głównej części są aplikowane do wynikowych PartOperations lub MeshParts:
- Kolizja: BasePart.AudioCanCollide, BasePart.CanCollide
Rozważania dotyczące wyników modelowania bryłowego
Kolory i UV
Kolory wynikowych części po modelowaniu bryłowym pochodzą z dwóch miejsc: kolorów twarzy i Color części.
- Jeśli wynik jest PartOperation, będzie miał Color pierwszej części, którą wybrałeś w Studio, ale Studio używa kolorów twarzy jako domyślnych, aby każda twarz miała taki sam kolor, jak przed operacją. Możesz włączyć właściwość UsePartColor w Studio, aby nadpisać to zachowanie i uczynić cały wynik jednego koloru.
- Jeśli wynik jest MeshPart, jego Color będzie biały, a kolory twarzy zawsze będą się przez nie przenikały. Możesz dostosować kolor wynikowych części, zmieniając ich Color, ale będzie on mieszany (mnożony) z kolorami twarzy. To nadaje wynikowi odcień, a nie całkowicie nadpisuje kolory twarzy. Jeśli chcesz mieć pełną kontrolę nad kolorem wyjścia, najlepiej jest najpierw ustawić wejścia na kolor biały.
UV są również obsługiwane różnie, w zależności od typu wyniku:
- PartOperations zawsze mają UV o formacie boxmapped, co oznacza, że każda twarz będzie miała materiał/teksturę/dekal z jednego kierunku (jednego z -x, +x, -y, +y, -z, +z) zastosowanego na niej. Może to rozciągać tekstury.
- MeshParts nie są mapowane do boxów. UV siatki głównej części są używane. Ponieważ Roblox obecnie nie obsługuje wielu materiałów, UV powierzchni pochodzących z innych części mają UV (0, 0). Aby uzyskać najlepsze rezultaty, upewnij się, że piksel (0, 0) jednej tekstury ma przyzwoity kolor.
Kąt wygładzania
Właściwość SmoothingAngle części modelowanej bryłowo wygładza kąty między sąsiednimi powierzchniami tego samego koloru. Wyższa wartość skutkuje gładszym wyglądem, podczas gdy niższa wartość skutkuje surowym wyglądem z większą ilością ostrych krawędzi.
Choć wartość między 30 a 70 stopni zwykle przynosi dobry rezultat, wartości między 90 a 180 nie są zalecane, ponieważ mogą powodować efekt "cieniowania" na uniach i przecięciach z ostrymi krawędziami.


Uproszczenie części
Jeśli operacja modelowania bryłowego skutkuje jakimikolwiek częściami z więcej niż 20 000 trójkątów, zostaną one uproszczone do 20 000. Jeśli nie jest to możliwe, zazwyczaj w przypadku tysięcy nie nakładających się komponentów, operacja kończy się błędem.

