👌

二つのCubeの重なり部分を削除したUSD primの生成方法

に公開

今回はふと気になったので、OpenUSDで二つのCubeを生成してその重なっているところを削除したような図形を作る方法を調べてみました。

※ ChatGPTに作り方を聞きました。いつもお世話になっております。

今回やってみたかったこと

もう少し厳密に今回やってみたかったことを文字起こしすると以下のようになります。

  • 二つのCubeをA、Bとする
  • AとBには重なる部分があるとする
  • AからAとBが重なっている場所を削除する

早速ChatGPTに聞いてみた

質問内容としては以下のように問い合わせました。

pxr.UsdGeom.Cubeで作成したキューブAとBがあるとします。例えばキューブAとキューブBが重なっている場所をキューブAから削除するようなPythonコードを書いてください

今回はPythonのusd-coreを利用したく、実際にPythonから利用するときはpxr.UsdGeom.Cubeを利用してCubeを生成するため、それを使うように聞いてみました。実際にいただいた回答は以下になります。

ChatGPTの回答

とても良い質問です。
USD(Pixar Universal Scene Description)で pxr.UsdGeom.Cube のような**ブーリアン演算(差分、和、交差など)**を行いたい場合、USD単体にはそれを直接行うAPIが存在しません。
したがって「キューブAからキューブBと重なっている部分を削除する」には、
実際の形状メッシュを取得してブーリアン演算を行い、その結果を新しいメッシュとしてUSDに書き戻す必要があります。

以下はその具体的な例です👇


🧩 概要

  • UsdGeom.Cube A, B を読み込む
  • pxr.UsdGeom.Mesh の形状データを numpy 配列として抽出
  • trimesh ライブラリを使ってブーリアン演算(difference)を実行
  • 結果を新たな UsdGeom.Mesh として保存

🧱 サンプルコード

from pxr import Usd, UsdGeom, Gf
import trimesh
import numpy as np

# === USD ステージ作成 ===
stage = Usd.Stage.CreateNew("cube_diff.usda")

# キューブA, B作成
cubeA = UsdGeom.Cube.Define(stage, "/CubeA")
cubeA.AddTranslateOp().Set(Gf.Vec3f(0, 0, 0))
cubeA.GetSizeAttr().Set(2.0)

cubeB = UsdGeom.Cube.Define(stage, "/CubeB")
cubeB.AddTranslateOp().Set(Gf.Vec3f(0.5, 0.5, 0.5))
cubeB.GetSizeAttr().Set(2.0)

stage.GetRootLayer().Save()

# === trimesh用のメッシュ生成関数 ===
def usd_cube_to_trimesh(size=2.0, translation=(0, 0, 0)):
    mesh = trimesh.creation.box(extents=(size, size, size))
    mesh.apply_translation(translation)
    return mesh

# === UsdGeom.Cube から trimesh オブジェクト作成 ===
A = usd_cube_to_trimesh(2.0, (0, 0, 0))
B = usd_cube_to_trimesh(2.0, (0.5, 0.5, 0.5))

# === 差分 (A - B) を計算 ===
diff_mesh = A.difference(B)

# === 結果を USD Mesh として出力 ===
out_stage = Usd.Stage.CreateNew("cube_diff_result.usda")
mesh_prim = UsdGeom.Mesh.Define(out_stage, "/DiffMesh")

# 頂点・面データ設定
vertices = [Gf.Vec3f(*v) for v in diff_mesh.vertices]
faceVertexCounts = [len(f) for f in diff_mesh.faces]
faceVertexIndices = [int(i) for f in diff_mesh.faces for i in f]

mesh_prim.GetPointsAttr().Set(vertices)
mesh_prim.GetFaceVertexCountsAttr().Set(faceVertexCounts)
mesh_prim.GetFaceVertexIndicesAttr().Set(faceVertexIndices)

out_stage.GetRootLayer().Save()

print("✅ cube_diff_result.usda に結果を保存しました。")

💡 補足

  • trimesh は純粋Pythonで動作するメッシュライブラリで、pip install trimesh で導入可能です。
  • 上記の difference はブーリアン演算の一種で、intersection()union() も使えます。
  • UsdGeom.Cube は単純なプロシージャル定義なので、ブーリアン演算のためには一度ポリゴンメッシュに変換(ここでは trimesh.creation.box)しています。
  • 生成された cube_diff_result.usda を USDView で開くと、キューブAからBが重なった部分が削除された形状が確認できます。

希望があれば、上記のコードを「既存の .usd ファイル内の CubeA と CubeB を読み取って差分を作る」バージョンにも書き換えられます。
そちらにしますか?

コードをみてみる

