Modellazione solida è il processo di unire parti insieme in modi unici per formare forme più complesse. Questo include le operazioni booleane unione, sottrazione e intersezione, comunemente note come geometria solida costruttiva (CSG). È possibile eseguire la modellazione solida ovunque: in Studio, nei plugin, persino in gioco sia sul server che sul client.
Oltre alla CSG booleana, la modellazione solida supporta anche le mesh, purché siano a tenuta stagna, e operazioni come estrusione e frammentazione che consentono a te e ai tuoi giocatori di affettare, tagliare e frantumare la geometria per interazioni di gioco uniche.


Geometria a tenuta stagna
Ci sono tre elementi base delle mesh:
- Vertice - Un singolo punto sulla mesh.
- Spigolo - Una linea che collega due vertici.
- Faccia - Un'area superficiale tra tre o più vertici.



Le operazioni di modellazione solida possono funzionare solo con geometria a tenuta stagna; in effetti, "solido" e "a tenuta stagna" sono sinonimi. In termini tecnici, una mesh essere a tenuta stagna significa che è chiusa, manifold e non intersecante. 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. Ciò 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.
- Triangoli adiacenti devono concordare su quale lato sia il lato "esterno".
- Ogni vertice deve avere esattamente una ventola di triangoli adiacenti.

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 è a tenuta stagna. Non esiste un metodo universale per riparare una mesh non a tenuta stagna esistente, ma ci sono diversi plugin di Blender che possono aiutare, come 3D Print Toolbox e Mesh Repair Tools. Come ulteriore opzione, Meshlab ha anche strumenti molto utili integrati per cercare di rendere la mesh manifold, che è il principale requisito per una mesh a tenuta stagna.
Un modo per vedere che una mesh sarà estremamente difficile da rendere a tenuta stagna è osservandola in Studio da tutti gli angoli, quindi provare ad attivare e disattivare la proprietà MeshPart.DoubleSided della mesh. Se riesci a vedere qualche differenza, la mesh è solo un guscio e gli strumenti menzionati sopra non funzioneranno perché non possono indovinare quale spazio si trova all'interno rispetto all'esterno della mesh. Tuttavia, se tutto ciò che desideri è una mesh sottile e non è importante mantenere le dimensioni della mesh esattamente le stesse, puoi utilizzare il modificatore di solidificazione di Blender per ispessire leggermente il guscio in una mesh a tenuta stagna.

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

| Strumento | Scorciatoia | Descrizione |
|---|---|---|
| Unione | ShiftCtrlG (Windows) Shift⌘G (Mac) | Unisci due o più parti per formare un'unica unione solida. |
| Intersezione | ShiftCtrlI (Windows) Shift⌘I (Mac) | Interseca parti sovrapposte in un'unica intersezione solida. |
| Negate | ShiftCtrlN (Windows) Shift⌘N (Mac) | Negare le parti, utile per creare buchi e impressioni. |
| Separare | ShiftCtrlU (Windows) Shift⌘U (Mac) | Separa l'unione o l'intersezione di nuovo nelle sue parti individuali. |
Unire parti
Lo strumento Unione unisce due o più parti insieme per formare una singola UnionOperation solida.


Per combinare le parti in un'unione:
- Seleziona tutte le parti che vuoi unire.
- Fai clic sul pulsante Unione. Tutte le parti si combinano in un'unica UnionOperation solida con il nome Unione.
Intersecare parti
Lo strumento Intersezione interseca parti sovrapposte in un'unica IntersectOperation solida.


Per intersecare le parti sovrapposte insieme:
- Seleziona tutte le parti che vuoi intersecare.
- Fai clic sul pulsante Intersezione. Tutte le parti si combinano in un'unica IntersectOperation solida con il nome Intersezione.
Negare parti
Lo strumento Negare nega una parte in modo che quando viene unita con un'altra parte, la forma della parte negata venga sottratta dall'altra parte.


