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は主に次のような流れで描画します。
またコードの処理としては次のような流れとなっています。
- WebGLの初期化
- シェーダープログラムを作成
- 頂点情報を準備
- 頂点情報を基に頂点を配置し形成
- 配置した頂点をピクセルに変換
- 描画(表示)
1. WebGLの初期化
WebGLの初期化を行います。
- カラーのクリア
- 深度テストの有効化
- 深度テストの評価方法
- バッファのクリア
- カラーバッファ
- 深度バッファ
2. シェーダープログラムを作成
シェーダーのプログラムを作成します。
- シェーダー用のプログラムを生成
- バーテックスシェーダーのソースを用意してコンパイル
- コンパイルしたバーテックスシェーダーを登録
- フラグメントシェーダーのソースを用意してコンパイル
- コンパイルしたフラグメントシェーダーを登録
3. 頂点情報を準備
頂点情報を準備します。
- 頂点情報(バーテックス)は行列配列として準備
- 頂点情報(バーテックス)を収めるためのバッファを作成
- 作成したバッファを既存のWebGLオブジェクトに紐(ひも)付け
- バッファに頂点情報(バーテックス)を格納
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とも連携することができます。
BabylonJS
Microsoftの従業員が開発したWebGLのフレームワークです。比較的アニメーションが得意で、TypeScriptでも書くことができます。
Grimoire.js
WebGLのフレームワークです。他のライブラリやフレームワークと違って、GOML(Grimoire Object Markup Language)というマークアップ言語と合わせて書きます。TypeScriptでも書くことができ、ReactやAngularとも連携することができます。
p5.js
WebGLのライブラリやフレームワークではありませんが、ブラウザー上でグラフィック表現が行えるライブラリです。Processingというグラフィック表現(分野的にはデジタルアート)が行えるJavaベースの言語で、そのJavaScript版です。アニメーションなんかも比較的簡単に実装が可能で、Reactとも連携することができます。
ライブラリ「Three.js」の例
ライブラリであるThree.jsを使用した例を、簡単にですがご紹介します。
Three.jsは主に次の六つで構成されています。
- 空間(canvas)
- 物体(ジオメトリ、マテリアル、メッシュ)
- カメラ
- シーン(場所)
- ライト(光)
- レンダラー(描画)
空間
ブラウザーでcanvas要素を使用して表現・表示する領域です。
物体
空間に形を持って存在する物です。物体は主に三つで構成されます。
- ジオメトリ(形状)
- マテリアル(色や質感)
- メッシュ(物体)
ジオメトリは形状を表すもので、立方体や球体など、さまざまな種類があります。マテリアルは色や質感といった情報を持ったものです。メッシュはジオメトリとマテリアルを使用して作られた物体です。
カメラ
物体を撮影するための機材(見る視点)です。カメラから映し出されるものがcanvas要素を通してレンダリングされた結果が表示されます。
シーン(場所)
物体を撮影するための場所(ブース)で、ステージともいいます。
ライト(光)
物体に陰影を付ける元で、いわゆる光源です。光源には幾つかの種類があります。
- DirectionalLight
- SpotLight
- AmbientLight
- PointLight
- HemisphereLight
- RectAreaLight
レンダラー(描画)
シーンに置かれたさまざまな要素を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);
});
})();