32
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TouchDesignerAdvent Calendar 2017

Day 24

TouchDesignerでComputeShader

Last updated at Posted at 2017-12-26

この記事は、TouchDesigner Advent Calendar 2017 24日目の記事です。

TouchDesignerでOpenCLをする予定でしたが、実装が間に合わなかったので方向転換して、ComputeShaderの記事を書くことにしました。。すみません

今回の記事で使っているサンプルファイルは こちら

ComputeShaderとは?

TouchDesignerではVertexShader、GeometryShader、FragmentShaderなど色々なシェーダーを使う事できますが、ComputeShaderもそれらと同じように使えます。

他のシェーダーでは頂点処理やピクセル処理など、特定用途の計算をするのが主なのですが、CudaやOpenCLなど、GPUに汎用計算をさせたいといったニーズからできたもののようです。言語的にはその他のシェーダーと同じく、GLSLで記述します。

仕様上は任意のバイト列のデータを自由に使えたりするものなのですが、現状でTouchDesignerでは GLSL TOP でしか使えないようです。。なのでピクセルデータの操作しかできません。。

ピクセルデータだけでなくvvvvのように頂点データも扱えるようになるとだいぶ色々出来て楽しいんですが… 今後に期待しましょう!

導入

/project1/COMPUTESHADER_BASIS を参照

setting_glsl_top.jpg

まずはComputeShaderを走らせるために GLSL TOP を作ります。ComputeShaderはOpenGLの中でも新しい機能なので、 GLSL Version の設定を 4.30 以降にする必要があります。そして Mode の設定を Compute Shader にします。

その後、 GLSL TOP の下のほうにある矢印を押してComputeShader用の Text DAT を表示します。そこに書いたシェーダーコードが実行される仕組みになっています。

ソースコードを書き換えてみましょう。例えばソースコードを次のようにすると、赤色が出力されます。

layout (local_size_x = 8, local_size_y = 8) in;

void main()
{
    vec4 color = vec4(1.0, 0, 0, 1.0);
    imageStore(sTDComputeOutputs[0], ivec2(gl_GlobalInvocationID.xy), TDOutputSwizzle(color));
}

簡単ですね?

ComputeShaderの処理はピクセル単位で行なわれます。現在どのピクセルを処理しているかは、グローバル変数の in uvec3 gl_GlobalInvocationID; に入ってくる事になっています。

その値を使ってよく見るuvの分布を作ってみましょう。

layout (local_size_x = 8, local_size_y = 8) in;

void main()
{
    vec4 color = vec4(float(gl_GlobalInvocationID.x) / 256, float(gl_GlobalInvocationID.y) / 256, 0, 1.0);
    imageStore(sTDComputeOutputs[0], ivec2(gl_GlobalInvocationID.xy), TDOutputSwizzle(color));
}

注意点として、gl_GlobalInvocationID に入ってくる値は整数のピクセル座標で、今回は色として出力したいので 0-1 の浮動小数点値にする必要があります。 0-1 の値にマップするには一旦floatにキャストしてから、ピクセルサイズで割りましょう。

バッファの操作

/project1/FUN_WITH_BUFFER を参照

Computeshaderでは、バッファに対して自由に読み書きができます。なので前のフレームを参照しながら更新していくといった操作をしたいときにもピンポンバッファがいりません。今まで Feedback TOP などを使って実現していた処理をよりシンプルに実装できます。

では、バッファを使う設定をしてみましょう。 Output AccessRead-Write を指定します。これで、バッファに対して読み書きアクセスの指定ができます。

setup_buffer.jpg

次に、ピクセルに対して読み書きの操作を行なう方法です。書き込みの操作はもうやってますよね?

void imageStore(gimage2D image, ivec2 P, gvec4 data); 関数を使います。第一引数の gimage2D image に書き込みたいバッファオブジェクト、 ivec2 P に2次元のピクセル座標、 gvec4 data に書き込みたい値を渡します。

ピクセルの読み込みは gvec4 imageLoad(gimage2D image, ivec2 P); 関数です。 第一引数は imageStore と同じくバッファオブジェクト、第二引数に読み込みたいピクセル座標を渡すと、該当する部分のピクセルデータを取得できます。

次のようなコードを書いて、実際に読み書きができているか確かめてみましょう。

layout (local_size_x = 8, local_size_y = 8) in;

void main()
{
    vec4 color = imageLoad(sTDComputeOutputs[0], ivec2(gl_GlobalInvocationID.xy)); // 前のフレームの値が入ってきているはず

    color.rgb += vec3(0.01, 0.02, 0.03); // 毎フレームちょっとづつ値を加算していく
    color.rgb = fract(color.rgb); // 1以上になったらまるめる

    color.a = 1.0;

    imageStore(sTDComputeOutputs[0], ivec2(gl_GlobalInvocationID.xy), TDOutputSwizzle(color)); // 新しい値を書き込む
}

色がチカチカアニメーションされていれば成功です!何もおこらない場合は単純にCookが走っていない可能性があるので、適当な時間変化があるパラメタをUniform変数として渡してあげましょう。

