ピュアなWebGLでDigitalCubeのロゴ描いてみた!
https://i.gyazo.com/7484f6bc6bd783061d3ea4b6b831e67f.gif
この資料はwgld.orgのdhoxasさんの図やコードを参照させていただいてます.
WebGLとは
JavaScriptでキャンバスにリアルタイムにCGを描画できるAPI.
webサイトにインタラクティブなCGを埋め込んだり,UIの装飾をしたりできます.
WebGLはブラウザに解釈されOpenGL ESというネイティブのグラフィクスAPIに変換されて実行される.ブラウザで動くけどネイティブ並みに高速!
ネイティブで動いているAPIと使い方はほぼ同じ.webGLがわかればOpenGLもわかるようになる.webGLを学ぶモチベーションの一つはネイティブと遜色ないAPIを学んでCGの描画の原理を理解すること.
今回やったこと
DigitalCubeのロゴの頂点データを作成.これをwebGLに配列として渡してポリゴンとして描画しました.
頂点データはホワイトボードにかいて確認しながら配列に手打ちしました.超アナログ
https://scrapbox.io/files/62e2b08b4f9c37001fbd5748.jpg
ロゴのデータがポリゴンとして描画されるまで
webGLでのポリゴンの描画は主に下の図のような流れで行われます.
https://scrapbox.io/files/62e2b411f85880002198b727.png
1. まず3Dモデルデータ(頂点座標や色)の配列をGPUに渡す.
GPUに直接値を渡すことはできないのでCPU上のVBO(Vertex Buffer Object)に値を入れる.
VBOとGPU側のバーテックスシェーダーというプログラムの変数とを結びつけて頂点データが渡される.https://scrapbox.io/files/62e2b76a23dcda0023422a1d.png
2. 渡された頂点データに対してモデル行列,ビュー行列,プロジェクション行列の3つの行列による座標変換を行う.この変換にはGPUで動くバーテックスシェーダーというプログラム使う.
モデル座標変換:頂点を三次元空間内で動かす.平行移動,拡大・縮小,回転など.https://scrapbox.io/files/62e2bcde3a91f700232a2054.png
ビュー座標変換:三次元空間のどの位置からどの位置を見ているかなど視点を考慮した変換を与える.「視点によってどのような影響が出るのか」を算出してくれる.
プロジェクション座標変換:三次元の広がりのある空間に定義された頂点を平面であるスクリーンに投影するための変換.(まだよくわかっていない)
3. 頂点を結んでできたポリゴンを画面に映し出されるピクセルへと落とし込むます.これをラスタライズと言います.そして,ラスタライズで描画されることが決まったピクセルの色をフラグメントシェーダーで計算し決定する.
https://scrapbox.io/files/62e2b9e0aa2fb4001d8b757f.png
4. カラー合成,深度テストなどの処理
5. やっとディスプレイに出力!
今回描画に用いたコードの一部を見てみる
キャンバスからwebGLコンテキストを取得
code: script.js
/**
* 初期化処理を行う
*/
init() {
// canvas エレメントの取得と WebGL コンテキストの初期化
this.canvas = document.getElementById('webgl-canvas');
this.gl = WebGLUtility.createWebGLContext(this.canvas);
...中略...
}
表示するロゴの頂点の色と座標の配列を用意してVBOに渡す.
code:script.js
/**
* 頂点属性(頂点ジオメトリ)のセットアップを行う
*/
setupGeometry() {
// 頂点座標の定義
this.position = [
0.05, 0.2, 0.0,
0.15, 0.2, 0.0,
-0.15, 0.1, 0.0,
0.05, 0.1, 0.0,
0.15, 0.1, 0.0,
-0.15, 0.0, 0.0,
-0.05, 0.0, 0.0,
0.05, 0.0, 0.0,
0.15, 0.0, 0.0,
-0.05, -0.1, 0.0,
0.05, -0.1, 0.0,
0.15, -0.1, 0.0,
-0.15, -0.2, 0.0,
-0.05, -0.2, 0.0,
0.15, -0.2, 0.0,
];
// 頂点の色の定義
this.color = [
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0,
];
// VBOを生成する
this.DClogoVBO = [
WebGLUtility.createVBO(this.gl, this.position),
WebGLUtility.createVBO(this.gl, this.color),
];
}
描画前に背景色などをセットし,各種バッファを初期化.
code:script.js
/**
* レンダリングのためのセットアップを行う
*/
setupRendering() {
const gl = this.gl;
// ビューポートを設定する
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
// クリアする色と深度を設定する
gl.clearColor(0.8, 0.8, 0.8, 1.0);
gl.clearDepth(1.0);
// 色と深度をクリアする
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
モデル,ビュー,プロジェクション座標変換を行う行列を用意.
code:script.js
/**
* レンダリングを行う
*/
render() {
... 中略 ...
// レンダリングのセットアップ
this.setupRendering();
// - 各種行列を生成する ---------------------------------------------------
const m4 = WebGLMath.Mat4;
const v3 = WebGLMath.Vec3;
// モデル座標変換行列
const rotateAxis = v3.create(0.0, 1.0, 0.0); // Y 軸回転を掛ける
const scaleVec = v3.create(4.0, 4.0, 4.0); // Y 軸回転を掛ける
let m = m4.rotate(m4.identity(), nowTime, rotateAxis); // 時間の経過が回転量
m = m4.scale(m,scaleVec); //拡大
// ビュー座標変換行列
const eye = v3.create(0.0, 0.0, 3.0); // カメラの位置
const center = v3.create(0.0, 0.0, 0.0); // カメラの注視点
const upDirection = v3.create(0.0, 1.0, 0.0); // カメラの天面の向き
const v = m4.lookAt(eye, center, upDirection);
// プロジェクション座標変換行列
const fovy = 45; // 視野角(度数)
const aspect = window.innerWidth / window.innerHeight; // アスペクト比
const near = 0.1; // ニア・クリップ面までの距離
const far = 10.0; // ファー・クリップ面までの距離
const p = m4.perspective(fovy, aspect, near, far);
// 行列を乗算して MVP 行列を生成する(掛ける順序に注意)
const vp = m4.multiply(p, v);
const mvp = m4.multiply(vp, m);`
// ------------------------------------------------------------------------
シェーダーに変換行列を渡す.
code:script.js
... 中略 ...
// ロケーションを指定して、uniform 変数の値を更新する
gl.uniformMatrix4fv(this.uniformLocation.mvpMatrix, false, mvp);
gl.uniform1f(this.uniformLocation.time, nowTime);
先ほど用意した頂点データを持ったVBOをGPUのシェーダー内のattribute変数という変数に渡して, gl.drawElementsでポリゴンとして描画.
code:script.js
// attribute location の取得
this.attributeLocation = [
gl.getAttribLocation(this.program, 'position'),
gl.getAttribLocation(this.program, 'color'),
];
// attribute のストライド
this.attributeStride = [
3,
4,
];
... 中略 ...
// VBOを設定し、描画する
WebGLUtility.enableBuffer(gl, this.DClogoVBO, this.attributeLocation, this.attributeStride, this.DClogoIBO);
gl.drawElements(gl.TRIANGLES, this.indices.length, gl.UNSIGNED_SHORT, 0);
}
}
描画完了!
https://i.gyazo.com/7484f6bc6bd783061d3ea4b6b831e67f.gif
長かった...普段目に見ているCGのポリゴンはこんなにたくさんの手順で描画されています.
ライブラリを使うだけならこんな低レイヤの知識は必要ないが,表現力を高めるために根本の原理から理解が必要になってくることがある.
アニメっぽい影をつける
ものの質感を自由に表現する
流体っぽい表現
早くイケてるwebGL作品を作ってシェアしたいです!ご清聴ありがとうございました!