Modelado sólido es el proceso de unir BaseParts de maneras únicas para formar formas más complejas. Esto incluye las operaciones booleanas unión, intersección y sustracción que son comúnmente conocidas como Geometría Sólida Constructiva (CSG). Puedes realizar modelado sólido en Studio, plugins y en el juego tanto en el servidor como en el cliente.
Además de la CSG booleana, el modelado sólido también admite mallas, siempre que sean herméticas, y operaciones como barrido y fragmento que te permiten a ti y a tus jugadores cortar, dividir y romper geometría para interacciones de juego únicas.


Geometría hermética
Las operaciones de modelado sólido solo pueden funcionar con geometría hermética. En términos técnicos, que una malla sea 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 los vértices 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 atravesar otras caras.
- Los triángulos adyacentes deben coincidir en qué lado es el lado "exterior".
- Cada vértice debe tener exactamente un abanico de triángulos adyacentes.
Cada uno de los siguientes ejemplos es no hermético por la razón indicada:




El sistema de modelado sólido puede reparar automáticamente problemas pequeños específicos con 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, luego intentar habilitar y deshabilitar la propiedad MeshPart.DoubleSided. 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 y cuál está 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.


Modelado sólido en Studio
Puedes realizar tres operaciones booleanas básicas utilizando cuatro herramientas dentro de la barra de herramientas de la pestaña Modelo.

| Herramienta | Atajo | Descripción |
|---|---|---|
| Unión | ShiftCtrlG (Windows) Shift⌘G (Mac) | Une dos o más partes para formar una única unión sólida. |
| Intersección | ShiftCtrlI (Windows) Shift⌘I (Mac) | Interseca partes superpuestas en una única intersección sólida. |
| Separar | ShiftCtrlU (Windows) Shift⌘U (Mac) | Separa la unión o intersección de nuevo en sus partes individuales. |
| Negar | ShiftCtrlN (Windows) Shift⌘N (Mac) | Niega partes, útil para hacer agujeros e indentaciones. |
Unión
La herramienta Unión une dos o más partes para formar una única UnionOperation sólida.


Para combinar partes en una unión:
- Selecciona todas las partes que deseas unir.
- Haz clic en el botón Unión. Todas las partes se combinan en una única UnionOperation sólida con el nombre Unión.
Intersección
La herramienta Intersección interseca partes superpuestas en una única IntersectOperation sólida.


Para intersecar partes superpuestas:
- Selecciona todas las partes que deseas intersecar.
- Haz clic en el botón Intersección. Todas las partes se combinan en una única IntersectOperation sólida con el nombre Intersección.
Negar
La herramienta Negar niega una parte de modo que cuando se une con otra parte, la forma de la parte negada se sustrae de la otra parte.


Para sustraer una parte de otras partes superpuestas:
- Selecciona la parte que deseas negar de otras partes.
- Haz clic en Negar. La parte se vuelve translúcida con un tinte rojizo para indicar su estado.
- Selecciona tanto la parte negada como las partes de las que deseas sustraerla.
- Haz clic en Unión. La parte negada se corta de las partes superpuestas incluidas.
Separar
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:
- Selecciona la unión o intersección.
- 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 un juego está en ejecución o desde un plugin a través de métodos de GeometryService.
Operaciones básicas
Similar a las herramientas de operación integradas de Studio, puedes usar métodos de GeometryService como UnionAsync(), IntersectAsync(), y SubtractAsync() para realizar operaciones booleanas básicas. Por ejemplo, el siguiente script utiliza SubtractAsync() para sustraer 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
endPara demostrar más, el siguiente ejemplo de código combina la geometría de mainPart y las partes en el array 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
}
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Recorrer 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
endSi bien no hay un método a nivel de motor para la negación de partes, puedes agregar la etiqueta rbxNegate dentro de un script o plugin para realizar una negación equivalente al botón de la barra de herramientas Negar de Studio.
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")Comparado con BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), los métodos de GeometryService difieren de la siguiente manera:
- La salida es un array de instancias en lugar de una única instancia.
- Las partes de entrada no necesitan estar parentadas a la escena, lo que permite operaciones en segundo plano.
- Cuando la opción SplitApart está configurada en 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 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.
Barrido de parte
El método 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. Este método puede ser muy útil para realizar interacciones de corte y división.
La entrada puede ser un 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 único CFrame, el resultado será un cascarón convexo de la parte de entrada.
Para demostrar cómo funciona este método, 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

