Modelagem sólida é o processo de unir BaseParts de maneiras únicas para formar formas mais complexas. Isso inclui as operações booleanas união, interseção e subtração, que são comumente conhecidas como Geometria Sólida Construtiva (CSG). Você pode realizar modelagem sólida no Studio, plugins e em jogo tanto no servidor quanto no cliente.
Além da CSG booleana, a modelagem sólida também suporta malhas, desde que sejam à prova d'água, e operações como sweep e fragment que permitem que você e seus jogadores fatiem, cortem e estilhaçam a geometria para interações de jogo únicas.


Geometria à prova d'água
As operações de modelagem sólida só podem funcionar com geometria à prova d'água. Em termos técnicos, uma malha ser à prova d'água significa que está fechada, é manifold e não se intersecta. Esses termos têm definições rigorosas, mas aqui estão algumas regras simples:
- Cada face deve ter um lado "interno" e um lado "externo". Esses são determinados pela ordem de rotação da face, que é a ordem de seus três vértices.
- Cada aresta deve ser compartilhada por exatamente 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 por outras faces.
- Triângulos adjacentes devem concordar sobre qual lado é o lado "externo".
- Cada vértice deve ter exatamente um leque de triângulos adjacentes.
Cada um dos seguintes exemplos é não à prova d'água pela razão indicada:




O sistema de modelagem sólida é capaz de reparar automaticamente problemas pequenos específicos com uma malha, mas, em geral, chamadas de API falharão se a malha não for à prova d'água. Não há uma solução única para reparar uma malha existente não à prova d'água, 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 possui ferramentas muito úteis integradas para tentar tornar a malha manifold, que é o principal requisito para uma malha ser à prova d'água.
Uma maneira de ver que uma malha será extremamente difícil de tornar à prova d'água é olhá-la no Studio de todos os ângulos e, em seguida, tentar habilitar e desabilitar a propriedade MeshPart.DoubleSided. Se você puder ver alguma diferença, então a malha é apenas uma casca e as ferramentas mencionadas acima não funcionarão porque não podem adivinhar qual espaço está dentro ou fora da malha. No entanto, se tudo o que você deseja é uma malha fina e não é importante manter as dimensões da malha exatamente as mesmas, você pode usar o modificador Solidify do Blender para engrossar ligeiramente a casca em uma malha à prova d'água.


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

| Ferramenta | Atalho | Descrição |
|---|---|---|
| União | ShiftCtrlG (Windows) Shift⌘G (Mac) | Junte duas ou mais partes para formar uma única união sólida. |
| Interseção | ShiftCtrlI (Windows) Shift⌘I (Mac) | Interseccione partes sobrepostas em uma única interseção sólida. |
| Separar | ShiftCtrlU (Windows) Shift⌘U (Mac) | Separe a união ou interseção de volta em suas partes individuais. |
| Negar | ShiftCtrlN (Windows) Shift⌘N (Mac) | Negue partes, útil para fazer buracos e indentação. |
União
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 você deseja juntar.
- Clique no botão União. Todas as partes se combinam em uma única UnionOperation sólida com o nome União.
Interseção
A ferramenta Interseção intersecciona partes sobrepostas em uma única IntersectOperation sólida.


Para interseccionar partes sobrepostas:
- Selecione todas as partes que você 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
A ferramenta Negar nega uma parte para que, quando for unida com outra parte, a forma da parte negada seja subtraída da outra parte.


Para subtrair uma parte de outras partes sobrepostas:
- Selecione a parte que você deseja negar em relação a outras partes.
- Clique em Negar. 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.
Separar
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 se separam de volta em sua forma original.
Modelagem sólida em jogo
Você também pode realizar operações de modelagem sólida enquanto um jogo está em execução ou através de um plugin usando métodos de GeometryService.
Operações principais
Semelhante às ferramentas de operação integradas do Studio, você pode usar métodos de GeometryService, como UnionAsync(), IntersectAsync(), e SubtractAsync() para realizar operações booleanas básicas. 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
endPara demonstrar ainda mais, o próximo exemplo de código combina a geometria de mainPart e as partes no array otherParts, e então 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
}
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
endEmbora não haja um método de nível de motor para negação de partes, você pode adicionar a tag rbxNegate dentro de um script ou plugin para realizar a negação equivalente ao botão de barra de ferramentas Negar do Studio.
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")Comparado a BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), os métodos de GeometryService diferem da seguinte forma:
- 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, então suas posições PVInstance.Origin são as mesmas que a da parte principal. Isso mantém os vértices da malha na mesma posição em relação 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.
Parte de Sweep
O método SweepPartAsync() cria uma MeshPart que tem a forma da parte de entrada arrastada através de um conjunto dado de posições CFrame. Este método pode ser muito útil para realizar interações de fatiamento e corte.
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 esse método funciona, o seguinte exemplo de código arrasta 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

