yoppa.org


芸大 – メディアアート・プログラミング I 2025

TouchDesigner 実践 5 TouchDesigner + Shader (GLSL) 応用

今回も前回に引き続いてTouchDesignerとGLSLを組合せて高度な表現に挑戦します。前回はフラグメントシェーダー (ピクセルシェーダー) を用いてテクスチャー (TOP) を生成する2次元的な表現に留まっていました。今回はさらにGLSLを応用して3次元の形態(SOP)にGLSLを適用していきます。まず始めにGLSL TOPを用いてバンプマッピング (Bump mapping) をPhongマテリアル(Phong MAT)に適用しレンダリングの際に3Dの形状を変形するという手法を行います。さらに、続いて、GLSL MATを使用して頂点シェーダー (vertex shader) を用いて座標の情報をもとにマテリアルを動的に生成する手法に挑戦します。

サンプルプログラム

今日の内容

TouchDesignerでShader (GLSL) を使ってみる – 応用編

  • フラグメントシェーダ (ピクセルシェーダー) を使ってSOPを変形
    • バンプマッピング、法線マッピング、ハイトマップ を使う
  • GLSL MAT
    • Vertex Shaderについて
    • GLSL MATの使い方

フラグメントシェーダ(ピクセルシェーダー)でSOPを変形

  • GLSL TOPのフラグメントシェーダー(ピクセルシェーダ)でSOPを変形
  • バンプマッピングという手法を使用する
    • 陰影計算に用いる物体表面の法線ベクトルをテクスチャによって「揺らす」ことによって物体表面に凹凸が付いたような陰影を得る手法

法線マッピング (Normal Mapping)

  • バンプマッピング的技法の一種
  • より詳細なオブジェクトの法線ベクトルのX, Y, Z座標に対応したRGB画像

今回やってみること

  • フラグメントシェーダーの出力のTOPを3つのマッピング用データに分解
  • カラーマップ – SOPに貼り付けるRGPのイメージ
  • 法線マップ – 法線ベクトル用に返還したデータ
  • ハイトマップ – 全体の大きな凹凸を表現するデータ

Fragment Shaerは前回作成した波紋状に拡がるフラングメントシェーダーを使用

uniform float time;
uniform vec2 resolution;
out vec4 fragColor;

void main() {
  vec2 uv = (gl_FragCoord.xy * 2.0 - resolution) / vec2(resolution.x, resolution.x);
  float len = length(uv);
  float r = sin(len * 24 - time * 2.0 - 0.8) * 0.5 + 0.5;
  float g = sin(len * 24 - time * 2.0 - 0.4) * 0.5 + 0.5;
  float b = sin(len * 24 - time * 2.0) * 0.5 + 0.5;
  vec4 color = vec4(r, g, b, 1.0);
  fragColor = TDOutputSwizzle(color);
}

GLSL TOPの出力を、カラーマップ、法線マップ、ハイトマップに分解、Phong MATのパラメータにする。

Phong MATのRGBのパラメータにそれぞれのマップを適用

バンプマッピングを適用したPhong MATで、Grid SOPをレンダリング

フラグメントシェーダーが立体の凹凸に変換される!

入力するSOPをGirdからSphereへ、カメラのアングルを調整

GLSLで複雑に変形する球体に!

入力するフラグメントシェーダーを変えたり、ノイズを追加したりなど工夫してみる

完成!

GLSL MATを使ってみる

  • ここまで使用してきたのはGLSL TOP → フラグメントシェーダーのみ
  • TouchDesignerには、GLAL MATというのもある
  • フラグメントシェーダーに加えて、頂点シェーダー (Vertex Shader) も記述可能
  • 頂点の情報を利用、編集できる

復習: 頂点の3D座標情報から画面の全ピクセルに描画されるまでの流れ

  • GLSL MATを配置してみる
  • 以下のように頂点シェーダーとフラグメントシェーダーが同時に追加される

初期状態の頂点シェーダー(glsl_vertex) ※解説入り

// 3Dオブジェクトの各頂点の最終的な描画位置を計算するシェーダー
void main() // 各頂点に対して並列で実行されるメイン関数
{
  // --- 1. 頂点のワールド座標を計算 ---
  // Pは入力された頂点の初期位置(モデル座標)
  // TDDeform()は、SOPなどで設定されたジオメトリの変形を適用し、
  // ワールド空間における頂点位置を算出するTouchDesignerの組み込み関数
  vec4 worldSpacePos = TDDeform(P);
  
  // --- 2. ワールド座標から最終的な描画位置(クリップ座標)へ変換 ---
  // TDWorldToProj()は、ワールド座標に対し、カメラの位置や向き(ビュー変換)と
  // 遠近法(プロジェクション変換)を適用する組み込み関数
  // gl_Positionは、頂点の最終的な座標を格納する必須の出力変数
  gl_Position = TDWorldToProj(worldSpacePos);
}

