この記事は、MIERUNE Advent Calendar 2021の5日目の記事です。
はじめに
____
/ \
/ _ノ ヽ、_ \
/ o゚((●)) ((●))゚o \ シェーダーやりたいお…
| (__人__) |
\ ` ⌒´ /
____
/ \
/ _ノ ヽ、_ \
/ o゚⌒ ⌒゚o \ でも生のWebGL操作するのつらいお…
| (__人__) |
\ ` ⌒´ /
____
/⌒ ⌒ \
/( ●) (● ) \
/::::⌒(__人__)⌒:::\ だからluma.glでやるお!
| |r┬-| |
\ `ー'´ /
本記事のゴール
luma.glで簡単なアニメーションを作成します。
WebGLについて
WebGLはブラウザからGPUにアクセスするためのJavaScript向けAPIです。GPUへの実際の命令はGLSLという言語で記述します。シェーダーと呼ばれたりします。シェーダーはJavaScript上では単なる文字列で、実行時にコンパイルされます。
このWebGL、なかなか低レベルなAPIとなっていて、ちょっと図形を描画するだけでもかなりのコード量になります。イメージ的には、DOMを生JSで操作する感じで結構つらい。WebGLももうちょっと高レベルなAPIでラップして欲しい訳です。
luma.glを使おうと思った理由
WebGL、というかブラウザで3Dとなると、デファクトスタンダードのライブラリとしてThree.jsがあります。普通はこれを使っておくのがよいでしょう。今回はWebGL自体の学習が目的なので、もう少しWebGLに近い低レベルなAPIがいいなと思い、そこで採用したのがluma.gl
です。
またもうひとつ、Three.jsはWebGL2で使えるようになる
Transform Feedback
に未対応らしいことも他の選択肢をさがす動機でした。luma.gl
は対応しています。
luma.glの特徴
あのdeck.glの裏側で動いているライブラリです。ドキュメントによれば、luma.glのAPIでも低レベル・中レベル・高レベルに分かれているようです。主に中・高レベルのAPIを操作することになるでしょう。モジュールごとに程度の差はありますが、まさにWebGLのAPIをラップするものです。WebGLほどめんどうくさくなく、でも同じような文脈でAPIを使う感じです。
ちなみにTypeScript対応は未完了です(2021-11)。
luma.glを使ってみる
インストール
中レベルAPIの@luma.gl/webgl
と、高レベルAPIの@luma.gl/engine
を使います。
npm install @luma.gl/webgl @luma.gl/engine
アニメーションループ
WebGLの描画の土台、いわゆるアニメーションループは、@luma.gl/engnine
が便利なクラスAnimationLoop
を提供しています。
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()
に渡します)。
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()
で定義します。
画面をお掃除 -> 頂点バッファを操作 -> 描画
を毎フレーム行うことで、アニメーションが実現します。
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を描画
},
// 以下略
この時点で、画面に三角形が表示されるようになります。
しかしアニメーションループとか、毎フレーム描画とか言いながら、微動だにしない三角形だけが表示されても面白くありません。
アニメーションさせてみる
頂点を操作するには、時間の推移を用いる方法や、前フレームの頂点から次フレームの頂点を計算する方法があります(きっとこれ以外にもありますが、私がやったことあるのがこれら)。後者はこの記事で紹介するにはヘヴィなので、前者の方法で三角形を動かしてみます。
フレームごとに、Modelの時間情報を更新する
結論のコードだけ。
// @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の世界では、頂点はベクトルであり、ベクトルの変換に行列
を使用できます。今回は回転行列
なるものを利用して、三角形を回してみます。
// 以上略
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
の値のまま回転させると余りにも回転速度が速いので、ちょうど良いくらいに値を小さくしています。以上のコードで、三角形が回転します。
上記のサンプルコードはすべてこちらに置いてあります
終わりに
私みたいな、シェーダーやってみたいけどWebGLは記述量が多くてつらい、という方には本記事の内容が刺さるかもしれません。
WebGLと聞くとThree.js
を使いがちですし、単にブラウザで3Dモデルを表示するなら、最も手早い選択肢でしょう。ただ、シェーダー自体を学びたいとか、WebGLに近いレイヤーを触りたい場合には、luma.gl
はおすすめできる選択肢だと思います。