Modelowanie bryłowe

*Ta zawartość została przetłumaczona przy użyciu narzędzi AI (w wersji beta) i może zawierać błędy. Aby wyświetlić tę stronę w języku angielskim, kliknij tutaj.

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.

Obiekt siatkowy utworzony przy użyciu SweepPartAsync()
Obiekt siatkowy rozbity przy użyciu FragmentAsync()

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.
Pojedynczy aktywny wierzchołek na siatce sześcianu.
Wierzchołek
Pojedyncza aktywna krawędź na siatce sześcianu.
Krawędź
Pojedyncza aktywna powierzchnia na siatce sześcianu.
Powierzchnia

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.
Przykłady geometrii niena wodoodpornej

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ę.

Przykład siatki, która wygląda inaczej, gdy DoubleSided jest włączony.
To jest siatka powłokowa i metody automatycznej naprawy nie zadziałają przy niej.

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ędzia modelowania bryłowego wyróżnione w pasku narzędzi Studio.
NarzędzieSkrótOpis
SumaShiftCtrlG (Windows)
ShiftG (Mac)
Połącz dwie lub więcej części w jedną bryłę.
PrzecięcieShiftCtrlI (Windows)
ShiftI (Mac)
Przecięcie nakładających się części w jeden bryłę.
NadajShiftCtrlN (Windows)
ShiftN (Mac)
Nadaj częściom, przydatne do tworzenia otworów i wklęsłości.
OddzielShiftCtrlU (Windows)
ShiftU (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.

Blok i cylinder.
Pojedyncze części
Blok i cylinder połączone w jeden obiekt.
Wynik sumy

Aby połączyć części w sumę:

  1. Wybierz wszystkie części, które chcesz połączyć.
  2. Kliknij przycisk Suma. Wszystkie części łączą się w jedną bryłę UnionOperation o nazwie Suma.

Przecięcie części

Narzędzie Przecięcie przecina nakładające się części w jedną solidną IntersectOperation.

Blok i cylinder.
Pojedyncze części
Blok i cylinder połączone w jeden obiekt.
Wynik przecięcia

Aby przeciąć nakładające się części:

  1. Wybierz wszystkie części, które chcesz przeciąć.
  2. 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.

Blok i cylinder.
Blok i nadany cylinder
Blok i cylinder połączone w jeden obiekt.
Wynik odejmowania

Aby odjąć część od innych nakładających się części:

  1. Wybierz część, którą chcesz odjąć od innych części.
  2. 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.
  3. Wybierz zarówno nadaną część, jak i części, od których chcesz ją odjąć.
  4. 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:

  1. Wybierz sumę lub przecięcie.
  2. 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
Blok odjęty od innego bloku.

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
Kształt spirali utworzony przez przesuwanie sfery.

Przykład

Przezroczysty zakrzywiony kształt odejmowany od bloku.

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:

  1. 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ęcie
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Odejmij przesunięcie od trafionej instancji
    local subtractSuccess, newParts = pcall( function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  2. 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ęcie
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Odejmij przesunięcie od trafionej instancji
    local subtractSuccess, newParts = pcall( function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  3. 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
Blok rozbity na kawałki.

Przykłady

Blok z rogu rozbitego na kawałki.

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ęść.

Szablon w kształcie logo Roblox Studio
Część (ciemnoszara) i część szablonu
Wyniki fragmentu w kształcie szablonu
Wynik skryptu fragmentu

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.

Szereg bloków
Szereg bloków
Szereg bloków zfragmentowanych
Każdy kawałek może pochodzić z wielu części wejściowych

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

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.

Siatka w dobrym stanie
Przed uproszczeniem
Siatka z widocznym zmniejszeniem jakości
Po uproszczeniu
©2026 Roblox Corporation. Nazwa Roblox, logo Roblox oraz hasło „Powering Imagination” należą do naszych zarejestrowanych i niezarejestrowanych znaków towarowych na terenie Stanów Zjednoczonych oraz w innych krajach.