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.


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.



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.

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.

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.

| Outil | Raccourci | Description |
|---|---|---|
| Union | ShiftCtrlG (Windows) Shift⌘G (Mac) | Joindre deux ou plusieurs pièces ensemble pour former une union solide unique. |
| Intersect | ShiftCtrlI (Windows) Shift⌘I (Mac) | Intersecter des pièces qui se chevauchent en une seule intersection solide. |
| Negate | ShiftCtrlN (Windows) Shift⌘N (Mac) | Nier des pièces, utile pour faire des trous et des indentations. |
| Separate | ShiftCtrlU (Windows) Shift⌘U (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.


Pour combiner des pièces ensemble dans une union :
- Sélectionnez toutes les pièces que vous souhaitez joindre ensemble.
- 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.


Pour intersecter des pièces se chevauchant :
- Sélectionnez toutes les pièces que vous souhaitez intersecter.
- 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.


Pour soustraire une pièce d'autres pièces se chevauchant :
- Sélectionnez la pièce que vous souhaitez négliger des autres pièces.
- 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.
- Sélectionnez à la fois la pièce niée et les pièces dont vous souhaitez la soustraire.
- 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 :
- Sélectionnez l'union ou l'intersection.
- 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

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

Exemple

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 :
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 balayagesweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Soustraire le balayage de l'instance touchéelocal 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)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 balayagesweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- Soustraire le balayage de l'instance touchéelocal 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)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

Exemples

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.


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.


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
Si la pièce principale est en mouvement pendant le calcul de l'opération, vous pouvez définir les pièces résultantes à la CFrame mise à jour de la pièce principale, puisque les pièces retournées sont dans le même espace de coordonnées que la pièce principale.
Il existe des fonctions pour échanger les données de maillage d'une instance, facilitant l'utilisation de la géométrie de l'opération tout en maintenant les propriétés, attributs, étiquettes et enfants de la pièce principale, tels que Attachments, Constraints, ParticleEmitters, objets lumineux, et décalcomanies. Cette approche contourne également le potentiel de "clignotement" lors du remplacement complet de l'original PartOperation par un autre.
- Si vous utilisez cette méthode avec une PartOperation comme pièce principale et qu'aucune des autres pièces n'est MeshParts, vous pouvez substituer la géométrie d'une autre PartOperation via SubstituteGeometry().
- Si la pièce principale est un MeshPart, vous pouvez utiliser MeshPart:ApplyMesh().
Il est possible d'appeler ces fonctions sur le client, mais avec certaines limitations. Tout d'abord, cela doit être fait avec des objets créés sur le client. Deuxièmement, il n'y a pas de réplication disponible du client vers le serveur.
Les propriétés suivantes de la pièce principale sont appliquées aux PartOperations ou MeshParts résultants :
- Collision : BasePart.AudioCanCollide, BasePart.CanCollide
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.

