先取り編 OpenGLとWebGL――CPUの処理をGPUへ
目標:第1・2章でCanvas 2DとJavaScriptに書いた処理がOpenGL・WebGLのどこに移るかを理解する。WebGL 2で深度テストを切り替え、背面カリングとの違いを実験する。 学習順の注意:本編第3章(ラスタライズとZ-buffer)は未公開。ここは先取り編で、番号順の必修章ではない。 操作サンプル / 第2章(面とカリング)
1. 「立方体を描く」を誰に任せるか
第1章では頂点の回転と透視投影をJavaScriptで計算し、Canvas 2Dで線を描いた。第2章では面法線を計算し、描く面を選んだ。しかし大量の三角形を毎フレーム処理するには、GPUの利用が重要になる。
OpenGLは、アプリケーションがグラフィックスハードウェアに頂点・描画状態・シェーダーを渡すためのAPIである。WebGLはOpenGL ESに基づき、ブラウザのJavaScriptからCanvasへ描画するAPIである。デスクトップ版OpenGLをそのままWebに移植したものではない。
処理の概略:
- 頂点バッファをGPUへ渡す。
- 頂点シェーダーで座標を変換する。
- 三角形を組み立て、クリッピング・ラスタライズする。
- フラグメントシェーダーで色を計算する。
- 深度テストなどを経て、フレームバッファに書く。
第1章の「回転と投影」は主に手順2へ移る。第2章の「裏面を消す」はGPUの背面カリングへ移る。カリング(裏向き面の除外)と深度テスト(奥行きによる遮蔽)は別物である。
2. OpenGLの歴史:固定機能とプログラム可能な処理
OpenGLは1992年から続くグラフィックスAPI。初期のOpenGLでは、座標変換や照明計算は固定機能パイプラインとして用意され、アプリケーションは設定値を渡した。
以下は古いOpenGLの描き方(レガシーAPI。現代のCore Profileでは使えない)。
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -5.0f);
glBegin(GL_TRIANGLES);
glVertex3f(-1.0f, -1.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 0.0f);
glVertex3f( 0.0f, 1.0f, 0.0f);
glEnd();
その後、プログラマブルシェーダーで自分の頂点変換・色計算を記述する方向に変わった。GLSL(OpenGL Shading Language)と頂点バッファを使う。OpenGL 3.0で従来の固定機能は非推奨となり、OpenGL 3.1以降のCoreからは除かれた(互換プロファイルには残る)。
// 現代的なデスクトップOpenGLの描画呼び出しの一部分
glUseProgram(shaderProgram);
glBindVertexArray(vao);
glEnable(GL_DEPTH_TEST);
glDrawArrays(GL_TRIANGLES, 0, 36);
これらはネイティブアプリ用C/C++の断片で、ブラウザでは直接動かない。プログラムやVAOの準備も省略している。
3. OpenGL / OpenGL ES / WebGLの整理
- OpenGL:デスクトップ等で使うネイティブ向けAPI。レガシーからCore Profileまで歴史が長い。
- OpenGL ES:組込み・モバイル向けの別仕様。デスクトップOpenGLとは完全互換ではない。
- WebGL 1.0:OpenGL ES 2.0系。GLSL ES 1.00、Canvasの
getContext('webgl')。 - WebGL 2.0:OpenGL ES 3.0系。GLSL ES 3.00、
getContext('webgl2')。VAOやインスタンシング等を標準で扱う。
WebGLはブラウザの安全性を保つための検証・制約・拡張機構を持ち、WebGL 2 = デスクトップOpenGL 4.xではない。ブラウザやGPUにより使える機能も異なる。
4. WebGL 2と深度テスト
WebGLはCanvas 2Dとは異なる描画コンテキストを使う。次は初期化の一部分。
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl2', { depth: true });
if (!gl) throw new Error('WebGL 2 が利用できません');
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS);
gl.enable(gl.CULL_FACE);
gl.frontFace(gl.CCW);
gl.cullFace(gl.BACK);
// シェーダー、バッファ、属性、描画呼び出しは別途必要
WebGLの深度テストは、新しいフラグメントの深度とその画素に保存された値を比較する。標準的な深度バッファで LESS を用いる場合は、概念的に次の判定に近い。
実際には透視投影と深度範囲への変換、バッファの精度、比較関数なども関係する。本編第3章ではこの仕組みを自前で実装する。
5. 頂点とフラグメントの2種類のシェーダー
WebGL 2ではGLSL ES 3.00を使う。頂点シェーダー:
#version 300 es
in vec3 aPosition;
uniform mat4 uProjection;
void main() {
gl_Position = uProjection * vec4(aPosition, 1.0);
}
フラグメントシェーダー:
#version 300 es
precision highp float;
out vec4 outColor;
void main() {
outColor = vec4(0.4, 0.85, 1.0, 1.0);
}
第1章の透視投影は uProjection に含められる。GPUは三角形内をラスタライズし、各フラグメントの色を計算する。ただしシェーダーだけでは画面は描けず、頂点属性・バッファ・プログラム・描画呼び出し等が必要。
WebGL 1のGLSL ES 1.00では attribute、varying、gl_FragColor を使う。WebGL 2の in・out の例と混ぜるとコンパイルエラーになる。
6. 2個の立方体で試す
WebGL 2の実験を開く。手前が橙色、奥が水色で、初期状態では手前を先に、奥を後に描く。
- 深度テストON:後から描いた奥の面が手前を不正に上書きしない。
- 深度テストOFF:後から描いた奥の水色が橙色を塗りつぶす。
- 描画順を反転:深度テストなしでも一部が正しく見えるが、面が交差する複雑な場面を順番だけで解決できない。
- 背面カリングOFF:裏向きの三角形も描く。カリングだけでは他物体の遮蔽を扱えない。
- 回転角変更:同じバッファから頂点シェーダーで座標を変換し直す。
このサンプルではWebGLが実装済みのZ-bufferを使う。第3章で自分で実装する理由は、APIの機能とアルゴリズムの区別を理解するためである。
7. Three.jsとWebGPUはどこにつながるか
Three.jsは、カメラ・シーン・メッシュ・材質等を扱いやすくする高水準のライブラリで、WebGLなどの描画バックエンドを利用する。一方WebGPUはWebGLの別名や「WebGL 3」ではない。GPUパイプラインやバインドグループ、計算処理を明示的に扱う別のWeb APIで、主にWGSLを使う。
8. 演習と一次資料
- 深度テストOFFで描画順を変えたときの前後関係を説明する。
- 背面カリングとZ-bufferで「消しているもの」がどう違うか説明する。
gl_Positionとgl_FragColorの違い、後者をWebGL 2で使わない理由を調べる。- WebGL 1/2と、それぞれに対応するOpenGL ESのバージョンを確認する。
参考:
- Khronos: WebGLの概要
- Khronos: WebGL 1.0仕様
- Khronos: WebGL 2.0仕様
- Khronos OpenGL Wiki: Fixed Function Pipeline
- MDN: WebGL2RenderingContext
デモは外部ライブラリなしの app.js。WebGL 2が利用できないブラウザでは案内を表示する。