React Three Fiberで作成したComponentをStorybookで管理する - GPGPU編

## 1. 概要
`React Three Fiber`にて作成した3Dオブジェクトを`Storybook`上で管理する、環境構築に関する記事に始まり、前回はEffect (PostProcessing)のComponentの管理に関する記事を書きました。

https://qiita.com/t_shi/items/87ac57051fbce712b2ae

今回は、Generativeなオブジェクトの生成などに役立つ<ins>GPGPU (Compute Shader)</ins>を活用したオブジェクトの生成を行いましたので、ご共有をさせていただきます。

## 2. 内容
前回は下図の様にEffectが掛かる所まで作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/6fbcecb4-a51e-75f9-9813-4254a4c8eef4.png)

今回は下図の様に、GPGPUを用いたGenerativeなオブジェクトが生成される所まで作成しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/ebac1336-d2bf-3986-85c3-aaaea1944a99.png)

全体的なコードは以下github repositoryから参照することができます

https://github.com/tominmin/react-fiber-storybook-boilerplate



GPGPUは簡潔に言えば、各座標毎の演算など、計算量の多い数値計算をCPUでなくGPUベースにて並列計算できる技術です
人工知能分野においても、ライブラリ内の演算の高速化などに主に使用されています。

今回は、1000x500のparticleの座標の演算結果を1000x500のtextureにて保持し、それぞれの要素 (座標)に対してGLSLによる演算を並列に行うことで処理の高速化を実現しています

## 3. 技術
`React Three Fiber`だけでは対応しきれなかったため、Three.js本体の`GPUComputationRenderer`を管理するclassを作成し、GPGPU周りの処理を管理しています。


**参考資料**
GPGPU周りの理論に関しては、主に以下の記事を参考とさせていただきました
[GPGPUがまったくわからないのでGPUComputationRenderer.jsをまず調べてみるメモ](https://nogson2.hatenablog.com/entry/2018/04/07/130727)

また、Simulator等の設計は以下記事を一部参考とさせていただきました
https://qiita.com/nemutas/items/b40baa2a1f33fae6b20d


## 4. 実装
先に、全体の構成としては下図のようなイメージです (雑で恐縮ですが・・)
`React Three Fiber`が適用する`useFrame` Hooksが各frame毎の演算処理を制御するライフサイクルとなっており
基本的には、以下の1~3を繰り返すイメージです
1. Simulatorクラスを利用し、GPGPU演算処理を行う
2. Simulatorクラスから座標演算結果を受け取り、Particle側の`geometry`, `material`に渡す
3. (ライフサイクルにて) レンダリングを行う

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/b49437cd-4567-8dad-4138-a2c7012b5677.png)

フォルダ構成は以下の様な形です
```text:フォルダ構成
src
 L components/molecules/Particle
                            L module
                                L simulator.ts
                                L simulator.type.ts
                                L update.ts
                            L index.stories.tsx
                            L index.tsx
 L utils/shader
            L Particle
                  L particlesShader.ts
                  L positionShader.ts
                  L velocityShader.ts
                  L snoise.ts
```


### Particle Part
**geometry**
```jsx:src/components/molecules/Particle/index.tsx
const geometry = useMemo(() => {
    const geo = new THREE.BufferGeometry();
    
    const uv = new Float32Array(width * height * 2);
    let p = 0;
    for (let i = 0; i < width; i++) {
        for (let j = 0; j < height; j++) {
            uv[p++]= i / (width - 1);
            uv[p++]= j / (height - 1);
        }
    }
    
    geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
    const positions = new Float32Array(width * height * 3);
    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    return geo;
}, []);
```
[1,1]~[width,height]を[0,0]~[1,1]に正規化します
BufferAttributeに対して、[..., ...{normalizedX, normalizedY}, ...]の順に交互に格納しuvします
positionはwidth x heightのRGB(3次元)として設定しています
(後でGPGPU側の初期値を初期座標として設定するので、この時点では特に何も代入しません)

**material**
```jsx:src/components/molecules/Particle/index.tsx
const uni = {
    time: { type: 'f', value: 1.0 },
    texturePosition: { value: null },
    textureVelocity: { value: null }
}
```

```jsx:src/components/molecules/Particle/index.tsx
const matShader = useMemo(() => {
    const mat = new THREE.ShaderMaterial({
        uniforms: uni,
        vertexShader: particlesVertexShader,
        fragmentShader: particlesFragmentShader,
        transparent: true
    });
    mat.extensions.drawBuffers = true;
    return mat;
}, []);

<points geometry={geometry} material={matShader}/>
```