さて、これでバッファのアニメーションができるようになりました。でも今の段階だと、そんなにおもしろい事が起きそうにないですね… では、計算した値を使ってジオメトリを操作してみましょう。

色情報からジオメトリを操作する

/project1/COLOR_TO_GEOMETRY を参照

では、まずはピクセルの色情報を使って点をインスタンスレンダリングしてみる部分を作ってみましょう。

instance_setting.jpg

適当なSOPを接続した Geometry COMP を作り、 Instance タブの中から InstancingON に、Instance CountManual にして、どれぐらいのインスタンスを描画したいかを指定します。今回の例だとピクセル数がそれになるので、GLSL TOP の幅と高さを掛けた値を指定します。

次に Phong MAT を作り、おもむろに Output Shader... のボタンを押して GLSL MAT に変換します。その後新しく作られた GLSL MAT のサンプラーにさきほどの GLSL MAT を指定しましょう。

setup_material.jpg

準備ができたら Geometry COMP のマテリアルに作成したマテリアルをアサインします。まだ何もしていないので、普通にジオメトリが表示されるだけだと思いますが、シェーダー側を書き換えることで値をアップデートできます。

では、新しく作成した GLSL MAT に接続されている頂点シェーダーを以下のように書き換えます

uniform vec4 uDiffuseColor;
uniform vec4 uAmbientColor;
uniform vec3 uSpecularColor;
uniform float uShininess;
uniform float uShadowStrength;
uniform vec3 uShadowColor;

out Vertex {
	vec4 color;
	vec3 worldSpacePos;
	vec3 worldSpaceNorm;
	flat int cameraIndex;
}vVert;

uniform sampler2D sPositionMap; // サンプラーの設定をした名前に変更
vec2 iUV = vec2(gl_InstanceID / 256, gl_InstanceID % 256) / vec2(256, 256) + vec2(0.5 / 256.0); // uvを計算

