GPGPUで3D色空間作ったお話


はじめに

12/1~2に開催されたOgakiMiniMakerFaire2018に出展したもののお話です。
友人が出展した喋る魚がめっちゃバズってました。

今回自分は「動く3D空間内で、自身の位置に対応した色に光るLEDポール」を作りました。
作り始めてから今日まで、未だにしっくりくる説明文が書けません。
とりあえず動作しているのはこんな感じ(gifにしたらめちゃめちゃ粗くなった、おすすめgif生成ツール募集中)。

この柱と基板(見えないけど)の部分は、友人にこちらの記事だけぶん投げたところ、いい感じに回路設計から制作までこなしてくれました。感謝です。

で、自分は
1. 描画部分
2. DMX送信部分
を担当しました。
TouchDesignerのアドカレに書くくらいなので、当然ここは全部TouchDesignerです。
今回はこの描画部分の話をしようと思います。
DMXとかハードの部分は書くのが間に合いませんでした。

やりたかったこと

何よりやりたかったのは、ちゃんと色空間の解像度を出すことです。
LEDの詳細な位置に対応して色を変えたかったので、その分シミュレーションもちゃんと解像度を出したいと思っていました。
今回具体的には100x100x100の精度で空間内で色を指定します。
おや、全部で1000000...GPGPUパーティクルだっ!!!

というわけで基本的にはこちらの記事の応用になります。

制作環境

Windows Home 64bit
Intel Corei7-6700HQ 2.60GHz
NVIDIA GeForce GTX 1060

実装

今回描画部分のみについてなので、いろいろ削りに削ったところ、めちゃくちゃ簡単になりました。
簡単というか、ほとんどさっきの記事通りです。

0. とりあえず全部出す

とりあえず使うオペレータを全部並べときます。

件のシステム制作時にはいろいろ対応できるよう他にも足したりしましたが、とりあえず最小単位で行きます。
基本的には先ほどの記事通りの並びです。

Grid SOPで欲しい頂点数分のグリッドを作成、
Convert SOPでParticleに変換、
それをGeometry COMPにつっこみ、
そのGeometry COMPのRender->MaterialにGlsl MATを指定、
そのGlsl MATではカラーマップとなるGlsl TOPを参照、
あとはいつもどおりRender TOPCamera COMPLight COMPを置いておく

といった感じです。

さて、とりあえず並べたらさらに少し詳細をいじっていきます。
まずgrid1
今回1000000粒を作るので、Rowsに1000、Columnsに1000を入れておけばとりあえず良いでしょう。
個人的には個数が直感的に分かるように、Rowsに1、Columnsに1000000でもいいと思います。自分はこちらです。
ちなみに、Rowsを1000000、Columnsを1にして少しいじると落ちる話があります、ちゃんとわかったわけではないですが。
まぁこの処理別にいらなかったみたいです。

次に、convert1
Particlesの記事どおり、Convert to を Particles にします。
これでgridのプレーンが粉になりました。
それと、今回単に粒を並べるだけなので、 Particle Type を Render as Point Sprites にしておきます。
これでGlsl MATから粒の大きさをいじれます。

glsl2では、Samplers1 でglsl1を参照します。
Sampler Name は colorMap にしておきましょう。
そして TOP に glsl1

最後にglsl1
ここは1000000粒の色を決めるため、解像度を1000x1000にします。
Common の Output Resolution を Custum Resolution にし、それぞれ 数値を1000にします。
ここで他の GPGPU では位置など float を扱うために Pixel Format を float にしますが、今回は結局 8bit の色情報を持つだけなので気にしなくて大丈夫です。
色をいじるのに時間などを使う場合、ここでユニフォーム変数に入れておきましょう。

1. カラーマップを作る

このカラーマップがこの後全てで利用する色の元になります。
色空間の描画部分のみの場合、本当は Glsl MAT のみで完結します。
が、そこから色を取得する前提で作ったものの解説なのでこちらで作っていきます。

さて今回のシステムですが、色空間自身をどうやって作るか書いていませんでした。
基本的に、位置情報から色を作ることを前提に今回のシステムは制作しています。
どういうことかというと、シェーダーアートと同じ原理を3Dでやっているということです。

