Modellazione solida

*Questo contenuto è tradotto usando AI (Beta) e potrebbe contenere errori. Per visualizzare questa pagina in inglese, clicca qui.

La modellazione solida è il processo di unione di BaseParts in modi unici per formare forme più complesse. Questo include le operazioni booleane unione, intersezione e sottrazione che sono comunemente conosciute come Geometria Solida Costruttiva (CSG). Puoi eseguire la modellazione solida in Studio, plugin e in‑game sia sul server che sul client.

Oltre alla CSG booleana, la modellazione solida supporta anche le mesh, purché siano impermeabili, e operazioni come sweep e fragment che ti permettono di tagliare, fendere e frantumare la geometria per interazioni di gioco uniche.

Geometria impermeabile

Le operazioni di modellazione solida possono funzionare solo con geometria impermeabile. In termini tecnici, una mesh impermeabile significa che è chiusa, manifold e non si interseca. Questi termini hanno definizioni rigorose, ma ecco alcune semplici regole:

  • Ogni faccia deve avere un lato "interno" e un lato "esterno". Questi sono determinati dall'ordine di avvolgimento della faccia, che è l'ordine dei suoi tre vertici.
  • Ogni spigolo deve essere condiviso esattamente da due triangoli. Questo significa che non possono esserci buchi nella mesh perché gli spigoli attorno al bordo di un buco avrebbero solo un triangolo.
  • Le facce non possono attraversare altre facce.
  • I triangoli adiacenti devono concordare su quale lato è il lato "esterno".
  • Ogni vertice deve avere esattamente un ventaglio di triangoli adiacenti.

Ognuno dei seguenti esempi è non impermeabile per il motivo indicato:

Il sistema di modellazione solida è in grado di riparare automaticamente specifici piccoli problemi con una mesh, ma in generale, le chiamate API falliranno se la mesh non è impermeabile. Non esiste un modo universale per riparare una mesh non impermeabile esistente, ma ci sono diversi plugin di Blender che possono aiutare, come 3D Print Toolbox e Mesh Repair Tools. Come altra opzione, Meshlab ha anche strumenti molto utili integrati per cercare di rendere la mesh manifold, che è il requisito principale affinché una mesh sia impermeabile.

Un modo per vedere che una mesh sarà estremamente difficile da rendere impermeabile è osservarla in Studio da tutti gli angoli, quindi provare ad abilitare e disabilitare la proprietà MeshPart.DoubleSided. Se noti qualche differenza, allora la mesh è solo un guscio e gli strumenti menzionati sopra non funzioneranno perché non possono indovinare quale spazio è all'interno rispetto a quello esterno della mesh. Tuttavia, se tutto ciò che desideri è una mesh sottile e non è importante mantenere esattamente le dimensioni della mesh, puoi utilizzare il modificatore Solidify di Blender per ispessire leggermente il guscio in una mesh impermeabile.

DoubleSided = false

Modellazione solida in Studio

Puoi eseguire tre operazioni booleane di base utilizzando quattro strumenti all'interno della barra degli strumenti della scheda Modello.

Strumenti di modellazione solida evidenziati nella barra degli strumenti di Studio.
StrumentoScorciatoiaDescrizione
UnioneShiftCtrlG (Windows)
ShiftG (Mac)
Unisci due o più parti insieme per formare un'unica unione solida.
IntersezioneShiftCtrlI (Windows)
ShiftI (Mac)
Interseca parti sovrapposte in un'unica intersezione solida.
SeparareShiftCtrlU (Windows)
ShiftU (Mac)
Separa l'unione o l'intersezione di nuovo nelle sue parti individuali.
NegareShiftCtrlN (Windows)
ShiftN (Mac)
Negare parti, utile per creare buchi e indentazioni.

Unione

Lo strumento Unione unisce due o più parti insieme per formare un'unica UnionOperation solida.

Un blocco e un cilindro.
Parti individuali
Un blocco e un cilindro combinati in un unico oggetto.
Risultato dell'unione

Per unire le parti insieme in un'unione:

  1. Seleziona tutte le parti che desideri unire.
  2. Clicca sul pulsante Unione. Tutte le parti si combinano in un'unica UnionOperation solida con il nome Unione.

Intersezione

Lo strumento Intersezione interseca parti sovrapposte in un'unica IntersectOperation solida.

