概要
この記事はWebGL Advent Calendar 2015 の5日目の記事です。
WebGLをThree.jsなどを介さずに、直に触ってみたい、あるいはそういうことに既にある程度慣れている層に向けた内容になります。
WebGLをより使いやすくするためのライブラリを自作してみようぜ! という啓蒙(?)記事です。
WebGL Advent Calendar 2015について一言(20151205)
8日、9日、25日がまだ空いていて、もったいないのでとりあえず9日も予約しました。残り時間的に25日にしようかとも思ったんですが、非常に基礎的なことを書くつもりなので、そんなので最終日を飾るのもどうよ、ってことで9日にしました。
で、そうなんです。25日はまだ余裕があるからともかく、8日が空いてるんですよ。どなたか、8日に小ネタでいいので書きませんか?
→ @toyoshim さんが埋めてくださいました!(つか、小ネタどころか大ネタなんですけど!素晴らしい!>▽<)
せっかくなので、WebGL Advent Calendar 2015、みんなで全日埋めましょう!
→ @yomotsu さんが最終日埋めてくださいました!>▽<
動機
さて、既にThree.jsをはじめとした数多くのライブラリがあるのに、なぜ自作のWebGLライブラリを作ろうというのでしょうか?
ご意見は色々ありましょうが、この記事では以下の利点・動機を挙げたいと思います。
- WebGL/3DCGについての知識が深まる(つまり、習作を通して学ぶことが主な目的の一つ)
- ライブラリ・フレームワークの設計知識・センスが深まる
- JavaScriptの知識が深まる
- 直にWebGL APIを使用するよりも、設計を熟知している自作ライブラリを通すことで、何かを作るにあたり生産効率が上がる。
- もし、ライブラリがうまく成長して他の人にも受け入れられた場合、それは貢献となるし自分の実績にもなる。
列挙してわかりますが、主に自己学習がメインであります。
まぁそりゃね。Three.jsとかのライブラリって、ガチで頭の良い人たち/現役のプロ が作っていますから、それに勝るものを作ることはなかなかできるものではありません。
しかし、ライブラリを自作してみることで、先ほど挙げたようなことを沢山学べると思います。
この記事のアプローチ
手前味噌ですが、かくいう私もちょっと前から自作WebGLライブラリ「GLBoost」というものを開発し始めています。
国内では割と有名なJavaScript向け国産ゲームライブラリで「tmlib.js」というものがあるんですが、その次期版として「phina.js」が今月リリースされました。
GLBoostは、な、なんと! そのphina.jsの3D機能として、今後採用される予定になっています。
(が、開発が遅れに遅れていて、実際どうなるか……。がんばらんと……><)
本記事では、私のGLBoostでの事例も紹介しながら、自作WebGLライブラリの開発の実際について紹介していきます。私の事例を通して、「ああ、WebGLライブラリを自作するとはこういう感じなのか」「こういうことを考えないといけないのか」「こういうところが面倒臭いのか」といったことを感じていただき、あなたのライブラリ自作へのモチベーションに繋げていただければ幸いです。
まず、生のWebGLを使って困ること
たっくさんありますね。
- GPUに渡す頂点データを準備するのが面倒臭い。初心者はそのやり方すらわからない。
- WebGLの作法として、いろんなオブジェクトを生成してバインドしないといけない。その作法を覚える必要がある。初心者のつまづきポイントの一つ。
- 何をするにもいちいちシェーダーを別途書かないといけない。初心者は(ry
- モデルビュー変換、プロジェクション変換といった座標変換を間違えると、そもそもオブジェクトが画面外に行ってしまい何も見えない何だこれ問題。初心者はよくこれにハマる。
- WebGLの各種ステートの管理が大変。初心者はこれを把握しきれないことで、「画面に何も映らない」問題が再発。
- ベクトル・行列といった知識は必須(Three.jsなどを使う場合でも必要ではあるが、そういうライブラリはある程度その必要性を緩和してくれる)
- つまり、WebGLを御すること自体にエネルギーが割かれ、肝心のコンテンツがなかなか作れない。
まぁ、主に初心の方だと生のWebGLを触るとまず心が折れる可能性がありますし、慣れた人にとっても、こうした性質は面倒臭い以外の何物でもありません。作りたいのはコンテンツであって、WebGLコードではないのですから。
そこでThree.jsじゃない? というのはこの記事では禁句w そこで自作WebGLライブラリを開発するのですよ。
自作ライブラリの形がある程度できてくると、その効果は劇的です。
テクスチャ貼ったポリゴンを表示したい? 生のWebGLなら、そんな簡単なことでも結構な行数のコードになってしまいますが、例えば私のGLBoostならこんな感じで書けます。
/*
* constant
*/
var SCREEN_WIDTH = 465; // 画面幅
var SCREEN_HEIGHT = 465; // 画面高さ
var SCREEN_CENTER_X = SCREEN_WIDTH/2; // 画面中央X座標値
var SCREEN_CENTER_Y = SCREEN_HEIGHT/2; // 画面中央Y座標値
GLBoost.TARGET_WEBGL_VERSION = 1;
/*
* main
*/
tm.main(function() {
// アプリケーション
var app = tm.webgl.WebGLApp("#world");
app.replaceScene(MainScene());
// 実行
app.run();
});
/*
* scene
*/
var MainScene = tm.createClass({
// three.js 用シーンを継承
superClass: tm.webgl.Scene,
init: function() {
this.superInit();
var positions = [
new tm.geom.Vector3(-0.5, -0.5, 0.0),
new tm.geom.Vector3(0.5, -0.5, 0.0),
new tm.geom.Vector3(-0.5, 0.5, 0.0),
new tm.geom.Vector3(-0.5, 0.5, 0.0),
new tm.geom.Vector3(0.5, -0.5, 0.0),
new tm.geom.Vector3(0.5, 0.5, 0.0)
];
var texcoords = [
new tm.geom.Vector2(0.0, 0.0),
new tm.geom.Vector2(1.0, 0.0),
new tm.geom.Vector2(0.0, 1.0),
new tm.geom.Vector2(0.0, 1.0),
new tm.geom.Vector2(1.0, 0.0),
new tm.geom.Vector2(1.0, 1.0)
];
var mesh = tm.webgl.MeshElement('#world');
var texture = new GLBoost.Texture('resouces/texture.png', '#world');
var material = new GLBoost.ClassicMaterial('#world');
material.diffuseTexture = texture;
mesh.materials = [material];
mesh.setVerticesData({
position: positions,
texcoord: texcoords
});
this.add( mesh );
this.prepareForRender();
}
});
他にも例えば、Objファイルを表示したい? ならGLBoostならこれだけで書けます。
/*
* constant
*/
var SCREEN_WIDTH = 800; // 画面幅
var SCREEN_HEIGHT = 800; // 画面高さ
var SCREEN_CENTER_X = SCREEN_WIDTH/2; // 画面中央X座標値
var SCREEN_CENTER_Y = SCREEN_HEIGHT/2; // 画面中央Y座標値
GLBoost.TARGET_WEBGL_VERSION = 1;
/*
* main
*/
tm.main(function() {
// アプリケーション
var app = tm.webgl.WebGLApp("#world");
app.replaceScene(MainScene());
// 実行
app.run();
});
/*
* scene
*/
var MainScene = tm.createClass({
// three.js 用シーンを継承
superClass: tm.webgl.Scene,
init: function() {
this.superInit();
var objLoader = GLBoost.ObjLoader.getInstance();
var promise = objLoader.loadObj('resouces/teapot/teapot.obj', '#world');
promise.then((mesh)=> {
this.add( mesh );
this.prepareForRender();
});
}
});
ゲームライブラリである「tmlib.js」のコードとかも混ざってますが、そういうのを省けば、本質的な部分はもっと短い行数で済んでいることがわかりますね。
自作ライブラリを使うことで、より少ないコーディングでやりたい処理を記述でき、ライブラリに任せている部分は検証済みの処理なので、思わぬトラブルが発生して時間が無駄に潰れていくこともないわけです。
しかも自分の勉強にもなるというのだから、一石二鳥にも三鳥にもなることがお分かりいただけるかと思います。
自作ライブラリの方向性を決めよう
で、なんとなく作り始めるのもいいんですけど、それよりせっかく作るんですから、方向性を決めましょう。
色々あると思います。
- 初心者でも簡単に使えるように、超高級APIを提供することを重視
- パフォーマンス最優先!
- 玄人が喜ぶような、カスタマイズ性を重視したギーク向け
私が今まで接した中で、enchant.jsのWebGLプラグインであるgl.enchant.jsなどは、1つ目の方向性に該当するでしょうね。
Three.jsは、私の感覚では、これら3つの方向性全てにおいてバランスが取れているような感じです。
ちなみに、私のGLBoostでは3つ目の方向性、ギーク向けを狙いました(そんなギークが唸るような凄いモノを作れるほどお前はギークなのかと言われると困るんですけど)。
どんなライブラリにも共通して求められるマスト機能(と思われるもの)
さて、どの部分から作り始めようか。自分のライブラリの売りになる部分から始めようか、とか色々考えると思うんですが。
まずポリゴンを表示できないと何も始まりませんよね。
というわけで、大抵はまずMeshクラスのような、ユーザーから渡された頂点データを、GPUに送るのに最適な形式に自動的に再構築し、そしてそれをオブジェクトとして簡単に取り扱えるような仕組みを作るのが、最初のステップになると思います。
(最適な形式って何よ? という方は、私の9日目の記事をご覧ください)
あと、カメラクラスとかないと困りますよね。つまり、座標変換を管理してくれるものです。
- GPUに送る頂点データの自動構築・管理
- カメラクラスによる、座標変換の自動化
これがまず最初の2ステップでしょうか。まずこれができて、実際メッシュが表示されたら、その時の喜びはひとしおのはずです。
他にもいろいろあります。
- テクスチャマッピングのサポート
- Lambert, Phongなどの古典シェーディングのサポート
- シーングラフ(ツリー状に3Dオブジェクトを管理し、座標変換を上から継承させていくことで3Dオブジェクト群の位置管理を簡便化するもの)
- その他、バンプマッピング、環境マッピング、影生成など
- 数学関係の機能の提供
最後の数学関連の機能については、別に自分では作らずに、他の数学ライブラリを併用するという手もあるのですが、あえてここを車輪の再開発してみるというのも一つです。気が早い話ですが、ライブラリを使ってもらう人のことを考えると、他ライブラリへの依存はできるだけ避けたいところ。
それだけでなく、やっぱり、勉強になるからなのですね。例えば、プロジェクション変換行列一つとっても、DirectXとOpenGL系ではどうして同じ右手系の関数でも行列内の値が若干違うのかとか、そういう理由を突き詰めていくのもまた勉強になります。
個々の機能について見ていきましょう
もう少し、多くの人が期待する、ライブラリに欲しいと思われるものを詳しく見ていきます。
メッシュ構築・管理機能
基本中の基本となる機能です。理想としては、ユーザーから何気なくポン、と渡された頂点座標、テクスチャ座標、カラー、法線などのデータが入った配列。これらのデータを、GPUが最もパフォーマンスを引き出せるような頂点データのフォーマットに自動的に再構築して、勝手に面倒を見てくれるのがベストです。
ユーザーは、気軽に渡した頂点データからMeshクラスを得ることができ、さらにそれをポン、とレンダラーに渡せば勝手にライブラリが描画してくれる、ってイメージですね。
(どんなのが最適なフォーマットなの? ということについては、私の9日目の記事をどうぞ。これは基礎中の基礎で、まだ最適化の余地はあります)
3Dデータファイルの読み込み
これは、メッシュ構築・管理機能とも密接に関わってくるところです。ファイル形式によって、メッシュデータの保存フォーマットはバラバラですので、それらを自作ライブラリメッシュクラスの内部フォーマットに変換する必要があります。当然、パーサーも書かないといけませんし。いろいろと面倒ですが、データファイルの読み込みができると、一気に表現の幅が広がります。
カメラクラス
カメラの位置、画角、カメラの向く方向などを指定するだけで、自動的にモデルビュー行列とプロジェクション行列を作ってくれるようにします。カメラクラスが作ったこれらの変換行列を、ユーザーが取り出してシェーダーに手動で渡すのも良いのですが、できればそのシェーダーとの繋ぎもライブラリが自動でやってくれるようにするとなお良いですね。
シェーダーコードの柔軟な構築・管理・カスタマイズ
WebGLではシェーダーが表現のキモとなります。しかし、普通にWebGLを扱っていると、やりたい表現を作る上で、ちょっとでも条件が変わると、すぐにシェーダーコード及びJavaScriptとシェーダーの接合部分(uniform変数の取得や設定など)を、それに合う形にいちいち書き直さなければなりません。これは非常にストレスですし、そもそもシェーダーコードのデバッグは面倒(あと、接合部分のコーディングも割と些細な凡ミスに気づくのに時間がかかったりする)なので、多くの場合、こうしたところで非常に多くの時間を浪費してしまいます。
つまり、シェーダーの構築・管理、といった部分は、ライブラリで面倒を見る意義が非常にある部分だと言えます。
手前味噌ですが、私のライブラリ「GLBoost」では、様々な表現をその性質・種類ごとに分け、それぞれに対応するシェーダークラスを作りました。
そのシェーダークラスでは、「その表現に関して必要なシェーダーコードの断片のみ」を保持しています。そして例えば、「ある表現Xは、表現Aと表現B、表現Cを組み合わせることで実現できる」というときは、以下のようにして、シェーダークラスA,B,Cを新規の空シェーダークラスにmixinさせることで、表現Xを実現できるようにしています。
class MyShader extends Shader {
constructor() {
super();
MyShader.mixin(BasicTransform);
MyShader.mixin(LambertShader);
MyShader.mixin(EmbossPostProcess);
}
}
(なぜ、クラスの継承でなくmixinというアプローチにしているかというと、継承のみでやろうとすると、表現の組み合わせが増えるごとに、継承しなければならないパターンが爆発的に増加してしまうからです)
( mixinの仕組みについては、JSBinにデモコードを挙げましたので、ご興味ある方は参考にしてみてください。)
mixinされたシェーダークラスが持っている各シェーダーコードの断片が、ライブラリによって接合され、最終的な表現Xのシェーダーコードになります。
このように、シェーダー断片を組み合わせることにより、多様なシェーダー要件に柔軟に対応する仕組みは、たいていのゲームエンジン・グラフィクスライブラリに実装されています。
他にも例を挙げると、例えばWebGLライブラリの一つ、Babylon.jsでは、大抵の要件を満たせるリッチで大型のシェーダーコードを最初に用意し、そこから、コンパイル時に不要な処理をそぎ落としていく、というアプローチが取られています。
(具体的に言うと、リッチシェーダー内で#ifdefなどのプリプロセッサ分岐が各機能単位で細かく定義されており、シェーダーコンパイル時に、ユーザーの使うマテリアル機能に合わせて必要最小限の#defineがシェーダーコードに挿入される、という感じです)
Babylon.jsでは、さらにこのアプローチを使って、貧弱なWebGL環境に合わせたフォールバックコンパイル機能(最初はリッチなシェーダーオプションでコンパイルを試み、コンパイルに失敗したら幾つかの#defineを省いて、今度はより簡素なバージョンでコンパイルを再度試みる)も実現しているようです。
他にもいろいろなアプローチがあると思いますので、皆様も是非研究されてみてください。
Lambert, Phong, といった反射モデル・マテリアルのサポート。
まぁ、お約束ですね。もちろん、それ以外にもBline、Oren–Nayarやら、もう少し複雑なものをサポートしても構いません。何しろ、デカールテクスチャをただ張って表示するだけではつまらないのでね。きちんとライティングしたいものです。
ちなみにGLBoostでは、HalfLife2などのゲームタイトルで使用されたハーフ・ランバートなんかもサポートしています。物理的には全く正しくないけど、非常に良い感じの陰影になります。実装もカンタンで、「早い安いうまい系」のこういうモノはサクッとサポートしとくが吉ですねw
シーングラフのサポート
シーングラフとは、「3Dオブジェクト群をツリー状に管理し、座標変換を上から継承させていくことで3Dオブジェクト群の位置管理などを簡便化するもの」とでも言えばいいでしょうか。
沢山のオブジェクトが複雑に配置されたシーンを作りたい場合に便利な仕組みです。
以前のOpenGLには、glPushMatrix()とかglPopMatrix()とか、変換行列をスタック領域に一時保管する仕組みがあって便利だったのですが、WebGLにはそういうものはないので、なおさら必要という気もします。
これは、変換座標の合成の仕組みを作れれば、そんなに実現は難しくありません。ただし、描画パフォーマンスを損なう恐れもあるので、いかに最適化を施すか、も肝になってくるかもしれません。
テクスチャへのレンダリングのサポート
Framebuffer Object(FBO)へのレンダリングのことですね。これがサポートできると、表現の幅がかなり広がります。現代のリアルタイムCGの高度な表現は、これ無しでは成り立たないと言ってもいいくらいです。FBOへのバッファの作成・アタッチとかも微妙に面倒だったりするので、こうしたところもライブラリで面倒を見てくれると嬉しい感じです。
マルチパスレンダリングのサポート
まず、シーンのオブジェクトを全て描画することを「描画パス」、と呼ぶことにしましょう。マルチパスレンダリングとは、異なる性質の複数の描画パスを実行することで、最終的なレンダリング結果を得るテクニックのことです。
シェーダーが搭載されていなかったPlayStation2などでは、よくこのマルチパスレンダリングを使って高度な表現が実現されていました(PS2は帯域モンスターだったので、なおさらでした)。もちろん、現在のPS4などの最新ゲーム機でもバリバリ使われています。
テクスチャに対して、最終的なレンダリングに必要なデータを前段階パスでレンダリングし、後段のパスで、そのテクスチャを読み取って最終レンダリングを行う、といった感じです。
このマルチパスレンダリングも、ライブラリで色々と手助けできる部分はあるでしょう。
ちなみにこのマルチパスレンダリング。シーングラフの仕組みとはあまり相性が良くない感じがします。が、サポートする方法はもちろんあります。
GLBoostではどうしているかというと、描画パスクラスというものを用意し、マルチパスレンダリングをする際は、3Dオブジェクトをシーングラフだけでなく、この描画パスクラスにも登録させるようにします。また、この描画パスクラスには、レンダーターゲットとなるバッファを指定できます。こうすることで、「これとこのオブジェクトは1パス目にテクスチャに対して描画、他のオブジェクトは2パス目に描画済みのテクスチャを参照しつつフレームバッファに描画」といった、前述したようなテクニックが可能になります。
影生成のサポート
現在のGPUのレンダリングパイプラインの仕様の性質によるものなんですが、現在の普通のGPU(WebGL)レンダリングでは、3Dオブジェクトの「陰」は作れても、「影」が作れないんです。
そこで、現在の3Dゲーム等では、前述のテクスチャへのレンダリングや、マルチパスレンダリングなどを活用することによって、どノーマルでは本来、実現不可能だった「影」を表現する、ということをうまくやってのけています。
「シャドウマッピング」というキーワードで検索すると色々情報が出てきます。一昔は「ステンシルシャドウボリューム法」とか、別のやり方もあったんですが、現在のゲームの殆どは「シャドウマッピング法」の改良手法に移行しています。
いずれにせよ、リアルな3Dシーンのレンダリングにおいて、影は必要不可欠な表現ですから、ぜひライブラリでサポートしたいものです。
ソフトウェアカリングやキャッシュシステムによるパフォーマンス最適化
WebGLに限らず、GPUを駆動するリアルタイム3D APIは、だいたいが頻繁なステート変更などが原因で実行速度が遅くなります。
- 変換行列の再計算
- WebGLステート
- テクスチャの作成・バインド
- シェーダーのコンパイル・リンク
- Uniform変数の設定
こういったものは、自作のMeshクラスやマテリアルクラスなどが処理を行うとき、過分に実行・変更されがちです。これを監視する仕組みを作り、たいていの場合はエンジン内で記録・吸収し、本当にGPUに伝えなければならないときにのみ、GPUにそれらの変更を送るようにすることが望ましいです。
これらの実装にあたり、よくあるテクニックの一つとして、ダーティ・フラグという仕組みがあります。また例としてGLBoostを引き合いに出しますが、以下はシーングラフにぶら下げる要素の基本クラスとなるElementクラスです(説明用に簡略化してあります)。
export default class Element {
constructor() {
this.children = [];
this._translate = Vector3.zero();
this._rotate = Vector3.zero();
this._scale = new Vector3(1, 1, 1);
this._matrix = Matrix44.identity();
this._dirty = false;
}
set translate(vec) {
if (this._translate.isEqual(vec)) {
return;
}
this._translate = vec;
this._dirty = true;
}
get translate() {
return this._translate;
}
set rotate(vec) {
if (this._rotate.isEqual(vec)) {
return;
}
this._rotate = vec;
this._dirty = true;
}
get rotate() {
return this._rotate;
}
set scale(vec) {
if (this._scale.isEqual(vec)) {
return;
}
this._scale = vec;
this._dirty = true;
}
get scale() {
return this._scale;
}
get transformMatrix() {
if (this._dirty) {
var matrix = Matrix44.identity();
this._matrix = matrix.scale(this._scale).
rotateX(this._rotate.x).
rotateY(this._rotate.y).
rotateZ(this._rotate.z).
translate(this._translate);
this._dirty = false;
return this._matrix.clone();
} else {
return this._matrix.clone();
}
}
}
おそらく読めばその意図することがわかると思いますが、一番処理として重いのが、transformMatrixの内部の行列計算ですよね。それを、まぁこんな風にthis._dirtyフラグがtrueの時にしか計算させないようにし、falseの時は内部キャッシュを返すわけです。そして、translateなどを更新する時にしかダーティ・フラグはtrueになりません(それも、同じ値で更新しようとした時はtrueになりません)。論理的に破綻しないギリギリまで、ダーティ・フラグはできるだけtrueにしないように最適化するわけです。そんな難しくないでしょ?
他にもGLBoostでは、ハッシュテーブル&ハッシュ関数を使って、一度コンパイルしたシェーダープログラムはハッシュテーブルに保存。mixinによって同じシェーダーコードの組み合わせが生まれた場合は、同じ内容のシェーダープログラムをまたコンパイルするのではなく、そのハッシュテーブルに保存したシェーダープログラムを再利用する、といったこともやっています。
キャッシュシステムだけでなく、ソフトウェアカリングも重要です。今日の市場に出回っている3Dゲームでは、カリング処理はGPUが自動でやってくれるハードウェア処理だけに頼っていません(もしそうだとしたら、グラフィックス品質は本来の半分以下になっているかもしれません)。
例えばシーングラフ及びカメラクラスの情報から、明らかに画面に入らないようなMeshオブジェクトは処理から外すなどは当然考えられますね。さらに、空間をOctreeなどで分割して管理するなどの、データ構造を見直すことも検討に値します(と言いつつ、ここら辺は私もそんなに詳しくないので、勉強しましょう。お互い^^;)。
ソフトウェアカリングを行うことで、無駄なCPU時間を大量に削減できるほか、GPUへのドローコール等も必然的に減ることになります。GPUに乗るVBOデータも減りますし、頂点シェーダーの負荷も減ります。とにかく良いこと多いんです。
ライブラリの開発初期は、なかなかこうした最適化までは手が回らないものですが(かくいう私のGLBoostもまだまだ充分ではありません^^;)、実用的なライブラリを志す上では必ず通る道です。
手が空いた時にでも、他のライブラリの実装を調べたり、3Dエンジン開発に関する書籍(洋書が多いですが、あります)を読むなどし、少しずつノウハウを貯めておきましょう。
最適化についての補足
最適化にあたっては、以下の要素を意識することが重要です。
- ボトルネック部分を特定すること(GPUパイプラインがストールするような事をしていないか、など)
- 冗長な処理、または無駄に繰り返している処理がないか
- 最適化のための処理コスト自体が、最適化によって得られるパフォーマンスベネフィットを超えてしまっていないか
- 複数の条件下で、ベンチマークを取ること(机上の計算だけで、チューニングしない)
特に、ベンチマークは非常に大事です。全ての理論を棚上げした客観的なデータが、ライブラリ作者の思い込みを正し、最適化の方針をより良い方に修正してくれることでしょう。
ディファードシェーディング、物理ベースレンダリングなどの今時のレンダリング技法のサポート
今や物理ベースレンダリングが花盛りですね。物理ベースの定義とは色々ありますが、例えば
* 3Dオブジェクトのマテリアルが「エネルギー保存則を満たす(反射する光のエネルギーの大きさは、入射する光のエネルギー以下であること)」
* 現実にある材質をレンダリングするなら、その材質は現実と同じ反射特性(BRDF)を持っていること
* リニアスペース(ガンマがかかっていない)での処理が行われること。
* 他、ライトの減衰の正しさ、間接光による光の総量が(途中実装上の問題で近似されるとしても)物理的に妥当なこと、などなど
といったようなことを満たしているレンダリングのことです。この「物理ベース」というのがどれくらい厳密に実現されているか、というレベルも実に様々で、「ウチのエンジンは物理ベースです!」とか言っても、実際のところ、現在のゲームエンジンではほぼ全てが、どこかで大胆な近似をせざるをえなくなっています。(そもそも、「反射モデル」というもの自体が近似と言えなくもない。本当に完璧物理ベースを謳うなら、「ラフネス」とか「メタルネス」がどうとか、んなこと言ってないで、本来であれば全てのマイクロファセット(微細な凹凸)とレイとのインタラクションをシミュレートせよ、みたいな話になってきてしまいます)
当然、現在の水準のWebGLで可能なものといえば、まぁたかが知れてはいますが、それでも現在のWebGLで物理ベースレンダリングを謳っている事例はすでにあったりします。
あと、ディファードシェーディングについても触れておきましょう。
従来のGPUの描画方式は、フォワードシェーディング方式といって、3Dオブジェクト単位で、色の計算(光をどれくらい跳ね返すか)という計算を1つ1つずつやっていくものでした。まぁ、今のGPUでも基本仕様はそうなっています。
ただ、これだと、シェーダープログラムで扱える光源の数が限られたり、固定数になってしまうという問題があります。
一方、ディファードシェーディング方式は、現在のGPUの仕様内でうまく工夫をし、これらの制約を取っ払おうというものです。
具体的には、3Dオブジェクトをとりあえず全部描画するんですが、ライティングの計算はせずに、最終的にライティングの計算をする際に必要となる中間値を、複数のバッファ(これをG-bufferと呼んだりします)に出力します。例えば、3Dオブジェクトの位置座標とか、法線とか、マテリアル情報などです。
そして最後に、画面をぴったり覆う四角形を光源の数だけ描画してフラグメントシェーダー(ピクセルシェーダー)で、画面の各ピクセルごとに、その複数のG-bufferをテクスチャとして読み出して、それらの情報と光源情報を使って、いわばライティング処理をポストエフェクト的に行います。
これが、WebGL 2.0(今年の12月中に正式仕様がリリースされるかも、という噂があるんですが、もしそうなったらクリスマス・プレゼントですね^^)になると、このディファードシェーディングがかなり楽に実装できるようになります。
実は、フォワードシェーディング方式の方も、最近だとForward+などと言ったりして、改良手法が登場しており、従来の問題点を解決できています。Forward+とディファード、どちらがいいか、というテーマでCEDECで討論になったりもしているくらいで、結構ホットな話題だったりするんですが、こうしたものをWebGLライブラリで実装するのも面白いと思います。
(もっとも、Forward+に関しては、DirectX11やOpenGL4以降でないと使えないコンピュートシェーダーという機能を使って実装されており、それがないWebGLでは、たとえWebGL 2.0のスペックを持ってしても実装は難しいかもしれませんが……)
開発中は、色んなブラウザ、色んなサンプルで随時動作をチェックしましょう
グラフィックスライブラリのコードは非常にセンシティブです。
数行書き換えただけで、一部の環境で予期せぬ動作になったりすることなどしょっちゅうです(特に、WebGL1.0だけでなく、拡張機能やバージョン2.0への対応なども条件分岐等で対応していたりすると、処理が複雑になりますので、その傾向が強くなります)。
そのため、少しコードを変更しただけでも、随時、各環境で動作を確認し、不具合が出た場合に早期に気づくことが重要です。
それも、一つのテストケースだけでは不十分です。いろんなコード分岐のカバレッジをカバーできるよう、各処理の分岐に対応したテスト用サンプルコードを用意し、それらすべてでテストを行うことが重要です。
GLBoostではまだできていませんが、できることならそのテストを自動化できることが望ましいですね。グラフィックスのテストの自動化はなかなか難しいと思いますが、例えば正となる画像を記憶しておいて、テスト結果の画像とファジーな比較を行い(厳密なピクセル単位の比較もいいですが、環境の差異による不具合の誤検出も多くなりそうですから、ある程度の許容値も設定が必要でしょう)、正の結果と明らかに違う場合は、アラートを出すような仕組みなど、あるといいですね。
エラーの自動検出の仕組みを作るところまでいかなくても、各テストサンプルを自動実行してその結果画像を自動保存してくれるだけでも助かるでしょう。テスト実行者は、その結果画像をバババーっと見るだけで良いのですから。
もちろん、グラフィックス(=画像)面以外のテストならば完全自動化も可能でしょうし、そういう仕組みを構築して、CIを回せるのが理想です(と偉そうに言っておきながら、GLBoostではまだできていませんが^^;)
開発初期段階はまだいいですが、徐々に利用者が増えてくると、リリースバージョンで環境によって不具合が出ることなどが続くと、ライブラリへの信用に関わります。開発のできるだけ初期の段階で、テスト環境の整備はしておきましょう(と、自分に言い聞かせる・・;)。
作りたいんだけど、右も左もわからないんだけど……。
という方は、まずはWebGLの参考書を熟読しましょう。
@doxas さんという、日本国内のWebGL普及活動の第一人者とも呼べるお方が運営しているサイト「WebGL総本山」にて各種WebGL参考書の書評が載っていたりしますので、ぜひ参考にされてみてください。個人的にはこの本がオススメです。
WebGLの知識はある程度身についた。しかしライブラリを自作しようとしたらいきなりつまづいた。
既存のライブラリから学びましょう! WebGLライブラリはその多くがオープンソースです。
それこそ、日本語の書籍なども出ているThree.jsのコードリーディングはかなりタメになるではないでしょうか。
え? Three.jsはソースコード量が多すぎて読む気が起きない? 大丈夫。オープンソースでGithubでソース管理されているのですから、Gitの履歴から開発初期のコードを持ってくればいいんです。それならコードも小規模なので、なんとかなりそうですよね。
また、洋書になってしまいますが、WebGL Insightという本では、幾つかのWebGLライブラリの設計についてのヒントが沢山書かれているので、自作ライブラリを作り始めたんだけど、もっと良い設計・実装方法はないものか、とウンウン唸ってる人(←かくいう私が今このステージ)にオススメです。
作りたい機能をリストアップしよう
機能のアイデアを思いついたら、すぐさま「ボクの考えた最強の機能リスト」を作って、そこに加えましょう。ちなみに私のGLBoostの場合は、以下のような感じです。
済)ポリゴン描画
済)カメラクラス
済)Lambert、Phong
済)Objファイルサポート
済)glTFファイルサポート
△ Tweener対応(カメラ、ライト、メッシュデータのtween(シェーダ使って高速に))
済)カスタムシェーダー
・ スポットライトの追加
済)シーングラフ構造
・ 古典的シャドウマッピング
・ 改良型シャドウマッピング(LSPSM, Variance, Cascade, etc...)
・ ディファードシェーディング
・ IBL(イメージベースドライティング)
・ 物理ベースグローバルイルミネーション(Light Probe, VPL, Sparse Voxel Octree+Voxel Cone Traching, etc...)
済)各Elementのマルチタイムライン対応(Elementの各アトリビュートが複数の時間軸を持つ)
・ スケルタルアニメーション(キャラクターアニメーションのこと)
・ Elementの複数のタイムライン間の遷移を管理するビヘイビアクラス(キャラクターアニメーションのモーション遷移・モーションブレンドを実現するのに役立つ)
・ GLBoost専用のPlay Ground環境(Runstantベース?)
・ Play Groundで作成したシーンデータのシリアライゼーション・zipダウンロード対応
偉そうなこと書いておきながら、まだシャドウマッピングすら未実装というねwwww
でもリストの最後の方になると、いよいよUnity・Unreal Engineのようなエンジンっぽさが出てきます。
こういうリストを見ていると、それだけで「やるぞー」という気力が湧いてきますし、一つ機能を実装して「済」マークを付けられると、なんかそれだけで今日は「良い日カニタマ!」って感じになります(謎
「ボクの考えた最強の機能リスト」の作成、オススメです。
ていうか、自作したオレのライブラリ、果たして他の人使ってくれるのか?
何はともあれ、Githubで公開しないことには始まらないっすよね。
細かいことですが、海外からも注目されたいのなら、ソース中のコメント、Git履歴のコメント、ReadMe.md、イシューやプルリクエストも全部英語でやるべきです。日本語&英語の併記くらいは構いませんが、日本語オンリーになっていると、それだけで数段下のプロジェクトだと見なされてしまいます。これは海外の方からすると脊髄反射的にそう感じられてしまうようなので、「いや良いモン作ってりゃ……」は通用しません。英語でいきましょう。
というようなことを、ある著名なJava使いの方がおっしゃっていたので、急遽慌ててGLBoostから日本語を追い出す作業を始めていたり……(笑)
あとは、アレですね。他の人が使ってくれるかは、逆にあなたがライブラリの選定を行う立場で考えてみましょう。
「機能が優れているか」というのは基本中の基本として、あとはそれ以外の割と「政治的?」な部分です。
「もし、メイン開発者が明日トラックに轢かれたら、このプロジェクトどうなんの?」
「コミュニティが小さすぎて、開発・メンテナンスが長期的に約束されるかが不安だな」
普及するかどうかは、結局はそういうところです。ですので、プロジェクト成功の鍵は、「手厚いサポートを約束できるくらい、コミュニティを成長させること」や「企業などの大きなパトロンを見つけること」だったりします。
とはいえそうしたことは、明らかに優位性のある優れたプロダクトを作り、それをうまく回りに露出・宣伝していれば、おのずとそうしたチャンスが転がってくる可能性もあります。
もともと習作として始める、というのがこの記事の主題なので、まぁ今からオープンソースプロジェクト的成功に執着する必要はないと思います。まぁまずは、「エエもん作りましょ」ってことですね♪
番外:「レンダリングライブラリを自作してる」ってどれくらい自慢できること?
難しいですね。多分、自慢した結果、相手によってこういう感じの反応ではないかと。
-
ちょっとCGをかじったことのある人 or 非技術系のCGデザイナー:
結構驚いてくれる。ある意味メインターゲット(笑) -
ガチの現役CGプログラマー:
「あ、そう。ところで君のって去年のSIGGRAPHで出たあの技法とかは実装してるの?」とか言われて心が折れる。 -
完全な一般人:
昨今の最新3Dゲームを見慣れていて大抵目が肥えているため、「……なんか見た目しょぼくない?」とか平気で言ってくるのである意味一番心が折れる。この層の人たちをアッと言わせるためには相当根気よく開発を続けることが必要です^o^;
とまぁ、ライブラリ/エンジンの実装がいかに面倒くさく、苦労があるか、というのは本人はもちろんよく知っているわけですが、結局のところそんなのよりモノを言うのは「見た目」というのも事実です。
かなりのクオリティを出せるようになるまで、根気よく開発を続けることが必要ですが、やはり「その開発の過程を楽しめるか」という性格的な資質が一番問われるかもしれません。
(というか個人的には、「自慢」のためよりも「自己スキルアップ」と「業界(?)貢献」のために開発したい……)
それでも「自慢したい」ということを重視するなら、一定のクオリティに達するまでそれなりに時間のかかるレンダリングライブラリよりも、「動き」が楽しめる「物理エンジン」系を自作する方がいいかもです。
(物理シミュレーションなら、一般層もわりと興味持ってくれそうですよね)
は、イカンイカン。せっかく仲間を増やそうとしているのに、なにネガティブなこと書いてんだ俺wwww
レンダリングライブラリ作るのタノシイヨー!ユメイッパイダヨー!(裏声)
最後に
さて、あなたの自作ライブラリに、大きなライバルが訪れようとしています。Three.js? いいえ。それはUnityやUnreal EngineのWebGL出力機能です。今はまだ、ランタイムのサイズが大きく、ロード時間が長いなどの理由から実用性に難があったりしますが、WebAssemblyなどの技術も手伝って、そうした問題はどんどん解消されていくでしょう。
そのうち、WebGLを直で触ることはおろか、Three.jsすらガン無視されて、ほとんどの人がWeb3Dコンテンツを作る際に「Unityでいいんじゃね?」「UE4でいいんじゃね?」という時代が来るかもしれません。というか来るでしょう。
しかし、中には、きめ細かいコントロールをしたいために、自分で3D処理をコーディングしたい、という層もある程度は残るはずです(その昔。カメラの登場が、写実派の画家たちを職なしにすると騒がれたそうですが、数は減ったものの、絶滅はしていませんよね)。
そんな未来、彼らがWebGLを直に触ろうとして、心が折れそうになった時、ふとあなたのライブラリをGithubで見つけ……。
そこから、あなたの新たな成功が始まるかもしれません。
習作、学びのためとか言いながら、最後に風呂敷広げすぎましたが(GLBoost作っている自分に言い聞かせている面もあるw)、まぁ堅いことは抜きにして、皆さんも作ってみませんか。
たいてい、プログラマは何かの継続した課題に取り込むとき、自作のライブラリ……とまでは行かないまでも、自分の道具を自ら構築するものです。それを、もうちょっとだけ……他の人が使っても「便利だね」と喜んでくれるような、そんなものをWebGLで作ってみましょうよ。というお話でした。
自己満足に近い長い文章、読んでいただきまして誠にありがとうございました。
最後の最後に:GLBoostについて
早速、試用していただいた方がいらっしゃいまして、大変ありがたく、恐縮です!
GLBoostはまだ本当に開発初期段階ですので、まだ実用途には全然使えるレベルではありません。
そして、中には笑ってしまうような実装のミス・荒などがある可能性が十分にあります。そういうのを見つけた際は、イシューやらプルリクエストやらしていただければ大変ありがたいです。
そして、「自分もライブラリ自作してみたい。GLBoostを実装の参考にしたい」という方がもしいらっしゃいましたら、遠慮なく Twitterで声をお掛け下さい。自分の実装では、理論通りに実装せずに楽をしている箇所なども正直ありますので、そのまま鵜呑みにせず、気になったことは、私に確認を取っていただくのがよろしいかと思います。
今後とも、よろしくお願いいたします。