Este exemplo usa SweepPartAsync() para alcançar um recurso de jogabilidade de fatiamento de 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, e então a malha de fatiamento é subtraída da parte que foi atingida.
Para fazer este 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-- Visualizar o sweepsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Subtrair o sweep 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-- Visualizar o sweepsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Subtrair o sweep 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.
Fragmentar
Os métodos FragmentAsync() e GenerateFragmentSites() permitem que você estilhaçe uma parte em pedaços com formas naturais. FragmentAsync() usa a 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 GenerateFragmentSites() é um método auxiliar que gera pontos conhecidos como sites voronoi para passar para FragmentAsync().
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

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á um array interno 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")
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("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
endO seguinte script quebra fragmentos da primeira parte dentro de uma forma definida usando uma segunda parte como estêncil. Apenas os sites voronoi dentro da segunda parte resultarão em pedaços separados. Todos os outros sites terão suas células combinadas em uma única parte.


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("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
endO seguinte script é um caso de uso muito mais específico, mas demonstra o poder dos dados de índice que são retornados de 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 marreta danificasse, você gostaria que todas as partes da parede fossem fragmentadas. Este script fragmenta todas as partes próximas e, em seguida, une os fragmentos de diferentes partes 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 marreta.


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("Falha 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("Falha 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
endO seguinte script é uma substituição quase idêntica em Luau para GenerateFragmentSites(). Se você deseja um comportamento semelhante ao de GenerateFragmentSites(), mas deseja fazer pequenas alterações, pode usar isso como ponto de partida.
Ele usa uma grade de pontos com jitter 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 jitter
if (origin and not radius) or (radius and not origin) then
warn("Ou ambos, origem e raio 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
endPreservar 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, então é recomendável usar 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, e 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
endResultados da modelagem sólida
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, uma vez que as partes retornadas estão no mesmo espaço de coordenadas que a parte principal.
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 um caso com milhares de componentes não sobrepostos, a operação resulta em um erro.
Existem métodos para trocar os dados de 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 decalques. 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 parte principal e nenhuma das outras partes for MeshParts, você pode substituir a geometria de outra PartOperation via SubstituteGeometry().
- Se a parte principal for uma MeshPart, você pode usar MeshPart:ApplyMesh().
É possível chamar esses métodos 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:
Cores e UVs
As cores das partes resultantes após a modelagem sólida vêm de dois lugares: as cores das faces 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 das faces por padrão para manter cada face da mesma cor que era antes da operação. Você pode habilitar sua propriedade UsePartColor no Studio para substituir esse comportamento e fazer com que todo o resultado seja de uma única cor.
- Se o resultado for uma MeshPart, sua Color será branca, e as cores das faces sempre aparecerão. Você pode ajustar o tom das partes resultantes alterando 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ê deseja controle total sobre a cor da saída, é melhor fazer as entradas brancas primeiro.
Os UVs também são tratados de maneira diferente dependendo do tipo de resultado:
- PartOperations sempre têm UVs mapeados em caixa, o que significa que cada face terá o material/textura/decalque de uma direção (uma de -x, +x, -y, +y, -z, +z) aplicada a ela. Isso pode esticar texturas.
- MeshParts não são mapeados em caixa. Os UVs da malha da parte principal são usados. Como o Roblox atualmente não tem suporte a múltiplos materiais, os UVs das faces que se originam das outras partes recebem UVs de (0, 0). Para melhores resultados, certifique-se de que o pixel (0, 0) de sua textura tenha uma cor razoável.
Ângulo de suavização
A propriedade SmoothingAngle de uma parte modelada sólida suaviza os â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 geralmente 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.

