TouchDesignerでHoudiniから書き出したVATを使用する

# 2023年2月現在最新のVAT3.0の使用方法については以下の記事をご覧ください。

https://yasuhirohoshino.com/posts/20230201_td_houdini_vat_3_soft

https://yasuhirohoshino.com/posts/20230201_td_houdini_vat_3_rigid

https://yasuhirohoshino.com/posts/20230201_td_houdini_vat_3_fluid

https://yasuhirohoshino.com/posts/20230201_td_houdini_vat_3_particles

https://yasuhirohoshino.com/posts/20230201_td_houdini_vat_3_dr_advanced

# はじめに
この記事では、Houdiniから出力したVAT(Vertex Animation Texture)ファイル群をTouchDesignerで読み込み、再生する方法を解説します。

Houdiniの`Labs Vertex Animation Texture`ノードには、Unreal EngineやUnityなど、ゲームエンジン向けに設定のプリセットが用意されていますが、今回はTouchDesigner側のアニメーション再生機能の実装ができるだけシンプルになるように、手動でパラメーターの設定を行い、書き出し作業を行います。
そのため、他のゲームエンジン用に書き出されたVATファイルをそのまま読み込んだり、本記事を元に作成したVATファイルを他の開発環境で読み込んでも、正しく動作しません。

なおTouchDesignerのVertex Shaderの実装方法は、Houdiniから書き出されたUE4のMaterial Blueprintを参考にしています。

