TouchDesignerでHoudiniから書き出したVAT3.0を使用する(Soft-Body編)

# 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

# はじめに
昨年のTouchDesignerアドベントカレンダーで[TouchDesignerでHoudiniから書き出したVATを使用する](https://qiita.com/yasuhiro0122/items/1222b61549891af6c2f4)という記事を書いたのですが、VAT3.0へのアップデートにより仕様が変わったため、複数回に分けて記事を書き直していこうと思います。

今回はSoft-Bodyについて解説します。

## サンプルプロジェクト
以下のGithubリポジトリをダウンロードしてください。
https://github.com/yasuhirohoshino/TouchDesigner_VAT3.0


## 動作環境
- Windows 10 21H1
- Houdini 19.0.445
    - SideFX Labsインストール済み
- TouchDesigner 2021.38110 (Commercial License)

# Houdini
![houdini_overview.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/8cb3370b-b804-b262-f271-f04ef4cc3990.jpeg)
[サンプルプロジェクト](https://github.com/yasuhirohoshino/TouchDesigner_VAT3.0)の`SoftBody/SoftBody_Simple.hiplc`を参照してください。
基本的には[以前の記事](https://qiita.com/yasuhiro0122/items/1222b61549891af6c2f4)と同じ、人間の3Dモデルにmixamoで歩行アニメーションをつけたものです。

## Labs Vertex Animation Texturesの設定
![labs_VAT.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/00a6b4c9-0015-5030-f8c0-4a436b0ec2a1.jpeg)

プロジェクトの`/out`ネットワークに`Labs Vertex Animation Textures`を作成します。

![VAT_main.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/95070eb2-f317-d03c-a42a-155c06254973.jpeg)

1. `Mode/Target Engine`を`Soft-Body Deformation(Soft)`,`Custom`に変更します。
1. `Start/End`で書き出したいアニメーションの開始フレームと終了フレームを設定します。
1. `Input Geometry`に書き出したいジオメトリのパスを設定します。

### Settings

![VAT_settings.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/e3c6a342-ddfe-cbd1-e9ae-1e33f0078468.jpeg)

1. `Texture Format`が`HDR(EXR/TIFF as RGBA 16/32 in Engine)`, `.exr`になっていることを確認します。
1. 必要であれば`Target Texture Width`を適宜変更します。(必ずしもSOPの頂点数以上に設定する必要はありません)

### Export

![VAT_export.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/bd450242-297c-62f8-04da-3a789c61d7c9.jpeg)

1. 必要であれば`Export Path`で書き出し先のフォルダ名を設定します。
1. `Include`に、書き出したいデータがすべて含まれているか確認します。
    1. もしColorデータが必要なければ、`Geometry, Position(s), Rotation`に変更してColor以外を書き出すことなどもできます。
    1. `Decide Individually`を選択することで、書き出しデータの個別選択や、書き出しファイル名を個別に設定できます。

### Target Engine

![VAT_targetEngine.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/b1f4fd26-8de3-a3dd-ffc8-dc143c3fe99b.jpeg)

1. `Coordinate System`を`Y-X-Z Clockwise (Right-Handed Y-Up)`に変更します。
1. `1 Metre in Engine Units`を`1`に設定します。

全ての設定が終わったら、`Render All` ボタンを押して書き出します。

## 書き出されたファイルの解説
以前は法線のデータを格納したテクスチャが書き出されていましたが、VAT3.0では代わりに、法線・接線・従法線の回転を表すクォータニオンのテクスチャが書き出されるようになりました。

|  ファイル名  |  説明  |
| ---- | ---- |
|  geo/[AssetName]_mesh.fbx  |  VATテクスチャ参照用のUVが追加されたFBX  |
|  tex/[AssetName]_pos.exr  |  最初のフレームからの頂点位置の変化量を格納したテクスチャ  |
|  tex/[AssetName]_rot.exr  |  各フレームでの法線・接線・従法線の回転を格納したテクスチャ  |
|  tex/[AssetName]_col.exr  |  各フレームでの色情報のテクスチャ  |

# TouchDesigner

## ノーマルマップを使用しない場合

![TD_simple.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/d2d76507-1aae-800c-19d3-01f4a251cfb2.jpeg)

[サンプルプロジェクト](https://github.com/yasuhirohoshino/TouchDesigner_VAT3.0)の`VAT3_0.toe`内の、`/VAT_SoftBody_Simple`を参照してください。

### GLSL MATのVertex Shader
Phong MATをGLSL MATに変換し、Vertex Shaderのみ編集しました。
フレーム補完の処理を入れているので、コードが少し長くなっています。

なお、書き出したアニメーションの総フレーム数を、Uniform変数としてShaderに渡す必要があります。

```glsl:phong_VAT_Soft_vert
// 頂点位置の変化量テクスチャ
uniform sampler2D VAT_Pos;
// 法線のテクスチャ
uniform sampler2D VAT_Rot;
// 色のテクスチャ
uniform sampler2D VAT_Col;

// アニメーションのフレーム
uniform float currentFrame;
// アニメーションの総フレーム数
uniform int numOfFrames;
// アニメーションがループするかどうか
uniform bool loop;

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

// Positionを取得
vec3 getVATPosition(vec3 P, vec2 prevUV, vec2 nextUV, float blend) {
	vec3 prevPos = P + texture(VAT_Pos, prevUV).xyz;
	vec3 nextPos = P + texture(VAT_Pos, nextUV).xyz;
    return mix(prevPos, nextPos, vec3(blend));
}

// Colorを取得
vec4 getVATColor(vec2 prevUV, vec2 nextUV, float blend) {
	vec4 prevCol = texture(VAT_Col, prevUV);
	vec4 nextCol = texture(VAT_Col, nextUV);
    return mix(prevCol, nextCol, vec4(blend));
}

// Normalを取得
vec3 getVATNormal(vec2 prevUV, vec2 nextUV, float blend) {
	vec3 n = vec3(0.0, 1.0, 0.0);
    vec4 prevQuat = texture(VAT_Rot, prevUV).xyzw;
	vec3 prevNormal = rotateVectorByQuatenion(n, prevQuat);
	vec4 nextQuat = texture(VAT_Rot, nextUV).xyzw;
	vec3 nextNormal = rotateVectorByQuatenion(n, nextQuat);
    return mix(prevNormal, nextNormal, vec3(blend));
}

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

void main()
{	
	vec3 position = vec3(0.0);
	vec4 color = vec4(0.0);
	vec3 normal = vec3(0.0, 1.0, 0.0);
	
	// 頂点がVAT参照用のUVを持っている場合
	if(uv[1].x != 0.0 && uv[1].y != 0.0) {
		// ピクセルをサンプリングする間隔を計算
		float stride = 1.0 / numOfFrames;
		
		// 現在のフレームを取得
		float frame = currentFrame;
		
		// VATテクスチャ読み込み用のUVを更新
		float u = uv[1].x;
		float prevV = uv[1].y - (floor(frame) / numOfFrames);
		float nextV = mod(prevV - stride, 1.0);
		
		// アニメーションがループしない場合、最終フレームの補完を切る
		if(loop == false && frame >= numOfFrames - 1) {
			nextV = prevV;
		}
		
		vec2 prevUV = vec2(u, prevV);
		vec2 nextUV = vec2(u, nextV);
		float frameBlend = fract(frame);
	
		// 頂点の位置の変化量を読み込み、初期位置に加算
		position = getVATPosition(P, prevUV, nextUV, frameBlend);
		// 色を読み込み
		color = getVATColor(prevUV, nextUV, frameBlend);
		// 法線を読み込み
		normal = getVATNormal(prevUV, nextUV, frameBlend);
	}
	
	// 頂点座標を設定
	vec4 worldSpacePos = TDDeform(position);
	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 * Cd);
	
	// 法線を設定
	oVert.worldSpaceNorm.xyz = normalize(TDDeformNorm(normal));

#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}
```

### 実行結果

![simple.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/d6081ff3-aa6d-9593-9fa6-559b0ebb5c32.png)

## ノーマルマップを使用する場合

![TD_normalmap.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/364cff0f-6a80-6e77-8ff1-b4c305111837.jpeg)

[サンプルプロジェクト](https://github.com/yasuhirohoshino/TouchDesigner_VAT3.0)の`VAT3_0.toe`内の、`VAT_SoftBody_NormalMap`を参照してください。

### GLSL MATのVertex Shader

rotテクスチャに格納されたクォータニオンを使い、Normalと同じようにTangent`vec3(1.0, 0.0, 0.0)`を回転し、NormalとTangentからTBN Matrixを作成することでノーマルマップに対応できます。

``` glsl:phong_VAT_Soft_NormalMap_vert
// 頂点位置の変化量テクスチャ
uniform sampler2D VAT_Pos;
// 法線のテクスチャ
uniform sampler2D VAT_Rot;
// 色のテクスチャ
uniform sampler2D VAT_Col;

// アニメーションのフレーム
uniform float currentFrame;
// アニメーションの総フレーム数
uniform int numOfFrames;
// アニメーションがループするかどうか
uniform bool loop;

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

// Positionを取得
vec3 getVATPosition(vec3 P, vec2 prevUV, vec2 nextUV, float blend) {
	vec3 prevPos = P + texture(VAT_Pos, prevUV).xyz;
	vec3 nextPos = P + texture(VAT_Pos, nextUV).xyz;
    return mix(prevPos, nextPos, vec3(blend));
}

// Colorを取得
vec4 getVATColor(vec2 prevUV, vec2 nextUV, float blend) {
	vec4 prevCol = texture(VAT_Col, prevUV);
	vec4 nextCol = texture(VAT_Col, nextUV);
    return mix(prevCol, nextCol, vec4(blend));
}

// Normalを取得
vec3 getVATNormal(vec2 prevUV, vec2 nextUV, float blend) {
	vec3 n = vec3(0.0, 1.0, 0.0);
    vec4 prevQuat = texture(VAT_Rot, prevUV).xyzw;
	vec3 prevNormal = rotateVectorByQuatenion(n, prevQuat);
	vec4 nextQuat = texture(VAT_Rot, nextUV).xyzw;
	vec3 nextNormal = rotateVectorByQuatenion(n, nextQuat);
    return mix(prevNormal, nextNormal, vec3(blend));
}

// Tangentを取得
vec3 getVATTangent(vec2 prevUV, vec2 nextUV, float blend) {
	vec3 t = vec3(1.0, 0.0, 0.0);
    vec4 prevQuat = texture(VAT_Rot, prevUV).xyzw;
	vec3 prevTangent = rotateVectorByQuatenion(t, prevQuat);
	vec4 nextQuat = texture(VAT_Rot, nextUV).xyzw;
	vec3 nextTangent = rotateVectorByQuatenion(t, nextQuat);
    return mix(prevTangent, nextTangent, vec3(blend));
}

out Vertex
{
	vec4 color;
	mat3 tangentToWorld;
	vec3 worldSpacePos;
	vec2 texCoord0;
	flat int cameraIndex;
} oVert;

void main()
{
	vec3 position = vec3(0.0);
	vec4 color = vec4(0.0);
	vec3 normal = vec3(0.0, 1.0, 0.0);
	vec3 tangent = vec3(1.0, 0.0, 0.0);
	
	// 頂点がVAT参照用のUVを持っている場合
	if(uv[1].x != 0.0 && uv[1].y != 0.0) {
		// ピクセルをサンプリングする間隔を計算
		float stride = 1.0 / numOfFrames;
		
		// 現在のフレームを取得
		float frame = currentFrame;
		
		// VATテクスチャ読み込み用のUVを更新
		float u = uv[1].x;
		float prevV = uv[1].y - (floor(frame) / numOfFrames);
		float nextV = mod(prevV - stride, 1.0);
		
		// アニメーションがループしない場合、最終フレームの補完を切る
		if(loop == false && frame >= numOfFrames - 1) {
			nextV = prevV;
		}
		
		vec2 prevUV = vec2(u, prevV);
		vec2 nextUV = vec2(u, nextV);
		float frameBlend = fract(frame);
	
		// 頂点の位置の変化量を読み込み、初期位置に加算
		position = getVATPosition(P, prevUV, nextUV, frameBlend);
		// 色を読み込み
		color = getVATColor(prevUV, nextUV, frameBlend);
		// 法線を読み込み
		normal = getVATNormal(prevUV, nextUV, frameBlend);
		// 接線を読み込み
		tangent = getVATTangent(prevUV, nextUV, frameBlend);
	}
	
	// 頂点座標を設定
	vec4 worldSpacePos = TDDeform(position);
	vec3 uvUnwrapCoord = TDInstanceTexCoord(TDUVUnwrapCoord());
	gl_Position = TDWorldToProj(worldSpacePos, uvUnwrapCoord);
	
	vec3 texcoord = TDInstanceTexCoord(uv[0]);
	oVert.texCoord0.st = texcoord.st;

#ifndef TD_PICKING_ACTIVE

	int cameraIndex = TDCameraIndex();
	oVert.cameraIndex = cameraIndex;
	// 頂点座標を設定
	oVert.worldSpacePos.xyz = worldSpacePos.xyz;
	// 色を設定
	oVert.color = TDInstanceColor(color * Cd);
	
	// 法線を設定
	vec3 worldSpaceNorm = normalize(TDDeformNorm(normal));
	// 接線を設定
	vec3 worldSpaceTangent = normalize(TDDeformNorm(tangent.xyz));
	// TBNMatrixを作成
	oVert.tangentToWorld = TDCreateTBNMatrix(worldSpaceNorm, worldSpaceTangent, 1);![TD_simple.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/ee5569f9-89f9-ea3a-d49e-ad5715d0fd16.jpeg)


#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}
```

### 実行結果

![normalmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/b8435189-c76c-74a9-776e-3488e3bbc6cd.png)