void main()
{
	vec4 pos = texture(sPositionMap, iUV); // 色情報を読み込み
	
	// First deform the vertex and normal
	// TDDeform always returns values in world space
	vec4 worldSpacePos =TDDeform(P + pos.xyz); // 頂点の位置と色情報を合成
	gl_Position = TDWorldToProj(worldSpacePos);

	// This is here to ensure we only execute lighting etc. code
	// when we need it. If picking is active we don't need this, so

    /// 以下略....

↑ のコメントを追加した所が書き換えた所です。

vec2 iUV = vec2(gl_InstanceID / 256, gl_InstanceID % 256) / vec2(256, 256) + vec2(0.5 / 256.0);

の部分がわかりにくい所かと思いますが、gl_InstanceID から 0-1 のuv座標を計算しています。このあたりは TouchDesignerでGPGPU Particle Systemを作る Vol.1 基本編 でも触れられているので、そちらもご参照ください!

そうやって計算したuv座標でテクスチャの色情報をひっぱってきて、元々の P に加算することでピクセルの色情報から座標値を引っ張ってきて新しいオフセットを与えることができます。

この状態でもComputeShaderのソースコードを書き変えることで色々な効果を作り出すことができます。その場合、ピクセルの色深度の設定が8bitだと問題になる事があるので、Common タブの Pixel Format から 16-bit float (RGBA) を選んでおくのがいいでしょう。

また、デフォルトの256x256ピクセルの解像度であっても65536個のインスタンスが生成されてしまうので、処理負荷に応じて適時ピクセルサイズを調整するのもいいと思います。

パーティクルエンジンを作る

/project/CREATE_PARTICLE_ENGINE を参照

では、つづけて応用編としてパーティクルエンジンを作ってみましょう!

森岡さんが 先々週先週 とGPUパーティクルの記事書いているので、なるべく実装がカブらないテクニックを使っていきたいと思います。はーこんな作り方もあるんだね~ というような感じで見ていただければ…

エミッタの定義

particle_emitter.jpg

まずはパーティクルを生成する元になるエミッタの部分です。

今回は適当なSOPを変形させて、各頂点座標から、法線方向に初期加速度をかけてパーティクルを出したいなと思っていたので SOP to CHOPt[xyz]n[xyz] を抜いてきてそれぞれ Select CHOP でわかりやすい名前をつけています。

シミュレーションの計算

compute_textures.jpg

次に GLSL TOP でComputeShaderを使ってパーティクルの簡単なシミュレーションをする部分です。

概ねこれまでとやったものと一緒なのですが、位置だけでなく、加速度やage/lifeのデータを保持したかったので、GLSL TOP# of Color Buffers の値をいじってカラーバッファを2つ作っています。

作ったカラーバッファは Render Select TOP を使って外から見えるようにしておきましょう。

それから、こちらも新しい要素として、さきほどエミッタの部分で定義した2つの Select CHOPArrays 1 タブの中で Texture Buffer として GLSL TOP に渡しています。

テクスチャバッファとして定義した値はGLSLから、texelFetch 関数を使って以下のようにして取り出せます。


uniform samplerBuffer uTextureBufferName; // GLSL TOPで指定したuniform名

void main()
{
    /// ...
    vec4 c = texelFetch(uTextureBufferName, PIXEL_POSITION);
    /// ...
}

PIXEL_POSITION にはピクセル座標が入ります。この機能によってCHOPからの値をダイレクトに取り出してくることができます。便利です。

シミュレーション部分のComputeShaderのコードは以下のようになっています。

uniform vec2 uResolution;
uniform float uNumSamples;
uniform vec2 uTime;
uniform vec2 uLife;

uniform samplerBuffer uPositions;
uniform samplerBuffer uNormals;

float rand(vec2 co){
    return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}

layout (local_size_x = 8, local_size_y = 8) in;

void main()
{
    // gl_GlobalInvocationID の2D座標から 1Dのidを導出する
    int id = int(gl_GlobalInvocationID.x + gl_GlobalInvocationID.y * uResolution.x);

    // カラーバッファから値を読み出す
    vec4 BUF0 = imageLoad(sTDComputeOutputs[0], ivec2(gl_GlobalInvocationID.xy));
    vec4 BUF1 = imageLoad(sTDComputeOutputs[1], ivec2(gl_GlobalInvocationID.xy));

    // 値を理解しやすい形にアンパックする
    vec3 P = BUF0.xyz;
    vec3 V = BUF1.xyz;
    float age = BUF0.w;
    float life = BUF1.w;

    vec3 Force = vec3(0);

    // パーティクルの継続時間が終わったら再度初期化する
    if (age > life) {
        // age / lifeを初期化する
        age = 0;
        life = uLife.x + uLife.y * rand(vec2(id * 0.001, uTime.x));

        // CHOPで渡ってきているポイントの中からランダムでサンプリングする
        int sample_id = int(rand(vec2(id * 0.001, uTime.x)) * uNumSamples);
		P = texelFetch(uPositions, sample_id).xyz;
		V = texelFetch(uNormals, sample_id).xyz;

        // 初期位置が動かないとカッチョ悪いのですこしバラけさせる
        vec3 jitter = vec3(0);
        jitter.x += rand(vec2(id * 0.001, uTime.x + 10));
        jitter.y += rand(vec2(id * 0.001, uTime.x + 20));
        jitter.z += rand(vec2(id * 0.001, uTime.x + 30));
        P += jitter * 0.05;
    }

    // 今のフレームでかかる力の計算をする
    {
        float sT = uTime.x * 0.1;
        vec3 sP = P * 0.1;

        // 位置からSimplexNoiseのベクトルを適当に出して動かす
        vec3 n = vec3(0);
        n.x += TDSimplexNoise(sP + vec3(1, 0, sT));
        n.y += TDSimplexNoise(sP + vec3(2, 0, sT));
        n.z += TDSimplexNoise(sP + vec3(3, 0, sT));
        Force += n * 1.0;

        // なんとなく上方向にあがっていってもらう
        Force.y += 2;

        // 簡易的に空気抵抗をシミュレーションする
        V *= 0.99;
    }

    // 力から加速度、位置をアップデートする
    {
        float frame_time = uTime.y;
        float time_scaler = 0.5; // 全体的な動きの速度のウェィト
        float D = frame_time * time_scaler;

        V += Force * D;
        P += V * D;
    }

    age += uTime.y;

    // 値を再度パッキングする
    BUF0.xyz = P;
    BUF0.w = age;
    BUF1.xyz = V;
    BUF1.w = life;

    // バッファに書き込む
    imageStore(sTDComputeOutputs[0], ivec2(gl_GlobalInvocationID.xy), TDOutputSwizzle(BUF0));
    imageStore(sTDComputeOutputs[1], ivec2(gl_GlobalInvocationID.xy), TDOutputSwizzle(BUF1));
}

2つのカラーバッファに、以下のようにして情報をパックしています

R G B A
BUF0 P.x P.y P.z Age
BUF1 V.x V.y V.z Life

レンダリング部分

render_particles.jpg

レンダリング部分は以前のものとほとんど変わりません。

バッファの名前を変更したのと、Age、Life の値を利用してスケールを調整できるようにした程度になります。

なんとなくできましたね?

まとめ

というわけでComputeShaderの導入から、簡単なパーティクルシステムの実装までをささっと書いてきました。個人的には気をつけて謎の飛躍がないように書いていったつもりなのですが、結果的には完全にフクロウの書き方的な感じになってしまいましたね…

6006c27b.jpg

単発な機能の説明程度だとテキストベースでもできるのですが、ノウハウがからんだ説明をするとなると細かい部分が多くなりすぎてウッ… きちんと書くのがめんどくさい… って感じになりますね。。ビデオチュートリアルとかのほうがいいのかな…

サンプルコードもアップロードしておくので、オイここ全然何やってるかわからないぞ や、 ここバグってるんじゃないか 的な所がありましたらどしどしコメント頂ければ幸いです! メリークリスマス!

32
21
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?