Subterranean Flower

WebGL2をオブジェクト指向っぽく触れるライブラリXenoGLを公開しました

Author
古都こと
ふよんとえんよぷよぐやま!!!

WebGL、大変ですよね。OpenGLベースなのでC言語っぽいAPIと付き合っていかなければならず、JavaScriptの文化とはなかなか馴染みません。

そこでWebGLをオブジェクト指向っぽく触れるように、WebGL2用のラッパーライブラリであるXenoGLを開発・公開しました。

経緯

WebGLのAPIはなかなか難しいです。昔ながらの低級言語向け(この程度で低級って言ってると怒られるかもしれませんが……)のAPIで、今風のオブジェクト指向などとは全く異なる文化を持っていると言えます。

自分で触るだけならまだしも、他人にオススメしようとすると、大抵ドン引きされます。状態管理などが難しく、「今どんな状態で、何をしなければいけないのか」を把握するのに、ものすごく時間がかかります。

なので現代では、WebGLを触るときはThree.jsなどのライブラリを使うのが一般的です。しかしThree.jsはすこし大仰というか、もっと生に近いWebGLを触りたい時があります。ついでにいうと生に近いだけで生のWebGLは触りたくありません。わかります?このフクザツな気持ち。

そんなわけでWebGL2のオブジェクト指向ラッパーであるXenoGLを作って公開しました。まだ未完成ですが、3連休も終わるので一旦公開です。

XenoGL

WebGL2用のラッパーライブラリ、名前はXenoGLです!本当は「ModernGL」とか「ObjectGL」がよかったんですが、そのあたりは片っ端から名前被りしてました。「xeno-」は「異質な」「異なる」とかの意味です。「ゼノギアス」の「ゼノ」ですね。

npmかgithubからダウンロードできます。

ライセンスはMITライセンスです。好きに使ったり再配布したり切り刻んだりしてください。

注意

XenoGLはまだ不安定版です。APIは変更される可能性があり、アップデートによりあなたのコードが動かなくなる可能性があります。信頼性が求められるソフトウェアにはまだ絶対に使わないでください。

インストール

インストール(Node.jsなし/ブラウザ用)

インストールの方法は、githubからzipファイルをダウンロード(https://github.com/kotofurumiya/xenogl/releases)してきて、中のbuildフォルダに入ってるxenogl.min.jsを取り出すだけです。

あとは適当なHTMLファイルと同じフォルダにxenogl.min.jsを入れ、scriptタグで挿入します。

<script src="xenogl.min.js"></script>
<script>
    // ここにコードを書いていく
</script>
これで準備OKです。

インストール(Node.js/npm)

npmでインストールするには、普通にnpm installするだけです。

$ npm install xenogl --save

あとはimportかrequireしてください。

const XenoGL = require('xenogl');

基本的な使い方

まずは適当なcanvasを用意し、XenoGLのWebGL2コンテキストを作成します。

// canvasを用意する
const canvas = document.body.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);

// XenoGLのコンテキストを作成する
const xgl = new XenoGL.WebGL2(canvas);

次にシェーダとプログラムを作成します。プログラムを作成したらコンテキストにプログラムを追加します。

// 適当にシェーダのソースを読み込む
const vertexShaderSource = await fetch('vertex_shader.glsl').then((res) => res.text());
const fragmentShaderSource = await fetch('fragment_shader.glsl').then((res) => res.text());

// シェーダを作成する
const vertexShader = new XenoGL.VertexShader(vertexShaderSource);
const fragmentShader = new XenoGL.FragmentShader(fragmentShaderSource);

// シェーダを元にプログラムを作成する
const program = new XenoGL.Program({
  vertexShader: vertexShader,
  fragmentShader: fragmentShader
});

// コンテキストにプログラムを追加する
xgl.addProgram(program);

次にデータを用意します。例えば頂点座標と頂点色としましょう。これは普通にFloat32Arrayで用意すればいいです。

const vertices = new Float32Array([
  -0.5, 0.5,  0.0,
  -0.5, -0.5, 0.0,
  0.5,  0.5,  0.0,
  -0.5, -0.5, 0.0,
  0.5,  -0.5, 0.0,
  0.5,  0.5,  0.0
]);

const colors = new Float32Array([
  1.0, 0.0, 0.0, 1.0,
  0.0, 1.0, 0.0, 1.0,
  0.0, 0.0, 1.0, 1.0,
  0.0, 1.0, 0.0, 1.0,
  0.0, 0.0, 0.0, 1.0,
  0.0, 0.0, 1.0, 1.0
]);

