#はじめに
岐阜県にある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
にカラー情報をテクスチャとして渡し、色情報を付加する
というプロセスを踏んでいます。余力のある方はぜひ試してみてください。