Modelagem Sólida é o processo de unir partes de maneiras únicas para formar formas mais complexas. Isso inclui operações booleanas união, subtração e interseção, que são conhecidas como Geometria Sólida Construtiva (CSG). Você pode realizar modelagem sólida em qualquer lugar: no Studio, plugins, até mesmo no jogo tanto no servidor quanto no cliente.
Além da CSG booleana, a modelagem sólida também suporta malhas, desde que sejam herméticas, e operações como varredura e fragmentação que permitem que você e seus jogadores cortem e estilhaçam a geometria para interações de jogo únicas.


Geometria hermética
Existem três elementos básicos das malhas:
- Vértice - Um único ponto na malha.
- Aresta - Uma linha que conecta dois vértices.
- Face - Uma área de superfície entre três ou mais vértices.



As operações de modelagem sólida podem funcionar apenas com geometria hermética; de fato, "sólido" e "hermético" são sinônimos. Em termos técnicos, uma malha sendo hermética significa que está fechada, manifold e não se interpõe a si mesma. Esses termos têm definições estritas, mas aqui estão algumas regras simples:
- Cada face deve ter um lado 'dentro' e um lado 'fora'. Estes são determinados pela ordem de enrolamento da face, que é a ordem de seus três vértices.
- Cada aresta deve ser compartilhada exatamente por dois triângulos. Isso significa que não pode haver buracos na malha, pois as arestas ao redor da borda de um buraco teriam apenas um triângulo.
- As faces não podem passar através de outras faces.
- Triângulos adjacentes devem concordar sobre qual lado é o lado 'fora'.
- Cada vértice deve ter exatamente um leque de triângulos adjacentes.

O sistema de modelagem sólida é capaz de reparar automaticamente problemas específicos pequenos em uma malha, mas, em geral, as chamadas de API falharão se a malha não for hermética. Não há uma maneira única de reparar uma malha não hermética existente, mas existem vários plugins do Blender que podem ajudar, como 3D Print Toolbox e Mesh Repair Tools. Como outra opção, Meshlab também tem ferramentas muito úteis incorporadas para tentar tornar a malha manifold, que é o principal requisito para que uma malha seja hermética.
Uma maneira de ver que uma malha será extremamente difícil de tornar hermética é olhá-la no Studio de todos os ângulos, e então tentar habilitar e desabilitar a propriedade MeshPart.DoubleSided da malha. Se você notar alguma diferença, a malha é apenas uma casca e as ferramentas mencionadas acima não funcionarão porque não conseguem adivinhar qual espaço está dentro versus fora da malha. No entanto, se tudo o que você deseja é uma malha fina e não for importante manter as dimensões da malha exatamente as mesmas, você pode usar o modificador Solidify do Blender para espessar ligeiramente a casca em uma malha hermética.

Modelagem sólida no Studio
Você pode realizar três operações booleanas básicas usando quatro ferramentas na barra de ferramentas da guia Modelo.

| Ferramenta | Atalho | Descrição |
|---|---|---|
| União | ShiftCtrlG (Windows) Shift⌘G (Mac) | Junta duas ou mais partes para formar uma única união sólida. |
| Interseção | ShiftCtrlI (Windows) Shift⌘I (Mac) | Intersecciona partes sobrepostas em uma única interseção sólida. |
| Negar | ShiftCtrlN (Windows) Shift⌘N (Mac) | Nega partes, útil para criar buracos e reentrâncias. |
| Separar | ShiftCtrlU (Windows) Shift⌘U (Mac) | Separa a união ou interseção de volta em suas partes individuais. |
União de partes
A ferramenta União junta duas ou mais partes para formar uma única UnionOperation sólida.


Para combinar partes em uma união:
- Selecione todas as partes que deseja juntar.
- Clique no botão União. Todas as partes se combinam em uma única UnionOperation sólida com o nome União.
Intersecção de partes
A ferramenta Interseção intersecção partes sobrepostas em uma única IntersectOperation sólida.


