Modélisation solide

*Ce contenu est traduit en utilisant l'IA (Beta) et peut contenir des erreurs. Pour consulter cette page en anglais, clique ici.

Modélisation solide est le processus de jonction de pièces ensemble de manière unique pour former des formes plus complexes. Cela inclut les opérations booléennes .union, soustraction, et intersection, qui sont couramment connues sous le nom de géométrie solide constructive (CSG). Vous pouvez réaliser la modélisation solide partout : dans Studio, les plugins, même dans le jeu à la fois sur le serveur et le client.

En plus de la CSG booléenne, la modélisation solide prend également en charge les maillages, tant qu'ils sont étanches, ainsi que des opérations comme balayer et fragmenter qui vous permettent, à vous et à vos joueurs, de trancher, couper et briser la géométrie pour des interactions de jeu uniques.

Un objet maillé créé avec SweepPartAsync()
Un objet maillé fragmenté avec FragmentAsync()

Géométrie étanche

Il y a trois éléments de base des maillages :

  • Sommet - Un seul point sur le maillage.
  • Arête - Une ligne qui relie deux sommets.
  • Face - Une surface entre trois sommets ou plus.
Un seul sommet actif sur un maillage cube.
Sommet
Une seule arête active sur un maillage cube.
Arête
Une seule face active sur un maillage cube.
Face

Les opérations de modélisation solide ne peuvent fonctionner qu'avec une géométrie étanche ; en fait, "solide" et "étanche" sont synonymes. En termes techniques, un maillage étant étanche signifie qu'il est fermé, maniable, et non auto-intersectant. Ces termes ont des définitions strictes, mais voici quelques règles simples :

  • Chaque face doit avoir un côté "intérieur" et un côté "extérieur". Ceux-ci sont déterminés par l'ordre de rotation des sommets de la face, qui est l'ordre de ses trois sommets.
  • Chaque arête doit être partagée par exactement deux triangles. Cela signifie qu'il ne peut y avoir aucun trou dans le maillage, car les arêtes autour du périmètre d'un trou n'auraient qu'un triangle.
  • Les faces ne peuvent pas traverser d'autres faces.
  • Les triangles adjacents doivent s'accorder sur quel côté est le côté "extérieur".
  • Chaque sommet doit avoir exactement un éventail de triangles adjacents.
Exemples de géométrie non étanche

Le système de modélisation solide peut automatiquement réparer certains petits problèmes avec un maillage, mais en général, les appels à l'API échoueront si le maillage n'est pas étanche. Il n'y a pas de solution universelle pour réparer un maillage non étanche existant, mais il existe plusieurs plugins Blender qui peuvent aider, tels que 3D Print Toolbox et Mesh Repair Tools. Comme autre option, Meshlab dispose également d'outils très utiles intégrés pour essayer de rendre le maillage maniable, ce qui est le principal critère pour qu'un maillage soit étanche.

Une façon de voir qu'un maillage sera extrêmement difficile à rendre étanche est de le regarder dans Studio sous tous les angles, puis d'essayer d'activer et de désactiver la propriété MeshPart.DoubleSided du maillage. Si vous remarquez une différence, alors le maillage est juste une coque et les outils mentionnés ci-dessus ne fonctionneront pas car ils ne peuvent pas deviner quel espace est à l'intérieur par rapport à l'extérieur du maillage. Cependant, si tout ce que vous voulez est un maillage fin et qu'il n'est pas important de garder les dimensions du maillage exactement les mêmes, vous pouvez utiliser le modificateur Solidify de Blender pour légèrement épaissir la coque en un maillage étanche.

Exemple d'un maillage qui a l'air différent lorsque DoubleSided est activé.
Ceci est un maillage coque et les méthodes de réparation automatiques ne fonctionneront pas.

Modélisation solide dans Studio

Vous pouvez effectuer trois opérations booléennes de base à l'aide de quatre outils dans la barre d'outils de l'onglet Modèle.

Outils de modélisation solide mis en évidence dans la barre d'outils de Studio.
OutilRaccourciDescription
UnionShiftCtrlG (Windows)
ShiftG (Mac)
Joindre deux ou plusieurs pièces ensemble pour former une union solide unique.
IntersectShiftCtrlI (Windows)
ShiftI (Mac)
Intersecter des pièces qui se chevauchent en une seule intersection solide.
NegateShiftCtrlN (Windows)
ShiftN (Mac)
Nier des pièces, utile pour faire des trous et des indentations.
SeparateShiftCtrlU (Windows)
ShiftU (Mac)
Séparer l'union ou l'intersection en ses pièces individuelles.

Union des pièces

