TouchDesignerのVATでNormal Mapを使用する

# 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

# はじめに
<b>この記事は、過去作成した記事([TouchDesignerでHoudiniから書き出したVATを使用する](https://qiita.com/yasuhiro0122/items/1222b61549891af6c2f4))の続きです。</b>
<b>HoudiniからのVATファイル群の書き出し設定などは、上記の記事と同様です。</b>

## 検証環境
- Windows 10 Pro 2004
- TouchDesigner 2020.28110 (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

# 実装

## Soft (Constant Topology)

![soft_Houdini.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/d4483bb8-b07d-e113-0481-630d4dd936ad.jpeg)![soft_substance.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/20e4dc8c-d9b2-d897-c47e-550b675ab797.jpeg)

UV展開した人の3Dモデルに、mixamoでアニメーションをつけました。
Normal MapはSubstance Painterで作成してみました。

### TouchDesignerオペレーター解説

![soft_TD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/4edbc446-f829-045f-6675-dcbea818eba4.jpeg)

サンプルプロジェクト`VATExamples.toe`内の`VAT_Soft_NormalMap`を参照してください。
SOPにTangentアトリビュートを追加するため、以下の手順で操作を行っています。

1. Houdiniから書き出されたFBXをインポート
1. `Attribute Create SOP`を作成し、`FBX COMP`内のSOPをインプットする(サンプルプロジェクトでは`Select SOP`で`FBX COMP`内のSOPを取得しています。)
1. `Attribute Create SOP`のパラメーター`Compute Tangent`を<b>オン</b>にする

なお、今回も`Phong MAT`を`GLSL MAT`に変換し、Vertex Shaderを改造してアニメーション再生機能を実装しています。

### Vertex ShaderでTangentの再計算
初期位置でのNormalから現在のフレームのNormalへの回転を表すクォータニオンを作成し、Vertex Shaderで`T.xyz`を回転させます。

```glsl:phong_VAT_Soft_NormalMap_vert
uniform float uBumpScale;
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;

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

// あるベクトルから別のベクトルへの回転を表すクォータニオンを作成
vec4 makeFromToRotation(vec3 from, vec3 to){
	vec4 q = vec4(0.0);
	vec3 a = cross(from, to);
	q.xyz = a;
	q.w = sqrt((pow(length(from), 2)) * pow(length(to), 2)) + dot(from, to);
	return q;
}

// ベクトルをクォータニオンで回転
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 newPos = P + texture(VAT_Pos, UV).xyz;
	
	// 法線を読み込み
	vec3 newNorm = texture(VAT_Norm, UV).xyz;
	
	// 初期フレームのNormalと、現在のフレームのNormalから、Tangentを回転させるためのクォータニオンを作成
	vec4 quat = makeFromToRotation(N, newNorm);
	
	// Tangentを回転
	vec3 newT = T.xyz + rotateVectorByQuatenion(T.xyz, quat);

	{
		vec3 texcoord = TDInstanceTexCoord(uv[0]);
		oVert.texCoord0.st = texcoord.st;
	}
	
	// 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(Cd);
	
	// Normalを設定
	vec3 worldSpaceNorm = normalize(TDDeformNorm(newNorm));
	
	// Tangentを設定
	vec3 worldSpaceTangent = TDDeformNorm(newT.xyz);
	worldSpaceTangent.xyz = normalize(worldSpaceTangent.xyz);
	oVert.tangentToWorld = TDCreateTBNMatrix(worldSpaceNorm, worldSpaceTangent, T.w);

#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}

```

### 結果

![soft_normalmap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/8ef624eb-216f-e93a-953f-01a1e3a4e438.gif)


## Rigid (Rigid Body Topology)

![rigid_Houdini.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/e8007e97-c17b-0653-c928-51a8600ce182.jpeg)
![rigid_substance.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/f235fea8-de20-e064-f9ca-0501e2284fe2.jpeg)

UV展開した3Dテキストに破砕アニメーションを付けました。

### TouchDesignerオペレーター解説

![rigid_TD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/fddb5fb4-3480-53ef-6c87-1c9aa56e1114.jpeg)

サンプルプロジェクト`VATExamples.toe`内の`VAT_Rigid_NormalMap`を参照してください。
Tangentアトリビュート追加の手順hあ、Softの時と同様です。

1. Houdiniから書き出されたFBXをインポート
1. `Attribute Create SOP`を作成し、`FBX COMP`内のSOPをインプットする(サンプルプロジェクトでは`Select SOP`で`FBX COMP`内のSOPを取得しています。)
1. `Attribute Create SOP`のパラメーター`Compute Tangent`を<b>オン</b>にする

### Vertex ShaderでTangentの再計算
`Attribute Create SOP`で追加した`T`アトリビュートを、Rotationテクスチャに格納されたクォータニオンを使い、Normalを回転させるときと同じようにVertex Shaderで回転させます。


```glsl:phong_VAT_Rigid_NormalMap_vert
uniform float uBumpScale;
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;

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

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


in vec4 T;
out Vertex
{
	vec4 color;
	mat3 tangentToWorld;
	vec3 worldSpacePos;
	vec2 texCoord0;
	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;
	
	// 現在のフレームの中心点の移動を反映した、頂点の位置を計算
	vec3 newP = P - pivot;
	newP = rotateVectorByQuatenion(newP, pivot_Rot);
	newP -= pivot;
	newP += pivot_Pos;
	newP += P;
	
	// Normalを回転させる
	vec3 newN = rotateVectorByQuatenion(N, pivot_Rot) + N;
	
	// Tangentを回転させる
	vec3 newT = T.xyz + rotateVectorByQuatenion(T.xyz, pivot_Rot);

	{
		vec3 texcoord = TDInstanceTexCoord(uv[0]);
		oVert.texCoord0.st = texcoord.st;
	}

	// 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(vec4(1.0));
	
	// Normalを設定
	vec3 worldSpaceNorm = normalize(TDDeformNorm(newN));

	// Tangentを設定
	vec3 worldSpaceTangent = TDDeformNorm(newT.xyz);
	worldSpaceTangent.xyz = normalize(worldSpaceTangent.xyz);
	oVert.tangentToWorld = TDCreateTBNMatrix(worldSpaceNorm, worldSpaceTangent, T.w);

#else // TD_PICKING_ACTIVE

	TDWritePickingValues();

#endif // TD_PICKING_ACTIVE
}

```

### 結果

![rigid_normalmap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55235/0fe7211f-9a29-b73e-d3da-beccb4ade8ff.gif)