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 BaseParts w unikalny sposób, aby utworzyć bardziej złożone kształty. Obejmuje to operacje boolowskie union, intersection i subtraction, które są powszechnie znane jako konstruktywna geometria bryłowa (CSG). Możesz wykonywać modelowanie bryłowe w Studio, wtyczkach oraz w grze zarówno na serwerze, jak i kliencie.

Oprócz boolowskiego CSG, modelowanie bryłowe wspiera również siatki, o ile są one wodoodporne, oraz operacje takie jak sweep i fragment, które pozwalają Tobie i Twoim graczom kroić, ciąć i łamać geometrię dla unikalnych interakcji w grze.

MeshPart utworzony za pomocą SweepPartAsync()
MeshPart złamany za pomocą FragmentAsync()

Wodoodporna geometria

Operacje modelowania bryłowego mogą działać tylko na wodoodpornej geometrii. W terminologii technicznej, siatka będąca wodoodporną oznacza, że jest zamknięta, wielomianowa i nie przecina się sama. Te terminy mają ścisłe definicje, ale oto kilka prostych zasad:

  • Każda powierzchnia musi mieć stronę "wewnętrzną" i "zewnętrzną". Te strony są określane przez kolejność wierzchołków powierzchni, która jest kolejnością jej trzech wierzchołków.
  • Każda krawędź musi być dzielona przez dokładnie dwa trójkąty. Oznacza to, że nie może być żadnych dziur w siatce, ponieważ krawędzie wokół krawędzi dziury miałyby tylko jeden trójkąt.
  • Powierzchnie nie mogą przechodzić przez inne powierzchnie.
  • Sąsiednie trójkąty muszą zgadzać się co do tego, która strona jest "zewnętrzna".
  • Każdy wierzchołek musi mieć dokładnie jeden wachlarz sąsiednich trójkątów.

Każdy z poniższych przykładów jest nie-wodoodporny z powodu wskazanej przyczyny:

System modelowania bryłowego jest w stanie automatycznie naprawić niektóre drobne problemy z siatką, ale ogólnie rzecz biorąc, wywołania API będą nieudane, jeśli siatka nie jest wodoodporna. Nie ma jednego uniwersalnego sposobu na naprawienie istniejącej nie-wodoodpornej siatki, ale istnieje kilka wtyczek do Blendera, które mogą pomóc, takich jak 3D Print Toolbox i Mesh Repair Tools. Jako alternatywę, Meshlab ma również bardzo przydatne narzędzia wbudowane, aby spróbować uczynić siatkę wielomianową, co jest głównym wymogiem dla siatki wodoodpornej.

Jednym ze sposobów, aby zobaczyć, że siatka będzie niezwykle trudna do uczynienia wodoodporną, jest spojrzenie na nią w Studio z wszystkich kątów, a następnie spróbować włączyć i wyłączyć właściwość MeshPart.DoubleSided siatki. Jeśli zauważysz jakąkolwiek różnicę, to siatka jest po prostu powłoką, a narzędzia wspomniane powyżej nie będą działać, ponieważ nie mogą zgadnąć, jaka przestrzeń jest wewnątrz, a jaka na zewnątrz siatki. Jednak jeśli chcesz tylko cienką siatkę i nie jest ważne, aby zachować dokładne wymiary siatki, możesz użyć modyfikatora Solidify w Blenderze, aby nieco pogrubić powłokę w wodoodporną siatkę.

DoubleSided = false

Modelowanie bryłowe w Studio

Możesz wykonać trzy podstawowe operacje boolowskie za pomocą 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
UnionShiftCtrlG (Windows)
ShiftG (Mac)
Połącz dwie lub więcej części w jedną solidną unie.
IntersectShiftCtrlI (Windows)
ShiftI (Mac)
Przecięcie nakładających się części w jedną solidną intersekcję.
SeparateShiftCtrlU (Windows)
ShiftU (Mac)
Oddziel unie lub intersekcję z powrotem na jej poszczególne części.
NegateShiftCtrlN (Windows)
ShiftN (Mac)
Neguj części, przydatne do tworzenia otworów i wgnieceń.