このVertex Shaderは、以下の2段階の処理を実行しています。

  1. 入力された頂点座標 P に対し、TouchDesignerで設定された変形を適用し、ワールド空間での位置 worldSpacePos を求めます。
  2. その worldSpacePos を、カメラの情報を用いてスクリーン上の描画座標 gl_Position へと変換します。

これは、3Dグラフィックスにおける頂点処理の基本的な流れを、TouchDesignerの便利な組み込み関数を使って実装した典型的な例です。

GLSL 頂点シェーダーで使用される座標系

モデル座標 (Model Coordinates)

オブジェクト単体を基準とした、その物体固有の座標系です。「ローカル座標 (Local Coordinates)」とも呼ばれます。

  • 目的: 3Dモデルを制作する際の基準となります。
  • 原点: 通常、そのオブジェクトの中心や基点(ピボット)が原点 (0, 0, 0) となります。
  • 特徴: この座標系はオブジェクトごとに独立しており、他のオブジェクトとの位置関係は考慮されません。例えば、自動車のモデルであれば、その車体の中心が原点となり、タイヤやドアの位置はそこからの相対的な座標で定義されます。

ワールド座標 (World Coordinates)

シーン内のすべてのオブジェクトを配置するための、共通のグローバルな座標系です。

  • 目的: 複数のオブジェクトやカメラ、ライトなどを一つの仮想空間上に配置し、それらの絶対的な位置関係を定義します。
  • 原点: シーン全体の基準となる不動の原点を持ちます。
  • 変換: オブジェクトをワールド座標に配置するには、モデル座標に対して移動(Translate)・回転(Rotate)・拡大縮小(Scale)といった「モデル変換」を行います。これにより、各オブジェクトがシーン内のどこに、どの向きで、どれくらいの大きさで存在しているかが決まります。

クリップ座標 (Clip Coordinates)

カメラから見える描画範囲を定義するための座標系です。Vertex Shaderにおける最終的な出力座標 (gl_Position)がこれにあたります。

  • 目的: 画面に描画する領域と、描画しない領域(クリッピング領域)を決定します。
  • 変換: ワールド座標に対し、以下の2つの変換を行って得られます。
    1. ビュー変換 (View Transform): ワールド座標系を、カメラの位置と向きを基準とした座標系(ビュー座標系)に変換します。
    2. プロジェクション変換 (Projection Transform): カメラの視野(画角、アスペクト比、見える距離の範囲など)を考慮し、遠近法を適用して、最終的な描画範囲(クリッピング空間)に座標を投影します。
  • 特徴: このクリッピング空間は通常、立方体や直方体で定義されます。この範囲内に収まるジオメトリだけが描画対象となり、範囲外のものは描画処理から除外(クリッピング)されます。

これらの座標系の関係性を、オブジェクトが画面に表示されるまでの流れで示すと以下のようになります。

  1. モデル座標でオブジェクト(部品)を作る。
  2. 「モデル変換」を行い、ワールド座標(仮想空間)にオブジェクトを配置する。
  3. 「ビュー変換」と「プロジェクション変換」を行い、カメラから見たクリップ座標に変換する。
  4. クリップ座標に基づいて描画範囲が決定され、最終的にスクリーン座標(画面上のピクセル位置)へと変換されて表示されます。

頂点シェーダーとフラグメントシェーダーの連携

  • 頂点シェーダーで取得した頂点の情報を、フラグメントシェーダーに送ってみる
  • 頂点シェーダ側: out、 フラグメントシェーダー側: in を使う

頂点シェーダーをから座標情報を出力 (コメント入り) (glsl_vertex)

// Vertex ShaderからFragment Shaderへ渡すデータ群を定義
// 'oVert'という名前の箱に詰めて渡すイメージ
out Vertex {
  vec4 color;           // 頂点カラー
  vec3 worldSpacePos;   // ワールド座標
  vec3 worldSpaceNorm;  // ワールド座標系での法線ベクトル
  flat int cameraIndex; // 描画しているカメラのインデックス
} oVert;

// メイン関数
void main() {
  // --- 1. 頂点位置の計算(前回と同様の処理) ---
  vec4 worldSpacePos = TDDeform(P);
  gl_Position = TDWorldToProj(worldSpacePos);

  // --- 2. Fragment Shaderへ渡す各種データを準備 ---
  // カメラのインデックスを取得
  int cameraIndex = TDCameraIndex();
  oVert.cameraIndex = cameraIndex;

  // ワールド座標を格納
  oVert.worldSpacePos.xyz = worldSpacePos.xyz;

  // インスタンシングを考慮した頂点カラーを取得
  oVert.color = TDInstanceColor(Cd);

  // 変形を適用した法線ベクトルを計算し、正規化して格納
  vec3 worldSpaceNorm = normalize(TDDeformNorm(N));
  oVert.worldSpaceNorm.xyz = worldSpaceNorm;
}

このシェーダーは、前回解説した基本的な頂点位置の計算に加え、後続のフラグメントシェーダー(Fragment Shader)で、より高度なライティング(陰影処理)や色付けを行うための様々な情報を準備し、引き渡す役割を担っています。

