Three.jsからWebGLまで行きて帰りし物語

2019/12/20
このエントリーをはてなブックマークに追加

WebGL Advent Calendar 2019の20日目の記事です。

はじめに

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: WebGLAttributesWebGLGeometriesをまとめて管理する
  • WebGLProgram: シェーダープログラムを管理する
    シェーダーを文字列として組み替えたり置換したり、設定に応じていろいろやった上で、attachShader()とかlinkProgram()とかします。

二つの塔

WebGLRenderer.render()

それではさっそくWebGLRendererrender()から見ていきます。なお、ソースコードは抜粋で、宗教上の理由により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ではなくWebGLIndexBufferRendererrender()メソッド内でgl.drawElements()が呼び出されます。2種類のドローコール、2つの塔ですね。

王の帰還

Three.jsのrender()メソッドから実際にドローコールたどり着くまでをざーっと見てみました。駆け足すぎて自分でも正直なにがなんだかという気持ちもありますが、とりあえず雰囲気は理解できました。今後はThree.jsをこれまでより落ち着いた気持ちで使える気がします。さあ、戻ってきただよ。

追補編

株式会社カブクでは生WebGLをバリバリ触れる開発者(業務で使うとは言ってない)を募集しています。

その他の記事

Other Articles

2022/06/03
拡張子に Web アプリを関連付ける File Handling API の使い方

2022/03/22
<selectmenu> タグできる子; <select> に代わるカスタマイズ可能なドロップダウンリスト

2022/03/02
Java 15 のテキストブロックを横目に C# 11 の生文字列リテラルを眺めて ECMAScript String dedent プロポーザルを想う

2021/10/13
Angularによる開発をできるだけ型安全にするためのKabukuでの取り組み

2021/09/30
さようなら、Node.js

→
←

関連職種

Recruit

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く