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, BaseParts auf einzigartige Weise zusammenzufügen, um komplexere Formen zu bilden. Dazu gehören die booleschen Operationen Vereinigung, Schnitt und Subtraktion, die allgemein als Konstruktive Solide Geometrie (CSG) bekannt sind. Sie können solides Modellieren im Studio, in Plugins und im Spiel sowohl auf dem Server als auch auf dem Client durchführen.

Neben der booleschen CSG unterstützt das solide Modellieren auch Meshes, solange sie wasserdicht sind, sowie Operationen wie Sweep und Fragment, die es Ihnen und Ihren Spielern ermöglichen, Geometrie für einzigartige Gameplay-Interaktionen zu schneiden, zu zerschneiden und zu zertrümmern.

Wasserdichte Geometrie

Solide Modellierungsoperationen können nur mit wasserdichter Geometrie arbeiten. Technisch bedeutet es, dass ein Mesh wasserdicht ist, wenn 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 Wicklungsreihenfolge der Fläche bestimmt, die die Reihenfolge ihrer drei Eckpunkte ist.
  • Jede Kante muss genau von zwei Dreiecken geteilt werden. Das bedeutet, dass es keine Löcher im Mesh geben kann, da die Kanten um den Rand eines Lochs nur ein Dreieck hätten.
  • Flächen dürfen nicht durch andere Flächen hindurchgehen.
  • Benachbarte Dreiecke müssen sich darauf einigen, welche Seite die "Außenseite" ist.
  • Jeder Eckpunkt muss genau einen Fächer von benachbarten Dreiecken haben.

Jedes der folgenden Beispiele ist nicht wasserdicht aus dem angegebenen Grund:

Das System für solides Modellieren kann bestimmte kleine Probleme mit einem Mesh automatisch reparieren, aber im Allgemeinen werden API-Aufrufe fehlschlagen, wenn das Mesh nicht wasserdicht ist. Es gibt keinen universellen Weg, um ein bestehendes nicht wasserdichtes Mesh zu reparieren, aber es gibt mehrere Blender-Plugins, die helfen können, wie 3D Print Toolbox und Mesh Repair Tools. Als weitere Option hat Meshlab auch sehr nützliche Werkzeuge eingebaut, um zu versuchen, das Mesh manifold zu machen, was die Hauptanforderung für ein wasserdichtes Mesh ist.

Eine Möglichkeit zu sehen, dass ein Mesh extrem schwierig wasserdicht zu machen ist, besteht darin, es im Studio aus allen Winkeln zu betrachten und dann die MeshPart.DoubleSided-Eigenschaft des Meshes zu aktivieren und zu deaktivieren. Wenn Sie einen Unterschied sehen können, dann ist das Mesh nur eine Hülle 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 Solidify Modifier von Blender verwenden, um die Hülle leicht zu verdicken und in ein wasserdichtes Mesh zu verwandeln.

DoubleSided = false

Solides Modellieren im Studio

Sie können drei grundlegende boolesche Operationen mit vier Werkzeugen in der Symbolleiste des Modell-Tabs durchführen.

Solides Modellieren Werkzeuge in der Symbolleiste des Studios hervorgehoben.
WerkzeugShortcutBeschreibung
VereinigungShiftCtrlG (Windows)
ShiftG (Mac)
Verbinden Sie zwei oder mehr Teile zu einer einzigen soliden Vereinigung.
SchnittShiftCtrlI (Windows)
ShiftI (Mac)
Überlappende Teile in einen einzigen soliden Schnitt schneiden.
TrennenShiftCtrlU (Windows)
ShiftU (Mac)
Die Vereinigung oder den Schnitt wieder in seine einzelnen Teile trennen.
NegierenShiftCtrlN (Windows)
ShiftN (Mac)
Teile negieren, nützlich zum Erstellen von Löchern und Vertiefungen.

Vereinigung

Das Vereinigung-Werkzeug verbindet zwei oder mehr Teile zu einer einzigen soliden UnionOperation.

Ein Block und ein Zylinder.
Einzelne Teile
Ein Block und ein Zylinder, die zu einem Objekt kombiniert sind.
Vereinigungsergebnis

Um Teile zu einer Vereinigung zusammenzuführen:

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

Schnitt

Das Schnitt-Werkzeug schneidet überlappende Teile in einen einzigen soliden IntersectOperation.

Ein Block und ein Zylinder.
Einzelne Teile
Ein Block und ein Zylinder, die zu einem Objekt kombiniert sind.
Schnittresultat

Um überlappende Teile zusammenzuschneiden:

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

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, die zu einem Objekt kombiniert sind.
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 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 es subtrahieren möchten.
  4. Klicken Sie auf Vereinigung. Das negierte Teil wird aus den eingeschlossenen überlappenden Teilen herausgeschnitten.

Trennen

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

Um eine Vereinigung oder einen Schnitt wieder in einzelne Teile zu trennen:

  1. Wählen Sie die Vereinigung oder den Schnitt 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 durchführen, während ein Spiel läuft oder über ein Plugin durch GeometryService-Methoden.