このあたりのデータを作る方法としてライブラリを使う方法がありますが、そのときは「WebGL用のJavaScript行列計算ライブラリMatrixGLを公開しました」もよろしくお願いします。

次にバッファを作ってプログラムに追加します。

// アトリビュート(in変数)の情報を作成します
const positionAttribute = new XenoGL.Attribute('vertexPosition', 3);
const colorAttribute = new XenoGL.Attribute('color', 4);

// アトリビュートとともにバッファを作成します
const positionBuffer = new XenoGL.ArrayBuffer({
  dataOrLength: vertices,
  attributes: [positionAttribute],
  dataType: XenoGL.FLOAT
});

const colorBuffer = new XenoGL.ArrayBuffer({
  dataOrLength: colors,
  attributes: [colorAttribute],
  dataType: XenoGL.FLOAT
});

// バッファをプログラムに加えます
program.addBuffer(positionBuffer);
program.addBuffer(colorBuffer);

あとは描画するだけ!

// 描画!
xgl.draw(XenoGL.TRIANGLES);

ね、簡単でしょう?

その他の使い方

マルチプログラム

WebGL2において、複数のプログラムを使いたい時があります。そのときはxgl.activateProgramかxgl.useProgramを使います。

xgl.addProgram(updaterProgram);
xgl.addProgram(rendererProgram);

// rendererProgramを有効にする
xgl.activateProgram(rendererProgram);

activateProgramとuseProgramの違いは、activateProgramがアトリビュートの切り替えの面倒を見てくれるのに対して、useProgramはただ単純にプログラムを切り替えるだけです。

activateProgramは非常に重い処理で、毎フレーム呼び出しているとパフォーマンスの問題が出てきます。単にプログラムを切り替えるだけでいいときはuseProgramを使いましょう。

ただし、OpenGL/WebGLの知識がない人は、できるだけactivateProgramを使いましょう。

バッファ

バッファには初期化時にデータを送る以外にも、後からデータを送ることもできます。データを送るにはbuffer.bufferDataを使います。

const positionBuffer = new XenoGL.ArrayBuffer({
  attributes: [positionAttribute],
  dataType: XenoGL.FLOAT
});

program.addBuffer(positionBuffer);

positionBuffer.bufferData(new Float32Array([1.0, 1.0, 1.0]));

このあたりは場面によって柔軟に使っていきましょう。

インターリーブドバッファ

バッファを作る時に複数のアトリビュートを指定すると、自動的にインターリーブしてくれます。

const vertices = new Float32Array([
  -30.0, 30.0, 0.0,   // position
  0.0, 1.0, 0.0, 1.0, // color
  -30.0, -30.0, 0.0,
  1.0, 0.0, 0.0, 1.0,
  30.0, 30.0, 0.0,
  1.0, 0.0, 0.0, 1.0,
  30.0, -30.0, 0.0,
  0.0, 0.0, 1.0, 1.0
]);

const positionAttribute = new XenoGL.Attribute('vertexPosition', 3);
const colorAttribute = new XenoGL.Attribute('color', 4);

const buffer = new XenoGL.ArrayBuffer({
  dataOrLength: vertices,
  attributes: [positionAttribute, colorAttribute],
  dataType: XenoGL.FLOAT,
  usage: XenoGL.DYNAMIC_DRAW
});

インデックスバッファ

インデックスバッファはnew XenoGL.ElementArrayBufferで作成することができます。作り方はArrayBufferと同じです。

const indices = new Uint16Array([0, 1, 2, 1, 3, 2]);

const indexBuffer = new XenoGL.ElementArrayBuffer({
  dataOrLength: indices,
  dataType: XenoGL.UNSIGNED_SHORT,
  usage: XenoGL.DYNAMIC_DRAW
});

program.addBuffer(indexBuffer);

あとはプログラムに追加すれば自動的にインデックスバッファとして使われます。

手動で切り替えたい場合はprogram.activateElemntArrayBuffer(indexBuffer)を使ってください。

他のバッファ(フレームバッファとか)

すいません。まだ対応してません。対応したらここに追記します。

Uniform変数

uniform変数もnewすることで作ることができます。

const modelUniform = new XenoGL.Uniform('model');
const viewUniform = new XenoGL.Uniform('view');
const projectionUniform = new XenoGL.Uniform('projection');

modelUniform.setMatrix(model);
projectionUniform.setMatrix(projection);

program.addUniform(modelUniform);
program.addUniform(viewUniform);
program.addUniform(projectionUniform);

データをセットするには、setValue(value, type)、setVector(vector, type)、setMatrix(matrix)を使います。作ったらプログラムに追加しましょう。後から値をセットし直すこともできます。

Vertex Array Object

