WebGLによる2Dおよび3Dグラフィック表現

2024年12月10日 Posted 野々瀨(フロントエンドエンジニア)

Webブラウザーではさまざまなグラフィック表現があります。今回はそのうちの一つである「WebGL」について、お話したいと思います。

* この記事は2020年ごろまでの内容であり、最新の内容とは異なる場合があります。

WebGLとは

WebGLとは、Web Graphics Libraryの略称で、Webブラウザー上でグラフィック表現(モデリング)を行うための標準仕様のことです。OpenGL ES 2.0というグラフィック用のAPIがあり、OpenGL ES 2.0の派生規格がWebGLということになります。OpenGLは、GPUによって高速にグラフィック描画処理を行い、画面に出力するための仕組みを提供しているライブラリです。

なお、WebGLは=3次元表現といわれることが多いですが、2次元の表現も行うことができます。

WebGLのバージョン

WebGLには幾つかのバージョンがあり、2020年9月時点ではWebGL 1.0とWebGL 2.0があります。簡単にいうとOpenGLのバージョンに合わせてWebGLのバージョンも変わっていきます。

Canvas API

Webブラウザー上でWebGLを使用して描画を行う場合は、Canvas APIを使用します。

そもそもCanvas APIは、ブラウザー上で2次元の図形を描画することのできる機能です。canvas要素を設置し、JavaScriptによって操作を行います。描画を更新することでアニメーションを表現することもできます。

<canvas width="150" height="150"></canvas>
window.addEventListener('DOMContentLoaded', () => {
  const canvasElem = document.querySelector('canvas');
  const ctx        = canvasElem.getContext('2d');
  const width      = canvasElem.width;
  const height     = canvasElem.height;

  let degree = 0;

  const rotateAnim = () => {
	if (degree >= 360) degree %= 360;

	ctx.save();

	ctx.clearRect(0, 0, width, height);

	ctx.translate(width / 2, height / 2);
	ctx.rotate(degree * (Math.PI / 180));
	ctx.translate(width / 2 * -1, height / 2 * -1);

	ctx.fillStyle = 'rgb(195, 39, 61)';
	ctx.fillRect(25, 25, 100, 100);

	ctx.restore();

	degree += 1;

	window.requestAnimationFrame(rotateAnim);
  };

  window.requestAnimationFrame(rotateAnim);
});

シェーダー

WebGLは「シェーダー」というプログラムを通して描画するAPIです。シェーダーとは、座標の変換処理(3次元の座標から2次元の座標へ変換する処理のこと)を記述するための仕組みのことです。

ポリゴンの頂点情報(X, Y, Z軸の座標)を扱うバーテックスシェーダー(または頂点シェーダー)、描画される際のピクセルの情報を扱うフラグメントシェーダー(またはピクセルシェーダー)の二つのシェーダーを用意する必要があります。

シェーダー自体は自分で用意する必要があり、GLSL(OpenGL Shading Language)という言語で作成します。GLSLはC言語をベースとしていて、GPUの機能を直接操作することができます。ちなみに自前でシェーダーを作ることを「プログラマブルシェーダ」といいます。

主な処理の流れ

WebGLは主に次のような流れで描画します。

頂点情報→バーテックスシェーダー→ラスタライズ→フラグメントシェーダー→レンダリング

またコードの処理としては次のような流れとなっています。

  1. WebGLの初期化
  2. シェーダープログラムを作成
  3. 頂点情報を準備
  4. 頂点情報を基に頂点を配置し形成
  5. 配置した頂点をピクセルに変換
  6. 描画(表示)

1. WebGLの初期化

WebGLの初期化を行います。

2. シェーダープログラムを作成

シェーダーのプログラムを作成します。

  1. シェーダー用のプログラムを生成
  2. バーテックスシェーダーのソースを用意してコンパイル
  3. コンパイルしたバーテックスシェーダーを登録
  4. フラグメントシェーダーのソースを用意してコンパイル
  5. コンパイルしたフラグメントシェーダーを登録

3. 頂点情報を準備

頂点情報を準備します。

  1. 頂点情報(バーテックス)は行列配列として準備
  2. 頂点情報(バーテックス)を収めるためのバッファを作成
  3. 作成したバッファを既存のWebGLオブジェクトに紐(ひも)付け
  4. バッファに頂点情報(バーテックス)を格納

4. 頂点情報を基に頂点を配置し形成

頂点情報を基にバーテックスシェーダーを使って頂点を配置し形成します。

5. 配置した頂点をピクセルに変換

配置した頂点をフラグメントシェーダーを使ってピクセルに変換し色を付けします。

6. 描画(表示)

canvas要素に描画して表示します。

コード例

簡単な三角形を作る場合は、次のような感じのコードになります。

