Modelado sólido

*Este contenido se traduce usando la IA (Beta) y puede contener errores. Para ver esta página en inglés, haz clic en aquí.

Modelado sólido es el proceso de unir piezas juntas de maneras únicas para formar formas más complejas. Esto incluye las operaciones booleanas unión, sustracción e intersección, que son comúnmente conocidas como Geometría Sólida Constructiva (CSG). Puedes realizar modelado sólido en todas partes: en Studio, plugins, incluso en el juego tanto en el servidor como en el cliente.

Además de CSG booleano, el modelado sólido también admite mallas, siempre que sean herméticas, y operaciones como barrido y fragmento que permiten a ti y a tus jugadores cortar, dividir y romper geometría para interacciones de juego únicas.

Un objeto de malla creado con SweepPartAsync()
Un objeto de malla roto con FragmentAsync()

Geometría hermética

Hay tres elementos básicos de las mallas:

  • Vértice - Un único punto en la malla.
  • Arista - Una línea que conecta dos vértices.
  • Cara - Un área de superficie entre tres o más vértices.
Un único vértice activo en una malla cúbica.
Vértice
Una única arista activa en una malla cúbica.
Arista
Una única cara activa en una malla cúbica.
Cara

Las operaciones de modelado sólido solo pueden funcionar con geometría hermética; de hecho, "sólido" y "hermético" son sinónimos. En términos técnicos, una malla hermética significa que está cerrada, es manifold y no se intersecta a sí misma. Estos términos tienen definiciones estrictas, pero aquí hay algunas reglas simples:

  • Cada cara debe tener un lado 'interior' y un lado 'exterior'. Estos se determinan por el orden de devanado de la cara, que es el orden de sus tres vértices.
  • Cada arista debe ser compartida por exactamente dos triángulos. Esto significa que no puede haber agujeros en la malla porque las aristas alrededor del borde de un agujero solo tendrían un triángulo.
  • Las caras no pueden pasar a través de otras caras.
  • Los triángulos adyacentes deben estar de acuerdo sobre cuál lado es el lado 'exterior'.
  • Cada vértice debe tener exactamente un ventilador de triángulos adyacentes.
Ejemplos de geometría no hermética

El sistema de modelado sólido puede reparar automáticamente problemas pequeños específicos de una malla, pero en general, las llamadas a la API fallarán si la malla no es hermética. No hay una solución única para reparar una malla no hermética existente, pero hay varios plugins de Blender que pueden ayudar, como 3D Print Toolbox y Mesh Repair Tools. Como otra opción, Meshlab también tiene herramientas muy útiles integradas para intentar hacer que la malla sea manifold, que es el principal requisito para que una malla sea hermética.

Una forma de ver que una malla será extremadamente difícil de hacer hermética es mirarla en Studio desde todos los ángulos, y luego intentar habilitar y deshabilitar la propiedad MeshPart.DoubleSided de la malla. Si puedes ver alguna diferencia, entonces la malla es solo una concha y las herramientas mencionadas anteriormente no funcionarán porque no pueden adivinar qué espacio está dentro versus fuera de la malla. Sin embargo, si todo lo que quieres es una malla delgada y no es importante mantener las dimensiones de la malla exactamente iguales, puedes usar el modificador Solidify de Blender para engrosar ligeramente la concha en una malla hermética.

Ejemplo de una malla que se ve diferente cuando DoubleSided está habilitado.
Esta es una malla de concha y los métodos de reparación automáticos no funcionarán para ella.

Modelado sólido en Studio

Puedes realizar tres operaciones booleanas básicas usando cuatro herramientas dentro de la barra de herramientas de la pestaña Modelo.

Herramientas de modelado sólido destacadas en la barra de herramientas de Studio.
HerramientaAtajoDescripción
UniónShiftCtrlG (Windows)
ShiftG (Mac)
Uni dos o más piezas para formar una sola unión sólida.
IntersecciónShiftCtrlI (Windows)
ShiftI (Mac)
Interseca piezas superpuestas en una sola intersección sólida.
NegarShiftCtrlN (Windows)
ShiftN (Mac)
Negar las piezas, útil para hacer agujeros e indentaciones.
SepararShiftCtrlU (Windows)
ShiftU (Mac)
Separa la unión o intersección de nuevo en sus partes individuales.

Unir partes

La herramienta Unión une dos o más partes para formar un solo UnionOperation sólido.