このVertex Shaderは、単に頂点を画面上のどこに描画するかを決めるだけでなく、後工程であるFragment Shaderがリアルな質感(陰影や色)を表現するために必要となる、以下の4つの重要な情報を計算し、提供する役割を果たしています。

  • カメラ情報: どのカメラから見ているか
  • ワールド座標: 3D空間内の正確な位置
  • 頂点カラー: インスタンスごとの色を含むカラー情報
  • 法線ベクトル: 光の当たり方を計算するための面の向き情報

次にこの情報を受けとって3D空間のワールド座標系の情報で模様を生成するフラグメントシェーダーを作成してみましょう。

フラグメントシェーダー (glsl_pixel)

// Vertex Shaderから渡されたデータを受け取るためのブロック
// 'iVert'という名前の箱からデータを取り出すイメージ
in Vertex {
  vec4 color;
  vec3 worldSpacePos;
  vec3 worldSpaceNorm;
  flat int cameraIndex;
} iVert;

// シェーダーの最終的な出力色を定義
out vec4 fragColor;

// メイン関数。ピクセルごとに並列で実行
void main() {
  // 1. ピクセルを描画せずに破棄するかチェック
  TDCheckDiscard();

  // 2. ワールド座標に基づいて色を計算
  // sin関数を使い、位置に応じて周期的に変化する値を生成
  float r = sin(iVert.worldSpacePos.x * 20.0); // X座標から赤(R)成分を生成
  float g = sin(iVert.worldSpacePos.y * 20.0); // Y座標から緑(G)成分を生成
  float b = sin(iVert.worldSpacePos.z * 20.0); // Z座標から青(B)成分を生成

  // 計算したRGB値と不透明度(A=1.0)を組み合わせて色を定義
  vec4 color = vec4(r, g, b, 1.0);

  // 3. アルファ値に基づいてピクセルを破棄するかチェック
  TDAlphaTest(color.a);

  // 4. 計算した色を最終的な出力として設定
  fragColor = TDOutputSwizzle(color);
}

実行結果: (x, y, z) 軸それぞれで縞模様が描けた!

各処理の解説

Vertex Shaderからのデータ受け取り

in Vertex { … } iVert; は、Vertex Shaderから出力されたデータを入力として受け取るためのインターフェースブロックです。Vertex ShaderのoVertに格納された値が、iVertを通じて参照できます。
ポリゴンの内側にあるピクセルに対しては、各頂点の値が自動的に補間されたものが渡されます。

ピクセル色の計算
このシェーダーの中心的な処理は、main関数内での色の計算です。

  • TDCheckDiscard(): TouchDesignerのRender TOPの設定に基づき、このピクセルを描画すべきでない場合は、ここで処理を中断しピクセルを破棄する組み込み関数です。
  • sin(iVert.worldSpacePos…): Vertex Shaderから受け取ったワールド座標 (iVert.worldSpacePos) を利用して色を生成しています。
    • X, Y, Zそれぞれの座標値に20を掛け、そのsin(サイン)値を計算しています。sin関数の結果は-1.0から1.0の間で周期的に変化します。
    • この計算により、オブジェクトの3D空間上の位置に応じて色が滑らかに、かつ周期的に変化する、サイケデリックな縞模様のような視覚効果が生まれます。
    • 計算で得られた値を、それぞれ色(カラー)のR(赤), G(緑), B(青)成分に割り当てています。
  • vec4 color = vec4(r, g, b, 1.0): 計算したRGB値に、アルファ値(不透明度)を1.0(完全に不透明)として加え、vec4型のカラー変数colorを作成しています。

出力前の調整

  • TDAlphaTest(color.a): colorのアルファ値(この場合は1.0)を使い、Render TOPのアルファテスト設定に基づいてピクセルを破棄するかどうかを判定する組み込み関数です。
  • fragColor = TDOutputSwizzle(color): 最終的な出力色をfragColorに設定します。TDOutputSwizzle()は、Render TOPのSwizzle設定(例:RGBをBGRに入れ替えるなど)を適用するための組み込み関数です。この処理を経て、色がフレームバッファに書き込まれます。

    Vertex Shaderから渡された情報の利用状況について

    このFragment Shaderは、Vertex Shaderから渡された4つの情報(color, worldSpacePos, worldSpaceNorm, cameraIndex)のうち、worldSpacePos(ワールド座標)のみを利用して色を計算しています。

    法線を使ったライティング計算や、頂点カラーの反映は行っていません。そのため、光源やマテリアルの設定に影響されず、オブジェクトの空間上の位置のみに依存した独自の色が描画されます。

    まとめ

    このFragment Shaderは、ライティングのような物理ベースの計算を行う代わりに、頂点のワールド座標と三角関数を用いて、空間と連動した動的な模様を生成するという、プロシージャル(手続き的)なアプローチでピクセルの色を決定しています。

    頂点の座標情報を用いていろいろ工夫してみる! (詳細はパッチで解説)

    完成!!

    アンケート

    アンケート