Para intersecção partes sobrepostas:
- Selecione todas as partes que deseja interseccionar.
- Clique no botão Interseção. Todas as partes se combinam em uma única IntersectOperation sólida com o nome Interseção.
Negar partes
A ferramenta Negar nega uma parte de modo que, quando é unida a outra parte, a forma da parte negada é subtraída da outra parte.


Para subtrair uma parte de outras partes sobrepostas:
- Selecione a parte que você deseja negar das outras partes.
- Clique em Negar. A parte é marcada como uma parte negada e um símbolo negado aparecerá no Explorer. A parte se torna translúcida com um tom avermelhado para indicar seu estado.
- Selecione tanto a parte negada quanto as partes das quais você deseja subtrair.
- Clique em União. A parte negada é cortada das partes sobrepostas incluídas.
A tag é exposta para scripting, então você também pode negar partes adicionando a tag rbxNegate a partir de um script ou plugin. NegateOperation não é mais utilizado.
Separar uniões ou interseções
A ferramenta Separar separa uma UnionOperation de volta em suas partes individuais, servindo essencialmente como uma ferramenta de "desfazer" para uniões e interseções.
Para separar uma união ou interseção de volta em partes individuais:
- Selecione a união ou interseção.
- Clique em Separar. As partes são separadas de volta em sua forma original.
Modelagem sólida no jogo
Você também pode realizar operações de modelagem sólida enquanto um jogo está em execução usando funções do GeometryService.
UnionAsync(), IntersectAsync() e SubtractAsync()
Semelhante às ferramentas de operação booleana básicas incorporadas no Studio, você pode usar funções GeometryService como UnionAsync(), IntersectAsync(), e SubtractAsync() para realizar operações booleanas básicas enquanto o jogo está em execução. Por exemplo, o seguinte script usa SubtractAsync() para subtrair o volume de uma parte de outra.
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

Para demonstrar ainda mais, o próximo exemplo de código combina a geometria de mainPart e as partes no array otherParts, e depois destrói as partes originais envolvidas na operação. Você pode substituir a chamada para UnionAsync() por IntersectAsync() ou SubtractAsync() para realizar as outras operações booleanas.
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,
}
-- Realizar operação de união em pcall() já que é assíncrona
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop através das partes resultantes para reparentar/reposicionar
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Destruir partes originais
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Contanto que todas as entradas sejam compostas por partes primitivas em vez de MeshParts, as operações booleanas de união, interseção e subtração produzirão uma PartOperation com duas peças de dados armazenados: uma árvore de operações CSG conhecida como CSGTree, e uma malha para renderização.
Comparado a BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), as funções booleanas de GeometryService diferem da seguinte maneira:
- A saída é um array de instâncias em vez de uma única instância.
- As partes de entrada não precisam estar parentadas à cena, permitindo operações em segundo plano.
- Quando a opção SplitApart está definida como true (padrão), cada corpo distinto será retornado em sua própria PartOperation/MeshPart.
- Todas as partes retornadas estão no espaço de coordenadas da parte principal, de modo que suas posições PVInstance.Origin sejam as mesmas que a da parte principal. Isso mantém os vértices da malha na mesma posição relativa ao objeto como antes da operação, mas também significa que o (0, 0, 0) de uma parte retornada não está necessariamente no centro de seu corpo.
SweepPartAsync()
A função GeometryService:SweepPartAsync() cria um MeshPart que tem a forma da parte de entrada arrastada através de um determinado conjunto de posições CFrame. Essa função pode ser muito útil para realizar interações de corte e fatiamento.
A entrada pode ser uma Part, PartOperation ou MeshPart. A forma do resultado é definida como a união dos cascos convexos de cada par adjacente de CFrames; se apenas um único CFrame for fornecido, o resultado será um casco convexo da parte de entrada.
Para demonstrar como essa função funciona, o seguinte exemplo de código varre uma bola através de um conjunto de posições CFrame para criar uma espiral:
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

Exemplo

