はじめに
岐阜県にあるIAMASという学校に通っている @minoge1001 と申します。
先日、VJとして参加させて頂いたInterim Report Edition4というイベントで作成したエフェクトについて紹介いたします。オーディオリアクティブなメッシュをモーフィングで変化させるもので、以下のようなイメージです。
Audio reactive mesh morphing 01 #TouchDesigner #GLSL #creativecoding pic.twitter.com/sg6pGMDRdP
— Yugo Minomo (@minoge1001) November 11, 2019
Audio reactive mesh morphing 02 #TouchDesigner #GLSL #creativecoding pic.twitter.com/6P9hAVUvNA
— Yugo Minomo (@minoge1001) November 12, 2019
Agenda
サンプルファイルは以下からダウンロードできます。
AudioreactiveMeshMorphing.toe
大まかな流れは次の通りです。
-
Grid SOPを準備し、ポイント位置と法線方向をTOPに変換する - オーディオリアクティブなTOPを作成し、法線方向への変化量とする
- 1.と2.で作成した3つのTOPを用い、
GLSL MATで変形するgridを作成する - 1.の
Grid SOPをTwist SOPなどで変形させ、複数のパターンを準備する -
ポイント位置のTOPと法線方向のTOPを
Switch TOPで徐々に変化させる
TOPでモーフィングを行うことで、頂点数が多くなってもスムーズな動作が可能となります(GTX1060で、100万頂点でも60fpsをキープ)。また、一度仕組みを作ってしまえば、SOPレベルでメッシュを変形させるだけで様々なバージョンを手軽に作成することができます。
途中、GLSLが出てきますが、GLSLを書いた事がない方にもなるべくわかるように書くつもりです。もし分からなければ、ソースをコピペして読み飛ばしてもらって大丈夫です。
ポイント位置と法線方向のテクスチャ化

Constant CHOP(constant1)で画像の縦横のサイズを設定します。とりあえず変数名=gridSize/値=100としておきます。
Grid SOPを作り、Orientation=ZX Plane/Compute Normals=ONにします。RowsとColumnsは先に作ったgridSizeを参照させておきます。
Grid SOPの後ろにはNull SOPをつなげておきます。

Null SOP(null1)をSOP to CHOPで位置データ(Position XYZ)に変換したのち、Shuffle CHOPのN ValueをgridSizeとすることで100サンプルずつに分割します。これによって、10000サンプル×3チャンネルのデータが100サンプル×300チャンネルのデータに変換されます。但し、このままではtx0→…→tx99→ty0→…→ty99→tz0→…→tz99というチャンネルの順序になっていますので、Reorder CHOPでChannel Reorder Method=Numeric Suffix Sortとすることで、tx0→ty0→tz0→tx1→…→tx99→ty99→tz99と並び変えます。この結果をCHOP to TOP(Data Format=RGB)に入れることで、Grid SOPの100×100の各ポイントの位置データ(XYZ)が、100×100の画像の各画素(RGB)に格納されます。

上記のSOP to CHOP~CHOP to TOPをコピペし、sopto2の変換データをNormalとして法線データのTOPも作成します。最後に、2つのCHOP to TOPの後段にNull TOPを繋げ、それぞれ名前をpositionとnormalとしておきます。
オーディオリアクティブなTOPの作成

Audio File In CHOP(Mono=ON)、Analyze CHOP(Function=RMS Power)、Null CHOP(null2)と繋げます。音源のファイルはデフォルトのままで構いません。また、音をモニタリングするためにAudio Device Out CHOPをAudio File In CHOPの後ろに接続します。Noise TOPを開き、パラメータを以下のようにします。
| タブ | パラメータ | 値 |
|---|---|---|
| Noise | Harmonics | 4 |
| Noise | Amplitude | op('null2')['chan1'] |
| Noise | Offset | 0 |
| Transform | Translate z | absTime.seconds |
| Common | Resolution w | op('constant1')['gridSize'] |
| Common | Resolution h | op('constant1')['gridSize'] |
Noise TOPの後ろにNull TOPを繋ぎ、heightと名前を付けておきます。
Vertex Shaderによるメッシュの変形

Null SOPのOUTを中クリック(もしくは右クリック)して後段にGeomrtry COMPを繋ぎ、Camera COMPとRender TOPを出しておきます。また、GLSL MATをGeometry COMPのMaterialに割り付けておきます。

