LoginSignup
5
1

More than 1 year has passed since last update.

この記事は、MIERUNE Advent Calendar 2021の5日目の記事です。

はじめに

       ____ 
     /      \ 
   /  _ノ  ヽ、_  \ 
  / o゚((●)) ((●))゚o \  シェーダーやりたいお…
 |     (__人__)    | 
  \     ` ⌒´     / 

       ____ 
     /      \ 
   /  _ノ  ヽ、_  \ 
  /  o゚⌒   ⌒゚o   \  でも生のWebGL操作するのつらいお…
 |     (__人__)    |   
  \     ` ⌒´     / 

       ____ 
     /⌒  ⌒  \ 
   /( ●)  (● ) \ 
  /::::⌒(__人__)⌒:::\   だからluma.glでやるお!
 |     |r┬-|     | 
  \     `ー'´     /

本記事のゴール

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

ダウンロード (1).png

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を描画
    },
// 以下略

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

ダウンロード.png

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

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

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

フレームごとに、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の値のまま回転させると余りにも回転速度が速いので、ちょうど良いくらいに値を小さくしています。以上のコードで、三角形が回転します。

ダウンロード (1).png

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

終わりに

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

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1