Union

Narzędzie Union łączy dwie lub więcej części w jedną solidną UnionOperation.

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

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

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

Intersect

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

Blok i cylinder.
Indywidualne 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 Intersect. Wszystkie części łączą się w jedną solidną IntersectOperation o nazwie Intersection.

Negate

Narzędzie Negate neguje część, tak że gdy jest połączona z inną częścią, kształt negowanej części jest odejmowany od drugiej części.

Blok i cylinder.
Blok i negowany 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 Negate. Część staje się przezroczysta z czerwonawym odcieniem, aby wskazać jej stan.
  3. Wybierz zarówno negowaną część, jak i części, od których chcesz ją odjąć.
  4. Kliknij Union. Negowana część jest wycinana z włączonych nakładających się części.

Separate

Narzędzie Separate oddziela UnionOperation z powrotem na jej indywidualne części, zasadniczo działając jako narzędzie "cofnij" dla unii i intersekcji.

Aby oddzielić unię lub intersekcję z powrotem na indywidualne części:

  1. Wybierz unię lub intersekcję.
  2. Kliknij Separate. Części oddzielają się z powrotem do swojej pierwotnej formy.

Modelowanie bryłowe w grze

Możesz również wykonywać operacje modelowania bryłowego podczas działania gry lub z wtyczki za pomocą metod GeometryService.

Operacje podstawowe

Podobnie jak wbudowane narzędzia operacyjne Studio, możesz używać metod GeometryService, takich jak UnionAsync(), IntersectAsync(), i SubtractAsync(), aby wykonywać podstawowe operacje boolowskie. Na przykład, poniższy skrypt używa SubtractAsync(), aby odjąć objętość jednej części od drugiej.

Odejmij jedną część 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 to dalej zademonstrować, następny przykład 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.

Połącz geometrię wielu części
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
-- Przejdź przez wynikowe części, aby zmienić rodzica/pozycję
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

Chociaż nie ma metody na poziomie silnika do negacji części, możesz dodać tag rbxNegate w skrypcie lub wtyczce, aby wykonać negację równoważną przyciskowi narzędzia Negate w Studio.

Negacja części za pomocą skryptu
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")

W porównaniu do BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), metody GeometryService różnią się następująco:

  • Wynik to tablica instancji, a nie pojedyncza instancja.
  • Wprowadzane części nie muszą być przypisane do sceny, co pozwala na operacje w tle.
  • Gdy opcja SplitApart jest ustawiona na true (domyślnie), każda odrębna bryła zostanie zwrócona 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 pozycje PVInstance.Origin są takie same jak głównej części. To utrzymuje wierzchołki siatki w tej samej pozycji względem 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.

Sweep part

Metoda SweepPartAsync() tworzy MeshPart, który ma kształt wprowadzonej części przeciągniętej przez dany zestaw pozycji CFrame. Ta metoda może być bardzo przydatna do wykonywania interakcji związanych z krojeniem i cięciem.

Wprowadzenie może być Part, PartOperation lub MeshPart. Kształt wyniku jest definiowany jako unia wypukłych powłok każdej sąsiedniej pary CFrames; jeśli podano tylko jedną CFrame, wynik będzie wypukłą powłoką wprowadzonej części.

Aby zademonstrować, jak działa ta metoda, poniższy przykład kodu przeciąga kulę przez zestaw pozycji CFrame, aby stworzyć spiralę:

Przeciągnij część wzdłuż spirali
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 przeciąganie sfery.
Przezroczysty zakrzywiony kształt odjęty od bloku.

Ten przykład używa SweepPartAsync(), aby osiągnąć funkcję rozgrywki związaną z cięciem mieczem lub laserowym pistoletem, gdzie ruch miecza oparty jest na pozycji myszy gracza. Ruch myszy użytkownika jest rejestrowany jako lista CFrames, SweepPartAsync() buduje siatkę cięcia z tych danych, a następnie siatka cięcia jest odejmowana od części, która została trafiona.

