2023年2月現在最新のVAT3.0の使用方法については以下の記事をご覧ください。
はじめに
昨年のTouchDesignerアドベントカレンダーでTouchDesignerでHoudiniから書き出したVATを使用するという記事を書いたのですが、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
サンプルプロジェクトのSoftBody/SoftBody_Simple.hiplc
を参照してください。
基本的には以前の記事と同じ、人間の3Dモデルにmixamoで歩行アニメーションをつけたものです。
Labs Vertex Animation Texturesの設定
プロジェクトの/out
ネットワークにLabs Vertex Animation Textures
を作成します。
-
Mode/Target Engine
をSoft-Body Deformation(Soft)
,Custom
に変更します。 -
Start/End
で書き出したいアニメーションの開始フレームと終了フレームを設定します。 -
Input Geometry
に書き出したいジオメトリのパスを設定します。
Settings
-
Texture Format
がHDR(EXR/TIFF as RGBA 16/32 in Engine)
,.exr
になっていることを確認します。 - 必要であれば
Target Texture Width
を適宜変更します。(必ずしもSOPの頂点数以上に設定する必要はありません)
Export
- 必要であれば
Export Path
で書き出し先のフォルダ名を設定します。 -
Include
に、書き出したいデータがすべて含まれているか確認します。- もしColorデータが必要なければ、
Geometry, Position(s), Rotation
に変更してColor以外を書き出すことなどもできます。 -
Decide Individually
を選択することで、書き出しデータの個別選択や、書き出しファイル名を個別に設定できます。
- もしColorデータが必要なければ、
Target Engine
-
Coordinate System
をY-X-Z Clockwise (Right-Handed Y-Up)
に変更します。 -
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
ノーマルマップを使用しない場合
サンプルプロジェクトのVAT3_0.toe
内の、/VAT_SoftBody_Simple
を参照してください。
GLSL MATのVertex Shader
Phong MATをGLSL MATに変換し、Vertex Shaderのみ編集しました。
フレーム補完の処理を入れているので、コードが少し長くなっています。
なお、書き出したアニメーションの総フレーム数を、Uniform変数としてShaderに渡す必要があります。
// 頂点位置の変化量テクスチャ
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
}
実行結果
ノーマルマップを使用する場合
サンプルプロジェクトのVAT3_0.toe
内の、VAT_SoftBody_NormalMap
を参照してください。
GLSL MATのVertex Shader
rotテクスチャに格納されたクォータニオンを使い、Normalと同じようにTangentvec3(1.0, 0.0, 0.0)
を回転し、NormalとTangentからTBN Matrixを作成することでノーマルマップに対応できます。
// 頂点位置の変化量テクスチャ
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
}