ぷよぐらみんぐ勉強

勉強したやつとか疑問などを載せていきます。割りと雑に。

GLSLのコンピュートシェーダーでGPUパーティクル

OpenGLの勉強をしている時、
wgld.org
このページを見つけました。
ざっくりと言うと
初期化、更新時にはFrameBufferにパラメータを書き込み
描画時に頂点に番号を持たせてGPUパーティクルを行っている。とのこと。

見てて思ったのは面倒だなと。他に方法がないか探してみたらコンピュートシェーダーを使えば計算部分は任せられるのでこれを使うことに。

というわけでマウスポインタに向かって移動するパーティクルをコンピュート仕様で書いてみます。

1. 使用するバッファとパラメーターとコンピュートシェーダーの変数

まず使用するパーティクルの画像はこちら
f:id:himigami:20170430044934p:plain

次に使用する構造体

Point構造体
vec2 position
vec2 velocity

位置、速度のパラメーターとして設定

そしてこのパラメーターを保持するバッファが
Shader Storage Buffer Object(通称ssbo)
これはシェーダー内でも読み書きができるというUBOよりも良さそうなバッファ。ただし使用するにはOpenGL4.3以上が必要
定義は

layout(std430) buffer 名前
{
要素
} 変数名;

layoutのstd430はメモリのレイアウトを決める際に使うものらしい。
wikiを見ると配置の最適化やらがあるそう。
今回はPoint構造体の配列をメンバ変数として使う。

ssboの配列設定には決まりがあり、Point ins[];のように要素数を指定してなければ割り当てられたバッファの数分要素数が伸びる模様。
ただし要素数無記入の場合には必ずその変数は最後に書くこと。かつ無記入は一つしか作れない。

Point ins[];
float fParam[5];

Point ins[];
float fParam[];

これらのような指定はできないということ。

コンピュートシェーダーの組み込み変数

コンピュートシェーダーではスレッド番号的なものをコード内で取得ができる。
以下の変数で取得ができる。

  1. gl_NumWorkGroups glComputeDispatchにて設定した値がここに入る。
  2. gl_WorkGroupSize シェーダー内にあるlayoutのlocalsizeがここに入る。
  3. gl_WorkGroupID 0からgl_NumWorkGroups-1の範囲で入るスレッド毎の値。
  4. gl_LocalInvocationID 0からgl_WorkGroupSize-1の範囲でスレッド毎の値。
  5. gl_GlobalInvocationID gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationIDの式の値が入る。

2. 初期化のコンピュートシェーダー

#version 430

struct Point{
    vec2 position;
    vec2 velocity;
};
layout(std430) buffer Points
{
    Point ins[];
} point;
uniform ivec2 texSize;
uniform vec2 screenSize;
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

void main()
{
    uint index = gl_WorkGroupSize.x * gl_NumWorkGroups.x * gl_GlobalInvocationID.y + gl_GlobalInvocationID.x;
    Point pos;
    pos.position.xy = vec2(index % texSize.x, index / texSize.x) + screenSize.xy / 2 - texSize / 2;
    pos.velocity = vec2(0);
    point.ins[index] = pos;
}

まず統一することとしてバッファの形式はPoint構造体を作って
それの配列を定義しておきます。
そして、indexの式について。
使用するxの長さ * yの現在値 + xの現在値となるように変数を使用。
こうしないとlocal sizeをxだけでなくyも使用するときにスレッド別に参照できない。

そしてpositionには使用するバッファの大きさをtexSizeとして定義して
描画時に画面中央に出せるように値をセット。

3. 更新時のコンピュートシェーダー

#version 430

struct Point{
    vec2 position;
    vec2 velocity;
};
layout(std430) buffer Points
{
    Point ins[];
} point;
uniform vec2 mousePosition;
uniform vec2 screenSize;
uniform float timeScale;
uniform float speed;
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

void main()
{
    uint index = gl_WorkGroupSize.x * gl_NumWorkGroups.x * gl_GlobalInvocationID.y + gl_GlobalInvocationID.x;
    Point pos = point.ins[index];
    pos.velocity.xy += normalize(mousePosition - pos.position.xy) * timeScale * speed;
    pos.position.xy += pos.velocity.xy;
    
    pos.position.x = clamp( pos.position.x, 0, screenSize.x);
    pos.position.y = clamp( pos.position.y, 0, screenSize.y);
    point.ins[index] = pos;
}

indexは統一してこの値を呼出。
そしてuniform変数で持ってきたマウスのポジションのベクトルをとり
スピードと1フレームの処理時間をかけて可変フレームレートでも同じ動きをするようにした。

ただしポジションは画面外に行かないようclampで制御。
最後は必ずバッファに代入。

4. 描画時のシェーダー

#version 430
struct Point{
    vec2 position;
    vec2 velocity;
};
layout(std430) buffer Points
{
    Point ins[];
} point;
in vec2 position;
in vec2 texUV;

uniform sampler2D mainTexture;
uniform float pointScale;
uniform vec2 mousePosition;
uniform vec2 screenSize;
out vec4 color;
out vec2 useTexUV;

void main()
{
    ivec2 texSize = textureSize(mainTexture, 0);
    vec2 rate = vec2(2) / screenSize;
    Point pos = point.ins[gl_InstanceID];
    vec4 info = vec4(0);
    info.xy = pos.position.xy * rate - vec2(1);
    info.y *= -1;
    gl_Position = vec4(info.xy + (position.xy * (texSize / screenSize)), 0, 1);
    useTexUV = texUV;
    color = vec4(vec3(1,1,1), 0.1f);
}

//以下はフラグメントシェーダー

#version 430
in vec4 color;
in vec2 useTexUV;
uniform sampler2D mainTexture;
out vec4 destColor;

void main()
{
    destColor = texture(mainTexture, useTexUV) * color;
}

まず頂点シェーダー
頂点位置の設定の流れとしては
バッファの座標をスクリーン座標に変換したもの+頂点バッファで設定した座標
ただしバッファの座標を使う際にはy座標が下に行くほど増えるようにしているので-1をかけておく。
※今回colorには直で色を指定している。

フラグメントシェーダー
使用するテクスチャから色を取得して設定した色をかけてあげるだけ。

5. 実行

バッファとuniform変数を適宜割り当てると
このようになる。

初期状態
f:id:himigami:20170505032206p:plain
ちょっと動かし
f:id:himigami:20170505033953p:plain

動画で
www.youtube.com


これは64x64のパーティクル数で表示したもの。
アルファブレンド処理を入れているため数を増やすとかなり重くなる。
こちらの環境 ryzen1700 gtx970を使用したところ、1024x1024で30fps前後で出力ができていた。
今回やったパーティクル、不具合がありbool型を構造体に入れて動かしていたらそのバイト数に調節しているのにもかからわず、バッファが足りてないのか動いていないパーティクルが出現・・・
ほかはssboのバインドの設定がuboあたりの同じなのでどういう割当で動かすかの研究が必要。

終わり

今回シェーダーをフルに活用して演出面の研究ができたと思います。
まだまだOpenGLについて知らないことがあるし、ssboなんてuboよりも使用できる領域が大きいし、使い勝手も良さそうなのに全然知らなかった。
DirectXにも同じような機能があるらしいので再現は可能。
あと思うところは今回やったところはほとんど日本語の記事がなかったので、英語のリファレンスやらチュートリアルやらをひたすら見つめる作業でした。
なのでもう少し記事が出てくれればなぁと。