今回提案されたコードは以下になります(問題なく動きます)。

from pxr import Usd, UsdGeom, Gf
import trimesh
import numpy as np

# === USD ステージ作成 ===
stage = Usd.Stage.CreateNew("cube_diff.usda")

# キューブA, B作成
cubeA = UsdGeom.Cube.Define(stage, "/CubeA")
cubeA.AddTranslateOp().Set(Gf.Vec3f(0, 0, 0))
cubeA.GetSizeAttr().Set(2.0)

cubeB = UsdGeom.Cube.Define(stage, "/CubeB")
cubeB.AddTranslateOp().Set(Gf.Vec3f(0.5, 0.5, 0.5))
cubeB.GetSizeAttr().Set(2.0)

stage.GetRootLayer().Save()

# === trimesh用のメッシュ生成関数 ===
def usd_cube_to_trimesh(size=2.0, translation=(0, 0, 0)):
    mesh = trimesh.creation.box(extents=(size, size, size))
    mesh.apply_translation(translation)
    return mesh

# === UsdGeom.Cube から trimesh オブジェクト作成 ===
A = usd_cube_to_trimesh(2.0, (0, 0, 0))
B = usd_cube_to_trimesh(2.0, (0.5, 0.5, 0.5))

# === 差分 (A - B) を計算 ===
diff_mesh = A.difference(B)

# === 結果を USD Mesh として出力 ===
out_stage = Usd.Stage.CreateNew("cube_diff_result.usda")
mesh_prim = UsdGeom.Mesh.Define(out_stage, "/DiffMesh")

# 頂点・面データ設定
vertices = [Gf.Vec3f(*v) for v in diff_mesh.vertices]
faceVertexCounts = [len(f) for f in diff_mesh.faces]
faceVertexIndices = [int(i) for f in diff_mesh.faces for i in f]

mesh_prim.GetPointsAttr().Set(vertices)
mesh_prim.GetFaceVertexCountsAttr().Set(faceVertexCounts)
mesh_prim.GetFaceVertexIndicesAttr().Set(faceVertexIndices)

out_stage.GetRootLayer().Save()

print("✅ cube_diff_result.usda に結果を保存しました。")

コードを見ると二段階に分かれていて、生成するAとBのCubeをそのまま保存するもの(cube_diff.usda)と、メッシュを作成して差分を反映した構成を作るもの(cube_diff_result.usda)になります。ChatGPTの返答にもありましたが、usd-core自体がデフォルトで差分を計算する仕組みを提供していないと言うことで(これはハルシネーションかもしれないのでちゃんと調べる必要はありますが今回は省略しています)、やりたいことをするにはメッシュの差分を計算して頂点や面の情報を設定する必要があるとのことでに段階に分かれているようです。つまり、

  • 一段階目はあくまでどのようなCubeを想定しているかを可視化する
  • 二段階目はCube上のメッシュを再現して差分を計算

しています。

Cubeの定義

Cubeの定義は以下のようにpxr.UsdGeomを利用するとできます。

from pxr import Usd, UsdGeom, Gf
import trimesh
import numpy as np

# === USD ステージ作成 ===
stage = Usd.Stage.CreateNew("cube_diff.usda")

# キューブA, B作成
cubeA = UsdGeom.Cube.Define(stage, "/CubeA")
cubeA.AddTranslateOp().Set(Gf.Vec3f(0, 0, 0))
cubeA.GetSizeAttr().Set(2.0)

cubeB = UsdGeom.Cube.Define(stage, "/CubeB")
cubeB.AddTranslateOp().Set(Gf.Vec3f(0.5, 0.5, 0.5))
cubeB.GetSizeAttr().Set(2.0)

stage.GetRootLayer().Save()

カラーなしにはなりますが、可視化すると以下のように二つのCubeが重なっています。

差分の計算

それでは差分を計算しているところを見てみましょう。

# === trimesh用のメッシュ生成関数 ===
def usd_cube_to_trimesh(size=2.0, translation=(0, 0, 0)):
    mesh = trimesh.creation.box(extents=(size, size, size))
    mesh.apply_translation(translation)
    return mesh

# === UsdGeom.Cube から trimesh オブジェクト作成 ===
A = usd_cube_to_trimesh(2.0, (0, 0, 0))
B = usd_cube_to_trimesh(2.0, (0.5, 0.5, 0.5))

# === 差分 (A - B) を計算 ===
diff_mesh = A.difference(B)

# === 結果を USD Mesh として出力 ===
out_stage = Usd.Stage.CreateNew("cube_diff_result.usda")
mesh_prim = UsdGeom.Mesh.Define(out_stage, "/DiffMesh")

