@h_doxas さんが講師のWebGLスクールの第二期に参加してきました。
復習も兼ねて、メモ書きをば。
免責事項
3Dや数学の専門家ではないので、所々おかなしな記述があるかもしれません。
予め、ご承知おきください。
レンダリングパイプラインについて
レンダリングパイプラインとは、コンピュータグラフィックスが画面に表示されるまでの一連の流れ
のこと。
具体的には下記のような流れとなります。
3Dモデルデータ -> モデル座標変換 -> ビュー座標変換
-> 投影変換・クリッピング -> ビューポート変換
-> ラスタライズ -> カラー合成など -> ディスプレイに表示
WebGLではこの内、モデル座標変換〜ビューポート変換およびカラー合成をシェーダによって制御することが可能です。
それぞれの工程について、簡単に補足すると下記のようおになります。
3Dモデルデータ
画面に表示する3Dモデルを定義する工程。
WebGLでは、Float32Array
というTypedArrayで渡してあげる必要があります。
詳しいいきさつは分かりませんが、Float32ArrayはC言語のFloat相当なので、GPUで動作させるための工夫ではないかと考えています...
実際には定義する場合は、下記のようにx, y, zがセットになった配列で定義することになります。
// 左から順にx, y, z
// ↓の例の場合、三角形が描画される。
var position = [
-1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
1.0, 0.0, 0.0
];
// WebGLのコンテキストに渡す際はFloat32Arrayにキャストしてあげる
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
モデル座標変換
疑似的な3次元空間にアプリケーションから送られてきたデータを配置する工程。
ビュー座標変換
疑似的な3次元空間に置かれたカメラを定義する工程。
カメラの向きや、注視点、カメラの位置をここで定義します。
投影変換・クリッピング
カメラから見える範囲を定義する工程。
現実世界のカメラに例えるなら、広角レンズにするのか望遠レンズにするのかといった具合です。
現実世界と同様に広角にすると被写体が歪んだりします。
ビューポート変換
描画エリアに合わせた変換が行われる工程。
ラスタライズ
ディスプレイの画素一つ一つに色を割り当てる工程。
テクスチャリングやカラー合成
入力された画像のデータを貼り付けたり、色を合成したりする工程。
ディスプレイに表示
そのまんまの工程となります。
WebGLで利用できるシェーダについて
WebGLでは下記2種類のシェーダを利用することができます。
頂点シェーダ
座標変換を担当するシェーダになります。
モデルの移動やカメラの定義など各種座標変換ができるシェーダです。
フラグメントシェーダ
最終的に出力される画素の一つ一つにどのような色を出力すればよいか定義できるシェーダとなります。
WebGLを動かしてみる
というわけで、実際にWebGLを動かしてみましょう。
実際の講義ではdoxas/glcubic.jsという@h_doxas さん謹製のライブラリを使ってWebGLの動作を検証しました。
ここではあえて、生のJavaScriptでWebGLを書いてみます。
補足
行列計算に、toji/gl-matrixというライブラリを使用しています。
流石にそこまで生のJSで書く元気はありませんでした...^^;
// app.js
(function() {
function main() {
var c = document.getElementById('canvas');
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
var canvasSize = Math.min(this.innerWidth, this.innerHeight);
c.width = canvasSize;
c.height = canvasSize;
var vs = createShader('vs');
var fs = createShader('fs');
if (!vs || !fs) {
return;
}
var program = createProgram([vs, fs]);
var locations = new Array(2);
locations[0] = gl.getAttribLocation(program, 'position');
locations[1] = gl.getAttribLocation(program, 'color');
var strides = [3, 4];
var position = [
-1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
1.0, 0.0, 0.0
];
// 色情報、左から順にRGBA
var color = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0
];
// vboの作成
var positionVbo = createVbo(position);
gl.bindBuffer(gl.ARRAY_BUFFER, positionVbo);
gl.enableVertexAttribArray(locations[0]);
gl.vertexAttribPointer(locations[0], strides[0], gl.FLOAT, false, 0, 0);
var colorVbo = createVbo(color);
gl.bindBuffer(gl.ARRAY_BUFFER, colorVbo);
gl.enableVertexAttribArray(locations[1]);
gl.vertexAttribPointer(locations[1], strides[1], gl.FLOAT, false, 0, 0);
var mMatrix = mat4.identity(mat4.create());
var vMatrix = mat4.identity(mat4.create());
var pMatrix = mat4.identity(mat4.create());
var vpMatrix = mat4.identity(mat4.create());
var mvpMatrix = mat4.identity(mat4.create());
// ビュー座標変換
mat4.lookAt(vMatrix, [0.0, 1.0, 3.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
// 投影変換・クリッピング
mat4.perspective(pMatrix, 90.0, 1, 0.1, 100.0);
// かける順番に注意
mat4.multiply(vpMatrix, pMatrix, vMatrix);
mat4.multiply(mvpMatrix, mMatrix, vpMatrix);
var uniLocation = gl.getUniformLocation(program, 'mvpMatrix');
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.viewport(0, 0, c.width, c.height);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
function createShader(id) {
var shaderSrouce = document.getElementById(id);
var shader;
if (!shaderSrouce) {
console.error('指定された要素が存在しません');
return;
}
switch(shaderSrouce.type){
case 'x-shader/x-vertex':
shader = gl.createShader(gl.VERTEX_SHADER);
break;
case 'x-shader/x-fragment':
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
default :
return;
}
gl.shaderSource(shader, shaderSrouce.text);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
return shader;
} else {
console.error(gl.getShaderInfoLog(shader));
}
}
function createProgram(shaders) {
var program = gl.createProgram();
shaders.forEach(function(shader){ gl.attachShader(program, shader); });
gl.linkProgram(program);
if(gl.getProgramParameter(program, gl.LINK_STATUS)){
gl.useProgram(program);
return program;
}else{
console.error(gl.getProgramInfoLog(program));
}
}
function createVbo(data) {
var vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return vbo;
}
}
this.addEventListener('load', main);
})();
<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>WebGL SAMPLE</title>
<script id="vs" type="x-shader/x-vertex">
attribute vec3 position;
attribute vec4 color;
uniform mat4 mvpMatrix;
varying vec4 vColor;
void main(void){
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
</script>
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main(void){
gl_FragColor = vColor;
}
</script>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="text/javascript" src="./js/gl-matrix-min.js"></script>
<script type="text/javascript" src="./js/app.js"></script>
</body>
</html>
実行例
言わずもがな...ですが、すごい分量になります。
これは単純な三角形を画面に描画しているのですが、それだけでもこの分量です。
WebGLの辛さを肌で感じてもらうために、今回あえて生で書いてみました。
特別な理由がない限り、doxas/glcubic.jsのようなヘルパーライブラリをつかうことをお勧めします。
実際に動かしてみたいかたは、dorayaki-kun/practice-webglからZIPファイルをダウンロードしてみてください。
ちょっとだけWebGLで画像を描画する手順を解説します。
まずおおまかに必要な手順は、以下の通りです。
- WebGLコンテキストの取得
- シェーダオブジェクトの生成
- プログラムオブジェクトの生成
- VBOの生成
- 行列計算
- 行列をuniformに紐づけ
- 画像の描画
WebGLコンテキストの取得
WebGLを利用するためには、WebGLコンテキストを取得する必要があります。
下記のような方法で、取得することができます。
var c = document.getElementById('canvas');
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
ここでなぜ、webgl
とexperimental-webgl
の二つを指定しているのかと言うとブラウザによって、webgl
ではWebGLコンテキストを取得することができないためです。
現在では大抵のブラウザはwebgl
でコンテキストを取得できますが、念のためexperimental-webgl
も指定しておくことが望ましいです。
シェーダオブジェクトの生成
シェーダオブジェクトは下記のように生成します。
ここでは、htmlに定義したGLSLの実装を読み込んでシェーダオブジェクトを生成しています。
ここでのポイントはシェーダを囲むscript
タグのMIMEにx-shader/x-vertex
もしくは、x-shader/x-fragment
と記載することです。
これはブラウザにシェーダがJavaScriptではないことを教えるための工夫となります。
var program = createProgram([vs, fs]);
// 中略
function createShader(id) {
var shaderSrouce = document.getElementById(id);
var shader;
if (!shaderSrouce) {
console.error('指定された要素が存在しません');
return;
}
switch(shaderSrouce.type){
case 'x-shader/x-vertex':
shader = gl.createShader(gl.VERTEX_SHADER);
break;
case 'x-shader/x-fragment':
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
default :
return;
}
gl.shaderSource(shader, shaderSrouce.text);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
return shader;
} else {
console.error(gl.getShaderInfoLog(shader));
}
}
<script id="vs" type="x-shader/x-vertex">
attribute vec3 position;
attribute vec4 color;
uniform mat4 mvpMatrix;
varying vec4 vColor;
void main(void){
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
</script>
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main(void){
gl_FragColor = vColor;
}
</script>
プログラムオブジェクトの生成
プログラムオブジェクトとはJS世界とシェーダ世界をつなぐ役目を担うオブジェクトです。
下記のような書き方で、実装ができます。
var program = createProgram([vs, fs]);
// 中略
function createProgram(shaders) {
var program = gl.createProgram();
shaders.forEach(function(shader){ gl.attachShader(program, shader); });
gl.linkProgram(program);
if(gl.getProgramParameter(program, gl.LINK_STATUS)){
gl.useProgram(program);
return program;
}else{
console.error(gl.getProgramInfoLog(program));
}
}
VBOの生成
VBO(Vertex Buffer Object)は頂点情報を格納するバッファです。
ここにモデルの頂点情報や色情報を格納してシェーダに情報を伝達することができます。
VBOの生成は下記のように書くことができます。
ロケーションの取得が、JSしか触ったことがない方には理解が難しいかも...です。
var locations = new Array(2);
locations[0] = gl.getAttribLocation(program, 'position');
locations[1] = gl.getAttribLocation(program, 'color');
var strides = [3, 4];
var position = [
-1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
1.0, 0.0, 0.0
];
// 色情報、左から順にRGBA
var color = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0
];
// vboの作成
var positionVbo = createVbo(position);
gl.bindBuffer(gl.ARRAY_BUFFER, positionVbo);
gl.enableVertexAttribArray(locations[0]);
gl.vertexAttribPointer(locations[0], strides[0], gl.FLOAT, false, 0, 0);
var colorVbo = createVbo(color);
gl.bindBuffer(gl.ARRAY_BUFFER, colorVbo);
gl.enableVertexAttribArray(locations[1]);
gl.vertexAttribPointer(locations[1], strides[1], gl.FLOAT, false, 0, 0);
// 中略
function createVbo(data) {
var vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return vbo;
}
行列計算
3Dプログラミングでは、モデルの移動や拡大・縮小に行列をよく使用します。
この実装はやりたいことによって、ガラリと変わってしまうので注意です。
var mMatrix = mat4.identity(mat4.create());
var vMatrix = mat4.identity(mat4.create());
var pMatrix = mat4.identity(mat4.create());
var vpMatrix = mat4.identity(mat4.create());
var mvpMatrix = mat4.identity(mat4.create());
// ビュー座標変換
mat4.lookAt(vMatrix, [0.0, 1.0, 3.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
// 投影変換・クリッピング
mat4.perspective(pMatrix, 90.0, 1, 0.1, 100.0);
// かける順番に注意
mat4.multiply(vpMatrix, pMatrix, vMatrix);
mat4.multiply(mvpMatrix, mMatrix, vpMatrix);
行列をuniformに紐づけ
計算した行列をuniform
というものに紐づけます。
これをやらないと、せっかくの計算結果もシェーダに反映されないので注意です。
var uniLocation = gl.getUniformLocation(program, 'mvpMatrix');
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
画像の描画
やっとこさ画像の描画...です。
実装は下記のような書き方でOKです。
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.viewport(0, 0, c.width, c.height);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
gl.drawArrays()
には頂点の結び方を指定できます。
これをプリミティブ形状と呼びます。
WebGLで指定できるプリミティブ形状は下記の通りです。
- gl.LINES
- gl.LINE_STRIP
- gl.LINE_LOOP
- gl.TRIANGLES
- gl.TRIANGLE_STRIP
- gl.TRIANGLE_FAN
慣れない内は、gl.TRIANGLES
一択で良いと思います。
最後に
今回は内容が濃いものだったので、かなり端折ったメモとなっています。
今回省いたものとして、以下のような事項が挙げられます。
- 深度テスト
- 頂点インデックス
もっとWebGLについて知りたいという方は下記サイトを見ていただくか
WebGLスクールに参加することをお勧めします。