というわけで、ここの流れはこんな感じです。

  1. uv座標から自身が対象とするxyz座標を計算する
  2. 計算したxyz座標を元に色を決める

以下glsl1のソースコード。
とりあえずxyz座標それぞれに応じてrgbが流れるやつを書いてみます。
コメントが日本語で文字化けすると思いますが放置です。

glsl1_pixel
uniform float uTime;

out vec4 fragColor;
void main()
{
    // 解像度を決める
    vec2 res2d = vec2(1000.0, 1000.0);
    vec3 res3d = vec3(100.0, 100.0, 100.0);

    // uv座標とres2dから頂点番号(id)を計算
    vec2 uv = floor(vUV.st * res2d);
    float id = uv.x + uv.y*res2d.x;

    // 頂点番号とres3dから頂点位置(pos)を計算
    float x = mod(id, res3d.x);
    float y = mod(floor(id/res3d.x), res3d.y);
    float z = mod(floor(id/(res3d.x*res3d.y)), res3d.z);
    vec3 pos = vec3(x,y,z)/res3d;
    pos = pos*2.0-1.0;

    // 頂点位置から色(color)を計算
    vec4 color = vec4(
        step(0.8, fract(pos.x+uTime)),
        step(0.8, fract(pos.y+uTime)),
        step(0.8, fract(pos.z+uTime)),
    1.0);

    // 出力
    fragColor = TDOutputSwizzle(color);
}

ここの見た目は関係ないので気にしないでください。
なんか意味あるようなないようなわからん絵ですが、いろいろやるとさらに意味わからん絵になります。

2. 粒を並べる

さっきの色情報を使って立方体を作っていきましょう。
glsl2 では頂点シェーダとフラグメントシェーダを書きます。
頂点シェーダで頂点番号から色を取得し頂点を並べて、
フラグメントシェーダで最終的な色を決定します。

まずは頂点シェーダ。

glsl2_vertex
uniform sampler2D colorMap;

uniform float uTime;

out Vertex{
    vec4 color;
} vert;

void main() 
{
    // 頂点番号(id)を取得
    float id = gl_VertexID;

    // 頂点番号からuv座標を計算、色をcolorMapから取得
    vec2 res2d = textureSize(colorMap, 0);
    vec2 uv = vec2(
        mod(id,int(res2d.x)),
        floor(id/int(res2d.x))
    ) / res2d;
    vec2 texCoord = uv + (1.0/res2d)*0.5;
    vert.color = texture(colorMap, texCoord);

    // 頂点番号から頂点位置を計算
    vec3 res3d = vec3(100.0, 100.0, 100.0);
    float x = mod(id, res3d.x);
    float y = mod(floor(id/res3d.x), res3d.y);
    float z = mod(floor(id/(res3d.x*res3d.y)), res3d.z);
    vec3 myP = vec3(x,y,z)/res3d - 0.5;
    vec4 worldSpacePos = TDDeform(myP);
    gl_Position = TDWorldToProj(worldSpacePos);

    // 頂点の大きさを決める
    gl_PointSize = 1.0;
}

続いてフラグメントシェーダ。
こちらは3行追加、1行変更で終わりです。

glsl2_pixel
in Vertex{
    vec4 color;
} vert;

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

結果、こうなります。そのままだとわかりにくいのでgeo1でy軸回転させました。

ちゃんとxyzに対応して色が変化していますね。
なんかざらざらして見えるのはモアレ効果的なあれです。

60fps出てます。

終わりに

ぶっちゃけ自分でもこれが最善かどうかだいぶあやふやなところはあります。
色を決める式があるんだからPythonとかC++で書けばいいんじゃないかとこの記事を書いている途中で思ったり。
が、とりあえず今回はこうやってみましたよ~ということでアドカレ参加してみました。
こうしたらいいよ~とかここ何言ってるかわからんとかいやいやそのやり方あかんやろとか、とりあえず何かあったらコメントいただけると嬉しいです。

今回の作品で、OMMFで多少はTouchDesignerの布教ができたのではと思っています。
Makerはハード派が多いからか、意外とみんな知らない。
「友達に勧めてみるよ~」と言っていた方もいました、やったね。