luma.gl
世の中には多くのWebGLフレームワークが存在します。
高度に抽象化された例では、three.jsが有名だし、PlayCanves、Babylon.js、A-frameなどのゲーム用のフレームワークもよく使われている気がします。
私が注目しているのが、deck.glというUberが出している可視化エンジンで、これのベースとなっているのが、今回注目するluma.glです。
特徴としては以下があるとのこと。
1. シンプルに抽象化されたハイパフォーマンスなデータ可視化のAPIを提供する
2. WebGL1とともに、WebGL2のAPIをサポートすることで、クロスプラットフォームにおける煩雑さを軽減する
deck.glに比べて注目度は低いように思いますが、既にこれをベースとしてエコシステムが生まれています。標準化を目指した志の高いライブラリらしいです。具体的には、以下のライブラリが良く使いそうです。
1.loaders.gl : GLTFなどの形状のインポートを行う
2.math.gl : 行列計算を行う
準備
以下は、公式ページからの引用です。node.jsとwebpack等を用いて環境を整備します。
適当なフォルダを作ったら以下のコマンドを実行します。
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
に起動時のコマンドを登録します。
{
"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用の設定を行います。
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()
が、いわゆるrequestAnimationFrame()
に対応する部分です。定義されているハンドラからは、様々な経過フレーム数など多様な情報の取得が可能です。
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つのレベルに抽象化されているようです。
- Low-Level : 直接的にWebGLのAPIを用いて、コンテキストやシェーダなどを管理する軽量な管理ツールを有する。
shadertools
、gltootls
、debug
モジュールを 主として用いる。 - Mid-Level : WebGLをラップする便利なAPIを有する。
webgl
モジュールを用いる。 - High-Level : モデルやリソースを管理する3Dエンジンによる開発ができる。
engine
、webgl
を用いる。
いずれにせよ、頂点シェーダ、フラグメントシェーダといったWebGL、GLSLの基本知識は必須です。wgld.orgなどで勉強しましょう。
High-Levelの例
まずはengine
モジュールを用いた、ハイレベルなAPIについてです。
普通のWebGLのプログラムをうまいこと抽象化しているイメージです。
以下では頂点シェーダvs
とフラグメントシェーダfs
を定義して、その後にモデルを構築しています。
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を参考にさせていただきました。
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();
上記のサンプルで基本的なところは押さえられたかと思います。