Three.jsからWebGLまで行きて帰りし物語
はじめに
Three.jsガッツリやりたいなぁと思いながら業務では日々、Go言語でジオメトリをこねこねしているあんどうです。
先日立てたThree.js Advent Calendarが無事に全日埋まり、Three.jsを育ててくれたWebGLへの限りなく大きな恩、自分なりに少しでも返すためWebGL Advent Calendarにも参加しようと思い立ったんですが、よく考えてみれば生WebGLなんも分からず。仕方がないのでThree.jsからどんな感じにWebGLのドローコールまで繋がっていくかを解説・・・するふりをしてコード読みながら勉強しようと思います。
プロローグ
Three.jsで立方体を描画するコードを削れるだけ削ると次のようになります。
// シーン
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial();
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// カメラ
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 10);
camera.position.z = 5;
// 描画
const renderer = new THREE.WebGLRenderer();
renderer.setSize(600, 600);
renderer.render(scene, camera);
描画対象の立体物群を木構造で表現したScene
オブジェクトと、3次元空間を2次元の画面に描画するための視点を表すCamera
オブジェクトを用意して、それらをWebGLRenderer
オブジェクトのrender()
メソッドに渡すと、指定されたシーンを指定された視点から眺めた様子が画面に描画されます。
実際に描画を行っているのはこのWebGLRenderer#render()
メソッドなのでこの辺りをゆるゆると見ていきましょう。
旅の仲間
WebGLRenderer
はGLコンテキストに関わる処理を取りまとめるオブジェクトです。内部ではGLコンテキストを管理するためにさらに次のようなオブジェクトを利用しています。
WebGLState
: GLのステートを管理するWebGLAttributes
: GLの属性と対応する頂点バッファを管理するWebGLGeometries
:WebGLAttributes
の頂点バッファを管理するWebGLObjects
:WebGLAttributes
とWebGLGeometries
をまとめて管理するWebGLProgram
: シェーダープログラムを管理する
シェーダーを文字列として組み替えたり置換したり、設定に応じていろいろやった上で、attachShader()
とかlinkProgram()
とかします。
二つの塔
WebGLRenderer.render()
それではさっそくWebGLRenderer
のrender()
から見ていきます。なお、ソースコードは抜粋で、宗教上の理由によりUndoobifyされています。
this.render = function(scene, camera) {
// 準備
// - 描画対象オブジェクトの姿勢を表す行列をposition、rotationなどから再計算
// - 視錐台を再計算
// - 描画対象オブジェクトを透明・不透明などに応じて深度ソート
if (opaqueObjects.length) renderObjects(opaqueObjects, scene, camera);
if (transparentObjects.length) renderObjects(transparentObjects, scene, camera);
// 後処理
}
実際の描画にはrenderObjects()
関数が使用されているようです。不透明か半透明によって描画順序が異なる(不透明なら手前を先に描画、半透明なら奥を先に描画)ので2回呼び出されています。
WebGLRenderer.renderObjects()
renderObjects()
関数の定義は次のような感じです。
function renderObjects(renderList, scene, camera, overrideMaterial) {
for (var i = 0, l = renderList.length; i < l; i ++) {
var renderItem = renderList[i];
var object = renderItem.object;
var geometry = renderItem.geometry;
var material = renderItem.material;
var group = renderItem.group;
renderObject(object, scene, camera, geometry, material, group);
}
}
renderObjects()
関数は要するにrenderList
から一つ一つ要素を取り出して、renderObject()
関数に渡しているだけです。renderList
は先ほど見たとおり描画対象の情報を保持したオブジェクトの配列で、不透明オブジェクトの場合と半透明オブジェクトの場合があります。
WebGLRenderer.renderObject()
renderObject()
関数の定義は次のようになります。
function renderObject(object, scene, camera, geometry, material, group) {
// ...
object.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld);
object.normalMatrix.getNormalMatrix(object.modelViewMatrix);
_this.renderBufferDirect(camera, scene.fog, geometry, material, object, group);
// ...
}
render()
メソッド内ですでにワールド座標系には変換済みでしたが、スクリーン座標系にはまだ変換されていなかったので、ここで変換します。その後はさらにrenderBufferDirect()
メソッドに処理を渡します。BufferとかDirectとか言っているので、今度こそドローコールに到達できそうですが、果たしてどうなるでしょう?
WebGLRenderer.renderBufferDirect()
this.renderBufferDirect = function (camera, fog, geometry, material, object, group) {
var program = setProgram(camera, fog, material, object);
state.setMaterial(material);
// ...
var position = geometry.attributes.position;
var dataCount = position.count;
var rangeStart = geometry.drawRange.start;
var rangeCount = geometry.drawRange.count;
var groupStart = group !== null ? group.start : 0;
var groupCount = group !== null ? group.count : Infinity;
var drawStart = Math.max(rangeStart, groupStart);
var drawEnd = Math.min(dataCount, rangeStart + rangeCount, groupStart + groupCount) - 1;
var drawCount = Math.max(0, drawEnd - drawStart + 1);
var renderer = bufferRenderer;
renderer.setMode(_gl.TRIANGLES);
renderer.render(drawStart, drawCount);
};
setProgram()
関数でマテリアルやライト、オブジェクトの姿勢などに応じてシェーダーが使用するuniform変数を設定します。その後WebGLState
オブジェクトのsetMaterial()
メソッドを使用して、深度バッファ、カラーバッファ、ステンシルバッファなどを設定します。最後にジオメトリの頂点数などから頂点配列のどの範囲を使用するかを決定して、WebGLBufferRenderer
オブジェクトのrender()
メソッドを呼び出します。まだ終わりませんでした。次こそ最後と信じてWebGLBufferRenderer#render()
のコードを見てみましょう。
WebGLBufferRenderer.render()
function render(start, count) {
gl.drawArrays(mode, start, count);
info.update(count, mode);
}
ついにここまで来ました。gl.drawArrays()
の登場です。短いメソッドですがこのGLメソッドの実行でついに3Dシーンが実際に画面に描画されます。
なお、今回は省略していますが、インデックスバッファを使用しているとrenderBufferDirect()
の処理が少し変わり、WebGLBufferRenderer
ではなくWebGLIndexBufferRenderer
のrender()
メソッド内でgl.drawElements()
が呼び出されます。2種類のドローコール、2つの塔ですね。
王の帰還
Three.jsのrender()
メソッドから実際にドローコールたどり着くまでをざーっと見てみました。駆け足すぎて自分でも正直なにがなんだかという気持ちもありますが、とりあえず雰囲気は理解できました。今後はThree.jsをこれまでより落ち着いた気持ちで使える気がします。さあ、戻ってきただよ。
追補編
株式会社カブクでは生WebGLをバリバリ触れる開発者(業務で使うとは言ってない)を募集しています。
その他の記事
Other Articles
関連職種
Recruit