Este exemplo usa GeometryService:SweepPartAsync() para conseguir um recurso de jogabilidade de corte com espada ou arma laser, onde o movimento da espada é baseado na posição do mouse do jogador. O movimento do mouse do usuário é registrado como uma lista de CFrames, SweepPartAsync() constrói uma malha de fatiamento a partir desses dados, então a malha de fatiamento é subtraída da parte que foi atingida.
Para fazer esse exemplo funcionar no Studio:
Crie o seguinte Script em ServerScriptService para realizar todas as operações de modelagem sólida.
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-- Visualiza a varredurasweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Subtrai a varredura da instância atingidalocal 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)Crie o seguinte LocalScript em StarterPlayerScripts para lidar com a entrada do usuário.
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-- Visualiza a varredurasweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Subtrai a varredura da instância atingidalocal 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)Crie um RemoteEvent em ReplicatedStorage chamado DrawCurveEvent.
FragmentAsync()
As funções GeometryService:FragmentAsync() e GeometryService:GenerateFragmentSites() permitem que você estilhaça uma parte em pedaços com formas de aparência natural. GeometryService:FragmentAsync() usa decomposição voronoi para dividir uma única parte em várias instâncias de MeshPart de acordo com o padrão de pontos passados, enquanto GeometryService:GenerateFragmentSites() é uma função auxiliar que gera pontos conhecidos como sites voronoi para passar para FragmentAsync().
Para demonstrar como essas funções funcionam juntas, o seguinte exemplo de código gera sites voronoi para fragmentar uma parte de bloco:
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

Exemplos

