GLSL
TouchDesigner

TouchDesignerでGPGPU Particle Systemを作る Vol.1 基本編

More than 1 year has passed since last update.

TouchDesigner Advent Calendar 2017の12/10の投稿となります。
ここでは、TouchDesignerでのGPGPU Particle Systemの作り方を2回に分けて書きます。

今回の完成ソースはここ。
でも結構丁寧に書いたので見ながら自分で作ったほうが勉強になるかもしれません。

前段

TouchDesignerに限らず、大量の粒を動的に扱いたい場合、一部の例外を除きGPU言語を利用してParticleを扱う必要があります。
TouchDesignerのPalletsにはparticlesGpuというGPGPU Prticleのサンプルがあるのですが、
触ってみると、
『40万ぐらいの時点でFPSが30ぐらいまで落ちる(openFrameworksだとraptopで100万ぐらいのParticleを60でぬるぬる飛ばしてる人がごろごろ居る)』
『そもそもテクスチャがちゃんと貼られててParticle感が薄い』
という感じでコレジャナイ感が漂います。
『まぁでも中身見てちょっといじればなんとかなるやろ』と思い、中身を見てみたのですが自分がtouchdesigner初心者だからか、GPGPU Particle System初心者だからか、それともこのソースが複雑なのか原因は分らないが兎に角読んでも理解できない。
ということで勉強がてら1からGPGPU Particle Systemを作ることにしました。

TouchDesigner的 GPGPU Particle System

そもそもGPGPU Particle Systemに最低限必要なShaderはたった1つで、
『画像のピクセル情報(RGB)を頂点(Particle)の位置情報(XYZ)に変換するシェーダ』です。
他の環境ではそれ以外にも準備が必要だったりします(pingpong bufferとか)が、TouchDesignerの場合、TOP系のノードが既にGPUに最適化されているため、他のものを制作せずとも良い感じにParticleをいい感じに飛ばせます。

作り方

基本構成を作る

geometry Container, Camera Container, Render TOP, Out TOPを設置します。
このあたりはおなじみですね。
そしてGLSL MAT。geometry Containerと繋いでおいてください。
また、GLSL MATのコンパイルがミスったときのInfo DATも設置しましょう。
GLSLを扱うときは必ずつけておいたほうが良いと思います。
また、今回は全体像をみやすくするため、geometry Containerの中のTorusを消してIn SOPを設置してます。その際、RenderとDisplayのチェックも忘れずに。
01_environment.PNG

ParticleとなるPointを作る

geometry Containerにつなぐ形でparticleの元となるPointを作ります。
何で作っても良いのですが、今回TOPと連携させるので分りやすさ重視でGrid TopでPointを生成しましょう。
Grid SOPはuv共に256を設定します。256 * 256ということで合計65,536個のPointを設置します。
この数でもCPUだと結構きついはず。
02_grid.PNG
次にConvert SOPでPointをParticleに変更します。
03_convert.PNG
次にArrtibute Create SOPです。が今回は何も処理させてません。
次回も使わないのでなくてもよいのですが、極稀に使う可能性があるようなのでこの際挟んでおきました。
これで65,536個のParticleが画面上に置かれました。
04_particleSetting.PNG

Noise TOPを使ってParticleのBufferを作る

最初に話した通り、GPGPU Particleでは頂点情報(Particleの位置などの情報)を画像から取得、編集します。
ので画像を作りましょう。
今回はNoise Topを使います。
まず、Noise TOPをPerlin Noiseに変更し、offsetを0、Amplitudeを1にします。monochormeもoff。
06_NoiseTop01.PNG

次にtransformにabsTime.seconds / 10.0を入れてぐねぐね動くようにします。
07_NoiseTop02.PNG

最後にcommonで画像を16bit floatのRGBかRGBAにしてください。
08_NoiseTop03.PNG

これによって、Noise TOPはRGB値を-1.0から1.0の間で振ってくれるようになります。
このRGB値をShaderを使ってさきほど作った頂点のXYZに当てはめてやれば、大量のParticleが自在に動くようになります。
最後にnull TOPを2つ設置して、それぞれをnull_posとnull_colorにして、それぞれNoise TOPに繋いでください。
折角なのでParticleに個別で色も付けてしまいしょう。

GLSL MATに2つのTOPを設定する

GLSL MATにnull_posとnull_colorを設定しましょう。
どちらもshader内で参照するため、名前を付ける必要があります。
今回はそれぞれuPosMap、uColorMapとつけました。

