luma.glでWebGLに入門する

この記事は、[MIERUNE Advent Calendar 2021](https://qiita.com/advent-calendar/2021/mierune)の5日目の記事です。

## はじめに

```
　 　 　　　＿＿＿_ 
　 　　　／　　 　 　＼ 
　　　／　 _ノ 　ヽ､_　 ＼ 
　 ／ oﾟ(（●）) (（●）)ﾟo ＼ 　シェーダーやりたいお…
 |　　　　 （__人__）　　　　| 
　 ＼　　 　 ｀ ⌒´ 　 　 ／ 

　 　 　　　＿＿＿_ 
　 　　　／　　 　 　＼ 
　　　／　 _ノ 　ヽ､_　 ＼ 
　 ／ 　oﾟ⌒　　　⌒ﾟo　　 ＼ 　でも生のWebGL操作するのつらいお…
 |　　　　 （__人__）　　　　|　　 
　 ＼　　 　 ｀ ⌒´ 　 　 ／ 

　 　 　　　＿＿＿_ 
　 　　　／⌒　　⌒  ＼ 
　　　／（ ●） 　（● ） ＼ 
　 ／::::⌒（__人__）⌒:::＼ 　　だからluma.glでやるお！
　|　　　　　|r┬-|　　　　　| 
　 ＼ 　　 　`ー'´ 　 　 ／
```

## 本記事のゴール

luma.glで簡単なアニメーションを作成します。

https://kanahiro.github.io/lumagl-example/

![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/8db5ec2c-9e9c-9ad5-8440-e4320aa9a9e8.png)


## WebGLについて

WebGLはブラウザからGPUにアクセスするためのJavaScript向けAPIです。GPUへの実際の命令はGLSLという言語で記述します。シェーダーと呼ばれたりします。シェーダーはJavaScript上では単なる文字列で、実行時にコンパイルされます。

このWebGL、なかなか低レベルなAPIとなっていて、ちょっと図形を描画するだけでもかなりのコード量になります。イメージ的には、<b>DOMを生JSで操作する感じ</b>で結構つらい。WebGLももうちょっと高レベルなAPIでラップして欲しい訳です。

## luma.glを使おうと思った理由

WebGL、というかブラウザで3Dとなると、デファクトスタンダードのライブラリとして[Three.js](https://threejs.org/)があります。普通はこれを使っておくのがよいでしょう。今回はWebGL自体の学習が目的なので、もう少しWebGLに近い低レベルなAPIがいいなと思い、そこで採用したのが`luma.gl`です。

> またもうひとつ、Three.jsはWebGL2で使えるようになる`Transform Feedback`に未対応らしいことも他の選択肢をさがす動機でした。`luma.gl`は対応しています。

https://luma.gl/

## luma.glの特徴

あの[deck.gl](https://deck.gl/)の裏側で動いているライブラリです。ドキュメントによれば、luma.glのAPIでも低レベル・中レベル・高レベルに分かれているようです。主に中・高レベルのAPIを操作することになるでしょう。モジュールごとに程度の差はありますが、まさに<b>WebGLのAPIをラップする</b>ものです。WebGLほどめんどうくさくなく、でも同じような文脈でAPIを使う感じです。

> ちなみにTypeScript対応は未完了です(2021-11)。

## luma.glを使ってみる

### インストール

中レベルAPIの`@luma.gl/webgl`と、高レベルAPIの`@luma.gl/engine`を使います。

```sh
npm install @luma.gl/webgl @luma.gl/engine
```

### アニメーションループ

WebGLの描画の土台、いわゆるアニメーションループは、`@luma.gl/engnine`が便利なクラス`AnimationLoop`を提供しています。

```typescript
import { AnimationLoop } from '@luma.gl/engine';

const loop = new AnimationLoop({
    // @ts-ignore
    onInitialize({gl}) {
        // ...頂点やシェーダーの初期化
        return { hoge, fuga };
    },
    // @ts-ignore
    onRender({gl, hoge, fuga...}) {
        // ...フレームごとの処理
    },
});

loop.start(); // 描画開始

```

クラス名、関数名から、使い方は明快です。
`onInitialize()`が返す値を`onRender()`の引数として受け取れるのがポイントです。
この状態では（そもそもサンプルコードとして不完全ですが）、頂点情報を全く定義していないので、画面には何も表示されません。頂点情報（Model）の定義、アニメーション描画定義を行う必要があります。

### Model定義

`luma.gl`では、頂点やシェーダーをひとまとめにする`Model`クラスが定義されています。
とりあえず三角形を描画するための頂点とシェーダーを定義してみます。
`AnimationLoop`では、`Model`は初期化時`onInitialize()`で定義します（`return { model}`とし、`onRender()`に渡します）。

```typescript

import { AnimationLoop, Model } from '@luma.gl/engine';
import { Buffer } from '@luma.gl/webgl';

// 以上略
        onInitialize: function ({ gl }) {
            const positions = [0.0, 0.6, 0.6, -0.6, -0.6, -0.6]; // 頂点の定義:[p1x, p1y, p2x, p2y, p3x, p3y]
            const positionBuffer = new Buffer(gl, new Float32Array(positions));
            const model = new Model(gl, {
                // Vertex Shader
                vs: `
                attribute vec2 position;
                varying vec2 fPosition;
    
                void main() {
                    fPosition = position;
                    gl_Position = vec4(position, 0.0, 1.0);
                }
                `,
                // Fragment Shader
                fs: `
                varying vec2 fPosition;
                void main() {
                    gl_FragColor = vec4(fPosition, length(fPosition), 1.0);
                }
                `,
                attributes: {
                    position: positionBuffer,
                },
                vertexCount: positions.length / 2, // 頂点の数
            });
            return { model }; // onRender()へ渡す
        },
// 以下略
```

### Modelをアニメーションループで描画

描画は`onRender()`で定義します。
画面をお掃除 -> 頂点バッファを操作 -> 描画
を毎フレーム行うことで、アニメーションが実現します。

```typescript

import { AnimationLoop, Model } from '@luma.gl/engine';
import { Buffer, clear } from '@luma.gl/webgl';

// 以上略
    onRender({ gl, model }) {
        clear(gl, { color: [0, 0, 0, 1] }); // 画面を黒一色で初期化
        model.draw(); // onInitialize()で定義したModelを描画
    },
// 以下略

```

この時点で、画面に三角形が表示されるようになります。

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/92d9a2d8-8513-5c5f-04cd-f3c41258ecf6.png)

しかしアニメーションループとか、毎フレーム描画とか言いながら、微動だにしない三角形だけが表示されても面白くありません。

## アニメーションさせてみる

頂点を操作するには、時間の推移を用いる方法や、前フレームの頂点から次フレームの頂点を計算する方法があります（きっとこれ以外にもありますが、私がやったことあるのがこれら）。後者はこの記事で紹介するにはヘヴィなので、前者の方法で三角形を動かしてみます。

### フレームごとに、Modelの時間情報を更新する

結論のコードだけ。

```typescript
    // @ts-ignore
    onRender({ gl, model }) {
        clear(gl, { color: [0, 0, 0, 1] });

        const time = performance.now(); // JavaScriptで時間情報を取得
        model.setUniforms({ time });  // modelに時間情報を付与
        
        model.draw();
    },
```

これで`Model`にフレーム単位で新たな時間情報が与えられます。
次に、`Model`側でそれを受け取る準備をします。

### 時間情報を用いた描画

時間ごとに頂点を動かしてみましょう。頂点の移動にはいろいろありますが、今回は`回転`させてみます。WebGLの世界では、頂点はベクトルであり、ベクトルの変換に`行列`を使用できます。今回は`回転行列`なるものを利用して、三角形を回してみます。

```typescript
// 以上略
        const model = new Model(gl, {
            vs: `
            uniform float time; // setUniforms()から値を受け取る

            attribute vec2 position;
            varying vec2 fPosition;

            // 回転行列: Z軸に対してラジアンでいうrだけ回転
            mat2 rot(float r) {
                float cr = cos(r);
                float sr = sin(r);
                return mat2(
                    cr, sr,
                    -sr, cr
                );
            }
            void main() {
                fPosition = position;
                gl_Position = vec4(rot(time * 0.001) * position, 0.0, 1.0); // 回転行列を用いてpositionベクトルを座標変換
            }
            `,
// 以下略
```

`time`の値のまま回転させると余りにも回転速度が速いので、ちょうど良いくらいに値を小さくしています。以上のコードで、三角形が回転します。

![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/8db5ec2c-9e9c-9ad5-8440-e4320aa9a9e8.png)


上記のサンプルコードはすべてこちらに置いてあります

https://github.com/Kanahiro/lumagl-example

## 終わりに

私みたいな、シェーダーやってみたいけどWebGLは記述量が多くてつらい、という方には本記事の内容が刺さるかもしれません。

WebGLと聞くと`Three.js`を使いがちですし、単にブラウザで3Dモデルを表示するなら、最も手早い選択肢でしょう。ただ、シェーダー自体を学びたいとか、WebGLに近いレイヤーを触りたい場合には、`luma.gl`はおすすめできる選択肢だと思います。