openFrameworksでCompute shaderを使う

# Compute shaderとは
[Compute Shader - OpenGL Wiki - Khronos Group](https://www.khronos.org/opengl/wiki/Compute_Shader)
頂点を処理するVertex Shaderや色を処理するFragment/Pixel Shaderと異なり、GPUでの計算に特化したShaderになります。
これにより、[GPU Particle](https://qiita.com/sketchbooks99/items/9d6676227f49a6ddd852)などを実装したいときに、わざわざFragment Shaderからテクスチャに頂点座標の計算結果を書き込まなくても良くなります。

# 対応環境
**OpenGL4.3** 以降に対応していること。
残念ながら **mac** はOpenGL 4.1までしか対応していないので、この記事は使えないことになります。。。

# 実行環境
私のマシンのスペックです。

- **PC**: Window 10
- **GPU**: Nvidia GeForce RTX 2060
- openFrameworks 0.10.1

# 実際にやってみる。
今回は導入的な感じとして、以下のような簡単な頂点計算を行って、それをParticleに適用するところまでをやりたいと思います。 

![out1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250282/0f02fa26-b58e-4baf-5868-d7e12492421f.gif)

## 注意事項
今回はOpenGL4.3以降を用いているので、main.cppの部分で設定を忘れないようにしてください。これをやっていないと無限にCompute shaderが動きません。たまにハマります。

```cpp:main.cpp
int main( ){
	ofGLWindowSettings s;
	s.setSize(1024, 768);
	s.setGLVersion(4, 5);
	ofCreateWindow(s);

	ofRunApp(new ofApp());
}
```

## ofApp.h/.cpp

CPU側で計算結果を使うためには、Buffer Objectが必要なので、shaderとbuffer objectを用意します。

```cpp:ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		void keyReleased(int key);
		void mouseMoved(int x, int y );
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);
		void mouseEntered(int x, int y);
		void mouseExited(int x, int y);
		void windowResized(int w, int h);
		void dragEvent(ofDragInfo dragInfo);
		void gotMessage(ofMessage msg);

		struct Particle {
			ofVec4f pos; // 頂点情報とparticleの年齢を格納
			ofVec4f vel; // 速度情報とparticleの寿命を格納
		};

		ofBufferObject dataBuffer;        // particle情報を格納するためのBuffer Object
		ofShader compute, renderParticle; // compute shaderとparticleを描画するshader
		ofVboMesh particle;               // particle
		int numParticle;                  // particleの個数
		ofEasyCam cam;                    // カメラ
		ofVec3f wallSize;                 
};
```

次にofApp.cppの`setup()`内でBuffer Object, Shader, VboMeshの初期化を行います。
Compute Shaderの読み込みは `compute.loadCompute(string name)` で行います。
Buffer Objectを`Particle`型の`vector`で初期化します。今回だと位置/速度情報と年齢/寿命を書き込みます。

`GL_DYNAMIC_DRAW`と書かれている部分はBuffer Objectの読み書き方法の指定です。
詳しくはこちらをご覧ください。
[glBufferData - OpenGL 4 Reference Pages - Khronos Group](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBufferData.xhtml)


```cpp:ofApp.cpp
// シェーダー読み込み
compute.loadCompute("shader/compute.glsl");							// Compute shader
renderParticle.load("shader/render.vert", "shader/render.frag");	// particleを描画するためのshader

numParticle = 256 * 256;            // particleの個数
vector<Particle> particleData;      // particleの位置/速度情報を初期化するためのvector
particleData.resize(numParticle);   // particleの個数に配列の要素数を合わせる

for (auto& p : particleData) {
    p.pos.x = ofRandom(-1.0, 1.0);
    p.pos.y = ofRandom(-1.0, 1.0);
    p.pos.z = ofRandom(-1.0, 1.0);
    p.pos.w = 0.0;  // particleの年齢

    p.vel.x = ofRandom(-5.0, 5.0);
    p.vel.y = ofRandom(-5.0, 5.0);
    p.vel.z = ofRandom(-5.0, 5.0);
    p.vel.w = (int)ofRandom(0.0, 4.0) * 0.5; // particleの寿命

    particle.addVertex(ofVec3f(p.pos));
    particle.addColor(ofFloatColor(1.0, 1.0, 1.0, 1.0));
}

// Buffer Objectの初期化
dataBuffer.allocate(particleData, GL_DYNAMIC_DRAW);
```

初期設定ができたら、draw()関数内でCompute shaderの実行とParticleの描画を行います。

ここでShaderからBuffer Objectを読み書きできるように`bind()`する必要があります。`bindBase()`ではshaderで読み書きするときのindex番号を設定します。

`bind()`できたら、Compute Shaderで計算します。計算する際に重要なのが、`dispatchCompute()`の部分です。これはCompute shaderの計算の並列度**(WorkGroupの数)**を設定し、計算を開始する関数です。この並列度に関してはshaderの部分で触れたいと思います。

```cpp:ofApp.cpp
// Buffer Objectをバインド
dataBuffer.bind(GL_SHADER_STORAGE_BUFFER);
dataBuffer.bindBase(GL_SHADER_STORAGE_BUFFER, 0);

// Compute shaderで頂点/速度を計算する
compute.begin();
compute.setUniform3f("wallSize", wallSize);
compute.setUniform3f("wallCenter", ofVec3f(0, 0, 0));
compute.dispatchCompute(64, 1, 1);
compute.end();

// 計算結果を基にParticleを描画
renderParticle.begin();
renderParticle.setUniform1f("time", time);
particle.draw(OF_MESH_POINTS);
renderParticle.end();

// Buffer Objectをアンバインド
dataBuffer.unbindBase(GL_SHADER_STORAGE_BUFFER, 0);
dataBuffer.unbind(GL_SHADER_STORAGE_BUFFER);

```
## Shader 
app側の設定が完了したので、Shaderのコードに移りたいと思います。
まずはCompute shaderです。
重要な部分だけ解説すると、`layout(std430, binding=0)...`の部分で、先ほど`bind`したBuffer Objectに配列としてアクセスすることができます。この時、bindingの番号は、CPU側で`bindBase()`で設定した番号にします。

次に`void main()`の上にある。`layout(local_size_x = BLOCK_SIZE, ...) in;`に注目してください。これは一度にGPUで並列処理する **Block数** を指定しています。このBlockごとの処理を先ほどの `dispatchCompute()` ではいくつずつ同時に並列で動かすかを指定しています。

例えば、dispatchCompute(64, 1, 1), layout(4, 1, 1)とすると
`(64 * 4) * (1 * 1) * (1 * 1) = 256`
のスレッドが同時にGPU内で計算されることになります。
dispatch...(4, 4, 1), layout(4, 4, 1)なら
`(4 * 4) * (4 * 4) * (1 * 1) = 16 * 16 = 256`
という感じです。
したがって、dispatchとlayoutの値をparticleや処理の数に合わせて設定することで、計算処理をより高速にすることができます。

各スレッドには**ID**が割り振られており、`gl_GlobalInvocationID.x`がそれにあたります。これは、全実行スレッドのうち、**今何番目のスレッドなのか**決めるIDが一意に割り振られているものになります。
IDには他にも種類があり、今の**WorkGroup**の番号を取得する`gl_WorkGroupID`、**WorkGroup**内で何番目のスレッドにいるのか取得する`gl_LocalInvocationID`などがあります。

**ID**が取得できたら、それを基に頂点と速度の更新を行います。計算部分は特別なことはしていないので割愛します。
計算が終わったら、最後に読み込んだ時と逆に書き込みを行います。`p[id] = par`の部分です。

```glsl:compute.glsl
#version 440

const int BLOCK_SIZE = 1024;

uniform vec3 wallSize;
uniform vec3 wallCenter;

struct Particle {
    vec4 pos;
    vec4 vel;
};

layout(std430, binding=0) buffer particle{
    Particle p[];
};

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

vec3 reflectWall(vec3 pos, vec3 vel) {
    vec3 ws = wallSize;   
    vec3 wc = wallCenter;

    if(pos.x < wc.x - ws.x * 0.5 || pos.x > wc.x + ws.x * 0.5) vel.x *= -1.0;    
    if(pos.y < wc.y - ws.y * 0.5 || pos.y > wc.y + ws.y * 0.5) vel.y *= -1.0;    
    if(pos.z < wc.z - ws.z * 0.5 || pos.z > wc.z + ws.z * 0.5) vel.z *= -1.0;
    
    return vel;
}

layout(local_size_x = BLOCK_SIZE, local_size_y = 1, local_size_z = 1) in;
void main() {
    uint id = gl_GlobalInvocationID.x;

    // Buffer Objectからid番目のデータを取得
    Particle par = p[id];
    vec3 pos = par.pos.xyz;
    vec3 vel = par.vel.xyz;
    float age = par.pos.w;
    float maxAge = par.vel.w;

    age += 0.01;
    if(age > maxAge) {
        age = 0.0;
        maxAge = 2.0;
        pos = vec3(random(pos.xx) * 2.0 - 1.0, random(pos.yy) * 2.0 - 1.0, random(pos.zz) * 2.0 - 1.0);
        vel = normalize(pos) * 5.0;
    }

    // 頂点に速度を足す
    pos += vel;
    vel = reflectWall(pos, vel);

    // Buffer Objectのデータ更新
    par.pos.xyz = pos;
    par.vel.xyz = vel;
    par.pos.w = age;
    par.vel.w = maxAge;
    p[id] = par;
}
```

さて、最後にparticleをレンダリングします。
同じように`layout(std430...`でBuffer Objectにアクセスできるようにし、particleの頂点番号`gl_VertexID`でBuffer Objectにアクセスします。読み込んできた頂点情報にモデルビュープロジェクション行列をかけたら、色を出力するだけです。

**注意**すべきは、Compute shaderとParticleを描画するshaderの両方でBuffer Objectにアクセスしているので、Compute Shaderが終わった段階で`unbind()`してしまうと、vertex shaderからBuffer Objectにアクセスできなくなります。

```glsl:render.vert
#version 440

struct Particle {
    vec4 pos;
    vec4 vel;
};

layout(std430, binding=0) buffer particle {
    Particle p[];
};

uniform mat4 modelViewProjectionMatrix;
uniform float time;

in vec4 position;
in vec4 color;

out vec4 vColor;

void main() {
    vec3 pos = p[gl_VertexID].pos.xyz;
    float age = p[gl_VertexID].pos.w;
    gl_Position = modelViewProjectionMatrix * vec4(pos, 1.0);
    vColor = vec4(mod(time * 0.3, 1.0), 0.5, age, 1.0);
    gl_PointSize = 5.0f;
}
```
```glsl:render.frag
#version 440 
precision mediump float;

in vec4 vColor;
out vec4 fragColor;

void main() {
    fragColor = vColor;
}
```

## CPU側ソースコード
Shaderコードは全て載せたので、最後にofApp.cppの全コードを載せて終わります。

```cpp:ofApp.cpp
void ofApp::setup(){
	ofSetFrameRate(60);
	ofBackground(0);
	ofSetVerticalSync(true);

	// シェーダー読み込み
	compute.loadCompute("shader/compute.glsl");							// Compute shader
	renderParticle.load("shader/render.vert", "shader/render.frag");	// particleを描画するためのshader

	numParticle = 256 * 256;			// particleの個数
	vector<Particle> particleData;		// particleの位置/速度情報を初期化するためのvector
	particleData.resize(numParticle);	// particleの個数に配列の要素数を合わせる
	for (auto& p : particleData) {
		p.pos.x = ofRandom(-1.0, 1.0);
		p.pos.y = ofRandom(-1.0, 1.0);
		p.pos.z = ofRandom(-1.0, 1.0);
		p.pos.w = 0.0;  // particleの年齢

		p.vel.x = ofRandom(-5.0, 5.0);
		p.vel.y = ofRandom(-5.0, 5.0);
		p.vel.z = ofRandom(-5.0, 5.0);
		p.vel.w = (int)ofRandom(0.0, 4.0) * 0.5; // particleの寿命

		particle.addVertex(ofVec3f(p.pos));
		particle.addColor(ofFloatColor(1.0, 1.0, 1.0, 1.0));
	}
	particle.setMode(OF_PRIMITIVE_POINTS);
	// particleの情報でBuffer Objectを初期化
	dataBuffer.allocate(particleData, GL_DYNAMIC_DRAW);

	wallSize = ofVec3f(500, 500, 500);
}

//--------------------------------------------------------------
void ofApp::draw(){
	float time = ofGetElapsedTimef();
	cam.setPosition(sin(time * 0.2) * 800, 100, cos(time * 0.2) * 800);
	cam.lookAt(ofVec3f(0, 0, 0));

	cam.begin();
	glEnable(GL_DEPTH_TEST);

	// Buffer Objectをバインド
	dataBuffer.bind(GL_SHADER_STORAGE_BUFFER);
	dataBuffer.bindBase(GL_SHADER_STORAGE_BUFFER, 0);

	// Compute shaderで頂点/速度を計算する
	compute.begin();
	compute.setUniform3f("wallSize", wallSize);
	compute.setUniform3f("wallCenter", ofVec3f(0, 0, 0));
	compute.dispatchCompute(64, 1, 1);
	compute.end();

        // 計算結果を基にParticleを描画
	renderParticle.begin();
	renderParticle.setUniform1f("time", time);
	particle.draw(OF_MESH_POINTS);
	renderParticle.end();

        // Buffer Objectをアンバインド
	dataBuffer.unbindBase(GL_SHADER_STORAGE_BUFFER, 0);
	dataBuffer.unbind(GL_SHADER_STORAGE_BUFFER);

	ofNoFill();
	ofDrawBox(0, 0, 0, wallSize.x, wallSize.y, wallSize.z);

	glDisable(GL_DEPTH_TEST);
	cam.end();
}
```

## 参考
- [Mapping between HLSL and GLSL](https://anteru.net/blog/2016/mapping-between-HLSL-and-GLSL/)
- [Compute Shader - OpenGL Wiki - Khronos Group](https://www.khronos.org/opengl/wiki/Compute_Shader)
- [glBufferData - OpenGL 4 Reference Pages - Khronos Group](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBufferData.xhtml)
- [Compute shaderについて](https://qiita.com/satoruhiga/items/b048e3ac17fac5f5a817)
- [ofBufferObject](https://openframeworks.cc/documentation/gl/ofBufferObject/)