ソリッドモデリング

*このコンテンツは、ベータ版のAI(人工知能)を使用して翻訳されており、エラーが含まれている可能性があります。このページを英語で表示するには、 こちら をクリックしてください。

ソリッドモデリングは、部品をユニークな方法で結合して、より複雑な形状を形成するプロセスです。これには、一般に構成的ソリッドジオメトリ (CSG) として知られるブール演算の交差が含まれます。ソリッドモデリングは、スタジオ、プラグイン、さらにはゲーム内でも、サーバーとクライアントの両方で実行できます。

ブールCSGに加えて、ソリッドモデリングは、水密性がある限りメッシュもサポートし、あなたやプレイヤーがジオメトリをスライス、カット、破壊するためのスイープフラグメントなどの操作を提供します。

SweepPartAsync()で作成されたメッシュオブジェクト
FragmentAsync()で壊されたメッシュオブジェクト

水密性ジオメトリ

メッシュの基本要素は3つです:

  • 頂点 - メッシュ上の単一の点。
  • エッジ - 2つの頂点を結ぶ線。
  • - 3つ以上の頂点間の表面積。
キューブメッシュ上の単一のアクティブな頂点。
頂点
キューブメッシュ上の単一のアクティブなエッジ。
エッジ
キューブメッシュ上の単一のアクティブな面。

ソリッドモデリング操作は、水密性のジオメトリでのみ機能します。実際、「ソリッド」と「水密性」は同義です。技術的には、メッシュが水密性であることは、それが閉じていて、多歧管状態であり、自己交差しないことを意味します。これらの用語には厳密な定義がありますが、以下のいくつかの簡単なルールがあります:

  • 各面には「内側」と「外側」の側面が必要です。これは、面の風向順序、すなわち3つの頂点の順序によって決まります。
  • 各エッジは、正確に2つの三角形に共有されなければなりません。これは、メッシュに穴があってはいけないことを意味します。なぜなら、穴の周囲のエッジは正確に1つの三角形しか持たないからです。
  • 面は他の面を通過してはなりません。
  • 隣接する三角形は、どちらの側が「外側」を表示するかで合意しなければなりません。
  • 各頂点は、正確に1つの周辺三角形のファンを持つ必要があります。
非水密性ジオメトリの例

ソリッドモデリングシステムは、メッシュの特定の小さな問題を自動的に修復できる機能を持っていますが、一般にAPI呼び出しはメッシュが水密でない場合、失敗します。既存の非水密性メッシュを修復するための「ワンサイズで全て」の方法は存在しませんが、3D Print ToolboxMesh Repair Toolsなど、役立つBlenderプラグインがいくつかあります。別のオプションとして、Meshlabにも、メッシュを多歧管状態にするための便利なツールが組み込まれています。これは、メッシュが水密性であるための主な要件です。

メッシュを水密にするのが非常に難しいことを確認する1つの方法は、スタジオからあらゆる角度で見て、次にメッシュのMeshPart.DoubleSidedプロパティを有効にしたり無効にしたりしてみることです。違いが見える場合、そのメッシュは単なるシェルであり、上記のツールはメッシュの内部と外部の空間を推測できないため働きません。ただし、薄いメッシュだけが必要で、メッシュの寸法を正確に保持することが重要でない場合は、BlenderのSolidify modifierを使用してシェルをわずかに厚くして水密メッシュにすることができます。

DoubleSidedが有効になっていると異なる外観のメッシュの例。
これはシェルメッシュであり、自動修復方法は適用できません。

スタジオでのソリッドモデリング

モデルタブのツールバー内の4つのツールを使用して、3つの基本的なブール演算を実行できます。

スタジオのツールバーに強調表示されたソリッドモデリングツール。
ツールショートカット説明
ShiftCtrlG (Windows)
ShiftG (Mac)
2つ以上の部品を結合して、単一のソリッド和を形成します。
交差ShiftCtrlI (Windows)
ShiftI (Mac)
重なり合う部品を交差させて、単一のソリッド交差を形成します。
否定ShiftCtrlN (Windows)
ShiftN (Mac)
部品を否定する。穴や凹みを作るのに便利です。
別々ShiftCtrlU (Windows)
ShiftU (Mac)
和または交差を元の個々の部品に分離します。

和の部品