Un bloque y un cilindro.
Piezas individuales
Un bloque y un cilindro combinados en un solo objeto.
Resultado de la unión

Para combinar piezas en una unión:

  1. Selecciona todas las piezas que deseas unir.
  2. Haz clic en el botón Unión. Todas las piezas se combinan en una sola operación sólida UnionOperation con el nombre Unión.

Intersectar partes

La herramienta Intersección interseca partes superpuestas en una sola IntersectOperation sólida.

Un bloque y un cilindro.
Piezas individuales
Un bloque y un cilindro combinados en un solo objeto.
Resultado de la intersección

Para intersecar piezas superpuestas:

  1. Selecciona todas las piezas que deseas intersectar.
  2. Haz clic en el botón Intersección. Todas las piezas se combinan en una sola IntersectOperation sólida con el nombre Intersección.

Negar partes

La herramienta Negar niega una parte para que cuando se una a otra parte, la forma de la parte negada se reste de la otra parte.

Un bloque y un cilindro.
Bloque y cilindro negado
Un bloque y un cilindro combinados en un solo objeto.
Resultado de la sustracción

Para restar una parte de otras partes superpuestas:

  1. Selecciona la parte que deseas negar de otras partes.
  2. Haz clic en Negar. La parte se etiqueta como parte negada y un símbolo de negado aparece en el Explorador. La parte se vuelve translúcida con un tinte rojizo para indicar su estado.
  3. Selecciona tanto la parte negada como las partes de las que deseas restarla.
  4. Haz clic en Unión. La parte negada se recorta de las partes superpuestas que se incluyeron.

La etiqueta se expone para scripting, por lo que también puedes negar partes añadiendo la etiqueta rbxNegate desde un script o plugin. NegateOperation ya no se utiliza.

Separar uniones o intersecciones

La herramienta Separar separa una UnionOperation de nuevo en sus partes individuales, sirviendo esencialmente como una herramienta de "deshacer" para uniones e intersecciones.

Para separar una unión o intersección de nuevo en partes individuales:

  1. Selecciona la unión o intersección.
  2. Haz clic en Separar. Las partes se separan de nuevo en su forma original.

Modelado sólido en el juego

También puedes realizar operaciones de modelado sólido mientras se ejecuta un juego usando funciones de GeometryService.

UnionAsync(), IntersectAsync() y SubtractAsync()

De manera similar a las herramientas de operación booleana básicas integradas de Studio, puedes usar funciones de GeometryService como UnionAsync(), IntersectAsync(), y SubtractAsync() para realizar operaciones booleanas básicas mientras se ejecuta un juego. Por ejemplo, el siguiente script usa SubtractAsync() para restar el volumen de una parte de otra.


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
Un bloque sustraído de otro bloque.

Para demostrar aún más, el siguiente ejemplo de código combina la geometría de mainPart y las partes en el arreglo otherParts, luego destruye las partes originales involucradas en la operación. Puedes reemplazar la llamada a UnionAsync() con IntersectAsync() o SubtractAsync() para realizar las otras operaciones 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 operación de unión en pcall() ya que es asíncrona
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Iterar a través de las partes resultantes para reparentar/reubicar
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Destruir partes originales
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Siempre que todas las entradas estén compuestas de partes primitivas en lugar de MeshParts, las operaciones de unión, intersección y sustracción producirán una PartOperation con dos piezas de datos almacenados: un árbol de operaciones CSG conocido como CSGTree y una malla para renderización.

Comparado con BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), las funciones booleanas de GeometryService difieren de la siguiente manera:

  • La salida es un arreglo de instancias en lugar de una sola instancia.
  • Las partes de entrada no necesitan ser parentadas a la escena, lo que permite operaciones en segundo plano.
  • Cuando la opción SplitApart está configurada como true (por defecto), cada cuerpo distinto se devolverá en su propio PartOperation/MeshPart.
  • Todas las partes devueltas están en el espacio de coordenadas de la parte principal, por lo que sus posiciones de PVInstance.Origin son las mismas que las de la parte principal. Esto mantiene los vértices de la malla en la misma posición relativa al objeto que antes de la operación, pero también significa que el (0, 0, 0) de una parte devuelta no está necesariamente en el centro de su cuerpo.

SweepPartAsync()