L'outil Union joint deux ou plusieurs pièces ensemble pour former une unique UnionOperation.

Un bloc et un cylindre.
Pièces individuelles
Un bloc et un cylindre combinés en un seul objet.
Résultat de l'union

Pour combiner des pièces ensemble dans une union :

  1. Sélectionnez toutes les pièces que vous souhaitez joindre ensemble.
  2. Cliquez sur le bouton Union. Toutes les pièces se combinent en une seule UnionOperation avec le nom Union.

Intersection des pièces

L'outil Intersect intercepte les pièces qui se chevauchent en une seule IntersectOperation.

Un bloc et un cylindre.
Pièces individuelles
Un bloc et un cylindre combinés en un seul objet.
Résultat de l'intersection

Pour intersecter des pièces se chevauchant :

  1. Sélectionnez toutes les pièces que vous souhaitez intersecter.
  2. Cliquez sur le bouton Intersect. Toutes les pièces se combinent en une seule IntersectOperation avec le nom Intersection.

Négation des pièces

L'outil Negate nie une pièce afin que lorsqu'elle est unifiée avec une autre pièce, la forme de la pièce niée soit soustraite de l'autre pièce.

Un bloc et un cylindre.
Bloc et cylindre nié
Un bloc et un cylindre combinés en un seul objet.
Résultat de la soustraction

Pour soustraire une pièce d'autres pièces se chevauchant :

  1. Sélectionnez la pièce que vous souhaitez négliger des autres pièces.
  2. Cliquez sur Negate. La pièce devient marquée comme une partie niée et un symbole nié apparaît dans l'Explorateur. La pièce devient translucide avec une teinte rougeâtre pour indiquer son état.
  3. Sélectionnez à la fois la pièce niée et les pièces dont vous souhaitez la soustraire.
  4. Cliquez sur Union. La pièce niée est découpée des pièces qui se chevauchent incluses.

L'étiquette est exposée pour le scripting, vous pouvez donc également nier des pièces en ajoutant l'étiquette rbxNegate depuis un script ou un plugin. NegateOperation n'est plus utilisé.

Séparer les unions ou intersections

L'outil Separate sépare un UnionOperation en ses pièces individuelles, servant essentiellement d'outil "annuler" pour les unions et intersections.

Pour séparer une union ou une intersection en pièces individuelles :

  1. Sélectionnez l'union ou l'intersection.
  2. Cliquez sur Separate. Les pièces se séparent à leur forme originale.

Modélisation solide en jeu

Vous pouvez également effectuer des opérations de modélisation solide pendant qu'un jeu est en cours d'exécution en utilisant les fonctions GeometryService.

UnionAsync(), IntersectAsync(), et SubtractAsync()

Similaire aux outils de base d'opération booléenne intégrés à Studio, vous pouvez utiliser des fonctions GeometryService comme UnionAsync(), IntersectAsync(), et SubtractAsync() pour réaliser des opérations booléennes de base pendant qu'un jeu est en cours d'exécution. Par exemple, le script suivant utilise SubtractAsync() pour soustraire le volume d'une pièce d'une autre.


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 bloc soustrait d'un autre bloc.

Pour montrer cela, le prochain exemple de code combine la géométrie de mainPart et les pièces dans le tableau otherParts, puis détruit les pièces originales impliquées dans l'opération. Vous pouvez remplacer l'appel à UnionAsync() par IntersectAsync() ou SubtractAsync() pour réaliser les autres opérations booléennes.


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,
}
-- Effectuer l'opération d'union dans pcall() puisqu'elle est asynchrone
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Boucle à travers les parties résultantes pour réaffecter/repositionner
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Détruire les pièces originales
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Tant que toutes les entrées sont composées de pièces primitives plutôt que de MeshParts, les opérations booléennes d'union, d'intersection et de soustraction produiront une PartOperation avec deux ensembles de données enregistrées : un arbre d'opérations CSG connu sous le nom de CSGTree, et un maillage pour le rendu.

Contrairement à BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), les fonctions booléennes GeometryService diffèrent comme suit :

  • La sortie est un tableau d'instances plutôt qu'une seule instance.
  • Les pièces d'entrée n'ont pas besoin d'être parentées à la scène, permettant des opérations en arrière-plan.
  • Lorsque l'option SplitApart est définie sur true (par défaut), chaque corps distinct sera retourné dans sa propre PartOperation/MeshPart.
  • Toutes les pièces retournées sont dans l'espace de coordonnées de la pièce principale, de sorte que leurs positions PVInstance.Origin sont les mêmes que celles de la pièce principale. Cela maintient les sommets du maillage dans la même position par rapport à l'objet qu'auparavant l'opération, mais cela signifie également que le (0, 0, 0) d'une pièce retournée n'est pas nécessairement au centre de son corps.