Un blocco e un cilindro.
Parti individuali
Un blocco e un cilindro combinati in un unico oggetto.
Risultato dell'intersezione

Per intersecare le parti sovrapposte insieme:

  1. Seleziona tutte le parti che desideri intersecare.
  2. Clicca sul pulsante Intersezione. Tutte le parti si combinano in un'unica IntersectOperation solida con il nome Intersezione.

Negare

Lo strumento Negare nega una parte in modo che quando viene unita con un'altra parte, la forma della parte negata viene sottratta dall'altra parte.

Un blocco e un cilindro.
Blocco e cilindro negato
Un blocco e un cilindro combinati in un unico oggetto.
Risultato della sottrazione

Per sottrarre una parte da altre parti sovrapposte:

  1. Seleziona la parte che desideri negare dalle altre parti.
  2. Clicca su Negare. La parte diventa traslucida con una tinta rossastra per indicare il suo stato.
  3. Seleziona sia la parte negata che le parti da cui desideri sottrarla.
  4. Clicca su Unione. La parte negata viene ritagliata dalle parti sovrapposte incluse.

Separare

Lo strumento Separare separa una UnionOperation di nuovo nelle sue parti individuali, fungendo essenzialmente da strumento "annulla" per unioni e intersezioni.

Per separare un'unione o un'intersezione di nuovo in parti individuali:

  1. Seleziona l'unione o l'intersezione.
  2. Clicca su Separare. Le parti si separano di nuovo nella loro forma originale.

Modellazione solida in gioco

Puoi anche eseguire operazioni di modellazione solida mentre un gioco è in esecuzione o da un plugin tramite i metodi GeometryService.

Operazioni principali

Simile agli strumenti di operazione integrati di Studio, puoi utilizzare i metodi GeometryService come UnionAsync(), IntersectAsync(), e SubtractAsync() per eseguire operazioni booleane di base. Ad esempio, il seguente script utilizza SubtractAsync() per sottrarre il volume di una parte da un'altra.

Sottrai Una Parte Da Un'Altra
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

Per dimostrare ulteriormente, il seguente campione di codice combina la geometria di mainPart e delle parti nell'array otherParts, quindi distrugge le parti originali coinvolte nell'operazione. Puoi sostituire la chiamata a UnionAsync() con IntersectAsync() o SubtractAsync() per eseguire le altre operazioni booleane.

Combina Geometria Di Più Parti
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
-- Loop attraverso le parti risultanti per riposizionare/riparentare
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Distruggi le parti originali
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Sebbene non ci sia un metodo a livello di motore per la negazione delle parti, puoi aggiungere il tag rbxNegate all'interno di uno script o plugin per eseguire una negazione equivalente al pulsante della barra degli strumenti Negare di Studio.

Negazione Di Parte Tramite Script
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")

Rispetto a BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), i metodi GeometryService differiscono come segue:

  • L'output è un array di istanze piuttosto che un'unica istanza.
  • Le parti di input non devono essere collegate alla scena, consentendo operazioni in background.
  • Quando l'opzione SplitApart è impostata su true (predefinito), ogni corpo distinto verrà restituito nel proprio PartOperation/MeshPart.
  • Tutte le parti restituite sono nello spazio delle coordinate della parte principale, quindi le loro posizioni PVInstance.Origin sono le stesse di quelle della parte principale. Questo mantiene i vertici della mesh nella stessa posizione relativa all'oggetto come prima dell'operazione, ma significa anche che il (0, 0, 0) di una parte restituita non è necessariamente al centro del suo corpo.

Parte di sweep

Il metodo SweepPartAsync() crea una MeshPart che ha la forma della parte di input trascinata attraverso un dato insieme di posizioni CFrame. Questo metodo può essere molto utile per eseguire interazioni di taglio e fessura.

L'input può essere una Part, PartOperation o MeshPart. La forma del risultato è definita come l'unione degli involucri convessi di ciascuna coppia adiacente di CFrames; se viene fornito solo un singolo CFrame, il risultato sarà un involucro convesso della parte di input.

Per dimostrare come funziona questo metodo, il seguente campione di codice esegue un sweep di una palla attraverso un insieme di posizioni CFrame per creare una spirale:

Sweep Part Along 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
Una forma a spirale creata facendo scorrere una sfera.
Una forma curva traslucida sottratta da un blocco.