La función GeometryService:SweepPartAsync() crea un MeshPart que tiene la forma de la parte de entrada arrastrada a través de un conjunto dado de posiciones de CFrame. Esta función puede ser muy útil para realizar interacciones de corte y segmentación.

La entrada puede ser una Part, PartOperation o MeshPart. La forma del resultado se define como la unión de los cascarones convexos de cada par adyacente de CFrames; si solo se proporciona un solo CFrame, el resultado será un cascarón convexo de la parte de entrada.

Para demostrar cómo funciona esta función, el siguiente ejemplo de código barre una esfera a través de un conjunto de posiciones de CFrame para crear una 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
Una forma espiral creada al barrer una esfera.

Ejemplo

Una forma curva translúcida restada de un bloque.

Este ejemplo usa GeometryService:SweepPartAsync() para lograr una característica de juego de corte de espada o pistola láser, donde el movimiento de la espada se basa en la posición del mouse del jugador. El movimiento del mouse del usuario se registra como una lista de CFrames, SweepPartAsync() construye una malla de corte a partir de estos datos, luego la malla de corte se resta de la parte que fue golpeada.

Para hacer funcionar este ejemplo en Studio:

  1. Crea el siguiente Script en ServerScriptService para realizar todas las operaciones de modelado sólido.


    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 el barrido
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Restar el barrido de la instancia golpeada
    local subtractSuccess, newParts = pcall( function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  2. Crea el siguiente LocalScript en StarterPlayerScripts para manejar la entrada del usuario.


    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 el barrido
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Restar el barrido de la instancia golpeada
    local subtractSuccess, newParts = pcall( function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  3. Crea un RemoteEvent en ReplicatedStorage llamado DrawCurveEvent.

FragmentAsync()

Las funciones GeometryService:FragmentAsync() y GeometryService:GenerateFragmentSites() te permiten romper una parte en piezas con formas naturales. GeometryService:FragmentAsync() utiliza voronoi para dividir una sola parte en múltiples instancias de MeshPart de acuerdo con el patrón de puntos que se pasa, mientras que GeometryService:GenerateFragmentSites() es una función auxiliar que genera puntos conocidos como sitios de voronoi para pasar a FragmentAsync().

Para demostrar cómo funcionan estas funciones juntas, el siguiente ejemplo de código genera sitios de voronoi para fragmentar una parte bloque:


local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Position = Vector3.new(0, 0.7, 20)
local sites = GeometryService:GenerateFragmentSites(inputPart)
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(inputPart, sites)
end)
if success and fragments then
for _, item in fragments do
local instance = item.Instance
instance.Parent = workspace
end
end
Un bloque hecho trizas.

Ejemplos

Un bloque con la esquina fragmentada en piezas.

El siguiente script fragmenta un área de una parte, dada una posición y radio. La posición podría provenir comúnmente de una colisión física o un raycast de un jugador.

El primer elemento del arreglo de sitios que GenerateFragmentSites() genera será un arreglo interno de todos los sitios que están fuera del radio solicitado. Si quieres hacer algo específico con la 'porción no destrozada' restante de la parte, puedes encontrar esa porción verificando fragments[i].Index == 1 al iterar sobre los 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("Falló al fracturar:"..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

El siguiente script rompe fragmentos de la primera parte dentro de una forma definida utilizando una segunda parte como plantilla. Solo los sitios de voronoi dentro de la segunda parte resultarán en piezas separadas. Todos los otros sitios tendrán sus celdas combinadas en una sola parte.

Plantilla en la forma del logo de Roblox Studio
Parte (gris oscuro) y parte plantilla
Resultados de un fragmento en forma de plantilla
Resultado del script de fragmento

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("Falló al fracturar:"..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

El siguiente script es un caso de uso mucho más específico, pero demuestra el poder de los datos de índice que se devuelven de GeometryService:FragmentAsync().

Por ejemplo, muchos lugares contienen edificios formados por múltiples partes de bloques que no están unidas. Si una granada, bola de cañón o martillo de choque llegara a dañarlo, querrías que todas las partes de la pared se fragmentaran. Este script fragmenta todas las partes cercanas, luego une los fragmentos de diferentes partes para ocultar completamente las costuras.

Esto implica múltiples operaciones Async, así que puede no ser adecuado para su uso en el juego como una respuesta instantánea a la entrada del usuario, como una herramienta de martillo de choque.

Una fila de bloques
Una fila de bloques
Una fila de bloques fragmentados
Cada pieza puede originarse de múltiples partes de entrada

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("Falló al fracturar:"..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("Falló al 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

El siguiente script es un reemplazo de Luau casi idéntico para GeometryService:GenerateFragmentSites(). Si quieres un comportamiento similar al de GeometryService:GenerateFragmentSites() pero deseas realizar ligeros cambios, puedes usar esto como punto de partida.

Utiliza una cuadrícula de puntos temblorosos y garantiza que el área fragmentada se comporte adecuadamente, a diferencia de los puntos completamente aleatorios.


local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Usa esto para ajustar la cantidad de temblor
if (origin and not radius) or (radius and not origin) then
warn("Ambos, origen y radio deben ser proporcionados, o ninguno.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized significa no fracturar toda la parte, solo una sección
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

Conservar restricciones

Si una parte de entrada tiene restricciones o uniones que deseas conservar, puedes transferirlas a las partes resultantes. Puede ser tedioso averiguar a qué parte de salida debe estar unida una restricción, por lo que se recomienda usar GeometryService:CalculateConstraintsToPreserve() para generar una tabla de recomendaciones que puedes recorrer y aplicar.

Para demostrar, el siguiente ejemplo de código realiza una operación de sustracción, itera a través de las partes resultantes para reparentar y reposicionar las partes resultantes, luego calcula una tabla de restricciones y uniones para conservar o eliminar antes de destruir todas las partes originales.


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 operación de sustracción en pcall() ya que es asíncrona
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Iterar a través de las partes resultantes para reparentar/reubicar
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Calcular restricciones/uniones a conservar o eliminar
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Conservar restricciones/uniones basado en la tabla 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 originales
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Detalles del comportamiento

Consideraciones sobre los resultados del modelado sólido

Colores y UVs

Los colores de la(s) parte(s) resultante(s) después del modelado sólido provienen de dos lugares: los colores de las caras y el Color de la parte.

  • Si el resultado es una PartOperation, tendrá el Color de la primera parte que seleccionaste en Studio, pero Studio utiliza los colores de las caras por defecto para mantener cada cara del mismo color que antes de la operación. Puedes habilitar su propiedad UsePartColor en Studio para sobrescribir este comportamiento y hacer que todo el resultado sea de un mismo color.
  • Si el resultado es una MeshPart, su Color será blanco y los colores de las caras siempre se mostrarán. Puedes ajustar el tinte de la(s) parte(s) resultante(s) cambiando su Color, pero se mezclará (multiplicará) con los colores de las caras. Esto tiñe el resultado en lugar de sobrescribir los colores de las caras por completo. Si deseas un control completo sobre el color de la salida, es mejor hacer que las entradas sean blancas primero.

Los UVs también se manejan de manera diferente dependiendo del tipo de resultado:

  • PartOperations siempre tienen UVs mappeados en caja, lo que significa que cada cara tendrá el material/textura/decal de una dirección (una de -x, +x, -y, +y, -z, +z) aplicada a ella. Esto puede estirar texturas.
  • MeshParts no están mappeados en caja. Se utilizan los UVs de la malla de la parte principal. Dado que Roblox no tiene actualmente soporte para múltiples materiales, los UVs de las caras que provienen de otras partes reciben UVs de (0, 0). Para obtener los mejores resultados, asegúrate de que el píxel (0, 0) de tu textura tenga un color razonable.

Ángulo de suavizado

La propiedad SmoothingAngle de una parte modelada con sólidos suaviza los ángulos entre superficies adyacentes del mismo color. Un valor más alto produce una apariencia más suave, mientras que un valor más bajo produce una apariencia más áspera con more bordes afilados.

Mientras que un valor entre 30 y 70 grados suele producir un buen resultado, los valores entre 90 y 180 no se recomiendan, ya que pueden causar un efecto de "sombreado" en uniones e intersecciones con bordes afilados.

Simplificación de partes

Si una operación de modelado sólido resulta en partes que tienen más de 20,000 triángulos, se simplificarán a 20,000. Si eso no se puede hacer, generalmente en un caso con miles de componentes que no se superponen, la operación resultará en un error.

Un MeshPart en buen estado
Antes de la simplificación
Un MeshPart con reducción obvia en la calidad de la malla
Después de la simplificación
©2026 Roblox Corporation. Roblox, el logotipo de Roblox y "Powering Imagination" son algunas de nuestras marcas registradas y no registradas en los Estados Unidos y otros países.