USDの実践メモ 05 : タイムサンプリング

今回は，時系列データに対応した USD ファイルを作成してみます。

## タイムサンプリング

時系列データを USD ファイルに含めるのは簡単です。

- ヘッダに始点タイムコードと終点タイムコードを定義する
- Primvar データに値をセットするときに，タイムコードで区切って与えるようにする

の 2 点だけです。具体的に見てみます。

## ステージファイルの修正

まず，タイムコードの始点と終点の設定ですが，これはステージファイルの作成プログラムを修正して行います。ここでは 16 ステップからなる時系列データを想定しています。

```Python
from pxr import Usd, UsdGeom, Sdf

def create_stage():
    stage = Usd.Stage.CreateInMemory()
    UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
    UsdGeom.SetStageMetersPerUnit(stage, 0.01)

    # タイムコードの始点と終点の設定
    stage.SetStartTimeCode(0)
    stage.SetEndTimeCode(15)
    
    return stage
```

## メッシュノードの修正

[以前](https://qiita.com/snowxcrash/items/4e6e58e13c04dafc5749)の回で，メッシュの頂点を基準にして表示色を設定する例を挙げました。今回はここに時系列データを与えます。前回のシンプルな平板ではつまらないので，少し複雑なトーラスメッシュでテストしてみます。

### トーラスメッシュ

トーラスメッシュは次式で定義されます。

```math
\begin{align}
x &= (R + r \cos \theta) \cos(\phi) \\
y &= (R + r \cos \theta) \sin(\phi) \\
z &= r \sin \theta \\
n_x &= \cos(\theta) \cos(\phi) \\
n_y &= \cos(\theta) \sin(\phi) \\
n_z &= \sin(\theta)
\end{align}
```
$n$ は法線ベクトルです。このメッシュにおいて頂点は何らかの「値」を持っており，それらは等$r$面に沿ったガウス的な分布であると仮定します。つまり，

```math
v = \exp \left(-\frac{s(\theta, \phi, t)^2}{\sigma^2} \right),　　\sigma は定数
```

を考えます。ただし，$s$ には $\theta$ と $\phi$ による「捻り」が加わっており，「経時変化」もあるとします。

```math
s(\theta, \phi) = \frac{1}{2} \left(1 + \cos \left(m \theta - n \phi + \omega t \right) \right)
```

このようにすると，始点 $t = 0$ では次のような分布になるはずです。ここでは $m, n$はそれぞれ $m = 2, n = 1$ としました。黄色で着色されているところが $v$ の高い領域を表します。

![torus_Q0_611x514.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/03255e72-24b8-b45b-1f50-a54505924d95.png)

コードの中身を具体的に見ていきます。

いま，配列 values に全頂点にわたって時系列データが格納されているとします。格納法は「時間」を外側のループとします。つまり，あるタイムコードにおける全頂点データのあとに次のタイムコードの全頂点データが並んでいるとします。

values をもとに全頂点の色データ（RGB）を作成する例を示します。itertools.batched 関数を利用してタイムコードごとにデータをまとめるようにしています（itertools.batched は Python 3.12 の新機能です）。

```Python
color3 = []
for v in values:
    color3.append((0.5+0.5*v, 0.5+0.25*v, 0.5-0.5*v))    # Yellow/Grey map
c = [batch for batch in itertools.batched(color3, len(vertices))]
```

前回と同様にして，PrimvarAPI を使って表示色を設定しますが，色情報とともにタイムコードも入力するところが異なります。ついでに，前回省略した法線ベクトルや表示範囲も設定しておきました。

```Python
def create_mesh(stage, path):
    mesh = UsdGeom.Mesh.Define(stage, path)

    # Points, FaceVertexCounts, FaceVertexIndices の設定は前回までと同様．
    ...

    # 表示色の設定
    primvar_api = UsdGeom.PrimvarsAPI(mesh)
    primvar = primvar_api.CreatePrimvar('displayColor', Sdf.ValueTypeNames.Color3f, interpolation=UsdGeom.Tokens.varying)
    frame_range = np.arange(stage.GetStartTimeCode(), stage.GetEndTimeCode() + 1, 1, dtype='int32')
    for frame in frame_range:
        time_code = Usd.TimeCode(float(frame))
        primvar.GetAttr().Set(c[frame], time_code)

    # 法線ベクトルの設定
    mesh.CreateNormalsAttr(normals)

    # 表示範囲
    extent = UsdGeom.Boundable(mesh.GetPrim()).ComputeExtent(Usd.TimeCode(0))
    mesh.CreateExtentAttr(extent)
    
    ...

    return mesh
```

以上より，USD ファイルは次のようなものになりました。usdchecker によるチェックもパスしています。

```USD
#usda 1.0
(
    defaultPrim = "World"
    endTimeCode = 15
    metersPerUnit = 0.01
    startTimeCode = 0
    upAxis = "Y"
)

def Xform "World"
{
    def Mesh "Mesh"
    {
        uniform bool doubleSided = 0
        float3[] extent = [(-2.5, -2.5, -0.5), (2.5, 2.5, 0.5)]
        int[] faceVertexCounts = [3, 3, 3, ... ]
        int[] faceVertexIndices = [0, 1, 2, ... ]
        normal3f[] normals = [(1, 0, 0), (0.980785, 0.19509, 0), ... ]
        point3f[] points = [(2.5, 0, 0), (2.45196, 0.487726, 0), ... ]
        color3f[] primvars:displayColor (
            interpolation = "varying"
        )
        color3f[] primvars:displayColor.timeSamples = {
            0: [(0.5, 0.5, 0.5), (0.5, 0.5, 0.5), ... ]
            1: [(0.5, 0.5, 0.5), (0.5, 0.5, 0.5), ... ]
            ...
            15: [(0.5, 0.5, 0.5), (0.5, 0.5, 0.5), ... ]
        }   
        uniform token subdivisionScheme = "none" 
    }
}            
```

```bash
$ usdchecker Mesh.usda 
Success!
```

ビューワで表示すると，着色部分が周回するように変化する様子が確かめられます（上段左から $\omega t = 0, \ \pi/2, \ \pi, \ 3\pi/2$ の図）。

![Q0_611x514_s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/04e09342-4f24-e886-7338-cc8ddccf590d.png)![Q1_611x514_s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/e0aac6ff-ea2f-5841-88cc-bb3d70aa9649.png)
![Q2_611x514_s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/62eb35f2-f965-9a4b-d0b4-ee2dc83f15b5.png)![Q3_611x514_s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/d497b672-3b9c-13d7-bf93-08cbd8a3cb90.png)

## まとめ

時系列データを USD ファイルに含める手法についてまとめました。

## 参考資料

ft-lab (Yutaka Yoshisaka) さん，あんどうめぐみさん による以下のサイトが理解の助けになりました。

https://ft-lab.jp/blog_3dcg/?p=4836

https://fereria.github.io/reincarnation_tech/usd/time_sampling