Solides Modellieren

*Dieser Inhalt wurde mit KI (Beta) übersetzt und kann Fehler enthalten. Um diese Seite auf Englisch zu sehen, klicke hier.

Solides Modellieren ist der Prozess des Zusammenfügens von Teilen auf einzigartige Weise, um komplexere Formen zu bilden. Dazu gehören die booleschen Operationen Vereinigung, Subtraktion und Schnitt, die allgemein als Konstruktive Solide Geometrie (CSG) bekannt sind. Sie können solides Modellieren überall durchführen: im Studio, in Plugins und sogar im Spiel sowohl auf dem Server als auch auf dem Klienten.

Neben der booleschen CSG unterstützt solides Modellieren auch Meshes, solange sie wasserdicht sind, sowie Operationen wie Sweep und Fragment, mit denen Sie und Ihre Spieler Geometrie schneiden, durchtrennen und zerbrechen können, um einzigartige Gameplay-Interaktionen zu ermöglichen.

Ein Mesh-Objekt, das mit SweepPartAsync() erstellt wurde
Ein Mesh-Objekt, das mit FragmentAsync() zerbrochen wurde

Wasserdichte Geometrie

Es gibt drei grundlegende Elemente von Meshes:

  • Vertex - Ein einzelner Punkt im Mesh.
  • Edge - Eine Linie, die zwei Vertizes verbindet.
  • Face - Ein Flächenbereich zwischen drei oder mehr Vertizes.
Ein einzelner aktiver Vertex auf einem Würfel-Mesh.
Vertex
Eine einzelne aktive Kante auf einem Würfel-Mesh.
Kante
Eine einzelne aktive Fläche auf einem Würfel-Mesh.
Fläche

Solid Modellierungsoperationen können nur mit wasserdichter Geometrie arbeiten; in der Tat sind "solid" und "wasserdicht" synonym. In technischen Begriffen bedeutet ein wasserdichtes Mesh, dass es geschlossen, manifold und nicht selbstschneidend ist. Diese Begriffe haben strenge Definitionen, aber hier sind einige einfache Regeln:

  • Jede Fläche muss eine 'Innenseite' und eine 'Außenseite' haben. Diese werden durch die Windungsreihenfolge der Fläche bestimmt, die die Reihenfolge ihrer drei Vertizes ist.
  • Jede Kante muss genau von zwei Dreiecken geteilt werden. Das bedeutet, dass es keine Löcher im Mesh geben kann, da die Kanten rund um den Rand eines Lochs nur ein Dreieck haben würden.
  • Flächen dürfen nicht durch andere Flächen hindurchgehen.
  • Benachbarte Dreiecke müssen sich darauf einigen, welche Seite die 'Außenseite' ist.
  • Jeder Vertex muss genau einen Fächer von benachbarten Dreiecken haben.
Beispiele für nicht-wasserdichte Geometrie

Das System für solides Modellieren kann spezifische kleine Probleme mit einem Mesh automatisch beheben, aber im Allgemeinen werden API-Aufrufe fehlschlagen, wenn das Mesh nicht wasserdicht ist. Es gibt keinen einheitlichen Ansatz zur Reparatur eines bestehenden nicht-wasserdichten Meshes, aber es gibt mehrere Blender-Plugins, die helfen können, wie 3D Print Toolbox und Mesh Repair Tools. Als weitere Option hat Meshlab ebenfalls sehr nützliche Werkzeuge eingebaut, um zu versuchen, das Mesh manifold zu machen, was die Hauptanforderung für ein Mesh ist, wasserdicht zu sein.

Eine Möglichkeit, festzustellen, dass ein Mesh extrem schwer wasserdicht zu machen ist, besteht darin, es im Studio aus allen Blickwinkeln zu betrachten und dann zu versuchen, die MeshPart.DoubleSided-Eigenschaft des Meshes zu aktivieren und zu deaktivieren. Wenn Sie einen Unterschied sehen können, dann ist das Mesh nur eine Schale und die oben genannten Werkzeuge funktionieren nicht, da sie nicht erraten können, welcher Raum innerhalb und außerhalb des Meshes ist. Wenn Sie jedoch nur ein dünnes Mesh möchten und es nicht wichtig ist, die Abmessungen des Meshes genau gleich zu halten, können Sie den Blender Solidify Modifier verwenden, um die Schale leicht zu verdicken und in ein wasserdichtes Mesh zu verwandeln.

Beispiel eines Meshes, das anders aussieht, wenn DoubleSided aktiviert ist.
Dies ist ein Schalen-Mesh und automatische Reparaturmethoden funktionieren nicht dafür.