GLSL MATのSampler 1タブに作成した3つのTOPを割り当てます。
| Sampler Name | TOP |
|---|---|
| uPosMap | position |
| uNormMap | normal |
| uHeightMap | height |
また、Vectors 1タブに新たに2つの変数を設定します。
| Uniform Name | Value | 役割 |
|---|---|---|
| uPosScale | 10 | ベースとなるグリッドの大きさを調整する係数 |
| uHeightScale | 4 | 法線方向の変位量を調整する係数 |
さらに、CommonタブのWire FrameをTopology Wire Frameにしておきます。
ここまで準備ができたら、Vertex Shaderを書いていきます。
uniform sampler2D uPosMap;
uniform sampler2D uNormMap;
uniform sampler2D uHeightMap;
uniform float uPosScale;
uniform float uHeightScale;
void main()
{
int id = gl_VertexID;
vec2 res = textureSize(uPosMap, 0);
vec2 uv = vec2(float(id % int(res.x)), float(floor(id / int(res.x)))) / res;
vec2 texCoord0 = uv + (1.0 / res) * 0.5;
vec3 pos = texture(uPosMap, texCoord0).rgb;
vec3 norm = texture(uNormMap, texCoord0).rgb;
float height = texture(uHeightMap, texCoord0).r;
vec3 newPos = pos * uPosScale + norm * height * uHeightScale;
vec4 worldSpacePos = TDDeform(newPos);
gl_Position = TDWorldToProj(worldSpacePos);
}
全体は上記のようになります。Vertex Shaderはジオメトリ(この場合はGrid SOP)の頂点データを制御するプログラムです。Vertex ShaderはGPU上で頂点ごとに並列動作するため、CPUで計算するSOPの世界と比較して非常に高速です。100×100=10000個の頂点のそれぞれに対して上記のmain関数が同時に走っているイメージです。
uniform sampler2D uPosMap;
uniform sampler2D uNormMap;
uniform sampler2D uHeightMap;
uniform float uPosScale;
uniform float uHeightScale;
先ほど設定したuniform変数を読み込みます。このように書くことで、GLSLのコードの外にあるデータを取り込むことができます。以降のコードではこの名前でアクセスします。
int id = gl_VertexID;
vec2 res = textureSize(uPosMap, 0);
vec2 uv = vec2(float(id % int(res.x)), float(floor(id / int(res.x)))) / res;
vec2 texCoord0 = uv + (1.0 / res) * 0.5;
森岡さんの記事のコードを借用しました。ざっくり言うとGrid SOPのあるポイントが、100×100の画像のどの画素に対応しているかを計算し、その画素の座標を算出しています。結果としてtexCoord0には、計算すべき画素の位置の座標が入っています。この時の座標はx方向、y方向ともに0~1に正規化された値となっています。
vec3 pos = texture(uPosMap, texCoord0).rgb;
vec3 norm = texture(uNormMap, texCoord0).rgb;
float height = texture(uHeightMap, texCoord0).r;
上で算出したtexCoord0を基に、位置情報を持つテクスチャ(uPosMap)/法線情報を持つテクスチャ(uNormMap)/変化量の情報を持つテクスチャ(uHeightMap)の当該画素の情報を抽出します。各関数の後ろについている.rgbや.rは各テクスチャのどのチャンネル(Red/Green/Blue/Alpha)のデータを取り出すかを示しています。位置データと法線データはXYZのデータがそれぞれRGBに格納されているので.rgb、変化量は1次元の値なので.rとなります。
vec3 newPos = pos * uPosScale + norm * height * uHeightScale;
位置情報(pos)を10(uPosScale)倍し、大きさが10のgridにします。さらに法線方向(norm)にオーディオリアクティブな変化量(height)×4(uHeigntScale)だけ動かします。uPosScaleやuHeightScaleの値はGLSL MATのVectors 1タブで自由に変更可能です。
vec4 worldSpacePos = TDDeform(newPos);
gl_Position = TDWorldToProj(worldSpacePos);
TDDeform関数の引数を、デフォルトのPから先ほど計算したnewPosに変更します。この部分は座標変換と組み込み変数(gl_Position)への代入を行っており、今回の実装では気にすることはありません。