(() => {
  /**
   * 頂点情報
   * @type {number[]}
   */
  const vertices = [
	 0.0,  1.0, 0.0,
	-1.0, -1.0, 0.0,
	 1.0, -1.0, 0.0
  ];

  /**
   * 頂点の数
   * @type {number[]}
   */
  const verticesLength = 3;

  /**
   * バーテックスシェーダーコード
   * @type {string}
   */
  const vertexShaderSrc = `
	attribute vec4 position;

	void main(void) {
	  // 頂点属性
	  gl_Position = position;
	}`;

  /**
   * フラグメントシェーダーコード
   * @type {string}
   */
  const fragmentShaderSrc = `
	void main(void) {
	  // 色を定義
	  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
	}`;

  window.addEventListener('DOMContentLoaded', () => {
	const canvasElem = document.getElementById('stage');

	const gl = canvasElem.getContext('webgl') || canvasElem.getContext('experimental-webgl');

	// 背面カラーを黒色に設定
	gl.clearColor(0, 0, 0, 1);

	// カラーバッファや深度バッファをクリアする
	gl.clear(gl.COLOR_BUFFER_BIT);

	// シェーダーを作成
	const vertexShader   = gl.createShader(gl.VERTEX_SHADER);
	const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

	// シェーダーにソースを割り当てる
	gl.shaderSource(vertexShader, vertexShaderSrc);
	gl.shaderSource(fragmentShader, fragmentShaderSrc);

	// シェーダープログラムをコンパイル
	gl.compileShader(vertexShader);
	gl.compileShader(fragmentShader);

	// コンパイルに成功したかどうか
	if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) return;
	if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) return;

	// シェーダープログラムを作成
	const shaderProgram = gl.createProgram();
	gl.attachShader(shaderProgram, vertexShader);
	gl.attachShader(shaderProgram, fragmentShader);

	// シェーダーをWebGLにリンク
	gl.linkProgram(shaderProgram);

	// シェーダープログラムを描画で利用できるようにする
	gl.useProgram(shaderProgram);

	// バーテックス用のバッファを作成
	gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());

	// 現在のバッファ(バーテックス)に頂点情報を転送
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

	// プログラム(バーテックスシェーダー)からpositionを取得
	const position = gl.getAttribLocation(shaderProgram, 'position');

	// 現在のバッファ(バーテックス)にpositionを割り当てる
	gl.vertexAttribPointer(position, verticesLength, gl.FLOAT, false, 0, 0);

	// positionのバッファを有効化する
	gl.enableVertexAttribArray(position);

	// 三角形の描画モードで0番目からverticesLength個の頂点を描画
	gl.drawArrays(gl.TRIANGLES, 0, verticesLength);
  });
})();

ライブラリ・フレームワーク

WebGLは生の状態で開発しようとしますと、シェーダーを用意したり手順が多かったりと複雑で難易度が高めです。そこでライブラリやフレームワークを使用することで、より簡単にWebGLを用いた実装を行うことができます。

Three.js

現在もっとも有名なWebGLのJavaScriptライブラリです。直観的で扱いやすくドキュメントも豊富なのが特長です。TypeScriptでも書くことができ、Reactとも連携することができます。

https://threejs.org/

BabylonJS

Microsoftの従業員が開発したWebGLのフレームワークです。比較的アニメーションが得意で、TypeScriptでも書くことができます。

https://www.babylonjs.com/

Grimoire.js

WebGLのフレームワークです。他のライブラリやフレームワークと違って、GOML(Grimoire Object Markup Language)というマークアップ言語と合わせて書きます。TypeScriptでも書くことができ、ReactやAngularとも連携することができます。

http://grimoire.gl/

p5.js

WebGLのライブラリやフレームワークではありませんが、ブラウザー上でグラフィック表現が行えるライブラリです。Processingというグラフィック表現(分野的にはデジタルアート)が行えるJavaベースの言語で、そのJavaScript版です。アニメーションなんかも比較的簡単に実装が可能で、Reactとも連携することができます。

https://p5js.org/

ライブラリ「Three.js」の例

ライブラリであるThree.jsを使用した例を、簡単にですがご紹介します。

Three.jsは主に次の六つで構成されています。

空間

ブラウザーでcanvas要素を使用して表現・表示する領域です。

物体

空間に形を持って存在する物です。物体は主に三つで構成されます。

ジオメトリは形状を表すもので、立方体や球体など、さまざまな種類があります。マテリアルは色や質感といった情報を持ったものです。メッシュはジオメトリとマテリアルを使用して作られた物体です。

カメラ

物体を撮影するための機材(見る視点)です。カメラから映し出されるものがcanvas要素を通してレンダリングされた結果が表示されます。

シーン(場所)

物体を撮影するための場所(ブース)で、ステージともいいます。

ライト(光)

物体に陰影を付ける元で、いわゆる光源です。光源には幾つかの種類があります。

レンダラー(描画)

シーンに置かれたさまざまな要素をcanvasを通してレンダリング(描画)します。変化を与えたものを更新する場合でも使用します。

実装例

(() => {
  const stageWidth  = 240;
  const stageHeight = 180;

  window.addEventListener('DOMContentLoaded', () => {
    // レンダラーを作成
    const renderer = new THREE.WebGLRenderer({ antialias : true });
    renderer.setSize(stageWidth, stageHeight);
    document.body.appendChild(renderer.domElement);

    // シーンを作成
    const scene = new THREE.Scene();

    // カメラの作成・設置
    const camera = new THREE.PerspectiveCamera(45, stageWidth / stageHeight, 1, 1000);
    camera.position.set(0, 0, 3);
    camera.lookAt(scene.position);

    // メッシュを作成
    const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
    const material    = new THREE.MeshPhongMaterial({ color : 0x44d576 });
    const mesh        = new THREE.Mesh(boxGeometry, material);
    scene.add(mesh);

    // ライト
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(-1, 1.5, 2);
    scene.add(light);

    // 環境光
    const ambientLight = new THREE.AmbientLight(0x666666);
    scene.add(ambientLight);

    // 描画
    renderer.render(scene, camera);

    /**
     * レンダリングを更新
     * @param {number} time 時間
     */
    const render = time => {
      time *= 0.001;

      // メッシュを回転
      mesh.rotation.y = time;
      mesh.rotation.z = time;

      renderer.render(scene, camera);

      requestAnimationFrame(render);
    };

    requestAnimationFrame(render);
  });
})();