Per sottrarre una parte da altre parti sovrapposte:
- Seleziona la parte che desideri negare dagli altri parti.
- Fai clic su Negare. La parte viene contrassegnata come parte negata e un simbolo negato appare nell'Explorer. La parte diventa traslucida con una tonalità rossastra per indicare il suo stato.
- Seleziona sia la parte negata che le parti da cui desideri sottrarla.
- Fai clic su Unione. La parte negata viene ritagliata dalle parti sovrapposte incluse.
Il tag è esposto per la programmazione, quindi puoi anche negare le parti aggiungendo il tag rbxNegate da uno script o da un plugin. NegateOperation non è più utilizzato.
Separare unioni o intersezioni
Lo strumento Separare separa una UnionOperation di nuovo nelle sue parti individuali, servendo essenzialmente come uno strumento "annulla" per unioni e intersezioni.
Per separare un'unione o intersezione di nuovo in parti individuali:
- Seleziona l'unione o l'intersezione.
- Fai clic 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 utilizzando funzioni GeometryService.
UnionAsync(), IntersectAsync() e SubtractAsync()
Simile agli strumenti di operazione booleana di base incorporati in Studio, puoi utilizzare funzioni GeometryService come UnionAsync(), IntersectAsync(), e SubtractAsync() per eseguire operazioni booleane di base mentre un gioco è in esecuzione. Ad esempio, il seguente script utilizza SubtractAsync() per sottrarre il volume di 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 esempio 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.
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,
}
-- Esegui l'operazione di unione in pcall() poiché è asincrona
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Itera attraverso le parti risultanti per riposizionare
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
Finché tutti gli input sono costituiti da parti primitive piuttosto che da MeshParts, le operazioni booleane di unione, intersezione e sottrazione produrranno una PartOperation con due pezzi di dati memorizzati: un albero di operazioni CSG noto come CSGTree e una mesh per il rendering.
Rispetto a BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), le funzioni booleane di GeometryService differiscono come segue:
- L'output è un array di istanze piuttosto che un'unica istanza.
- Le parti di input non devono essere parentate alla scena, consentendo operazioni in background.
- Quando l'opzione SplitApart è impostata su true (impostazione predefinita), ciascun corpo distinto verrà restituito nella propria PartOperation/MeshPart.
- Tutte le parti restituite si trovano nello spazio delle coordinate della parte principale, quindi le 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.
SweepPartAsync()
La funzione GeometryService:SweepPartAsync() crea una MeshPart che ha la forma della parte di input trascinata attraverso un determinato set di posizioni CFrame. Questa funzione può essere molto utile per eseguire interazioni di affettamento e taglio.
L'input può essere una Part, PartOperation o MeshPart. La forma del risultato è definita come l'unione dei gusci convessi di ciascuna coppia adiacente di CFrames; se viene fornito solo un singolo CFrame, il risultato sarà un guscio convesso della parte di input.
Per dimostrare come funziona questa funzione, il seguente esempio di codice esegue un'estrusione di una palla attraverso un insieme di posizioni CFrame per creare una 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

Esempio

Questo esempio utilizza GeometryService:SweepPartAsync() per ottenere una funzionalità di gioco di affettamento della spada o della 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 affettamento da questi dati, quindi la mesh di affettamento viene sottratta dalla parte che è stata colpita.
Per eseguire questo esempio in Studio:
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 l'estrusionesweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Sottrai l'estrusione dall'istanza colpitalocal subtractSuccess, newParts = pcall( function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)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 l'estrusionesweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Sottrai l'estrusione dall'istanza colpitalocal subtractSuccess, newParts = pcall( function()r eturn GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)Crea un RemoteEvent in ReplicatedStorage chiamato DrawCurveEvent.
FragmentAsync()
Le funzioni GeometryService:FragmentAsync() e GeometryService:GenerateFragmentSites() ti consentono di frantumare una parte in pezzi con forme naturali. GeometryService:FragmentAsync() utilizza la decomposizione voronoi per dividere una singola parte in più istanze di MeshPart in base al modello di punti passato, mentre GeometryService:GenerateFragmentSites() è una funzione di supporto che genera punti noti come siti voronoi da passare a FragmentAsync().
Per dimostrare come queste funzioni funzionano insieme, il seguente esempio di codice genera siti voronoi per frammentare una parte a blocchi:
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