## 検証環境
- Windows 10 Pro 1909
- TouchDesigner 2020.27390 (Commercial License)
- Houdini Indie 18.5.408
    - SideFX Labsインストール済み( https://www.sidefx.com/ja/products/sidefx-labs/sidefx-labs-tools/ )

## サンプルプロジェクト
以下のURLからダウンロードしてください。
https://drive.google.com/file/d/1kchKxh3PpYGo8r71aJLTNSbax0fvW5IX/view?usp=sharing

# 実装
Houdiniの`Labs Vertex Animation Textures`ノードの`Method`の設定によって、書き出されるファイルやデータが変わるので、TouchDesigner側の実装方法も変わってきます。
Houdini 18.5では、以下の4つのMethodが使用できます。

|  Method  |  解説  |
| ---- | ---- |
|  Soft (Constant Topology)  |  キャラクターアニメーションなど、各頂点の位置は変わるが頂点数は変わらず、エッジやプリミティブの構成も変化しないアニメーションを書き出す際に選択  |
|  Rigid (Rigid Body Topology)  |  破砕アニメーションなど、変形しない複数のピースのアニメーションを書き出す際に選択  |
|  Fluid (Changing Topology)  |  流体アニメーションなど、頂点の位置も頂点数も変わるアニメーションを書き出す際に選択  |
|  Sprite (Camera Facing Cards)  |  パーティクルアニメーションなどを書き出す際に選択  |

以下、Soft, Rigid, FluidのMethodごとに、書き出し設定とTouchDesignerでの実装方法を解説していきます。

## Soft (Constant Topology)

### Houdiniでアニメーション作成

![vat_Soft_houdini.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/86b7a867-a07c-9d1f-5bce-562fb5046fe8.jpeg)

人間の3Dモデルにmixamoでループする歩行アニメーションをつけました。
フレームによってポイントの色も変化させています。

### Houdiniから書き出し

outに`Labs vertex Animation Textures`ノードを作成します。

![vat_Soft_houdini_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/a53c380c-a8f2-1c25-24ae-a376d0141148.jpeg)

![vat_soft.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/2a18cff7-8718-358d-6f1d-61a50d43ee60.jpeg)

#### パラメーター設定
1. `User interface - Normal`を設定
1. `Start/End`にアニメーションの開始フレーム、終了フレームを設定
1. `Method`を`Soft (Constant Topology)`に設定
1. `SOP Path`で書き出したいSOPを選択
1. `Frame Range`にアニメーションのフレーム数を設定
1. `Normalize data range to 0-1 space`をオフにする
1. 必要であれば、`Target Texture Size`を任意のサイズに変更
1. `Paths/Normal`のチェックボックスをオンにする
1. SOPが色情報を持っている場合、`Paths/Color`のチェックボックスをオンにする
1. `Settings/Position Coord`を`XYZ`, `+X +Y +Z`に変更する
1. `Scale`を`1`に設定する 
1. `Render`を押して書き出し

### 書き出されたファイル群の解説

以下のファイルが出力されます。
`Paths/Project`, `Paths/Component`にチェックを入れ、任意のパスとファイル名で書き出すこともできます。

|  ファイル名  |  解説  |
| ---- | ---- |
|  [Project]/meshes/[Component]_mesh.fbx  |  VATテクスチャ参照用のUVが追加されたFBX  |
|  [Project]/textures/[Component]_pos.exr  |  最初のフレームからの頂点位置の変化量を格納したテクスチャ  |
|  [Project]/textures/[Component]_norm.exr  |  各フレームでの法線を格納したテクスチャ  |
|  [Project]/textures/[Component]_col.exr  |  各フレームでの色情報のテクスチャ  |
|  [Project]/materials/[Component]_data.json  |  アニメーション再生時に使用する、各種パラメーターのJson  |

また、書き出されたFBXには、以下の頂点アトリビュートが追加されています

| SOPアトリビュート名 |  GLSLアトリビュート名  |  解説  |
| ---- | ---- | ---- |
|  uv(3), uv(4)  |  UV[1].xy  |  VATテクスチャ座標  |

### TouchDesignerでアニメーション再生機能の実装

![vat_soft_TD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/4974bd93-ad15-7919-9c44-c6fabb91689f.jpeg)

書き出されたFBXとテクスチャを読み込み、マテリアルを作成します。
サンプルプロジェクト内の`VAT_Soft_Simple`では、`Phong MAT`のVertex Shaderに、アニメーション再生機能を追加しました。
Uniform変数`numOfFrames`には、Houdiniの`Labs vertex Animation Textures`ノードの`Frame Range`の値を渡します。

```
// Phong_VAT_Soft

uniform vec4 uDiffuseColor;
uniform vec4 uAmbientColor;
uniform vec3 uSpecularColor;
uniform float uShininess;
uniform float uShadowStrength;
uniform vec3 uShadowColor;

// 頂点位置の変化量テクスチャ
uniform sampler2D VAT_Pos;
// 法線のテクスチャ
uniform sampler2D VAT_Norm;
// 色のテクスチャ
uniform sampler2D VAT_Col;

// アニメーションの時間(0-1)
uniform float time;
// アニメーションの総フレーム数
uniform int numOfFrames;

out Vertex
{
	vec4 color;
	vec3 worldSpacePos;
	vec3 worldSpaceNorm;
	flat int cameraIndex;
} oVert;

void main()
{
	// 現在のフレームを計算
	float frame = clamp(time, 0.0, 0.99999) * numOfFrames;
	
	// VATテクスチャ読み込み用のUVを作成
	float u = uv[1].x;
	float v = uv[1].y - (floor(frame) / numOfFrames);
	vec2 UV = vec2(u, v);
	
	// 頂点の位置の変化量を読み込み、初期位置に加算
	vec3 newPos = P + texture(VAT_Pos, UV).xyz;
	
	// 色を読み込み
	vec4 newCol = texture(VAT_Col, UV);
	
	// 法線を読み込み
	vec3 newNorm = texture(VAT_Norm, UV).xyz;
	
	
	// Positionを設定
	vec4 worldSpacePos = TDDeform(newPos);
	vec3 uvUnwrapCoord = TDInstanceTexCoord(TDUVUnwrapCoord());
	gl_Position = TDWorldToProj(worldSpacePos, uvUnwrapCoord);

#ifndef TD_PICKING_ACTIVE

	int cameraIndex = TDCameraIndex();
	oVert.cameraIndex = cameraIndex;
	oVert.worldSpacePos.xyz = worldSpacePos.xyz;
	
	// 色を設定
	oVert.color = TDInstanceColor(newCol);
	
	// 法線を設定
	vec3 worldSpaceNorm = normalize(TDDeformNorm(newNorm));
	
	oVert.worldSpaceNorm.xyz = worldSpaceNorm;

#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}

```

### 実行結果

![TDMovieOut.0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/9242fa2a-6a60-ff0a-cfab-93a7022f45d7.gif)

## Rigid (Rigid Body Topology)

### Houdiniでアニメーション作成
![vat_rigid_houdini.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/2537ed6d-58f5-df8c-4936-ad2b5e76f1dd.jpeg)

Rigid Bodiesシェルフ内のRBD Fractured Objectをもとにして、Voronoi Fractureで分割したSOPから破砕アニメーションを作成しました。

### Houdiniから書き出し

![vat_rigid_houdini_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/e1055fed-8128-7f43-d5d6-b47af24b7f1b.jpeg)

#### パラメーター設定
1. `User interface - Normal`を設定
1. `Start/End`にアニメーションの開始フレーム、終了フレームを設定
1. `Method`を`Rigid (Rigid Body Topology)`に設定
1. `SOP Path`で書き出したいSOPを選択
1. `Frame Range`にアニメーションのフレーム数を設定
1. `Normalize data range to 0-1 space`をオフにする
1. 必要であれば、`Target Texture Size`を任意のサイズに変更
1. SOPが色情報を持っている場合、`Paths/Color`をオンにする
1. `Settings/Position Coord`を`XYZ`, `+X +Y +Z`に変更する
1. `Settings/Rotation Coord`を`+X+Y+Z+W`に変更する
1. `Scale`を`1`に設定する 
1. `Render`を押して書き出し

### 書き出されたファイル群の解説

以下のファイルが出力されます

|  ファイル名  |  解説  |
| ---- | ---- |
|  [Project]/meshes/[Component]_mesh.fbx  |  ピースごとの中心点を格納したCdアトリビュート, VATテクスチャ参照用のUVアトリビュートが追加されたFBX  |
|  [Project]/textures/[Component]_pos.exr  |  中心点の位置を格納したテクスチャ  |
|  [Project]/textures/[Component]_rot.exr  |  中心点の回転(クォータニオン)を格納したテクスチャ  |
|  [Project]/textures/[Component]_col.exr  |  色情報のテクスチャ  |
|  [Project]/materials/[Component]_data.json  |  アニメーション再生時に使用する、各種パラメーターのJson  |

書き出されたFBXには、以下の頂点アトリビュートが追加されています

| SOPアトリビュート名 |  GLSLアトリビュート名  |  解説  |
| ---- | ---- | ---- |
|  Cd(0), Cd(1), Cd(2)  |  Cd.xyz  |  ピースごとの中心点の初期位置  |
|  uv(3), uv(4)  |  UV[1].xy  |  VATテクスチャ座標  |

### TouchDesignerでアニメーション再生機能の実装

![vat_rigid_TD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/a0d0a4b1-ef67-44d0-d5a6-2ede66941939.jpeg)

サンプルプロジェクト内の`VAT_Rigid`を参照してください。
注意点としては、Houdiniからの書き出し時に`Normalize data range to 0-1 space`をオフにしていても、FBXのCdアトリビュートに含まれるピースの中心点の位置情報は0-1に正規化されてしまうため、HoudiniのVATノードの`Pivot Min / Max`の値をUniform変数で渡し、シェーダー内で初期フレームの中心点を計算する必要があります。

```

// Phong_VAT_Rigid

uniform vec4 uDiffuseColor;
uniform vec4 uAmbientColor;
uniform vec3 uSpecularColor;
uniform float uShininess;
uniform float uShadowStrength;
uniform vec3 uShadowColor;

// ピースの中心点の位置情報テクスチャ
uniform sampler2D VAT_Pos;
// 回転情報のテクスチャ
uniform sampler2D VAT_Rot;
// 色のテクスチャ
uniform sampler2D VAT_Col;

// アニメーションの時間(0-1)
uniform float time;
// アニメーションの総フレーム数
uniform int numOfFrames;

// 中心点の最小値
uniform float pivot_Min;
// 中心点の最大値
uniform float pivot_Max;

out Vertex
{
	vec4 color;
	vec3 worldSpacePos;
	vec3 worldSpaceNorm;
	flat int cameraIndex;
} oVert;

// ベクトルをクォータニオンで回転
vec3 rotateVectorByQuatenion(vec3 v, vec4 quat) {
	vec3 c1 = cross(quat.xyz, v.xyz);
	vec3 m1 = v.xyz * quat.w;
	vec3 a1 = c1 + m1;
	vec3 c2 = cross(quat.xyz, a1);
	vec3 m2 = c2 * 2.0;
	return m2;
}

void main()
{
	// 現在のフレームを計算
	float frame = clamp(time, 0.0, 0.99999) * numOfFrames;
	
	// VATテクスチャ読み込み用のUVを作成
	float u = uv[1].x;
	float v = uv[1].y - (floor(frame) / numOfFrames);
	vec2 UV = vec2(u, v);
	
	// 初期フレームでの中心点を計算
	vec3 pivot = Cd.rgb;
	pivot *= (pivot_Max - pivot_Min);
	pivot += pivot_Min;
	
	// テクスチャからデータを取得
	vec3 pivot_Pos = texture(VAT_Pos, UV).xyz;
	vec4 pivot_Rot = texture(VAT_Rot, UV).xyzw;
	vec4 color = texture(VAT_Col, UV);
	
	// 現在のフレームの中心点の移動を反映した、頂点の位置を計算
	vec3 newP = P - pivot;
	newP = rotateVectorByQuatenion(newP, pivot_Rot);
	newP -= pivot;
	newP += pivot_Pos;
	newP += P;
	
	// 法線を回転させる
	vec3 newN = rotateVectorByQuatenion(N, pivot_Rot) + N;
	
	// Positionを設定
	vec4 worldSpacePos = TDDeform(newP);
	
	vec3 uvUnwrapCoord = TDInstanceTexCoord(TDUVUnwrapCoord());
	gl_Position = TDWorldToProj(worldSpacePos, uvUnwrapCoord);

#ifndef TD_PICKING_ACTIVE

	int cameraIndex = TDCameraIndex();
	oVert.cameraIndex = cameraIndex;
	oVert.worldSpacePos.xyz = worldSpacePos.xyz;
	
	// 色を設定
	oVert.color = TDInstanceColor(color);
	
	// 法線を設定
	vec3 worldSpaceNorm = normalize(TDDeformNorm(newN));
	
	oVert.worldSpaceNorm.xyz = worldSpaceNorm;

#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}


```

### 実行結果

![rigid.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/3c83ffe9-7c7e-955e-4d29-68f5a7e69e2d.gif)

## Fluid (Changing Topology)

### Houdiniでアニメーション作成

![vat_fluid_houdini.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/97c33fc0-85ab-e166-194b-8c94214c46a6.jpeg)

流体シミュレーションをFLIPで作成しました。

### Houdiniから書き出し

![vat_fluid_houdini_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/b7c9c1a8-f38c-c203-e94c-a70abeb2b22e.jpeg)

#### パラメーター設定
1. `User interface - Normal`を設定
1. `Start/End`にアニメーションの開始フレーム、終了フレームを設定
1. `Method`を`Fluid (Changing Topology)`に設定
1. `SOP Path`で書き出したいSOPを選択
1. `Frame Range`にアニメーションのフレーム数を設定
1. `Normalize data range to 0-1 space`をオフにする
1. `Packed normal into Position Alpha`のチェックボックスをオフにする
1. 必要であれば、`Target Texture Size`を任意のサイズに変更
1. `Target Prim Count`にプリミティブの最大数を設定
1. `Paths/Normal`のチェックボックスをオンにする
1. SOPが色情報を持っている場合、`Paths/Color`のチェックボックスをオンにする
1. `Settings/Position Coord`を`XYZ`, `+X +Y +Z`に変更する
1. `Render`を押して書き出し

### 書き出されたファイル群の解説

以下のファイルが出力されます

|  ファイル名  |  解説  |
| ---- | ---- |
|  [Project]/meshes/[Component]_mesh.fbx  |  VATテクスチャ参照用のUVが追加されたFBX  |
|  [Project]/textures/[Component]_pos.exr  |  頂点の位置を格納したテクスチャ  |
|  [Project]/textures/[Component]_norm.exr  |  法線を格納したテクスチャ  |
|  [Project]/textures/[Component]_col.exr  |  色情報のテクスチャ  |
|  [Project]/materials/[Component]_data.json  |  アニメーション再生時に使用する、各種パラメーターのJson  |

書き出されたFBXには、以下の頂点アトリビュートが追加されています

| SOPアトリビュート名 |  GLSLアトリビュート名  |  解説  |
| ---- | ---- | ---- |
|  uv(0), uv(1)  |  UV[0].xy  |  VATテクスチャ座標  |

### TouchDesignerでアニメーション再生機能の実装

![vat_fluid_TD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/9463750a-ec5f-f678-a9e0-8ee02ccb9d85.jpeg)

サンプルプロジェクト内の`VAT_Fluid`を参照してください。
Softの時と大体同じですが、VATテクスチャの参照に使うUVが、uv[1]ではなくuv[0]になっています。
(なお、サンプルではHoudiniから色情報を書き出してはおらず、Vertex Shader内で単色を設定しています)

```
// Phong_VAT_Fluid

uniform vec4 uDiffuseColor;
uniform vec4 uAmbientColor;
uniform vec3 uSpecularColor;
uniform float uShininess;
uniform float uShadowStrength;
uniform vec3 uShadowColor;

// 頂点位置の変化量テクスチャ
uniform sampler2D VAT_Pos;
// 法線のテクスチャ
uniform sampler2D VAT_Norm;

// アニメーションの時間(0-1)
uniform float time;
// アニメーションの総フレーム数
uniform int numOfFrames;

out Vertex
{
	vec4 color;
	vec3 worldSpacePos;
	vec3 worldSpaceNorm;
	flat int cameraIndex;
} oVert;

void main()
{
	// 現在のフレームを計算
	float frame = clamp(time, 0.0, 0.99999) * numOfFrames;
	
	// VATテクスチャ読み込み用のUVを作成
	float u = uv[0].x;
	float v = uv[0].y - (floor(frame) / numOfFrames);
	vec2 UV = vec2(u, v);
	
	// 頂点の位置を読み込み
	vec3 newPos = texture(VAT_Pos, UV).xyz;
	
	// 法線を読み込み
	vec3 newNorm = texture(VAT_Norm, UV).xyz;
	
	// Positionを設定
	vec4 worldSpacePos = TDDeform(newPos);
	vec3 uvUnwrapCoord = TDInstanceTexCoord(TDUVUnwrapCoord());
	gl_Position = TDWorldToProj(worldSpacePos, uvUnwrapCoord);

#ifndef TD_PICKING_ACTIVE

	int cameraIndex = TDCameraIndex();
	oVert.cameraIndex = cameraIndex;
	oVert.worldSpacePos.xyz = worldSpacePos.xyz;
	
	oVert.color = TDInstanceColor(vec4(0.5, 0.75, 1.0, 1.0));
	
	// 法線を設定
	vec3 worldSpaceNorm = normalize(TDDeformNorm(newNorm));
	
	oVert.worldSpaceNorm.xyz = worldSpaceNorm;

#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}

```

### 実行結果

![fluid.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/94aee4b2-57dc-4cb9-032f-7c22914bc60f.gif)

# おわりに
後日、応用編を書く予定です。

- 法線マップを使用するための方法(Tangentアトリビュートを作成する方法)
- 破砕シミュレーション以外のRigidの使い方
- テクスチャのサイズを減らす方法(法線情報をPositionのアルファに書き込み、TD側で読み出す方法)
- Fluidのアニメーションをきれいにする方法

などのトピックを考えています。