Questo esempio utilizza SweepPartAsync() per ottenere una funzionalità di gioco di taglio con spada o pistola laser, dove il movimento della spada è basato sulla posizione del mouse del giocatore. Il movimento del mouse dell'utente viene registrato come un elenco di CFrames, SweepPartAsync() costruisce una mesh di fessura da questi dati, quindi la mesh di fessura viene sottratta dalla parte colpita.

Per far funzionare questo esempio in Studio:

  1. Crea il seguente Script in ServerScriptService per eseguire tutte le operazioni di modellazione solida.

    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
    -- Visualizza lo sweep
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Sottrai lo sweep dall'istanza colpita
    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. Crea il seguente LocalScript in StarterPlayerScripts per gestire l'input dell'utente.

    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
    -- Visualizza lo sweep
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Sottrai lo sweep dall'istanza colpita
    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. Crea un RemoteEvent in ReplicatedStorage chiamato DrawCurveEvent.

Frammento

I metodi FragmentAsync() e GenerateFragmentSites() ti permettono di frantumare una parte in pezzi con forme dall'aspetto naturale. FragmentAsync() utilizza la decomposizione voronoi per dividere una singola parte in più istanze di MeshPart secondo il modello di punti passati, mentre GenerateFragmentSites() è un metodo di supporto che genera punti noti come siti voronoi da passare a FragmentAsync().

Il seguente campione di codice genera siti voronoi per frammentare una parte a blocco:

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
Un blocco frantumato in pezzi.
Un blocco con l'angolo frammentato in pezzi.

Il seguente script frammenta un'area di una parte, data da una posizione e un raggio. La posizione potrebbe provenire comunemente da una collisione fisica o da un raycast di un giocatore.

Il primo elemento dell'array dei siti che GenerateFragmentSites() genera sarà un array interno di tutti i siti che sono al di fuori del raggio richiesto. Se desideri fare qualcosa di specifico per la porzione 'non frantumata' rimanente della parte, puoi trovare quella porzione controllando fragments[i].Index == 1 quando cicli sui risultati di 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("Frattura fallita:"..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

Il seguente script rompe i frammenti dalla prima parte all'interno di una forma definita utilizzando una seconda parte come stencil. Solo i siti voronoi all'interno della seconda parte risulteranno in pezzi separati. Tutti gli altri siti avranno le loro celle combinate in un'unica parte.