Solides Modellieren im Studio

Sie können drei grundlegende boolesche Operationen mithilfe von vier Werkzeugen in der Modell-Tab-Leiste durchführen.

Die Werkzeuge für solides Modellieren, die in der Symbolleiste des Studios hervorgehoben sind.
WerkzeugShortcutBeschreibung
VereinigungShiftCtrlG (Windows)
ShiftG (Mac)
Verbinden Sie zwei oder mehr Teile zu einer einzigen soliden Vereinigung.
SchnittShiftCtrlI (Windows)
ShiftI (Mac)
Übereinanderliegende Teile zu einer einzigen soliden Schnittmenge schneiden.
NegierenShiftCtrlN (Windows)
ShiftN (Mac)
Teile negieren, nützlich zum Erstellen von Löchern und Vertiefungen.
TrennenShiftCtrlU (Windows)
ShiftU (Mac)
Die Vereinigung oder Schnittmenge wieder in ihre einzelnen Teile trennen.

Teile vereinen

Das Vereinigungs-Werkzeug verbindet zwei oder mehr Teile und bildet eine einzelne solide UnionOperation.

Ein Block und ein Zylinder.
Individuelle Teile
Ein Block und ein Zylinder, die zu einem Objekt vereint sind.
Vereinigungsresultat

Um Teile zu einer Vereinigung zusammenzufügt:

  1. Wählen Sie alle Teile aus, die Sie verbinden möchten.
  2. Klicken Sie auf die Vereinigung-Schaltfläche. Alle Teile verbinden sich zu einer soliden UnionOperation mit dem Namen Vereinigung.

Teile schneiden

Das Schnitt-Werkzeug schneidet überlappende Teile zu einer einzigen soliden IntersectOperation.

Ein Block und ein Zylinder.
Individuelle Teile
Ein Block und ein Zylinder zu einem Objekt kombiniert.
Schnittresultat

Um überlappende Teile zu schneiden:

  1. Wählen Sie alle Teile aus, die Sie schneiden möchten.
  2. Klicken Sie auf die Schnitt-Schaltfläche. Alle Teile verbinden sich zu einer soliden IntersectOperation mit dem Namen Schnittmenge.

Teile negieren

Das Negieren-Werkzeug negiert ein Teil, sodass, wenn es mit einem anderen Teil vereint wird, die Form des negierten Teils von dem anderen Teil subtrahiert wird.

Ein Block und ein Zylinder.
Block und negierter Zylinder
Ein Block und ein Zylinder zu einem Objekt kombiniert.
Subtraktionsresultat

Um ein Teil von anderen überlappenden Teilen zu subtrahieren:

  1. Wählen Sie das Teil aus, das Sie von anderen Teilen negieren möchten.
  2. Klicken Sie auf Negieren. Das Teil wird als negiertes Teil gekennzeichnet und ein negatives Symbol erscheint im Explorer. Das Teil wird durchscheinend mit einem rötlichen Farbton, um seinen Zustand anzuzeigen.
  3. Wählen Sie sowohl das negierte Teil als auch die Teile aus, von denen Sie subtrahieren möchten.
  4. Klicken Sie auf Vereinigung. Das negierte Teil wird aus den eingeschlossenen überlappenden Teilen herausgeschnitten.

Das Tag ist für das Skripting verfügbar, sodass Sie Teile auch negieren können, indem Sie das Tag rbxNegate von einem Skript oder Plugin hinzufügen. NegateOperation wird nicht mehr verwendet.

Vereinigungen oder Schnittmengen trennen

Das Trennen-Werkzeug trennt eine UnionOperation wieder in ihre einzelnen Teile und dient im Wesentlichen als "Rückgängig"-Werkzeug für Vereinigungen und Schnittmengen.

Um eine Vereinigung oder Schnittmenge wieder in einzelne Teile zu trennen:

  1. Wählen Sie die Vereinigung oder Schnittmenge aus.
  2. Klicken Sie auf Trennen. Die Teile trennen sich wieder in ihre ursprüngliche Form.

Solides Modellieren im Spiel

Sie können auch solide Modellierungsoperationen während des Spiels durchführen, indem Sie die Funktionen von GeometryService verwenden.

UnionAsync(), IntersectAsync() und SubtractAsync()