# 頂点・面データ設定
vertices = [Gf.Vec3f(*v) for v in diff_mesh.vertices]
faceVertexCounts = [len(f) for f in diff_mesh.faces]
faceVertexIndices = [int(i) for f in diff_mesh.faces for i in f]

mesh_prim.GetPointsAttr().Set(vertices)
mesh_prim.GetFaceVertexCountsAttr().Set(faceVertexCounts)
mesh_prim.GetFaceVertexIndicesAttr().Set(faceVertexIndices)

out_stage.GetRootLayer().Save()

trimeshはメッシュを計算できるライブラリであり、今回はboxメッシュおよびその位置(translation)を指定してAとBのメッシュを作成しています。そしてdifference関数を利用してメッシュの差分を計算しています。

# === trimesh用のメッシュ生成関数 ===
def usd_cube_to_trimesh(size=2.0, translation=(0, 0, 0)):
    mesh = trimesh.creation.box(extents=(size, size, size))
    mesh.apply_translation(translation)
    return mesh

# === UsdGeom.Cube から trimesh オブジェクト作成 ===
A = usd_cube_to_trimesh(2.0, (0, 0, 0))
B = usd_cube_to_trimesh(2.0, (0.5, 0.5, 0.5))

# === 差分 (A - B) を計算 ===
diff_mesh = A.difference(B)

差分の計算が終わったら、頂点座標などを計算しています。

  • vertices: 全ての頂点座標
  • faceVertexCounts: ポリゴン(メッシュを構成する面)の頂点の数(通常のCubeに準ずるものであれば3になりポリゴンは三角形であるとのこと)
  • faceVertexIndices: 全ポリゴンを形成する頂点のインデックスを一次元リストとして保持
vertices = [Gf.Vec3f(*v) for v in diff_mesh.vertices]
faceVertexCounts = [len(f) for f in diff_mesh.faces]
faceVertexIndices = [int(i) for f in diff_mesh.faces for i in f]

ポリゴンの情報が生成できたらあとはメッシュprimに対してその情報を設定するだけになります。

mesh_prim.GetPointsAttr().Set(vertices)
mesh_prim.GetFaceVertexCountsAttr().Set(faceVertexCounts)
mesh_prim.GetFaceVertexIndicesAttr().Set(faceVertexIndices)

それではこちらのコードを実行してみます。結果としては以下のようなAとBの重複箇所が削除された図形が得られました。見た目的にだいぶわかりにくいので2枚載せておきます!


なお、実際に生成されたusdaファイルは以下になります。ポリゴン情報が大量に羅列されていてこれだけみてもなんのことやらと言う感じですw

#usda 1.0

def Mesh "DiffMesh"
{
    int[] faceVertexCounts = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
    int[] faceVertexIndices = [0, 1, 2, 3, 0, 2, 2, 1, 4, 4, 3, 2, 5, 6, 1, 5, 1, 0, 7, 0, 3, 8, 5, 0, 0, 7, 9, 0, 9, 8, 1, 6, 10, 1, 10, 4, 11, 3, 4, 12, 11, 4, 4, 10, 13, 4, 13, 12, 14, 7, 3, 3, 11, 15, 3, 15, 16, 17, 14, 3, 3, 16, 18, 3, 18, 17, 7, 14, 9, 15, 11, 12, 5, 19, 6, 17, 18, 19, 19, 5, 8, 19, 8, 17, 19, 10, 6, 13, 10, 19, 19, 18, 16, 19, 16, 13, 8, 9, 14, 8, 14, 17, 15, 12, 13, 15, 13, 16]
    point3f[] points = [(-1, -1, 1), (-1, 1, 1), (-1, -1, -1), (1, -1, -1), (-1, 1, -1), (-0.5, -0.5, 1), (-0.5, 1, 1), (1, -1, 1), (0, -0.5, 1), (1, -0.5, 1), (-0.5, 1, -0.5), (1, 1, -1), (1, 1, -0.5), (0, 1, -0.5), (1, -0.5, 0.5), (1, 0.5, -0.5), (1, 0, -0.5), (1, -0.5, 0), (1, -0.5, -0.5), (-0.5, -0.5, -0.5)]
}

まとめ

今回は二つのCubeの重なりを取り除いた複雑な形状の図形をusdファイル上に展開する方法を調べてみました。実際に利用するときはBlenderなどの3Dデザインソフトで生成することがほとんどであり手動で今回したような作業をコーディングすることは基本的にないと思いますが、もしやるとするとメッシュを生成してからポリゴン生成して、、、のように手続きが多くて大変なことが理解できました。改めて3Dデザイナーの方はすごいなと思ったという乾燥で今回は締めにしようと思います。

Discussion