Esempi

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 raggio di illuminazione di un giocatore.
Il primo elemento dell'array dei siti che GenerateFragmentSites() genera sarà un array interno di tutti i siti che si trovano al di fuori del raggio richiesto. Se desideri fare qualcosa di specifico sulla "porzione non frantumata" rimanente della parte, puoi trovare quella porzione controllando fragments[i].Index == 1 durante l'iterazione sui risultati di FragmentAsync().
local GeometryService = game:GetService("GeometryService")
function fragmentAtPosition(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("Failed to 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
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 produrranno pezzi separati. Tutti gli altri siti avranno le loro celle combinate in un'unica parte.


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("Failed to 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
Il seguente script è un caso d'uso molto più di nicchia, ma dimostra il potere dei dati di indice che vengono restituiti da GeometryService:FragmentAsync().
Ad esempio, molti luoghi contengono edifici formati da più parti di blocco non unite. Se una granata, una palla di cannone o un martello da orso dovessero danneggiarlo, vorresti frammentare tutte le parti delle pareti. Questo script frammenta tutte le parti nelle vicinanze, quindi unisce i frammenti di diverse parti 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 un attrezzo da martello.


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("Failed to 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("Failed to 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
Il seguente script è un sostituto Luau quasi identico per GeometryService:GenerateFragmentSites(). Se desideri un comportamento simile a GeometryService:GenerateFragmentSites() ma desideri apportare piccole modifiche, puoi usare questo come punto di partenza.
Utilizza una griglia di punti jitterati e garantisce che l'area frammentata si comporti bene, a differenza di 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("E' necessario fornire sia l'origine che il raggio, oppure nessuno dei due.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized significa non frammentare 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 le vincoli
Se una parte di input ha vincoli o attacchi che desideri preservare, puoi trasferirli sulle parti risultanti. Può essere noioso capire a quale parte di output dovrebbe essere attaccato un vincolo, quindi è consigliato utilizzare GeometryService:CalculateConstraintsToPreserve() per generare una tabella di raccomandazioni che puoi cycled over e applicare.
Per dimostrare, il seguente esempio di codice esegue un'operazione di sottrazione, itera attraverso le parti risultanti per riposizionare e riposizionare le parti risultanti, quindi calcola una tabella di vincoli e attacchi da preservare o scartare 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
-- Itera attraverso le parti risultanti per riposizionare
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Calcola vincoli/attacchi da preservare o scartare
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Preserva vincoli/attacchi 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
Dettagli sul comportamento
Se la parte principale si sta muovendo durante il calcolo dell'operazione, puoi impostare le parti risultanti sul nuovo CFrame della parte principale, poiché le parti restituite sono nello stesso spazio delle coordinate della parte principale.
Esistono funzioni 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 sottrae anche il potenziale "sfarfallio" di sostituire completamente l'PartOperation originale con un'altra.
- Se utilizzi questo metodo con un'operazione PartOperation come parte principale e nessuna delle altre parti è MeshParts, puoi sostituire la geometria di un'altra PartOperation tramite SubstituteGeometry().
- Se la parte principale è un MeshPart, puoi usare MeshPart:ApplyMesh().
È possibile richiamare queste funzioni sul client, ma con alcune limitazioni. In primo luogo, deve essere fatto con oggetti creati sul client. In secondo luogo, non c'è replicazione disponibile dal client al server.
Le seguenti proprietà dalla parte principale vengono applicate alle PartOperations o MeshParts risultanti:
- Collisione: BasePart.AudioCanCollide, BasePart.CanCollide
Considerazioni sui risultati della modellazione solida
Colori e UV
I colori delle parti risultanti dopo la modellazione solida provengono da due fonti: i colori delle facce e il Color della parte.
- Se il risultato è un PartOperation, avrà il Color della prima parte selezionata in Studio, ma Studio utilizza i colori delle facce per impostazione predefinita per mantenere ogni faccia dello stesso colore rispetto a 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 attraverso. Puoi regolare la tonalità 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 trattati anche diversamente a seconda del tipo di risultato:
- I PartOperations hanno sempre UV a mappatura cubica, il che significa che ogni faccia avrà il materiale/texture/decalcomania da una direzione (una di -x, +x, -y, +y, -z, +z) applicato a esso. Questo può allungare le texture.
- I MeshParts non sono mappati a cubo. Gli UV della mesh della parte principale vengono utilizzati. Poiché Roblox attualmente non ha il supporto multi-materiale, gli UV delle facce originate dalle altre parti ricevono 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 bordi più acuti.
Sebbene un valore compreso tra 30 e 70 gradi produca solitamente un buon risultato, valori compresi tra 90 e 180 non sono raccomandati in quanto potrebbero causare un effetto di "ombreggiatura" su unioni e intersezioni con bordi affilati.


Semplificazione delle Parti
Se un'operazione di modellazione solida darebbe come risultato parti con più di 20.000 triangoli, queste 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.