Kernoperationen

Ähnlich wie bei den integrierten Operationstools im Studio können Sie GeometryService-Methoden wie UnionAsync(), IntersectAsync() und SubtractAsync() verwenden, um grundlegende boolesche Operationen durchzuführen. Zum Beispiel verwendet das folgende Skript SubtractAsync(), um das Volumen eines Teils von einem anderen abzuziehen.

Einen Teil von einem anderen abziehen
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

Um dies weiter zu demonstrieren, kombiniert das nächste Codesample 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 durchzuführen.

Geometrie mehrerer Teile kombinieren
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
-- Durch die resultierenden Teile schleifen, 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 der ursprünglichen Teile
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Obwohl es keine engine-level Methode für die Negation von Teilen gibt, können Sie das rbxNegate-Tag innerhalb eines Skripts oder Plugins hinzufügen, um eine Negation durchzuführen, die der Negieren-Schaltfläche im Studio entspricht.

Teilnegation über Skript
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")

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

  • Die Ausgabe ist ein Array von Instanzen anstelle einer einzelnen Instanz.
  • Die Eingabeteile müssen nicht in der Szene verankert sein, was Hintergrundoperationen ermöglicht.
  • Wenn die SplitApart-Option auf true (Standard) gesetzt ist, wird jeder einzelne Körper in seiner eigenen PartOperation/MeshPart zurückgegeben.
  • Alle zurückgegebenen Teile befinden sich im Koordinatenraum des Hauptteils, sodass ihre PVInstance.Origin-Positionen dieselben sind wie die des Hauptteils. Dies hält die Vertices 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.

Sweep-Teil

Die Methode SweepPartAsync() erstellt ein MeshPart, das die Form des Eingabeteils hat, das durch eine gegebene Menge von CFrame-Positionen gezogen wird. Diese Methode kann sehr nützlich sein, um Schnitt- und Schneideinteraktionen durchzuführen.

Der Eingang kann ein Part, PartOperation oder MeshPart sein. Die Form des Ergebnisses wird als die Vereinigung der konvexen Hüllen jedes benachbarten Paars 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 Methode funktioniert, fegt das folgende Codesample einen Ball durch eine Menge von CFrame-Positionen, um eine Spirale zu erstellen:

Sweep-Teil entlang einer Spirale
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 Fegen einer Kugel erstellt wurde.
Eine durchscheinende gebogene Form, die von einem Block subtrahiert wurde.

Dieses Beispiel verwendet SweepPartAsync(), um eine Gameplay-Funktion für das Schneiden mit einem Schwert oder einer Laserwaffe zu erreichen, bei der die Bewegung des Schwertes auf der Mausposition des Spielers basiert. Die Mausbewegung des Benutzers wird als Liste von CFrames aufgezeichnet, SweepPartAsync() erstellt ein Schnittmesh aus diesen Daten, das dann vom Teil, das getroffen wurde, subtrahiert wird.

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 Teil
    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 Teil
    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 ein RemoteEvent in ReplicatedStorage mit dem Namen DrawCurveEvent.

Fragment

Die Methoden FragmentAsync() und GenerateFragmentSites() ermöglichen es Ihnen, ein Teil in Stücke mit natürlichen Formen zu zertrümmern. FragmentAsync() verwendet Voronoi-Zerlegung, um ein einzelnes Teil in mehrere MeshPart-Instanzen gemäß dem Muster der übergebenen Punkte zu unterteilen, während GenerateFragmentSites() eine Hilfsmethode ist, die Punkte generiert, die als Voronoi-Stellen bekannt sind, um sie in FragmentAsync() zu übergeben.

Das folgende Codesample generiert Voronoi-Stellen, 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 zertrümmert wurde.
Ein Block mit der Ecke, die in Stücke fragmentiert wurde.

Das folgende Skript fragmentiert einen Bereich eines Teils, der durch eine Position und einen Radius angegeben wird. Die Position könnte häufig aus einer physikalischen Kollision oder einem Raycast von einem Spieler stammen.

