UsdSkelについて
原則 RIG は持てない USD ですが、Joint と Skinbind に関しては対応していてキャラクターなどは Skeleton を使用して作成することができます。
現状だと、Omniverse の MayaExport または SOLARIS の群衆の Agent のいずれかのみできますが 今回は Omniverse で出力したデータを確認しながら USD における Joint のデータ構造を見ていこうと思います。
UsdSkel の基本構造
まず、USD で Joint と SkinBind を使用する場合は
- UsdSkelRoot
- UsdSkeleton
- UsdSKelAnimation
この3つの構造を利用します。
基本はこのような構造になります。 (Animation に関してはこことは限らない)
UsdSkelRoot
UsdSKelRoot は、Joint を使用する場合に Skeleton の親ノードとして作成します。 SkelRoot は Boundabule(BoundingBox を持つ)Prim で、これ以下に Skel が定義されている Prim があると識別するための Prim です。 また、SkinBind されたモデルを配置する時などは この SkelRoot を動かすことで、シーン内に配置することができます。
UsdSkelSkeleton
次が Skeleton。 この Skeleton は、Joint のトポロジを定義し、BindPose を保持します。 UsdSkel は、Maya のように Joint ごとに Prim があるわけではなく この SkeletonPrim 以下に Joint の構造を持ちます。
Skeleton には、このように jointsJoint を持ちます。 Joint1 つごとに Index が割り振られ、親子構造は / で表されます。
UsdSkelAnimation
最後の Animation。 UsdSkel の Animation は、この AnimationPrim という形で別途定義されます。 (Skeleton はあくまでも BindPose のみを持つ)
それをふまえて
以上のような Root Skeleton Animation の 3 つで構成された UsdSkel で シーンを構成する場合は、このようになります。
多くの場合は、キャラクターモデルの usd と各 Shot ごとのキャラアニメーションの usd という構成に なります。
上で説明したとおり、UsdSkel は Maya の Joint とは違い、 構造すべてをまとめたものが1つの Prim として表現されます。 なので、Joint の間に Group ノードなどの、Joint 以外のノードが混ざることが 許容されていません。 なので、Maya でモデルを作る段階から Skeleton だけの構造として切り分けておく必要があります。
そして、その SkeletonPrim を動かすための Animation は別 Prim になるので 各 Shot ごとの Animation は 別レイヤー扱いにして、Reference またはサブレイヤーで合成して Skeleton に対して Relationship で Prim を指定すれば モデル部分は共通化した上で、Shot ごとは別のレイヤーでデータを切り分けることができます。
データ構造を理解する
ざっくりと Prim の関係性などはわかりましたが、 ここからは Skel に関係するデータがどのように定義されていているのかを もう少し詳しく確認してみます。
Skeleton と Joint
まず Joint について。
上の Skeleton で書いたとおり、Skeleton は、joints アトリビュートに この Skeleton が持つ Joint のリストを持ちます。
たとえば、このようなキャラクターの Skeleton があった場 合。 この Skeleton は1つの SkeletonPrim になっていて、
このように、Skelton の階層構造のフルパスのリストを持ちます。 この各 Joint がどの位置にあるかは、
BindTransforms アトリビュートに保存されています。 この Transforms は Matrix の配列になっていて、 joints の Index(0:Hips とある場合は :より前の数字が Index)にそれぞれ対応しています。 そのため、 bindTransforms と joints の要素数は同じになります。
Animation
Skeleton は、BindPose のみなのでアニメーションは保持しません。 アニメーションをセットしたい場合は、SkeletonPrim に対して AnimationPrim を Relation で接続します。
skelPrim = skel.GetPrim()
# SkeletonのAnimationはRelationでAnimationPrimが指定されている
animPath = skelPrim.GetRelationship("skel:animationSource").GetTargets()[0]
# Animationの値はVector
print(anim.GetRotationsAttr().Get())
print(anim.GetTranslationsAttr().Get())
print(anim.GetScalesAttr().Get())
Relation 接続先は skel:animationSource なので、 GetRelationship で Relation を取得し、AddTarget で、AnimationPrim の SdfPath を 指定します。
Topology
Joint は、SkeletonPrim では /root/a/b のような / で区切られたパスで記述されます。 この Joint 内の親子階層を検索したり、現在の Joint の Path がどこにあたるのかを 調べたい場合、文字列で検索したりするのはさすがに面倒です。
この Skeleton 内の Joint の構造は UsdSkelTopology を利用することで 把握することができます。
# Skelの構造は Topology を利用すると解析できる
joints = skel.GetJointsAttr().Get()
topology = UsdSkel.Topology(skel.GetJointsAttr().Get())
# Joint数を取得
print(topology.GetNumJoints())
# 引数のIndexがRootかどうか返す
print(topology.IsRoot(0))
# 引数のIndexのParentのIndexを取得する
print(joints[1])
parentIndex = topology.GetParent(1)
print(joints[parentIndex])
Topology に対して、joints のリストを渡すと、 それ以降は ある Joint の親がどれなのか などを、Topology に指定したリストの Index で取得できます。
親 Joint を探したりする場合も、joints の Index の数字でコントロールするというのが 最初はわかりにくいですが Skeleton で Joint をリストとして持ち、親子階層の確認は UsdSkelTopology を 利用するというところを押さえれば、UsdSkeleton の Joint の構造は理解しやすいです。
BindSkin
最後に BindSkin。 USD の場合は、SkinWeight の情報は MeshPrim が持ちます。 その情報が、 primvars:skel:jointIndices と primvars:skel:jointWeights の2つです。
他のアトリビュートとは違い、Weight のアトリビュートは「primvars」になっています。 primvar とは、「プリミティブ変数」の略で プリミティブの表面・体積にわたって値を変化させる(補完する)ものです(例:UV)
つまり、Weight 情報は各 Vertex ごとに持っていますが Vertex と Vertex の間は primvar によって補完されることになります。
jointIndeces と jointWeights は、 Skeleton の Joint 数 × 頂点数分持ちます。 並び順は、Mesh の Vertex の Index 順になっているようで
jointIndices は、Skeleton の Index jointWeights はその頂点の SkeletonIndex の Weight 値を 0-1 で持ちます。
たとえば、ある Mesh に Joint が2つある Skeleton を Bind したとすると 構造的にはこのように Mesh の Vertex 順に Skeleton の数だけ並び、、、というふうになります。 Joint の Index は順不同で、かならず 0 1 2 ... と増えていくわけではありません。 あくまでもある Vertex に対しての joitnWeights との組み合わせになります。
meshPrim = stage.GetPrimAtPath("/World/Root/Geom/pCube1")
bindingAPI = UsdSkel.BindingAPI(meshPrim)
indicesPrimvar = bindingAPI.GetJointIndicesPrimvar() # UsdGeomPrimvar
weightPrimvar = bindingAPI.GetJointWeightsPrimvar() # UsdGeomPrimvar
# Indexの並び順は
# 上の頂点のWeight
print(indicesPrimvar.Get(0)[4:6])
print(weightPrimvar.Get(0)[4:6])
# 下の頂点のWeight
print(indicesPrimvar.Get(0)[0:2])
print(weightPrimvar.Get(0)[0:2])
Bind したり、Bind 情報を取得する場合は、BindingAPI を経由して取得します。 bindingAPI の GetJoitIndicesPrimvar と GetJointWeightsAttr を使用すると 値が取得できるし、CreateJointIndicesPrimvar CreateJointWeightsPrimvar で 値をセットすることもできます。
まとめ
ここまでで、Skeleton - Joint - Animation - SkinBind の関係性が ざっくりと理解できました。
- Joint は SkeletonPrim が持つ
- Animation は Skeleton とは別に AnimationPrim が持つ
- SkinBind は Mesh が持つ
これを踏まえて USD のコンポジションと組み合わせて考えると Skeleton と Mesh は別レイヤーにして 同じキャラでも服や髪型違いがあったときに Skeleton は別レイヤーにしておいて共有し コンポジ ションで合成させたり、 SkinWeight と Skeleton と Mesh を別レイヤーにしておいてコンポジションで組み合わせたり といったことができるようになります。
Animation も、キャラモデルは共有しつつ、Shot ごとの Animation は別レイヤーとして出力 (Omniverse 的には AnimationClip と呼ぶ)そのファイルを Reference して Skeleton の animationSource に接続すれば キャラとモーションを別ファイルにすることで モデルが更新されたときに、Shot のレイヤーのモデルも更新する... みたいなことができるようになります。
欠点として、Joint の階層の間に Joint 以外の階層が入るのは NG なので それを踏まえて構造を考える必要があります。