前の段階まで設定すると音に反応してグリッドが変形していると思います。但し、カメラが真横から捉えているので、いい感じの位置に移動させます。まずNull COMPを作成し、Camera COMP(cam1)のLook Atに設定します。こうすることで、カメラがどの位置にあっても常にNull COMP(初期値で(0, 0, 0))の方を向くようになります。その後、cam1のTranslate=(10, 10, 10)とすることで、斜め上から俯瞰で見たような映像となります。

この段階での全体のネットワークとRender TOPの表示結果は上記の通りです。
モジュール化
Grid SOPの各種データを画像データに変換する部分をモジュール化し、使い回しし易くします。

grid1からgeo1に直接接続すると、null1が分岐されます。その後、上図で赤く記した9つのノードを選択し、ネットワーク上のノードの無い部分でCollapse Selectedを選択します。その結果、選択部分が一つのBase COMPにまとまります。

エラーの原因はShuffle CHOPの参照先のパスが変わってしまったことなので、N Valueの値をop('../constant1')['gridSize']に修正します(opのカッコ内に../を追記)。また、base1の2つのTOP出力が交差するのを修正するために、out1のConnect Orderを1にします。
複数パターンの生成と切り替え

上の階層に戻ってbase1を複製します。生成されたbase2とgrid1の間にTwist SOP(twist1)を挿入します。もう一つbase1を複製し、生成されたbase3とgrid1の間に2つのTwist SOP(twist2、twist3)を挿入します。Twist SOPの各パラメータは画像を参照してください。

base1とpositionの間、base1とnormalの間にSwitch TOPを挿入し、それぞれposSwitch、normSwitchと名前を変更します。また、base2とbase3の2つのOUTも、それぞれのSwitch TOPに接続します。ここで、2つのSwitch TOPのBlend between InputsをONとしておきます。こうすることで、3つ以上の入力に対してもCross TOPのようなフェードの切り替わりができるようになります。

次に、切り替えのためのボタンを作ります、Button COMP、Count CHOP、Limit CHOP、Filter CHOP、Null CHOPを順に接続します。Button COMPのButton TypeはMomentaryにしておきます。Limit CHOPのTypeはZigzagにし、Maximumを2(接続するbaseの数-1)に設定します。こうすることで、ボタンを押すたびに値が0→1→2→1→0→1→…と変化していきます。Filter CHOPのFilter Widthで、モーフィングに要する時間を調整します(今回は8とします)。最後に、Null CHOPの値をposSwitchとnormSwitchのindexから参照することで、ボタンを押すたびにモーフィングができるようになります。

最終的なネットワークは上図のようになります。このような動作になります。
おまけ
ここまででかなり長文になってしまいましたので、あとは少しだけ補足情報を書きます。
Paletteの活用
基となるGrid SOPを変形させることで無数のバリエーションが創り出せますが、個性的な形状をイチから作成するのは大変です。そのような時にはPalleteを活用するのがおすすめです。例えば、このようなモーフィングが簡単に作れます。
mesh

PalleteのGeneratorsの中に、その名もmeshというコンポーネントがあります。これは、様々なパラメトリック方程式でメッシュを変形させたもので、RowsとColumnsにgridSizeを指定することで、簡単に入力の一つとして使うことが出来ます。Shapeを変更することで種類を選択し、Extra *を変更することで形状を調整できます。
superFormula

同じGeneratorsの中にsuperFormulaというコンポーネントがあり、こちらでも複雑な形状を作成することができます。使用する際には、Resolutionの値をgridSizeをします。ご興味のある方はwikipediaの記事をご覧ください。
波形が流れていくような動き
記事の最初で引用したTwitterでの作例では、音の波形が流れています。これは、Feedback TOPを使った手法で作成しており、詳細はMatthew Raganさんの記事をご参照ください。
色情報の付加
Twitterの作例では、メッシュ自体に色がついており、かつ波形の高さで色が変わるようになっています。本記事では詳細は省略しますが、ざっくり紹介すると
-
Ramp TOPでカラーマップを用意する -
Lookup TOPを使い、heightの値(0に近いほど低く、1に近いほど高い)とRamp TOPの色を対応させる -
GLSL MATにカラー情報をテクスチャとして渡し、色情報を付加する
というプロセスを踏んでいます。余力のある方はぜひ試してみてください。