Das erste Element des Stellenarrays, das GenerateFragmentSites() generiert, wird ein inneres Array aller Stellen sein, die außerhalb des angeforderten Radius liegen. Wenn Sie etwas Spezifisches mit dem verbleibenden "nicht zertrümmerten Teil" des Teils tun möchten, können Sie diesen Teil finden, indem Sie fragments[i].Index == 1 überprüfen, während Sie über die Ergebnisse von FragmentAsync() iterieren.

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("Fehler beim Fragmentieren:"..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 vom ersten Teil innerhalb einer Form, die durch ein zweites Teil als Schablone definiert ist. Nur Voronoi-Stellen innerhalb des zweiten Teils führen zu separaten Stücken. Alle anderen Stellen werden in ein einzelnes Teil kombiniert.

Schablone in Form des Roblox Studio-Logos
Teil (dunkelgrau) und Schablonenteil
Ergebnisse eines Fragments in Form einer Schablone
Fragment-Skript-Ergebnis
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("Fehler beim Fragmentieren:"..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, zeigt jedoch die Leistungsfähigkeit der Indexdaten, die von FragmentAsync() zurückgegeben werden.

Zum Beispiel enthalten viele Orte Gebäude, die aus mehreren nicht vereinigten Blockteilen bestehen. Wenn eine Granate, eine Kanonenkugel oder ein Vorschlaghammer sie beschädigen würde, möchten Sie, dass alle Wandteile fragmentiert werden. 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, sodass es möglicherweise nicht geeignet ist, im Spiel als sofortige Reaktion auf Benutzereingaben, wie z. B. ein Vorschlaghammer-Werkzeug, verwendet zu werden.

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")
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("Fehler beim Fragmentieren:"..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 Vereinen:"..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 nahezu identischer Luau-Ersatz für GenerateFragmentSites(). Wenn Sie ein ähnliches Verhalten wie GenerateFragmentSites() wünschen, aber kleine Änderungen vornehmen möchten, können Sie dies als Ausgangspunkt verwenden.

Es verwendet ein jittered Gitter von Punkten und garantiert, dass der fragmentierte Bereich gut funktioniert, 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 an Jitter anzupassen
if (origin and not radius) or (radius and not origin) then
warn("Entweder sollten sowohl Ursprung als auch Radius bereitgestellt werden, oder keiner.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized bedeutet, dass das gesamte Teil nicht zertrümmert 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 Ausgangsteil eine Einschränkung angehängt werden soll, daher wird empfohlen, CalculateConstraintsToPreserve() zu verwenden, um eine Tabelle von Empfehlungen zu generieren, die Sie durchlaufen und anwenden können.

Um dies zu demonstrieren, führt das folgende Codesample 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() durch, da sie asynchron ist
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Durch die resultierenden Teile schleifen, 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 die Einschränkungen/Anhänge, die entweder beibehalten oder fallen gelassen werden sollen
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Behalten Sie die Einschränkungen/Anhänge basierend auf der empfohlenen Tabelle bei
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 der ursprünglichen Teile
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Ergebnisse des soliden Modellierens

Verhaltensdetails

  • Die ursprünglichen Teile bleiben nach einer erfolgreichen Operation intakt, und die zurückgegebenen Teile haben kein festgelegtes Parent. In den meisten Fällen wird empfohlen, das/die zurückgegebene(n) Teil(e) an denselben Ort wie das Hauptteil zu parenten und dann das ursprüngliche Teil mit Destroy() zu zerstören.

  • Wenn das Hauptteil während der Berechnung der Operation bewegt wird, können Sie die resultierenden Teile auf das aktualisierte CFrame des Hauptteils setzen, da die zurückgegebenen Teile im selben Koordinatenraum wie das Hauptteil sind.

  • Wenn eine solide Modellierungsoperation Teile mit mehr als 20.000 Dreiecken ergeben würde, werden sie auf 20.000 vereinfacht. Wenn dies nicht möglich ist, normalerweise in einem Fall mit Tausenden von nicht überlappenden Komponenten, führt die Operation zu einem Fehler.

  • Es gibt Methoden, um die Mesh-Daten einer Instanz auszutauschen, was es einfacher macht, die Geometrie der Operation zu nutzen, während Eigenschaften, Attribute, Tags und Kinder des Hauptteils beibehalten werden, wie Attachments, Constraints, ParticleEmitters, Lichtobjekte und Decals. Dieser Ansatz umgeht auch das potenzielle "Flackern", das durch das vollständige Ersetzen des ursprünglichen PartOperation durch einen anderen verursacht wird.

  • Es ist möglich, diese Methoden auf dem Client aufzurufen, jedoch mit einigen Einschränkungen. Erstens muss dies mit Objekten erzeugt auf dem Client erfolgen. Zweitens gibt es keine Replikation von Client zu Server.

Farben und UVs

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

  • Wenn das Ergebnis eine PartOperation ist, hat es die Color des ersten Teils, den Sie im Studio ausgewählt haben, aber das Studio verwendet standardmäßig die Flächenfarben, um jede Fläche in der gleichen Farbe zu halten, wie sie vor der Operation war. 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 seine 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 gemischt (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 behandelt, abhängig von der Art des Ergebnisses:

  • PartOperations haben immer boxmapped UVs, was bedeutet, dass jede Fläche das Material/Textur/Decal aus einer Richtung (einer von -x, +x, -y, +y, -z, +z) angewendet bekommt. Dies kann Texturen dehnen.
  • MeshParts sind nicht boxmapped. Die UVs des Meshes des Hauptteils werden verwendet. Da Roblox derzeit keine Multi-Material-Unterstützung 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 haben.

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, werden Werte zwischen 90 und 180 nicht empfohlen, da sie einen "Schatten"-Effekt bei Vereinigungen und Schnitten mit scharfen Kanten verursachen können.

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