luma.glへの挑戦

# luma.gl
世の中には多くのWebGLフレームワークが存在します。

高度に抽象化された例では、three.jsが有名だし、PlayCanves、Babylon.js、A-frameなどのゲーム用のフレームワークもよく使われている気がします。

私が注目しているのが、[deck.gl](https://deck.gl/)というUberが出している可視化エンジンで、これのベースとなっているのが、今回注目する[luma.gl](https://luma.gl/)です。

特徴としては以下があるとのこと。
1. シンプルに抽象化されたハイパフォーマンスなデータ可視化のAPIを提供する
2. WebGL1とともに、WebGL2のAPIをサポートすることで、クロスプラットフォームにおける煩雑さを軽減する

deck.glに比べて注目度は低いように思いますが、既にこれをベースとしてエコシステムが生まれています。標準化を目指した志の高いライブラリらしいです。具体的には、以下のライブラリが良く使いそうです。

1.[loaders.gl](https://loaders.gl/) : GLTFなどの形状のインポートを行う
2.[math.gl](https://math.gl) : 行列計算を行う 

# 準備
以下は、公式ページからの引用です。node.jsとwebpack等を用いて環境を整備します。
適当なフォルダを作ったら以下のコマンドを実行します。

```sh
npm init -y
npm i @luma.gl/engine @luma.gl/webgl
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
```

`package.json`に起動時のコマンドを登録します。

```json 
{
  "name": "luma-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@luma.gl/engine": "^8.1.2",
    "@luma.gl/shadertools": "^8.1.2",
    "@luma.gl/webgl": "^8.1.2",
    "deck.gl": "^8.1.9"
  },
  "devDependencies": {
    "html-webpack-plugin": "^4.3.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0"
  }
}

```

続いて、webpack用の設定を行います。

```webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './index.js',
  plugins: [
    new HtmlWebpackPlugin({
      title: 'luma.gl Demo',
    }),
  ],
  output: {
    filename: 'bundle.js'
  },
};
```

これで準備は終わりです。簡単な事例として、公式サイトには以下の例が記されています。特に何もせずに、画面を黒で初期化する例です。[`AnimationLoop()`](https://luma.gl/docs/api-reference/engine/animation-loop)が、いわゆる`requestAnimationFrame()`に対応する部分です。定義されているハンドラからは、様々な経過フレーム数など多様な情報の取得が可能です。


```index.js
import {AnimationLoop} from '@luma.gl/engine';
import {clear} from '@luma.gl/webgl';

const loop = new AnimationLoop({
  onInitialize({gl}) {
    // Setup logic goes here
  },

  onRender({gl}) {
    // Drawing logic goes here
    clear(gl, {color: [0, 0, 0, 1]});
  }
});

loop.start();
```

# APIのコンセプト
luma.glでは、様々なレベルの開発要求に対応できるように3つのレベルに抽象化されているようです。

1. Low-Level : 直接的にWebGLのAPIを用いて、コンテキストやシェーダなどを管理する軽量な管理ツールを有する。`shadertools`、`gltootls`、`debug`モジュールを
主として用いる。
2. Mid-Level : WebGLをラップする便利なAPIを有する。`webgl`モジュールを用いる。
3. High-Level : モデルやリソースを管理する3Dエンジンによる開発ができる。`engine`、`webgl`を用いる。


いずれにせよ、頂点シェーダ、フラグメントシェーダといったWebGL、GLSLの基本知識は必須です。[wgld.org](https://wgld.org/)などで勉強しましょう。

## High-Levelの例
まずは`engine`モジュールを用いた、ハイレベルなAPIについてです。
普通のWebGLのプログラムをうまいこと抽象化しているイメージです。

以下では頂点シェーダ`vs`とフラグメントシェーダ`fs`を定義して、その後にモデルを構築しています。

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

const colorShaderModule = {
  name: 'color',
  vs: `
    varying vec3 color_vColor;

    void color_setColor(vec3 color) {
      color_vColor = color;
    }
  `,
  fs: `
    varying vec3 color_vColor;

    vec3 color_getColor() {
      return color_vColor;
    }
  `
};

const loop = new AnimationLoop({
  onInitialize({gl}) {
    const positionBuffer = new Buffer(gl, new Float32Array([
      -0.2, -0.2,
      0.2, -0.2,
      0.0, 0.2
    ]));

    const colorBuffer = new Buffer(gl, new Float32Array([
      1.0, 0.0, 0.0,
      0.0, 1.0, 0.0,
      0.0, 0.0, 1.0,
      1.0, 1.0, 0.0
    ]));

    const offsetBuffer = new Buffer(gl, new Float32Array([
      0.5, 0.5,
      -0.5, 0.5,
      0.5,  -0.5,
      -0.5, -0.5
    ]));

    const model = new Model(gl, {
      vs: `
        attribute vec2 position;
        attribute vec3 color;
        attribute vec2 offset;

        void main() {
          color_setColor(color);
          gl_Position = vec4(position + offset, 0.0, 1.0);
        }
      `,
      fs: `
        void main() {
          gl_FragColor = vec4(color_getColor(), 1.0);
        }
      `,
      modules: [colorShaderModule],
      attributes: {
        position: positionBuffer,
        // divisor: glVertexAttribDivisorのこと
        color: [colorBuffer, {divisor: 1}],
        offset: [offsetBuffer, {divisor: 1}]
      },
      vertexCount: 3,
      instanceCount: 4,
      instanced: true
    });

    return {model};
  },

  onRender({gl, model}) {
    clear(gl, {color: [0, 0, 0, 1]});
    model.draw();
  }
});

loop.start();
```

modelの`attributes`内に記載されているキーと値が、シェーダに渡されます。
構築したmodelに対して、`onRender`ハンドラ部分で`draw()`が呼ばれていま`onRender'ハンドラの引数になっているのは、`onInitialize'ハンドラの返り値ですね。。
この構成が基本で、Mid-Levelの例では、Modelをより細かく設定し、シェーダを組み合わせてプログラムを作って実行する例が示されています。Low-Levelは、ほとんどそのままWebGLを書いている感じです。

## メッシュを描く
上の例はサンプルそのままなので、Geometryクラスを使って、メッシュを描くことをやってみます。
WebGLでいうと、`drawElements`に当たるところ。ドキュメントがあまりに不親切だったので、ソースを読み解きながらの実装でした。
うまくいくと、以下のようにカラフルな四角形が回転を始めます。[wgld.org](https://wgld.org/d/webgl/w018.html)を参考にさせていただきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51703/78480125-80f6-ef4c-b806-8dc6c20c5c6d.png)

```javascript
import {AnimationLoop, Model, Geometry} from '@luma.gl/engine';
import {Buffer, clear} from '@luma.gl/webgl';
import {setParameters} from '@luma.gl/gltools';
import {Matrix4} from 'math.gl';

const vs = `
  attribute vec3 positions;
  attribute vec4 color;
  uniform   mat4 mvpMatrix;
  varying   vec4 vColor;

  void main(void){
      vColor = vec4(color);
      gl_Position = mvpMatrix * vec4(positions, 1.0);
  }
`;

const fs = `
  precision mediump float;

  varying vec4 vColor;

  void main(void){
    gl_FragColor = vColor;
  }
`;
    // 頂点属性を格納する配列
var position = [
  0.0,  1.0,  0.0,
  1.0,  0.0,  0.0,
  -1.0,  0.0,  0.0,
  0.0, -1.0,  0.0
 ];

 var color = [
  1.0, 0.0, 0.0, 1.0,
  0.0, 1.0, 0.0, 1.0,
  0.0, 0.0, 1.0, 1.0,
  1.0, 1.0, 1.0, 1.0
];

 var index = [
  0, 1, 2,
  1, 2, 3
];

const loop = new AnimationLoop({
    onInitialize({gl}) {

      setParameters(gl, {
        depthTest: true,
        depthFunc: gl.LEQUAL
      });

      // ModelのAttributesに通す場合はBufferになる
      const colorBuffer = new Buffer(gl, new Float32Array(color));       
      
      const eyePosition = [0, 0, 5];
      const viewMatrix = new Matrix4().lookAt({eye: eyePosition});
      const mvpMatrix = new Matrix4();

      const model = new Model(gl, {
        vs,
        fs,
        geometry: new Geometry({
          attributes: {
            positions: new Float32Array(position),
          },
          indices: new Uint16Array(index)   
        }),
        attributes: {
          color: [colorBuffer]
        }

    });

    return {
      model,
      viewMatrix,
      mvpMatrix
    };
    },

    onRender({gl, aspect, tick, model, mvpMatrix, viewMatrix}) {
      mvpMatrix.perspective({fov: Math.PI / 3, aspect})
        .multiplyRight(viewMatrix)
        .rotateX(tick * 0.01)
        .rotateY(tick * 0.013);

      clear(gl, {color:[0,0,0,1], depth:1.0});

      model.setUniforms({mvpMatrix: mvpMatrix})
      .draw();
    }
});

loop.start();
```

上記のサンプルで基本的なところは押さえられたかと思います。

# 参考
1. [luma.gl](https://luma.gl/) : 公式サイト
2. [wgld.org](https://wgld.org/) : WebGLについて総合的な説明があります。とても勉強になる。