O seguinte script fragmenta uma área de uma parte, dada por uma posição e um raio. A posição pode vir comumente de uma colisão física ou um raycast de um jogador.
O primeiro elemento do array de sites que GenerateFragmentSites() gera será uma matriz interna de todos os sites que estão fora do raio solicitado. Se você quiser fazer algo específico com a 'porção não estilhaçada' restante da parte, pode encontrar essa porção verificando fragments[i].Index == 1 ao percorrer os resultados de 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("Falha ao Fraturar:"..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
O seguinte script quebra fragmentos da primeira parte dentro de uma forma definida usando uma segunda parte como estêncil. Apenas sites voronoi dentro da segunda parte resultarão em peças separadas. Todos os outros sites terão suas células combinadas em uma única 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("Falha ao Fraturar:"..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
O seguinte script é um caso de uso muito mais nichado, mas demonstra o poder dos dados de índice que são retornados de GeometryService:FragmentAsync().
Por exemplo, muitos lugares contêm edifícios formados por várias partes de bloco não unidas. Se uma granada, bola de canhão ou martelo de pedreiro danificasse isso, você desejaria que todas as partes da parede fossem fragmentadas. Este script fragmenta todas as partes próximas, então une os fragmentos de partes diferentes para ocultar completamente as costuras.
Isso envolve várias operações Async, então pode não ser adequado para uso em jogo como uma resposta instantânea à entrada do usuário, como uma ferramenta de martelo de pedreiro.


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("Falhou ao Fraturar:"..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("Falhou ao Unir:"..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
O seguinte script é uma substituição Luau quase idêntica para GeometryService:GenerateFragmentSites(). Se você deseja um comportamento semelhante ao de GeometryService:GenerateFragmentSites() mas quer fazer pequenas alterações, pode usá-lo como ponto de partida.
Ele usa uma grade de pontos perturbada e garante que a área fragmentada se comporte bem, ao contrário de pontos totalmente aleatórios.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Use isso para ajustar a quantidade de perturbação
if (origin and not radius) or (radius and not origin) then
warn("Ou tanto origin quanto radius devem ser fornecidos, ou nenhum.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized significa não fraturar toda a parte, apenas uma seção.
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
Preservar restrições
Se uma parte de entrada tiver restrições ou anexos que você deseja preservar, você pode transferi-los para as partes resultantes. Pode ser tedioso descobrir a qual parte de saída uma restrição deve ser anexada, por isso é recomendável usar GeometryService:CalculateConstraintsToPreserve() para gerar uma tabela de recomendações que você pode percorrer e aplicar.
Para demonstrar, o seguinte exemplo de código realiza uma operação de subtração, percorre as partes resultantes para reparentar e reposicionar as partes resultantes, então calcula uma tabela de restrições e anexos para preservar ou descartar antes de destruir todas as partes originais.
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,
}
-- Realizar operação de subtração em pcall() já que é assíncrona
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Loop através das partes resultantes para reparentar/reposicionar
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Calcular restrições/anexos para preservar ou descartar
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Preservar restrições/anexos com base na tabela recomendada
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
-- Destruir partes originais
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
Detalhes do comportamento
Se a parte principal estiver se movendo durante o cálculo da operação, você pode definir as partes resultantes para o CFrame atualizado da parte principal, pois as partes retornadas estão no mesmo espaço de coordenadas que a parte principal.
Existem funções para trocar os dados da malha de uma instância, facilitando a utilização da geometria da operação enquanto mantém propriedades, atributos, tags e filhos da parte principal, como Attachments, Constraints, ParticleEmitters, objetos de luz e decals. Essa abordagem também contorna o potencial "piscar" de substituir completamente a PartOperation original por outra.
- Se usar esse método com uma PartOperation como a parte principal e nenhuma das outras partes for MeshParts, você pode substituir a geometria de outra PartOperation através de SubstituteGeometry().
- Se a parte principal for uma MeshPart, você pode usar MeshPart:ApplyMesh().
É possível chamar essas funções no cliente, mas com algumas limitações. Primeiro, deve ser feito com objetos criados no cliente. Em segundo lugar, não há replicação disponível do cliente para o servidor.
As seguintes propriedades da parte principal são aplicadas às PartOperations ou MeshParts resultantes:
- Colisão: BasePart.AudioCanCollide, BasePart.CanCollide
Considerações sobre os resultados da modelagem sólida
Cores e UVs
As cores das partes resultantes após a modelagem sólida vêm de dois lugares: as cores da face e a Color da parte.
- Se o resultado for uma PartOperation, terá a Color da primeira parte que você selecionou no Studio, mas o Studio usa as cores da face por padrão para manter cada face da mesma cor que era antes da operação. Você pode ativar sua propriedade UsePartColor no Studio para substituir esse comportamento e fazer com que todo o resultado tenha uma única cor.
- Se o resultado for uma MeshPart, sua Color será branca, e as cores das faces sempre aparecerão. Você pode ajustar a tonalidade das partes resultantes mudando sua Color, mas isso será misturado (multiplicado) com as cores das faces. Isso tinge o resultado em vez de substituir completamente as cores das faces. Se você quiser controle total sobre a cor da saída, é melhor fazer as entradas brancas primeiro.
Os UVs também são tratados de forma diferente dependendo do tipo de resultado:
- PartOperations sempre têm UVs mapeados em caixa, o que significa que cada face terá o material/textura/decoração de uma direção (uma de -x, +x, -y, +y, -z, +z) aplicada a ela. Isso pode esticar texturas.
- MeshParts não são mapeadas em caixa. Os UVs da malha da parte principal são utilizados. Como Roblox não possui suporte atual para múltiplos materiais, os UVs das faces que se originam das outras partes recebem UVs de (0, 0). Para melhores resultados, garanta que o pixel (0, 0) de sua textura tenha uma cor razoável.
Ângulo de suavização
A propriedade SmoothingAngle de uma parte modelada solidamente suaviza ângulos entre superfícies adjacentes da mesma cor. Um valor mais alto produz uma aparência mais suave, enquanto um valor mais baixo produz uma aparência mais áspera com mais bordas afiadas.
Embora um valor entre 30 e 70 graus normalmente produza um bom resultado, valores entre 90 e 180 não são recomendados, pois podem causar um efeito de "sombreamento" em uniões e interseções com bordas afiadas.


Simplificação de partes
Se uma operação de modelagem sólida resultar em partes com mais de 20.000 triângulos, elas serão simplificadas para 20.000. Se isso não puder ser feito, geralmente em uma situação com milhares de componentes não sobrepostos, a operação resulta em um erro.