Aby uruchomić ten przykład w Studio:

  1. Utwórz następujący Script w ServerScriptService, aby wykonać 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 przeciąganie
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Odejmij przeciąganie 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łużyć wejście użytkownika.

    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 przeciąganie
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Odejmij przeciąganie 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.

Fragment

Metody FragmentAsync() i GenerateFragmentSites() pozwalają Ci rozbić część na kawałki o naturalnych kształtach. FragmentAsync() używa dekompozycji voronoi, aby podzielić jedną część na wiele instancji MeshPart zgodnie z wzorem punktów przekazanych, podczas gdy GenerateFragmentSites() jest metodą pomocniczą, która generuje punkty znane jako witryny voronoi do przekazania do FragmentAsync().

Poniższy przykład kodu generuje witryny voronoi, aby rozbić blok:

local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Position = Vector3.new(0, 0.7, 20)
local sites = GeometryService:GenerateFragmentSites(inputPart)
local success, fragments = pcall(function()
return GeometryService:FragmentAsync(inputPart, sites)
end)
if success and fragments then
for _, item in fragments do
local instance = item.Instance
instance.Parent = workspace
end
end
Blok rozbity na kawałki.
Blok z rogiem rozbitym na kawałki.

Poniższy skrypt fragmentuje obszar części, podany przez pozycję i promień. Pozycja może pochodzić z kolizji fizycznej lub promienia rzutowanego przez gracza.