09_GLSLMAT.PNG

いよいよGLSLをコーディングする

さて、コーディングです。TouchDesignerのGLSL MATで利用できるshaderは3種類、vertex shaderとpixel shaderとgeometry shaderです。
ただし、geometry shaderはオプション扱いでなくても動作するのでGLSL MATを置いただけでは生成されません。また、pixelをParticleとして飛ばすだけなら必要ないので今回は利用しません。
vertex shaderは各頂点情報に対しての処理を書くshader、pixel shaderは文字通り各pixelに対しての処理を書くshaderです。他の本などではfragment shaderと呼ばれたりもします。shadertoyなどのWebGL系の投稿型サイトはこのpixel shaderのみを取り扱ってるサイトが多いかと思います。
それではコードです。短いので一気に載せます。

glsl1_vertex.dat
uniform sampler2D uPosMap;
uniform sampler2D uColorMap;

out Vertex{
    vec4 color;
}vert;

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 newP = texture(uPosMap, texCoord0.st).xyz;

    vec4 worldSpaceVert =TDDeform(newP);
    gl_Position = TDWorldToProj(worldSpaceVert);
    vert.color = texture(uColorMap, texCoord0.st);
}
glsl1_pixel.dat
in Vertex{
    vec4 color;
}vert;

out vec4 fragColor;

void main()
{
    TDCheckDiscard();
    vec4 color = vert.color;
    TDAlphaTest(color.a);
    fragColor = TDOutputSwizzle(color);
}

どうでしょう。意外と短くないですか?
コピペすると、いい感じにパーティクルが画面中央で広がってるのではないでしょうか。
010_result.PNG

最後にコードの解説をしていきます。まずはVertex Shaderから。
各頂点情報(Particle)に対してこの処理が走っています。

1. 頂点と各画像の対応付け

まず int id = gl_VertexID; で0から65535までの頂点のユニークなIDを取得します。
これを使ってuPosMap上の対応した座標のpixelの色情報を取りたい。
なので vec2 res = textureSize(uPosMap,0); でuPosMapの縦横の解像度を取得し、 vec2 uv = vec2(float(id % int(res.x)), float(floor(id / int(res.x)))) / res; でuPosMap上のuv座標に変換しています。ぱっと見見慣れないかもしれませんが、よく画像のpixelを舐めるときにfor文で書いてるアレです。
ただ、これだとpixelの左上端を見てしまうので、真ん中に持ってくるために vec2 texCoord0 = uv + (1.0 / res) * 0.5; の処理をします。

2. uPosMapの値を座標値として取得

vec3 newP = texture(uPosMap, texCoord0.st).xyz; これだけです。

3. TouchDesigner上のCamera Containerで見た座標に変換

vec4 worldSpaceVert =TDDeform(newP);
gl_Position = TDWorldToProj(worldSpaceVert);
Particleを新しく置きたい座標であるnewPは空間座標上、またカメラからみたときに何処にあるべきなのかを行列を使って計算しています。まぁ、おまじないだと思っておいて良いかと。
これを書かないとTouchDesigner上のカメラを動かしてもGPGPU Particleが移動してくれません。

4. pixel shaderに頂点色情報を渡す

Vertex Shaderの最後の仕事はParticleの色情報をpixel shaderに送ることです。
Particleの色はpixel shaderで直接uColorMapを参照することでも塗れるのですが、こうすると後々geometry shaderでpixelを3角形にした時に困ることになります。
out Vertex{ vec4 color; }vert;
構造体を最初に宣言しておいて、
vert.color = texture(uColorMap, texCoord0.st);
そこに値を流し込む。
これでpixel shaderで頂点座標の色情報が参照できます。

5. pixel shaderで色付け

in Vertex{ vec4 color;}vert
で構造体を受け取って、
vec4 color = vert.color;
fragColor = TDOutputSwizzle(color);
で最終出力のfragColorに色指定しているだけです。
TDCheckDiscard、TDAlphaTest、TDOutputSwizzleはまぁお守りみたいなもんで直接は関係ありません。

まとめ

今、このチュートリアルでは65,536個しか動かしていませんが、noise TOPとGrid SOPの数をいじれば仕組みを変えることなく増やしていけます。
オフィシャルのParticle SOPは勿論、PalleteのparticlesGpuよりも低負荷です。
私の持っているGTX 1060の載ったRazer Bladeでは200万Particleまで行っても60FPSをキープできました。
Vol.2では表示以外の部分、動きや出し方、消え方を工夫していきます。