SweepPartAsync()

La fonction GeometryService:SweepPartAsync() crée un MeshPart qui a la forme de la pièce d'entrée traînée à travers un ensemble donné de positions CFrame. Cette fonction peut être très utile pour effectuer des interactions de découpe et de tranchage.

L'entrée peut être un Part, PartOperation, ou MeshPart. La forme du résultat est définie comme l'union des coques convexes de chaque paire adjacente de CFrames ; si un seul CFrame est fourni, le résultat sera une coque convexe de la pièce d'entrée.

Pour illustrer comment fonctionne cette fonction, l'exemple de code suivant fait passer une sphère à travers un ensemble de positions CFrame pour créer une spirale :


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
Une forme en spirale créée en faisant passer une sphère.

Exemple

Une forme courbée translucide soustraite d'un bloc.

Cet exemple utilise GeometryService:SweepPartAsync() pour obtenir une fonctionnalité de gameplay de découpe d'épée ou de laser, où le mouvement de l'épée est basé sur la position de la souris du joueur. Le mouvement de la souris de l'utilisateur est enregistré sous forme de liste de CFrames, SweepPartAsync() crée un maillage de découpe à partir de ces données, puis le maillage de découpe est soustrait de la pièce qui a été touchée.

Pour exécuter cet exemple dans Studio :

  1. Créez le Script suivant dans ServerScriptService pour effectuer toutes les opérations de modélisation solide.


    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
    -- Visualiser le balayage
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Soustraire le balayage de l'instance touchée
    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. Créez le LocalScript suivant dans StarterPlayerScripts pour gérer l'entrée utilisateur.


    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
    -- Visualiser le balayage
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- Soustraire le balayage de l'instance touchée
    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. Créez un RemoteEvent dans ReplicatedStorage nommé DrawCurveEvent.

FragmentAsync()

Les fonctions GeometryService:FragmentAsync() et GeometryService:GenerateFragmentSites() vous permettent de briser une pièce en morceaux avec des formes naturelles. GeometryService:FragmentAsync() utilise la décomposition voronoi pour diviser une seule pièce en plusieurs instances MeshPart selon le motif de points passés, tandis que GeometryService:GenerateFragmentSites() est une fonction d'assistance qui génère des points connus sous le nom de sites voronoi à passer à FragmentAsync().

Pour démontrer comment ces fonctions fonctionnent ensemble, l'exemple de code suivant génère des sites voronoi pour fragmenter une pièce bloc :


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 bloc brisé en morceaux.

Exemples

Un bloc avec le coin fragmenté en morceaux.

Le script suivant fragmente une zone d'une pièce, donnée par une position et un rayon. La position peut généralement provenir d'une collision physique ou d'un raycast d'un joueur.