Vertex Array Object(VAO)も使えます。これもnewしてください。

const buffer = new XenoGL.ArrayBuffer({
  dataOrLength: particleInitialDataF32,
  attributes: [positionAttr, velocityAttr, ageAttr, lifeAttr],
  dataType: XenoGL.FLOAT,
  usage: XenoGL.DYNAMIC_COPY
});

// 第二引数はオプショナルです
const vao = new XenoGL.VertexArrayObject(buffer, { 
  dataOrLength: particleInitialDataF32, // 初期データ
  attributes: [positionAttr, velocityAttr] // 有効化するアトリビュート
});

// プログラムに追加します
program.addVertexArrayObject(vao);

第一引数にバッファを、第二引数(省略化)にオプションを指定します。あとはプログラムに追加するだけです。

複数のVAOを追加したとき、別のVAOを有効化するには、program.activateVertexArrayObject(vao)を使います。

program.activateVertexArrayObject(vao);

Uniform Buffer Object

Uniform Buffer Objectはuniform変数をバッファとして扱える機能です。プログラムの個数だけ作って、それぞれのプログラムに追加します。

// バッファを作成する
const sharedUniformBuffer = new XenoGL.UniformBuffer({
  dataOrLength: new Float32Array([1.0, 0.0, 0.0, 1.0]),
  dataType: XenoGL.FLOAT
});

// Uniform Buffer Objectを作成する
const ubo1 = new XenoGL.UniformBufferObject('param', sharedUniformBuffer);
const ubo2 = new XenoGL.UniformBufferObject('param', sharedUniformBuffer);

// プログラムに追加する
program1.addUniformBufferObject(ubo1);
program2.addUniformBufferObject(ubo2);

Transform Feedback

Transform Feedbackを利用するには、まずプログラム作成時にfeedbackVaryingsを指定します。

const program = new XenoGL.Program({
  vertexShader: vs,
  fragmentShader: fs,
  feedbackVaryings: ['vertexPosition', 'vertexVelocity', 'vertexAge', 'vertexLife'], // フィードバックする変数
  feedbackBufferMode: XenoGL.INTERLEAVED_ATTRIBS // XenoGL.SEPARATE_ATTRIB か XenoGL.INTERLEAVED_ATTRIBS
});

そしてTransformFeedbackを作成し、コンテキスト(プログラムではないです!)に追加します。

const tf = new XenoGL.TransformFeedback();
xgl.addTransformFeedback(tf);

あとは適当なバッファを作り、tf.feedbackメソッドでフィードバックします。

tf.feedback({
  mode: XenoGL.POINTS,
  targetBuffers: [buffer], // 書き込むバッファの配列
  count: 100 // 計算回数
});

テクスチャ

まだサポートしてないです。サポートしたらここに追記します。

(2018/02/13 追記)テクスチャをサポートしました。

テクスチャを使用するには、使いたい画像を読み込み、XenoGL.Texture2Dをnewします。Texture2Dオブジェクトを作成したらコンテキストに追加します。

const textureSource = await fetch('texture-300x300.png').then((res) => res.blob())
                                                        .then((blob) => createImageBitmap(blob));
const texture = new XenoGL.Texture2D(textureSource);
xgl.addTexture(texture);

テクスチャのソースとして使えるのはimg要素、canvas要素、video要素、ImageBitmap、ImageData、ArrayBufferViewです。

細かいオプションを指定して作成することもできます。

const texture = new XenoGL.Texture2D(textureSource, {
    target: XenoGL.TEXTURE_2D,
    mipmapLevel: 0,
    internalFormat: XenoGL.RGBA,
    format: XenoGL.RGBA,
    dataType: XenoGL.UNSIGNED_BYTE,
    width: 500,
    height: 500
});

複数のテクスチャを使用する場合、xgl.activateTextureを使用します。

xgl.activateTexture(texture2);

その他

clearとかenableとか使えます。

xgl.clearColor(0.0, 0.0, 0.0, 1.0);
xgl.clear(XenoGL.COLOR_BUFFER_BIT | XenoGL.DEPTH_BUFFER_BIT);
xgl.enable(XenoGL.RASTERIZER_DISCARD);
xgl.disable(XenoGL.RASTERIZER_DISCARD);

APIドキュメント

より詳しくは、APIドキュメント(https://kotofurumiya.github.io/xenogl/)をご覧ください。

今後の展望

まだまだ作り始めたばかりで機能が全然足りてないですが、徐々に拡充していく予定です。

要望やバグ報告などはTwitterか、GitHubのissue(https://github.com/kotofurumiya/xenogl/issues)にどうぞ。issueは日本語で大丈夫です。