Modelagem sólida

*Este conteúdo é traduzido por IA (Beta) e pode conter erros. Para ver a página em inglês, clique aqui.

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.

DoubleSided = false

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.

Ferramentas de modelagem sólida destacadas na barra de ferramentas do Studio.
FerramentaAtalhoDescrição
UniãoShiftCtrlG (Windows)
ShiftG (Mac)
Junte duas ou mais partes para formar uma única união sólida.
InterseçãoShiftCtrlI (Windows)
ShiftI (Mac)
Interseccione partes sobrepostas em uma única interseção sólida.
SepararShiftCtrlU (Windows)
ShiftU (Mac)
Separe a união ou interseção de volta em suas partes individuais.
NegarShiftCtrlN (Windows)
ShiftN (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.

Um bloco e um cilindro.
Partes individuais
Um bloco e um cilindro combinados em um único objeto.
Resultado da união

Para combinar partes em uma união:

  1. Selecione todas as partes que você deseja juntar.
  2. 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.

Um bloco e um cilindro.
Partes individuais
Um bloco e um cilindro combinados em um único objeto.
Resultado da interseção

Para interseccionar partes sobrepostas:

  1. Selecione todas as partes que você deseja interseccionar.
  2. 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.

Um bloco e um cilindro.
Bloco e cilindro negado
Um bloco e um cilindro combinados em um único objeto.
Resultado da subtração

Para subtrair uma parte de outras partes sobrepostas:

  1. Selecione a parte que você deseja negar em relação a outras partes.
  2. Clique em Negar. A parte se torna translúcida com um tom avermelhado para indicar seu estado.
  3. Selecione tanto a parte negada quanto as partes das quais você deseja subtrair.
  4. 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:

  1. Selecione a união ou interseção.
  2. 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.

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

Combinar Geometria de Múltiplas Partes
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
end

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

Negação de Parte via Script
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:

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
Uma forma espiral criada ao arrastar uma esfera.
Uma forma curva translúcida subtraída de um bloco.

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:

  1. 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 sweep
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Subtrair o sweep da instância atingida
    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. 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 sweep
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Subtrair o sweep da instância atingida
    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. 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
Um bloco estilhaçado em pedaços.
Um bloco com o canto fragmentado em pedaços.

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
end

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

Estêncil na forma do logotipo do Roblox Studio
Parte (cinza escuro) e parte estêncil
Resultados de um fragmento na forma de um estêncil
Resultado do script de fragmentação
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
end

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

Uma fileira de blocos
Uma fileira de blocos
Uma fileira de blocos fragmentados
Cada peça pode originar de várias partes de entrada
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
end

O 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
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, 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
end

Resultados da modelagem sólida

Detalhes do comportamento

  • As partes originais permanecem intactas após uma operação bem-sucedida, e as partes retornadas não têm Parent definido. Na maioria dos casos, é recomendável parentar a(s) parte(s) retornada(s) ao mesmo lugar que a parte principal, e então Destroy() as partes originais.

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

  • É 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.

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.

©2026 Roblox Corporation, Roblox, o logotipo Roblox e Powering Imagination estão entre nossas marcas registradas e não registradas nos EUA e em outros países.