ツールは、2つ以上の部品を結合して、単一のソリッドUnionOperationを形成します。

ブロックとシリンダー。
個別の部品
1つのオブジェクトに結合されたブロックとシリンダー。
和の結果

部品を和に結合するには:

  1. 結合したいすべての部品を選択します。
  2. ボタンをクリックします。すべての部品が結合されて、1つのソリッドUnionOperationとなり、名前はになります。

交差の部品

交差ツールは、重なり合う部品を交差させて、単一のソリッドIntersectOperationを形成します。

ブロックとシリンダー。
個別の部品
1つのオブジェクトに結合されたブロックとシリンダー。
交差の結果

重なり合う部品を交差させるには:

  1. 交差させたいすべての部品を選択します。
  2. 交差ボタンをクリックします。すべての部品が結合されて、1つのソリッドIntersectOperationとなり、名前は交差になります。

否定の部品

否定ツールは、部品を否定し、他の部品と和を取ると、否定された部品の形状が他の部品から引かれます

ブロックとシリンダー。
ブロックと否定されたシリンダー
1つのオブジェクトに結合されたブロックとシリンダー。
引き算の結果

他の重なり合う部品から部品を引くには:

  1. 他の部品から引きたい部品を選択します。
  2. 否定をクリックします。その部品は否定された部品としてタグ付けされ、エクスプローラーに否定されたシンボルが表示されます。その部品は、その状態を示すために赤みがかった色合いの半透明になります。
  3. 否定された部品と引き算したい部品の両方を選択します。
  4. をクリックします。否定された部品が重なり合った部品に切り取られます。

このタグはスクリプトに公開されているため、スクリプトやプラグインからrbxNegateタグを追加して部品を否定することもできます。NegateOperationはもはや使用されません。

和または交差の分離

別々ツールは、UnionOperationを個々の部品に分離します。これは、和や交差に対する「元に戻す」ツールとして機能します。

和や交差を元の個々の部品に分離するには:

  1. 和または交差を選択します。
  2. 別々をクリックします。部品は元の形に戻ります。

ゲーム内ソリッドモデリング

ゲームが実行中の間に、GeometryService関数を使用してソリッドモデリング操作を実行することもできます。

UnionAsync(), IntersectAsync(), および SubtractAsync()

スタジオの組込基本ブール操作ツールと同様に、GeometryService関数を使用して、ゲームが実行中に基本的なブール操作を行うことができます。例えば、以下のスクリプトは、SubtractAsync()を使用して、ある部品のボリュームを他の部品から引き算します。


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
あるブロックから引き算されたブロック。

さらに、次のコードサンプルでは、mainPartのジオメトリとotherParts配列内の部品を組み合わせ、その後、操作に関与した元の部品を破壊します。UnionAsync()の呼び出しをIntersectAsync()SubtractAsync()に置き換えて、他のブール操作を実行できます。


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,
}
-- 非同期なのでpcall()内で和演算を実行します
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- 結果の部品を再親にし、再配置するためのループ
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- 元の部品を破壊します
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

すべての入力がMeshPartsではなく原始部品で構成されている限り、和、交差、および引き算ブール操作は、2つの格納データを持つPartOperationを生成します:CSG操作の木を示すCSGTree、およびレンダリング用のメッシュです。

BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync()と比較して、GeometryServiceのブール関数は次のように異なります:

  • 出力は単一のインスタンスではなくインスタンスの配列です。
  • 入力部品はシーンに親付けされている必要はなく、バックグラウンドでの操作が可能です。
  • SplitApartオプションがtrueに設定されている(デフォルト)場合、各異なるボディは独自のPartOperation/MeshPartで返されます。
  • すべての返された部品は、主部品の座標空間にあるため、そのPVInstance.Origin位置は主部品と同じです。これにより、操作前と同じ位置にメッシュの頂点が位置しますが、その返された部品の(0, 0, 0)は必ずしもそのボディの中心にあるわけではありません。

SweepPartAsync()

GeometryService:SweepPartAsync()関数は、与えられた一連のCFrame位置を通して引きずられた入力部品の形状を持つMeshPartを作成します。この関数は、スライスやカットのインタラクションを実行するために非常に役立ちます。

入力はPartPartOperation、またはMeshPartです。結果の形状は、各隣接するCFrameの凸包の和として定義されます。1つのCFrameのみが提供される場合、結果は入力部品の凸包になります。