Ähnlich wie bei den integrierten grundlegenden booleschen Operationswerkzeugen von Studio können Sie Funktionen von GeometryService wie UnionAsync(), IntersectAsync() und SubtractAsync() verwenden, um grundlegende boolesche Operationen während eines Spiels auszuführen. Zum Beispiel verwendet das folgende Skript SubtractAsync(), um das Volumen eines Teils von einem anderen zu subtrahieren.


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
Ein Block, der von einem anderen Block subtrahiert wurde.

Um dies weiter zu demonstrieren, kombiniert das nächste Codebeispiel die Geometrie von mainPart und den Teilen im otherParts-Array und zerstört dann die ursprünglichen Teile, die an der Operation beteiligt sind. Sie können den Aufruf von UnionAsync() durch IntersectAsync() oder SubtractAsync() ersetzen, um die anderen booleschen Operationen auszuführen.


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,
}
-- Führen Sie die Vereinigungsoperation in pcall() aus, da sie asynchron ist
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Durchlaufen Sie die resultierenden Teile, um sie neu zu parenten/neu zu positionieren
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Zerstören Sie die ursprünglichen Teile
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Sofern alle Eingaben aus primitiven Teilen und nicht aus MeshParts bestehen, erzeugen die Vereinigungs-, Intersekts- und Subtraktionsbooleschen Operationen ein PartOperation mit zwei Datenbestandteilen: einem Baum von CSG-Operationen, der als CSGTree bekannt ist, und einem Mesh zum Rendern.

Im Vergleich zu BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync() unterscheiden sich die Boolesefunktionen von GeometryService wie folgt:

  • Die Ausgabe ist ein Array von Instanzen und nicht eine einzelne Instanz.
  • Die Eingabeteile müssen nicht an die Szene angehängt werden, was Hintergrundoperationen ermöglicht.
  • Wenn die Option SplitApart auf true (Standard) gesetzt ist, wird jeder einzelne Körper in seiner eigenen PartOperation/MeshPart zurückgegeben.
  • Alle zurückgegebenen Teile befinden sich im Koordinatensystem des Hauptteils, sodass ihre PVInstance.Origin-Positionen dieselbe Position wie der Hauptteil haben. Das hält die Vertizes des Meshes in derselben Position relativ zum Objekt wie vor der Operation, bedeutet jedoch auch, dass das (0, 0, 0) eines zurückgegebenen Teils nicht unbedingt im Zentrum seines Körpers liegt.

SweepPartAsync()

Die Funktion GeometryService:SweepPartAsync() erstellt ein MeshPart, das die Form des Eingabeteils hat, das durch ein gegebenes Set von CFrame-Positionen gezogen wird. Diese Funktion kann sehr nützlich sein, um Schnitt- und Schneideinteraktionen durchzuführen.

Der Input kann ein Part, PartOperation oder MeshPart sein. Die Form des Ergebnisses wird als Vereinigung der konvexen Hüllen jedes benachbarten Paares von CFrames definiert; wenn nur ein einzelnes CFrame bereitgestellt wird, wird das Ergebnis eine konvexe Hülle des Eingabeteils sein.

Um zu demonstrieren, wie diese Funktion funktioniert, sweepen die folgenden Codebeispiele eine Kugel durch ein Set von CFrame-Positionen, um eine Spirale zu erstellen:


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
Eine spiralförmige Form, die durch das Sweepen einer Kugel erstellt wurde.

Beispiel

Eine durchsichtige gebogene Form, die von einem Block subtrahiert wurde.

Dieses Beispiel verwendet GeometryService:SweepPartAsync(), um eine Gameplay-Funktion für Schwert- oder Laserpistolen-Schnitte zu erreichen, bei der die Bewegung des Schwertes auf der Mausposition des Spielers basiert. Die Mausbewegung des Benutzers wird als Liste von CFrames erfasst, SweepPartAsync() baut ein Schnittmesh aus diesen Daten und zieht dann das Schnittmesh von dem Teil ab, das getroffen wurde.

Um dieses Beispiel im Studio zum Laufen zu bringen:

  1. Erstellen Sie das folgende Script in ServerScriptService, um alle soliden Modellierungsoperationen durchzuführen.


    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
    -- Visualisieren Sie den Sweep
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Subtrahieren Sie den Sweep vom getroffenen Objekt
    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. Erstellen Sie das folgende LocalScript in StarterPlayerScripts, um Benutzereingaben zu verarbeiten.


    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
    -- Visualisieren Sie den Sweep
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Subtrahieren Sie den Sweep vom getroffenen Objekt
    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. Erstellen Sie einen RemoteEvent in ReplicatedStorage mit dem Namen DrawCurveEvent.

FragmentAsync()