Le premier élément du tableau des sites que GenerateFragmentSites() génère sera un tableau interne de tous les sites qui sont en dehors du rayon demandé. Si vous voulez faire quelque chose de spécifique à la 'portion non brisée' restante de la pièce, vous pouvez trouver cette portion en vérifiant fragments[i].Index == 1 lors de la boucle sur les résultats 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("Échec de la fracture:" .. 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

Le script suivant brise des fragments de la première pièce à l'intérieur d'une forme définie en utilisant une seconde pièce comme pochoir. Seuls les sites voronoi à l'intérieur de la seconde pièce donneront des morceaux séparés. Tous les autres sites auront leurs cellules combinées en une seule pièce.

Pochoir à la forme du logo de Roblox Studio
Pièce (gris foncé) et pièce pochoir
Résultats d'un fragment sous la forme d'un pochoir
Résultat du script de fragmentation

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("Échec de la fracture:" .. 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

Le script suivant est un cas d'utilisation beaucoup plus spécifique, mais il démontre la puissance des données d'index qui sont retournées à partir de GeometryService:FragmentAsync().

Par exemple, de nombreux endroits contiennent des bâtiments formés de plusieurs pièces de blocs non unies. Si une grenade, un boulet de canon ou un marteau-piqueur devaient les endommager, vous voudriez que toutes les pièces de mur soient fragmentées. Ce script fragmente toutes les pièces proches, puis unifie les fragments de différentes pièces ensemble pour cacher complètement les coutures.

Cela implique plusieurs opérations Async, donc cela peut ne pas être adapté à une utilisation en jeu comme réponse instantanée à une entrée utilisateur, comme un outil de marteau-piqueur.

Une rangée de blocs
Une rangée de blocs
Une rangée de blocs fragmentée
Chaque pièce peut provenir de plusieurs pièces d'entrée

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("Échec de la fracture:" .. 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("Échec de l'union:" .. 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

Le script suivant est un remplacement Luau presque identique à GeometryService:GenerateFragmentSites(). Si vous souhaitez un comportement similaire à GeometryService:GenerateFragmentSites() mais souhaitez apporter des modifications légères, vous pouvez utiliser ceci comme point de départ.

Il utilise une grille de points jittered et garantit que la zone fragmentée se comporte bien, contrairement à des points complètement aléatoires.


local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- Utilisez ceci pour ajuster la quantité de jitter
if (origin and not radius) or (radius and not origin) then
warn("Soit l'origine et le rayon doivent être fournis, soit aucun des deux.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized signifie ne pas fracturer la pièce entière, juste une section.
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

Préserver les contraintes

Si une pièce d'entrée a des contraintes ou des adaptations que vous souhaitez préserver, vous pouvez les transférer sur les pièces résultantes. Il peut être fastidieux de déterminer à quelle pièce de sortie une contrainte doit être attachée, il est donc recommandé d'utiliser GeometryService:CalculateConstraintsToPreserve() pour générer une table de recommandations que vous pouvez parcourir et appliquer.

Pour démontrer, l'exemple de code suivant effectue une opération de soustraction, parcourt les pièces résultantes pour réaffecter et repositionner les pièces résultantes, puis calcule une table de contraintes et d'attachements à préserver ou à abandonner avant de détruire toutes les pièces 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,
}
-- Effectuer l'opération de soustraction dans pcall() puisque c'est asynchrone
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- Boucle à travers les pièces résultantes pour réaffecter/repositionner
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- Calculer les contraintes/attachements à préserver ou à abandonner
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- Conserver les contraintes/attachements en fonction de la table recommandée
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
-- Détruire les pièces originales
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

Détails Comportement

Considérations sur les résultats de modélisation solide

Couleurs et UVs

Les couleurs des pièces résultantes après modélisation solide proviennent de deux endroits : les couleurs de face et la Color de la pièce.

  • Si le résultat est une PartOperation, il aura la Color de la première pièce que vous avez sélectionnée dans Studio, mais Studio utilise par défaut les couleurs de face pour garder chaque face de la même couleur qu'elle l'était avant l'opération. Vous pouvez activer sa propriété UsePartColor dans Studio pour remplacer ce comportement et faire en sorte que l'ensemble du résultat soit d'une seule couleur.
  • Si le résultat est un MeshPart, sa Color sera blanche, et les couleurs de face passeront toujours. Vous pouvez ajuster la teinte des pièces résultantes en changeant leur Color, mais cela sera mélangé (multiplié) avec les couleurs de face. Cela teinte le résultat plutôt que de remplacer complètement les couleurs de face. Si vous voulez un contrôle total sur la couleur de la sortie, il est préférable de rendre les entrées blanches d'abord.

Les UVs sont également traités différemment selon le type de résultat :

  • PartOperations ont toujours des UVs mappés sur des boîtes, ce qui signifie que chaque face aura le matériau/texture/décalcomanie d'une direction appliquée (l'une de -x, +x, -y, +y, -z, +z). Cela peut étirer les textures.
  • MeshParts ne sont pas mappés sur des boîtes. Les UVs du maillage de la pièce principale sont utilisés. Comme Roblox ne prend actuellement pas en charge plusieurs matériaux, les UVs des faces provenant des autres pièces se voient attribuer des UVs de (0, 0). Pour de meilleurs résultats, assurez-vous que le pixel (0, 0) de votre texture ait une couleur raisonnable.

Angle de lissage

La propriété SmoothingAngle d'une pièce modélisée solide adoucit les angles entre les surfaces adjacentes de la même couleur. Une valeur plus élevée produit une apparence plus douce tandis qu'une valeur plus basse produit une apparence plus rugueuse avec plus de bords nets.

Bien qu'une valeur comprise entre 30 et 70 degrés produise généralement un bon résultat, les valeurs entre 90 et 180 ne sont pas recommandées, car elles peuvent causer un effet d'ombre sur les unions et intersections avec des bords nets.

Simplification des pièces

Si une opération de modélisation solide devait aboutir à des pièces avec plus de 20 000 triangles, elles seront simplifiées à 20 000. Si cela ne peut pas être fait, généralement dans un cas avec des milliers de composants non chevauchants, l'opération se solde par une erreur.

Un MeshPart en bon état
Avant la simplification
Un MeshPart avec une réduction évidente de la qualité du maillage
Après la simplification
©2026 Société Roblox. Roblox, le logo Roblox et Powering Imagination font partie de nos marques déposées aux États-Unis et dans d'autres pays.