この記事は、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
を参照
まずは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 Access
に Read-Write
を指定します。これで、バッファに対して読み書きアクセスの指定ができます。
次に、ピクセルに対して読み書きの操作を行なう方法です。書き込みの操作はもうやってますよね?
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
を参照
では、まずはピクセルの色情報を使って点をインスタンスレンダリングしてみる部分を作ってみましょう。
適当なSOPを接続した Geometry COMP
を作り、 Instance
タブの中から Instancing
を ON
に、Instance Count
を Manual
にして、どれぐらいのインスタンスを描画したいかを指定します。今回の例だとピクセル数がそれになるので、GLSL TOP
の幅と高さを掛けた値を指定します。
次に Phong MAT
を作り、おもむろに Output Shader...
のボタンを押して GLSL MAT
に変換します。その後新しく作られた GLSL MAT
のサンプラーにさきほどの GLSL MAT
を指定しましょう。
準備ができたら 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パーティクルの記事書いているので、なるべく実装がカブらないテクニックを使っていきたいと思います。はーこんな作り方もあるんだね~ というような感じで見ていただければ…
エミッタの定義
まずはパーティクルを生成する元になるエミッタの部分です。
今回は適当なSOPを変形させて、各頂点座標から、法線方向に初期加速度をかけてパーティクルを出したいなと思っていたので SOP to CHOP
で t[xyz]
、n[xyz]
を抜いてきてそれぞれ Select CHOP
でわかりやすい名前をつけています。
シミュレーションの計算
次に GLSL TOP
でComputeShaderを使ってパーティクルの簡単なシミュレーションをする部分です。
概ねこれまでとやったものと一緒なのですが、位置だけでなく、加速度やage/lifeのデータを保持したかったので、GLSL TOP
の # of Color Buffers
の値をいじってカラーバッファを2つ作っています。
作ったカラーバッファは Render Select TOP
を使って外から見えるようにしておきましょう。
それから、こちらも新しい要素として、さきほどエミッタの部分で定義した2つの Select CHOP
を Arrays 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 |
レンダリング部分
レンダリング部分は以前のものとほとんど変わりません。
バッファの名前を変更したのと、Age、Life の値を利用してスケールを調整できるようにした程度になります。
なんとなくできましたね?
まとめ
というわけでComputeShaderの導入から、簡単なパーティクルシステムの実装までをささっと書いてきました。個人的には気をつけて謎の飛躍がないように書いていったつもりなのですが、結果的には完全にフクロウの書き方的な感じになってしまいましたね…
単発な機能の説明程度だとテキストベースでもできるのですが、ノウハウがからんだ説明をするとなると細かい部分が多くなりすぎてウッ… きちんと書くのがめんどくさい… って感じになりますね。。ビデオチュートリアルとかのほうがいいのかな…
サンプルコードもアップロードしておくので、オイここ全然何やってるかわからないぞ や、 ここバグってるんじゃないか 的な所がありましたらどしどしコメント頂ければ幸いです! メリークリスマス!