Este ejemplo utiliza 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 sustrae de la parte que fue golpeada.
Para hacer que este ejemplo funcione en Studio:
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 barridosweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Sustraer el barrido de la instancia golpeadalocal subtractSuccess, newParts = pcall(function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)Crea 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 barridosweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Sustraer el barrido de la instancia golpeadalocal subtractSuccess, newParts = pcall(function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)Crea un RemoteEvent en ReplicatedStorage llamado DrawCurveEvent.
Fragmento
Los métodos FragmentAsync() y GenerateFragmentSites() te permiten romper una parte en piezas con formas naturales. FragmentAsync() utiliza la descomposición voronoi para dividir una única parte en múltiples instancias de MeshPart de acuerdo con el patrón de puntos que se pasan, mientras que GenerateFragmentSites() es un método auxiliar que genera puntos conocidos como sitios voronoi para pasar a FragmentAsync().
El siguiente ejemplo de código genera sitios voronoi para fragmentar una parte de 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

El siguiente script fragmenta un área de una parte, dada por una posición y un radio. La posición puede provenir comúnmente de una colisión física o un raycast de un jugador.
El primer elemento del array de sitios que GenerateFragmentSites() genera será un array interno de todos los sitios que están fuera del radio solicitado. Si deseas hacer algo específico con la 'porción no fragmentada' restante de la parte, puedes encontrar esa porción verificando fragments[i].Index == 1 al recorrer los 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("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
endEl siguiente script rompe fragmentos de la primera parte dentro de una forma definida usando una segunda parte como plantilla. Solo los sitios voronoi dentro de la segunda parte resultarán en piezas separadas. Todos los demás sitios tendrán sus celdas combinadas en una sola 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("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
endEl 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 FragmentAsync().
Por ejemplo, muchos lugares contienen edificios formados por múltiples partes de bloques no unidas. Si una granada, bola de cañón o martillo de guerra dañara esto, 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(), por lo 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 guerra.


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("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
endEl siguiente script es un reemplazo de Luau casi idéntico para GenerateFragmentSites(). Si deseas un comportamiento similar al de GenerateFragmentSites() pero quieres hacer ligeros cambios, puedes usar esto como punto de partida.
Utiliza una cuadrícula de puntos con jitter, y garantiza que el área fragmentada se comporte bien, 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 jitter
if (origin and not radius) or (radius and not origin) then
warn("O bien se deben proporcionar tanto el origen como el radio, 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
endPreservar restricciones
Si una parte de entrada tiene restricciones o adjuntos que deseas preservar, puedes transferirlos a las partes resultantes. Puede ser tedioso averiguar a qué parte de salida debe estar adjunta una restricción, por lo que se recomienda usar 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, recorre las partes resultantes para reparentar y reposicionar las partes resultantes, luego calcula una tabla de restricciones y adjuntos para preservar 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
-- Recorrer 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/adjuntos para preservar o eliminar
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Preservar restricciones/adjuntos basados 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
endResultados del modelado sólido
Detalles del comportamiento
Si la parte principal se está moviendo durante el cálculo de la operación, puedes establecer las partes resultantes en el CFrame actualizado de la parte principal, ya que las partes devueltas están en el mismo espacio de coordenadas que la parte principal.
Si una operación de modelado sólido resultara en partes con 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 no superpuestos, la operación resulta en un error.
Hay métodos para intercambiar los datos de malla de una instancia, facilitando la utilización de la geometría de la operación mientras se mantienen propiedades, atributos, etiquetas e hijos de la parte principal, como Attachments, Constraints, ParticleEmitters, objetos de luz y decals. Este enfoque también evita el posible "parpadeo" de reemplazar completamente la PartOperation original por otra.
- Si usas este método con una PartOperation como parte principal y ninguna de las otras partes son MeshParts, puedes sustituir la geometría de otra PartOperation a través de SubstituteGeometry().
- Si la parte principal es una MeshPart, puedes usar MeshPart:ApplyMesh().
Es posible llamar a estos métodos en el cliente, pero con algunas limitaciones. Primero, debe hacerse con objetos creados en el cliente. En segundo lugar, no hay replicación disponible de cliente a servidor.
Las siguientes propiedades de la parte principal se aplican a las PartOperations o MeshParts resultantes:
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 anular este comportamiento y hacer que todo el resultado sea de un solo 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 anular completamente los colores de las caras. Si deseas un control total 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 mapeados 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) aplicado a ella. Esto puede estirar texturas.
- MeshParts no están mapeados en caja. Se utilizan los UVs de la malla de la parte principal. Dado que Roblox actualmente no tiene soporte para múltiples materiales, los UVs de las caras que se originan en las otras partes se les asignan 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 sólidamente 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 más bordes afilados.
Si bien un valor entre 30 y 70 grados generalmente produce 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.