この関数の動作を示すために、以下のコードサンプルは球体を一連のCFrame位置を通してスイープしてスパイラルを作成します:


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
球体をスイープして作成されたスパイラル形状。

ブロックから引き算された半透明の曲面。

この例ではGeometryService:SweepPartAsync()を使用して、プレイヤーのマウス位置に基づいて剣やレーザー銃のスライスゲームプレイ機能を実現しています。ユーザーのマウスの動きはCFramesのリストとして記録され、SweepPartAsync()はこのデータからスライスメッシュを構築し、そのスライスメッシュをヒットした部品から引き算します。

この例をスタジオで実行するには:

  1. ServerScriptService内にすべてのソリッドモデリング操作を実行するScriptを作成します。


    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
    -- スイープを視覚化します
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- ヒットしたインスタンスからスイープを引き算します
    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. StarterPlayerScripts内にユーザー入力を処理するLocalScriptを作成します。


    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
    -- スイープを視覚化します
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- ヒットしたインスタンスからスイープを引き算します
    local subtractSuccess, newParts = pcall( function()
    r eturn 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. ReplicatedStorage内にDrawCurveEventという名前のRemoteEventを作成します。

FragmentAsync()

GeometryService:FragmentAsync()およびGeometryService:GenerateFragmentSites()関数は、自然な形状の部品を破壊することを可能にします。GeometryService:FragmentAsync()は、渡された点のパターンに従って単一の部品を複数のMeshPartインスタンスに分割するためにVoronoi分解を使用し、GeometryService:GenerateFragmentSites()は、FragmentAsync()の入力として渡すVoronoiサイトとして知られる点を生成するヘルパー関数です。

これらの関数がどのように連携して機能するかを示すために、以下のコードサンプルはブロック部品をフラグメントするためのVoronoiサイトを生成します。


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
破片にされたブロック。

コーナーが破片化されたブロック。

以下のスクリプトは、位置と半径に基づいて部品の領域をフラグメント化します。位置は通常、物理コリジョンやプレイヤーからのレイキャストに由来します。

GenerateFragmentSites()が生成するサイト配列の最初の要素は、要求された半径の外部にあるすべてのサイトの内側の配列になります。部品の残りの「割れていない部分」に特定の操作を行いたい場合は、FragmentAsync()の結果をループする際にfragments[i].Index == 1を確認することでその部分を見つけることができます。


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("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

次のスクリプトは、2番目の部品を stencil(型紙)として使用して、最初の部品からフラグメントを取り除きます。2番目の部品内のVoronoiサイトのみが別々の部分として結果します。他のすべてのサイトは、1つの部品に結合されます。

Roblox Studioロゴの形をしたステンシル
部品(ダークグレー)とステンシル部品
ステンシルの形をした破片の結果
フラグメントスクリプト結果

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("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

以下のスクリプトは、用途が非常に限定されていますが、GeometryService:FragmentAsync()から返されたインデックスデータの力を示しています。

例えば、多くの場所には、結合されない複数のブロック部品からなる建物があります。手榴弾、大砲、またはハンマーがそれに損傷を与えると、全ての壁部品が破片化される必要があります。このスクリプトは、周囲の部品をフラグメント化し、異なる部品の断片を結合して継ぎ目が完全に隠れるように編集します。

これは複数のAsync操作を含むため、スレッジハンマーのツールなどのユーザー入力への即応としてはゲーム内での使用には適さないかもしれません。

ブロックの一列
ブロックの一列
破片化された一列のブロック
各ピースは複数の入力部品に由来するかもしれません

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("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("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

以下のスクリプトは、GeometryService:GenerateFragmentSites()のほぼ同じLuauの代替です。GeometryService:GenerateFragmentSites()と同様の動作を望むが、わずかに変更を加えたい場合は、これを出発点として使用できます。

これは揺らした点のグリッドを使用し、破片の領域がよく振る舞うことを保証します。完全にランダムな点とは異なります。


local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- これを使って揺らしの量を調整します
if (origin and not radius) or (radius and not origin) then
warn("原点と半径の両方を提供するか、どちらも提供する必要があります。")
return {}
end
local isLocalized = (radius ~= nil) -- localizedは、部品全体が破片化されるのではなく、部分的に破片化されることを意味します。
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

制約を保持する

入力部品に保持したい制約やアタッチメントがある場合、それらを結果部品に転送することができます。どの出力部品に制約を取り付けるべきかを把握するのは面倒なので、GeometryService:CalculateConstraintsToPreserve()を使用して推奨テーブルを生成し、それをループして適用することをお勧めします。

以下のコードサンプルは、引き算操作を実行し、その結果の部品を再親にし再配置するループを行い、元の部品を破棄する前に保持または破棄するべき制約やアタッチメントのテーブルを計算します。


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,
}
-- 非同期なのでpcall()内で引き算操作を実行します
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- 結果の部品を再親にし、再配置するためのループ
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- どの制約/アタッチメントを保持または破棄するかを計算
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- 推奨テーブルに基づいて制約/アタッチメントを保持します
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
-- 元の部品を破壊します
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

動作の詳細

  • 操作が成功した場合、元の部品はそのまま保持され、返された部品には設定されたParentがありません。ほとんどのケースでは、返された部品を主部品と同じ場所に親付けし、その後Destroy()を呼び出して元の部品を破壊することをお勧めします。

  • メイン部品が操作の計算中に移動している場合は、返された部品を更新されたCFrameに設定できます。返された部品はメイン部品と同じ座標空間にあるためです。

  • メイン部品のプロパティ、属性、タグ、子を保持しつつ、操作のジオメトリを利用できるようにするために、インスタンスのメッシュデータを置き換える関数があります。これにより、元のPartOperationを別のもので完全に置き換えることによる潜在的な「ちらつき」を回避できます。

  • クライアントでこれらの関数を呼び出すことは可能ですが、いくつかの制限があります。まず、クライアントで作成されたオブジェクトで行う必要があります。第二に、クライアントからサーバーへの複製はありません。

ソリッドモデリング結果に関する考慮事項

色とUVs

ソリッドモデリング後の結果の部品の色は、面の色と部品のColorの2つの場所から来ます。

  • 結果がPartOperationの場合、Studioで選択した最初の部品のColorを持ちますが、Studioはデフォルトで面の色を使用し、各面を操作前と同じ色に保ちます。StudioでそのUsePartColorプロパティを有効にすると、この動作を上書きして結果全体を単一の色にすることができます。
  • 結果がMeshPartの場合、そのColorは白くなり、面の色は常に透過します。結果の部品の色合いを変更するには、Colorを変更しますが、面の色と混ざり合います(乗算されます)。これにより、結果が着色されるが面の色を完全に上書きするわけではありません。出力の色を完全に制御したい場合は、入力を最初に白にすることをお勧めします。

UVは、結果のタイプに応じて異なる方法で処理されます:

  • PartOperationsは常にボックスマッピングされたUVを持ちます。これは、各面に対して素材/テクスチャ/デカールが1つの方向(-x、+x、-y、+y、-z、+zのいずれか)から適用されることを意味します。これにより、テクスチャが引き伸ばされることがあります。
  • MeshPartsはボックスマッピングされていません。主部品のメッシュのUVが使用されます。ロブロックスは現在、多素材サポートを持っていないため、他の部品から派生した面のUVは(0, 0)のUVを与えられます。最高の結果を得るために、テクスチャのピクセル(0, 0)が適切な色を持つことを確認してください。

スムージング角

ソリッドモデリングされた部品のSmoothingAngleプロパティは、同じ色の隣接する表面間の角度を滑らかにします。値が高いほど、より滑らかな外観になり、値が低いほど、鋭いエッジが多い粗い外観になります。

通常30度から70度の値が良好な結果を生むことが多いですが、90度から180度の値はお勧めしません。これは、鋭角のある和や交差で「影」効果を引き起こす可能性があります。

部品の簡素化

ソリッドモデリングの操作により、三角形が20,000を超える部品が生成される場合、それらは20,000に簡素化されます。それができない場合、通常は非重なり合うコンポーネントが数千個ある場合、操作はエラーになります。

良好な状態のMeshPart
簡素化前
メッシュの質が明らかに低下したMeshPart
簡素化後
©2026 Roblox Corporation。Roblox(ロブロックス)、RobloxロゴおよびPowering Imaginationは、米国並びにその他の国における登録商標および非登録商標です。