**useFrameによるframe毎の更新**
```jsx:src/components/molecules/Particle/index.tsx
useFrame(() => {
    ...
    simulator.compute(time); // GPGPUの更新
    updateParticlesUniforms(matShader, simulator); // 演算結果をmaterialに反映する
    ...
    matShader.needsUpdate = true; // 初期描画以降にpositionを更新する場合にはフラグを指定する (重要)
    setTime(time + 1);
});

```

### GPGPU (Simulator) Part
処理手順は以下となります
1. 基本的にはuseFrameにてframe単位に`Simulator#compute`を呼び出し、`texture`を更新する
2. その後、`Simulator#getPositionTexture`で取得したtexture (position計算の集合)をParticle側VertexShaderの`uniform`として渡す
3. 最終的に`gl_Position`の計算に使用される事で<ins>Particle - GPGPU</ins>が繋がる


```typescript:src/components/molecules/Particle/module/simulator.ts
export class Simulator {
      ...
    constructor(gl: THREE.WebGLRenderer) {
        this.computeRenderer = new GPUComputationRenderer(1000, 500, gl);
        this.init();
    }
    
    init // computationRendererの初期化
    compute // 更新
    getPositionTexture // positionのtexture (演算結果)を取得する
    getVelocityTexture // velocityの演算結果を取得する
...
```
関数`init`にて、positionとvelocityに関し、それぞれ初期化を行うものの、処理自体は殆ど類似しています

**1. 初期化(例: position)**

```typescript:src/components/molecules/Particle/module/simulator.ts

// 1. textureの初期化
const initPositionTex = this.computeRenderer.createTexture();

// 2. textureデータを格納しておくオブジェクトを作成する
// GPGPUにて使用するShader (Fragment Shader)のattach
// 第二引数にはfragment shader
// 第三引数には、variableの初期化に用いるtextureを設定する
this.comTexs.position.texture = this.computeRenderer.addVariable(
    'texturePosition',
    positionFragmentShader,
    initPositionTex
);

// 3. Shader間でtextureを参照できる様にする (variableの依存関係を設定する)
// addVariableしたtexture (position.texture)から、position.texture, velocity.textureを参照できる様に紐付ける
this.computeRenderer.setVariableDependencies(
    this.comTexs.position.texture,
    [this.comTexs.position.texture, this.comTexs.velocity.texture]
);
this.comTexs.position.uniforms = this.comTexs.position.texture.material.uniforms;
...

// オフスクリーンレンダリング用のWebGLRenderTargetを作成する
this.computeRenderer.init();
```
- `init`により、`GPUComputationRenderer`の内部では`WebGLRenderTarget`が2つ作成されます
- `setVariable`にて設定したtextureをライブラリ側で`uniform sampler2D texturePosition`の様に自動的に挿入してくれる仕様となっております。
- 尚、この時点では、`uniforms.texturePosition`の値は`null`となります


**2. 更新 (GPGPU演算)**
```typescript:src/components/molecules/Particle/module/simulator.ts
compute = () => {
    this.computeRenderer.compute();
    ...
}
```
- computeを呼び出す毎 (各フレーム毎)に参照用・格納用を入れ替えます
- shader処理の実行結果を格納用textureに入れる。次回のcompute時にswapし、参照用となったtextureを読み込みます


**3. 演算結果取得**
```typescript:src/components/molecules/Particle/module/simulator.ts
getPositionTexture() {
    const target = this.computeRenderer.getCurrentRenderTarget(this.comTexs.position.texture!) as THREE.WebGLRenderTarget;
    return target.texture;
}
```



## 5. 総括
GPGPUを活用し作成したオブジェクトをStorybook上で動かしてみました。
全体的なコードは以下github repositoryから参照することができます

https://github.com/tominmin/react-fiber-storybook-boilerplate


今回作成した`Particle`の<ins>絵的な描画処理</ins>自体に関してはあまり触れていませんが
いずれ機会があれば掘り下げたいとは思います
(正直、2年程前に作成したネタだったので、詳細をゆっくり整理してからにします)

今後の更新計画は未定ですが、もう少しStorybook自体にfocusした内容にするか、WebAssemblyなどを絡めた内容にするか・・

<ins>以上</ins>