Stencil nella forma del logo di Roblox Studio
Parte (grigio scuro) e parte stencil
Risultati di un frammento nella forma di uno stencil
Risultato dello script di frammento
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("Frattura fallita:"..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

Il seguente script è un caso d'uso molto più di nicchia, ma dimostra il potere dei dati di indice restituiti da FragmentAsync().

Ad esempio, molti luoghi contengono edifici formati da più parti a blocchi non unite. Se una granata, una palla di cannone o un martello a mano dovessero danneggiarlo, vorresti che tutte le parti delle pareti venissero frammentate. Questo script frammenta tutte le parti vicine, quindi unisce i frammenti di parti diverse insieme per nascondere completamente le giunture.

Questo comporta più operazioni Async(), quindi potrebbe non essere adatto per l'uso in gioco come risposta istantanea all'input dell'utente, come uno strumento martello a mano.

Una fila di blocchi
Una fila di blocchi
Una fila di blocchi frammentati
Ogni pezzo può provenire da più parti di input
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("Frattura fallita:"..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("Unione fallita:"..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

Il seguente script è un sostituto Luau quasi identico per GenerateFragmentSites(). Se desideri un comportamento simile a GenerateFragmentSites() ma vuoi apportare lievi modifiche, puoi utilizzare questo come punto di partenza.

Utilizza una griglia di punti jitterati e garantisce che l'area frammentata si comporti bene, a differenza dei punti completamente casuali.

local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Usa questo per regolare la quantità di jitter
if (origin and not radius) or (radius and not origin) then
warn("Devono essere forniti sia l'origine che il raggio, oppure nessuno dei due.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized significa non fratturare l'intera parte, solo una sezione.
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

Preservare i vincoli

Se una parte di input ha vincoli o allegati che desideri preservare, puoi trasferirli sulle parti risultanti. Può essere noioso capire a quale parte di output un vincolo dovrebbe essere attaccato, quindi è consigliabile utilizzare CalculateConstraintsToPreserve() per generare una tabella di raccomandazioni che puoi ciclare e applicare.

Per dimostrare, il seguente campione di codice esegue un'operazione di sottrazione, cicla attraverso le parti risultanti per riposizionare/riparentare le parti risultanti, quindi calcola una tabella di vincoli e allegati da preservare o abbandonare prima di distruggere tutte le parti originali.

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,
}
-- Esegui l'operazione di sottrazione in pcall() poiché è asincrona
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Cicla attraverso le parti risultanti per riposizionare/riparentare
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Calcola i vincoli/allegati da preservare o abbandonare
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Preserva i vincoli/allegati in base alla tabella raccomandata
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
-- Distruggi le parti originali
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Risultati della modellazione solida

Dettagli sul comportamento

  • Le parti originali rimangono intatte dopo un'operazione riuscita, e le parti restituite non hanno un Parent impostato. Nella maggior parte dei casi, è consigliabile collegare la parte restituita(i) allo stesso posto della parte principale, quindi Destroy() le parti originali.

  • Se la parte principale si sta muovendo durante il calcolo dell'operazione, puoi impostare le parti risultanti sul CFrame aggiornato della parte principale, poiché le parti restituite sono nello stesso spazio delle coordinate della parte principale.

  • Se un'operazione di modellazione solida dovesse risultare in parti con più di 20.000 triangoli, verranno semplificate a 20.000. Se ciò non può essere fatto, di solito in un caso con migliaia di componenti non sovrapposti, l'operazione risulta in un errore.

  • Ci sono metodi per sostituire i dati della mesh di un'istanza, rendendo più facile utilizzare la geometria dell'operazione mantenendo proprietà, attributi, tag e figli della parte principale, come Attachments, Constraints, ParticleEmitters, oggetti di luce e decalcomanie. Questo approccio evita anche il potenziale "sfarfallio" di sostituire completamente l'originale PartOperation con un altro.

  • È possibile chiamare questi metodi sul client, ma con alcune limitazioni. Prima di tutto, deve essere fatto con oggetti creati sul client. In secondo luogo, non c'è replicazione disponibile dal client al server.

Colori e UV

I colori delle parti risultanti dopo la modellazione solida provengono da due luoghi: i colori delle facce e il Color della parte.

  • Se il risultato è un PartOperation, avrà il Color della prima parte che hai selezionato in Studio, ma Studio utilizza i colori delle facce per impostazione predefinita per mantenere ogni faccia dello stesso colore di prima dell'operazione. Puoi abilitare la sua proprietà UsePartColor in Studio per sovrascrivere questo comportamento e rendere l'intero risultato di un unico colore.
  • Se il risultato è un MeshPart, il suo Color sarà bianco e i colori delle facce mostreranno sempre. Puoi regolare la tinta delle parti risultanti cambiando il loro Color, ma sarà mescolato (moltiplicato) con i colori delle facce. Questo tinge il risultato piuttosto che sovrascrivere completamente i colori delle facce. Se desideri un controllo completo sul colore dell'output, è meglio rendere prima le parti di input bianche.

Gli UV vengono gestiti anche in modo diverso a seconda del tipo di risultato:

  • PartOperations hanno sempre UV mappati a scatola, il che significa che ogni faccia avrà il materiale/texture/decal da una direzione (uno di -x, +x, -y, +y, -z, +z) applicato ad essa. Questo può allungare le texture.
  • MeshParts non sono mappati a scatola. Gli UV della mesh della parte principale vengono utilizzati. Poiché Roblox attualmente non ha supporto multi-materiale, gli UV delle facce che originano dalle altre parti vengono dati UV di (0, 0). Per ottenere i migliori risultati, assicurati che il pixel (0, 0) della tua texture abbia un colore ragionevole.

Angolo di smussatura

La proprietà SmoothingAngle di una parte modellata solida smussa gli angoli tra superfici adiacenti dello stesso colore. Un valore più alto produce un aspetto più liscio mentre un valore più basso produce un aspetto più ruvido con più bordi affilati.

Sebbene un valore compreso tra 30 e 70 gradi produca di solito un buon risultato, valori compresi tra 90 e 180 non sono raccomandati poiché possono causare un effetto di "ombreggiatura" su unioni e intersezioni con bordi affilati.

© 2026 Roblox Corporation. Roblox, il logo Roblox e Powering Imagination sono tra i nostri marchi registrati e non registrati negli Stati Uniti. e altri paesi.