Die Funktionen GeometryService:FragmentAsync() und GeometryService:GenerateFragmentSites() ermöglichen es Ihnen, ein Teil in Stücke mit natürlichen Formen zu zerbrechen. GeometryService:FragmentAsync() verwendet voronoi-Zerlegung, um ein einzelnes Teil in mehrere MeshPart-Instanzen entsprechend dem Muster von übergebenen Punkten zu teilen, während GeometryService:GenerateFragmentSites() eine Hilfsfunktion ist, die Punkte generiert, die als voronoi-Stätten bekannt sind und die an FragmentAsync() übergeben werden.

Um zu demonstrieren, wie diese Funktionen zusammenarbeiten, generiert das folgende Codebeispiel voronoi-Stätten, um ein Blockteil zu fragmentieren:


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
Ein Block, der in Stücke zerbrochen wurde.

Beispiele

Ein Block, dessen Ecke in Stücke fragmentiert wurde.

Das folgende Skript fragmentiert einen Bereich eines Teils, der von einer Position und einem Radius gegeben wird. Die Position könnte üblicherweise aus einer physikalischen Kollision oder einem Raycast eines Spielers stammen.

Das erste Element des Standort-Arrays, das GenerateFragmentSites() generiert, wird ein inneres Array aller Stätten sein, die außerhalb des angeforderten Radius liegen. Wenn Sie etwas bestimmes mit dem verbleibenden 'ungeschädigten Teil' des Teils tun möchten, können Sie diesen Teil finden, indem Sie fragments[i].Index == 1 beim Durchlaufen der Ergebnisse von FragmentAsync() überprüfen.


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("Fehler beim Fracture:"..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

Das folgende Skript zerbricht Fragmente des ersten Teils innerhalb einer Form, die durch ein zweites Teil als Schablone definiert ist. Nur voronoi-Stätten innerhalb des zweiten Teils führen zu separaten Stücken. Alle anderen Standorte haben ihre Zellen zur einer einzigen Teil kombiniert.

Schablone in Form des Roblox Studio-Logos
Teil (dunkelgrau) und Schablonen-Teil
Ergebnisse einer Fragmentierung, die der Form einer Schablone entspricht
Fragmentierungsskriptresultat

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("Fehler beim Fracture:"..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

Das folgende Skript ist ein viel spezifischerer Anwendungsfall, demonstriert aber die Macht der Indexdaten, die aus GeometryService:FragmentAsync() zurückgegeben werden.

Wenn viele Orte Gebäude aus mehreren nicht vereinheitlichten Blockteilen enthalten, möchten Sie, dass alle Wandteile fragmentiert werden, wenn eine Granate, eine Kanonenkugel oder ein Vorschlaghammer Schaden anrichtet. Dieses Skript fragmentiert alle nahegelegenen Teile und vereint die Fragmente verschiedener Teile, um die Nähte vollständig zu verbergen.

Dies beinhaltet mehrere Async-Operationen und ist daher möglicherweise nicht für den Einsatz im Spiel geeignet, als sofortige Antwort auf Benutzereingaben, wie z.B. mit einem Vorschlaghammerwerkzeug.

Eine Reihe von Blöcken
Eine Reihe von Blöcken
Eine Reihe von Blöcken fragmentiert
Jedes Stück kann von mehreren Eingabeteilen stammen

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("Fehler beim Fracture:"..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("Fehler beim Union:"..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

Das folgende Skript ist ein fast identischer Luau-Ersatz für GeometryService:GenerateFragmentSites(). Wenn Sie ein ähnliches Verhalten wie GeometryService:GenerateFragmentSites() wünschen, aber kleine Änderungen vornehmen möchten, können Sie dies als Ausgangspunkt verwenden.

Es verwendet ein jittered Raster von Punkten und garantiert, dass der fragmentierte Bereich gut aussieht, im Gegensatz zu vollständig zufälligen Punkten.


local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Verwenden Sie dies, um die Menge des Jitters anzupassen
if (origin and not radius) or (radius and not origin) then
warn("Entweder sollten beide Ursprung und Radius bereitgestellt werden oder keiner.")
return {}
end
local isLocalized = (radius ~= nil) -- istLocalized bedeutet, dass das ganze Teil nicht zerbrochen wird, sondern nur ein Abschnitt.
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

Einschränkungen beibehalten

Wenn ein Eingabeteil Einschränkungen oder Anhänge hat, die Sie beibehalten möchten, können Sie diese auf die resultierenden Teile übertragen. Es kann mühsam sein, herauszufinden, an welches Ausgabeobjekt ein Constraint angehängt werden soll, daher wird empfohlen, GeometryService:CalculateConstraintsToPreserve() zu verwenden, um eine Tabelle von Empfehlungen zu generieren, die Sie durchlaufen und anwenden können.

Um zu demonstrieren, führt das folgende Codebeispiel eine Subtraktionsoperation durch, durchläuft die resultierenden Teile, um sie neu zu parenten und neu zu positionieren, und berechnet dann eine Tabelle von Einschränkungen und Anhängen, die entweder beibehalten oder fallen gelassen werden sollen, bevor alle ursprünglichen Teile zerstört werden.


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,
}
-- Führen Sie die Subtraktionsoperation in pcall() aus, da sie asynchron ist
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Durchlaufen Sie die resultierenden Teile, um sie neu zu parenten/neu zu positionieren
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Berechnen Sie Einschränkungen/Anhänge, die entweder beibehalten oder fallen gelassen werden sollen
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Behalten Sie Einschränkungen/Anhänge basierend auf der empfohlenen Tabelle
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
-- Zerstören Sie die ursprünglichen Teile
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Verhaltensdetails

Überlegungen zu den Ergebnissen des soliden Modellierens

Farben und UVs

Die Farben der resultierenden Teile(n) nach dem soliden Modellieren stammen aus zwei Quellen: den Flächenfarben und der Color des Teils.

  • Wenn das Ergebnis ein PartOperation ist, hat es die Color des ersten Teils, den Sie im Studio ausgewählt haben, aber Standardmäßig verwendet Studio die Flächenfarben, um jede Fläche in dieselbe Farbe zu halten wie zuvor. Sie können die UsePartColor-Eigenschaft im Studio aktivieren, um dieses Verhalten zu überschreiben und das gesamte Ergebnis in einer einzigen Farbe zu machen.
  • Wenn das Ergebnis ein MeshPart ist, wird dessen Color weiß sein, und die Flächenfarben werden immer durchscheinen. Sie können den Farbton der resultierenden Teile(n) ändern, indem Sie ihre Color ändern, aber es wird mit den Flächenfarben multipliziert. Dies färbt das Ergebnis, anstatt die Flächenfarben vollständig zu überschreiben. Wenn Sie die vollständige Kontrolle über die Farbe der Ausgabe wünschen, ist es am besten, die Eingaben zuerst weiß zu machen.

UVs werden ebenfalls unterschiedlich abhängig vom Typ des Ergebnisses behandelt:

  • PartOperations haben immer box-mapped UVs, was bedeutet, dass jede Fläche das Material/Textur/Decal aus einer Richtung (einer von -x, +x, -y, +y, -z, +z) darauf angewendet bekommt. Dies kann Texturen dehnen.
  • MeshParts sind nicht box-mapped. Die UVs des Meshes des Hauptteils werden verwendet. Da Roblox derzeit keine Unterstützung für mehrere Materialien hat, erhalten die UVs von Flächen, die von den anderen Teilen stammen, UVs von (0, 0). Für die besten Ergebnisse stellen Sie sicher, dass Pixel (0, 0) Ihrer Textur eine angemessene Farbe hat.

Glättungswinkel

Der SmoothingAngle-Wert eines solid modellierten Teils glättet die Winkel zwischen benachbarten Oberflächen derselben Farbe. Ein höherer Wert erzeugt ein glatteres Aussehen, während ein niedrigerer Wert ein raueres Aussehen mit mehr scharfen Kanten erzeugt.

Während ein Wert zwischen 30 und 70 Grad normalerweise ein gutes Ergebnis liefert, sind Werte zwischen 90 und 180 nicht empfohlen, da sie einen "Schatten"-Effekt auf Vereinigungen und Schnittmengen mit scharfen Kanten verursachen können.

Vereinfachung von Teilen

Wenn eine solide Modellierungsoperation Teile mit mehr als 20.000 Dreiecken als Ergebnis erzeugen würde, werden diese auf 20.000 vereinfachst. Wenn das nicht möglich ist, was normalerweise bei Tausenden von nicht überlappenden Komponenten der Fall ist, führt die Operation zu einem Fehler.

Ein MeshPart in gutem Zustand
Vor der Vereinfachung
Ein MeshPart mit offensichtlicher Reduzierung der Mesh-Qualität
Nach der Vereinfachung
©2026 Roblox Corporation. Roblox, das Roblox-Logo und "Powering Imagination" gehören zu unseren eingetragenen und nicht eingetragenen Markenzeichen in den USA und anderen Ländern.