Pierwszym elementem tablicy witryn, którą generuje GenerateFragmentSites(), będzie wewnętrzna tablica wszystkich witryn, które znajdują się poza żądanym promieniem. Jeśli chcesz zrobić coś konkretnego z pozostałą "niezniszczoną częścią" części, możesz znaleźć tę część, sprawdzając fragments[i].Index == 1, gdy przechodzisz przez wyniki FragmentAsync().

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("Nie udało się rozbić:"..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 z pierwszej części w obrębie kształtu zdefiniowanego przez użycie drugiej części jako szablonu. 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 fragmentacji
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("Nie udało się rozbić:"..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 jest znacznie bardziej niszowym przypadkiem użycia, ale demonstruje moc danych indeksowych, które są zwracane z FragmentAsync().

Na przykład, wiele miejsc zawiera budynki utworzone z wielu niepołączonych części blokowych. Jeśli granat, kula armatnia lub młot kowalski uszkodziłby je, 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, aby całkowicie ukryć szwy.

To wymaga wielu operacji Async(), więc może nie być odpowiednie do użycia w grze jako natychmiastowa odpowiedź na wejście użytkownika, takie jak narzędzie młota kowalskiego.

Rząd bloków
Rząd bloków
Rząd bloków fragmentowanych
Każdy kawałek może pochodzić z wielu części wejściowych
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("Nie udało się rozbić:"..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 jest niemal identycznym zamiennikiem Luau dla GenerateFragmentSites(). Jeśli chcesz uzyskać podobne zachowanie do GenerateFragmentSites(), ale chcesz wprowadzić drobne zmiany, możesz użyć tego jako punktu wyjścia.

Używa jitterowanej siatki punktów i gwarantuje, że obszar fragmentacji jest dobrze zachowany, w przeciwieństwie do całkowicie losowych punktów.

local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Użyj tego, aby dostosować ilość jitteru
if (origin and not radius) or (radius and not origin) then
warn("Muszą być podane zarówno pochodzenie, jak i promień, lub żaden z nich.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized oznacza, że nie łamiesz 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 część wejściowa ma ograniczenia lub załączniki, które chcesz zachować, możesz je przenieść do wynikowych części. Może być to uciążliwe, aby ustalić, do której wynikowej części powinno być przypisane ograniczenie, dlatego zaleca się użycie CalculateConstraintsToPreserve(), aby wygenerować tabelę rekomendacji, którą możesz przejść i zastosować.

Aby to zademonstrować, poniższy przykład kodu wykonuje operację odejmowania, przechodzi przez wynikowe części, aby zmienić rodzica i pozycję wynikowych części, a następnie oblicza tabelę ograniczeń i załączników, które należy zachować lub usunąć 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
-- Przejdź przez wynikowe części, aby zmienić rodzica/pozycję
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 usunąć
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Zachowaj ograniczenia/załączniki na podstawie rekomendowanej tabeli
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

Wyniki modelowania bryłowego

Szczegóły zachowania

  • Oryginalne części pozostają nienaruszone po udanej operacji, a zwrócone części nie mają ustawionego Parent. W większości przypadków zaleca się przypisanie zwróconej części (części) do tego samego miejsca, co główna część, a następnie Destroy() oryginalnych części.

  • 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 znajdują się w tej samej przestrzeni współrzędnych co główna część.

  • Jeśli operacja modelowania bryłowego spowodowałaby powstanie jakichkolwiek części z więcej niż 20 000 trójkątów, zostaną one uproszczone do 20 000. Jeśli nie można tego zrobić, zazwyczaj w przypadku tysięcy nieprzechodzących komponentów operacja kończy się błędem.

  • Istnieją metody, aby 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 naklejki. To podejście omija również potencjalne "migotanie" całkowitego zastąpienia oryginalnego PartOperation innym.

  • Możliwe jest wywoływanie tych metod po stronie klienta, ale z pewnymi ograniczeniami. Po pierwsze, musi to być zrobione z obiektami utworzonymi po stronie klienta. Po drugie, nie ma replikacji dostępnej z klienta do serwera.

Kolory i UV

Kolory wynikowej części (części) po modelowaniu bryłowym pochodzą z dwóch miejsc: kolorów powierzchni i koloru części Color.

  • Jeśli wynik to PartOperation, będzie miał Color pierwszej części, którą wybrałeś w Studio, ale Studio domyślnie używa kolorów powierzchni, aby każda powierzchnia miała ten sam kolor, co przed operacją. Możesz włączyć właściwość UsePartColor w Studio, aby nadpisać to zachowanie i uczynić cały wynik jednolitym kolorem.
  • Jeśli wynik to MeshPart, jego Color będzie biały, a kolory powierzchni zawsze będą widoczne. Możesz dostosować odcień wynikowej części (części) zmieniając ich Color, ale będzie to mieszane (mnożone) z kolorami powierzchni. To zabarwia wynik, a nie całkowicie nadpisuje kolory powierzchni. Jeśli chcesz mieć pełną kontrolę nad kolorem wyjściowym, najlepiej najpierw uczynić wejścia białymi.

UV są również obsługiwane inaczej w zależności od typu wyniku:

  • PartOperations zawsze mają UV o mapowaniu pudełkowym, co oznacza, że każda powierzchnia będzie miała materiał/teksturę/naklejkę z jednej strony (jednej z -x, +x, -y, +y, -z, +z) zastosowaną do niej. To może rozciągać tekstury.
  • MeshParts nie są mapowane pudełkowo. UV siatki głównej części są używane. Ponieważ Roblox obecnie nie ma wsparcia dla wielu materiałów, UV powierzchni pochodzących z innych części mają UV (0, 0). Aby uzyskać najlepsze wyniki, upewnij się, że piksel (0, 0) Twojej tekstury ma rozsądny kolor.

Kąt wygładzania

Właściwość SmoothingAngle modelowanej bryły wygładza kąty między sąsiednimi powierzchniami tego samego koloru. Wyższa wartość daje gładszy wygląd, podczas gdy niższa wartość daje szorstszy wygląd z większą ilością ostrych krawędzi.

Chociaż wartość między 30 a 70 stopni zazwyczaj daje dobry wynik, wartości między 90 a 180 nie są zalecane, ponieważ mogą powodować efekt "cieniowania" na uniach i intersekcjach z ostrymi krawędziami.

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