LoginSignup
30
17

More than 5 years have passed since last update.

趣味から仕事へ。GLBoostの教訓とこれから

Last updated at Posted at 2018-12-22

(2019/02: 記事公開後のひと言)

 なんだか「GLBoost更新止まってない?」とか「作者、Twitter垢どうしたの?」 とか、ご心配(?)というか当然の疑問をお持ちになった方もいらっしゃるようなので、説明しておきますと…。
 GLBoostの更新が滞りがちなのは、すでに開発が完全に新ライブラリの方に移ってきているためです。基本的に今後GLBoostはメンテナンスモードとなり、新ライブラリの正式公開を待つばかりの状況でございます。

 あと、私のTwitterなどにつきましては、ちょっと思うところがありましてSNSのいっさいがっさいを少し前に一気に断ってしまいました(笑)
 当時それなりに色々あったからなのですが、今となっては私個人としては何に対しても誰に対しても、もはや何のわだかまりもありませんし、元気にしております。みんな仲良く元気にやれるといいね!

 しばらくはQiita記事や一部のSlackチャットくらいしかネットでの露出がないかもしれませんが。またそのうち気が変わることもあるかもしれません。こんなダラダラ長い記事にもかかわらず、興味深いと読んでくださっている方もいらっしゃるようで、ありがたい限りです。

 今年中に新ライブラリは公開できるかと思います。その時に皆様、またお会いいたしましょう^^/

TL;DR (長文嫌な人向けの要約)

  • GLBoostというWebGLライブラリを2015年頃から作っており、最近はQiitaで状況を定期報告しています。
  • 商業利用が一つ一つ増え始めています。
  • 現状版は課題点が多くあります。設計・実装をやり直した新バージョンを開発中です。
  • 開発する中で悩んだこと、学んだことを本記事でつらつら書いています。
  • CGを学びたい人は、こういうことから始めてみたら? という私なりの提案をさせていただいています。

はじめに

 初めましての方は初めまして。emadurandalというネームで活動をしております。
 今年12月より、株式会社GUNCY'Sという会社のCTOになりました。

 2015年ごろからGLBoostという名のWebGLライブラリを趣味で開発しています。2016年末、国内のあるWebサービスで商業採用されたのをきっかけに、その後も1つ、また1つと商業利用が増えてまいりました。

 今後、許されるならば、個人の俺俺ライブラリから始まった物が商業でも利用されるようになり、その過程で何に苦しみ、そして乗り越えていったのか。その過程をこのWebGLアドベントカレンダーの場をお借りして、定期的にご報告できればと考えております。貴重な1枠をお借りしてしまいますが、どうかご容赦ください。浅はかな宣伝に終始するようなことのないよう、読んで何かしらご参考いただけるような内容にはしたいと考えております。

GLBoostとは

GLBoostは、私emadurandalが開発しているJavaScript(ECMAScript 2015+FlowType)製のWebGLライブラリです。

機能としては、以下のようなものがあります。

  • ジオメトリ、メッシュ、マテリアルなどの基本クラス
  • スフィアやキューブなどのプリミティブクラス
  • シーングラフ管理
  • マルチパスレンダリングに対応(シャドウマッピングやポストエフェクトなどの技法を実装可能)
  • 「ギズモ」という、CGソフトで見られるような何のオブジェクトであるかを示すシンボル形状クラス
  • Obj, glTF1.0, glTF2.0の読み込み対応
  • HTML背景との半透明を伴ったWebGLCanvasのアルファ合成をサポート
  • 階層アニメーション、スケルタルアニメーションに対応
  • PBR(物理ベースレンダリング)とIBL(イメージベースドライティング)に対応
  • 専用ビューアーを公開中 https://viewer.glboost.org/

末尾に、これまでの商用採用事例を(ややぼかして)まとめています。

開発者emadurandalの経歴

 元々、大学・大学院(修士)時代にCGの研究室におりました。全国的に見てそれほど優れた学生ではなかったように思います。
 最初に入社したベンチャー企業で、運よくゲームエンジンの開発に携わることもできましたが、そこは生粋のゲーム企業ではなく、その後すぐにSIerに転職したり、フリーランスになったりしました。
 正直ゲーム業界やCG業界で、きちんとプロとしてゲームエンジンやレンダラーの開発に携わった経験はありません。
「CGを少しかじった、素人ではないが、決してプロでもない」という中途半端な期間を、かなり長いこと過ごしました。

 それでも、元々温めていた創作のアイデアを自作のCGプログラムで具現化してみたい、という強い欲求があったこと、そして仕事の過程で素晴らしい人たちとの巡り合わせの結果、私のようなある意味「下手の横好き」でも、なんとかここまでこれた気がしています。

GLBoost、今年の変化

昨年のまとめについては、こちらの記事をご覧ください。
オレオレWebGLライブラリだった「GLBoost」が商業案件に採用され、作者が経験したこと

この頃は痛さが残る文体で、なかなかエモ深いですね。

商用事例の増加

 商用採用が2例増えました(後述)。

少しずつコントリビュータが増加

 バグを報告してくれる方、FlowType対応やESDoc対応などの周辺コードへの寄与など、
 少しずつですが、確実にコントリビュータが増えてきました。大変ありがたいことです。

他ツールとの連携を開始

 準備が整うまで詳細は伏せておきますが、他CGツールとの連携も進めています。それらツールのランタイムと協調動作することで、GLBoostの表現力に大きな要素が加わる予定です(一部は試験実装済み)。

GLBoostは完全に作り直しへ

 いきなりですが。
 元々オレオレライブラリとして始めたコードベースに増築を重ね、3件商業案件の要望に応えてきました。
 設計上の課題点ややり直したい点が増えすぎ、これ以上の将来性はあまり望めません。

 当初はRust言語での作り直しを考えていましたが、Rust言語がそのメモリ安全性についてなどの厳密性により、サクサクコーディングして何か実験しながら気軽にライブラリを作れる性質の言語ではない(個人の感想です)ため、まずはTypeScriptで作り直すことにしました。
 このTS版で多くの設計上の挑戦がうまくいったら、一部のコア機能をRustに置き換えるアプローチでの高速化を図るかもしれません(その頃にはRustのバージョンアップ版であるRust2018も普及しているでしょう)。

 新ライブラリは、現行GLBoostについて後述で指摘する問題点の、ほぼすべてを解消することを目指しています。

今までに得た知見と反省点

 お待たせしました。本題です。それぞれ毛色の異なる内容なのでまとまりはないですが、使えそうなところは参考にしてみてください。

ライブラリバージョンの管理を改善

 バージョン管理はきわめて重要です。従来は適当でしたが、今年からビルドしたライブラリにビルド番号を自動的に付加するようにしました。

version: 0.0.4-371-g05d5-mod
branch: develop
77f4c6c2e4c076de6d984dc30bd6a2004bdf7849790aba69df89073198bc89cb ./build/glboost.js
64a9aa5a32eb77346e4bd0b45aa6d08b1c80c6dd1566f10e966eeb1b63a0b0e3 ./build/glboost.min.js
6292d8ce29663dfb48a81d4fa0223e56797eed14de865388064f17ccf2671697 ./build/glboost.min.js.map

 ビルド番号というと、ユニークかつトレーサビリティも備えた値を簡単につけたいと、と誰もが思うはず。ですが意外と難しいです。
 まず思いつくのは、Gitのコミット値(SHA1 Hash)。
 しかし、残念ながらGitのフックスクリプト(pre-commitなど)では、コミットを作成したときに生成されるコミット値をあらかじめ知ることができない1のです。さて、困りました。

 そこで見つけたのが以下の記事です。
https://code.i-harness.com/en/q/7d88c
 これは、Gitのタグを打った時点からの変更に対して番号を重ねていくという発想です。
0.0.4-371-g05d5-modを例にあげると、 0.0.4がGitタグ、371がそこからの内容の変更を伴ったビルドを重ねた回数、modが直前のコミットからローカルコピーに未コミットの変更があることを意味しています。
 この生成された「バージョン文字列」を独自にシェルコマンドを追加して、glboost.jsなどのビルド成果物にうまく末尾に追加した上で、Gitコミットするようにしています。
 さらに、glboost.jsを実行すると、このソースに埋め込んだバージョン文字列をブラウザの開発者コンソールに出力するようにしてあるので、かなりトレーサビリティが良くなりました。
 これは、商業案件先との話し合いの上で生まれたものです。

 どういう仕組みか気になる方は、実際にGLBoostのソースコードをクローンして、プロジェクト構成を調べてみてください。

テストカバレッジ

 今年はJestによるテストを導入しましたが、現行のGLBoostが設計・実装面でテストを考慮したものになっておらず、カバレッジを上げるのに苦労しています。
 一方、完全作り直しの「新GLBoost」(まだ未公開)では、Jestでほぼ完全にテストファーストで開発するようになりました。
 今更言うことでもないですが、テストコードで守られている安心感と、手戻りの少なさは今後の開発シーンで必須だと思います。CG分野でも例外ではありませんね。
 テストはそのうち導入すればいいや、などと思っているとテストしにくいコードになり結局導入できない路線へまっしぐらです。作り出した時点でテストコードを書きましょう。

 新GLBoostでのテストコードになりますが、以下のような感じです。

test('create Parents and children.', () => {
  // generate entities
  const sceneEntity = generateEntity();
  const parentEntity = generateEntity();
  const childEntity = generateEntity();

  // set transform info
  sceneEntity.getTransform().translate = new Vector3(1, 0, 0);
  parentEntity.getTransform().translate = new Vector3(1, 0, 0);
  childEntity.getTransform().translate = new Vector3(1, 0, 0);

  // setup scene graph
  parentEntity.getSceneGraph().addChild(childEntity.getSceneGraph());
  sceneEntity.getSceneGraph().addChild(parentEntity.getSceneGraph());

  expect(childEntity.getSceneGraph().worldMatrix.isEqual(
    new Matrix44(
      1, 0, 0, 2,
      0, 1, 0, 1,
      0, 0, 1, 0
      0, 0, 0, 0))).toBe(true);
});

ちなみにJestはFacebookが開発するテストライブラリですが、Mocha&Chaiなどよりも、このjestパッケージ一つでほぼすべての機能を網羅しており、導入がとても楽なのでおすすめです。テストカバレッジ率の測定もできます。

入門ドキュメント

 現在はAPIドキュメントのカバレッジをあげようとしている状況で、入門的なドキュメントがまだまだほとんどないのが実情で、導入・コントリビュートしていただく上での大変な壁になっています。これも、現行GLBoostは諦めて、新GLBoostで挽回しようと思っています。
 ドキュメントを充実させれば、コントリビュータが早めに現れてくれて、元が十分取れるかもしれません。

ライブラリを書くならTypeScriptを使うのは当たり前の時代に

 1から作り始めるならTypeScriptを、または途中からならせめてFlowTypeへの移行を検討しましょう。
 次のようなメリットが得られるため、ほぼ必須です。

  • ESLint以上の事前エラーチェックが可能
  • undefinedやnullもコーディング時点で撲滅できる。

 前者は明らかです。タイプミスを含めた些細な問題に実行時エラーでようやく気づく、といったウンザリな時間浪費も撲滅できます。
なお、Flowtypeは既存プロジェクトを型チェック対応しなければならない、という既存案件にのみにしてください。Flowtypeは様々なライブラリの型定義ファイルがTypeScriptほど揃っておらず、また型チェックを行うflowコマンドを打たなければ、型定義を削除したJavaScriptコードをそのまま実行できてしまいます。そのため、型定義を全コードに完全に付加し終えることや、その更新を怠ってしまい、型チェックの導入運用に失敗してしまう恐れもあります(駄目駄目ケースですね)。

TypeScriptなら、全体のコンパイルが通らなければそもそも実行コードができませんので、そうした駄目ケースがそもそも起こりえません。AST(抽象構文木)の互換がJavaScriptとは異なるといった部分がありますが、TypeScriptは言語としてすでに十分世に普及しており、事実上それで不都合になるケースもかなり限定されるでしょう。新規プロジェクトであれば、ぜひTypeScriptを導入してみてください。

 ちなみに、既存プロジェクトの場合は、まずFlowType化してから、その次にTypeScript化するというステップ方式の対応アプローチがあります。これは、FlowTypeの型文法の大半がTypeScriptとの互換を意識して作られているためです。

null安全でコードすっきり。エラーも減少。

 後者についても、「これからはnull安全な言語でないとだめ」といった話題を聞いた方も多いでしょう。
 TypeScriptなら、コーディング時点でnullやundefinedになる可能性を排除したコーディングが可能であり、ライブラリコード中からこれらのチェックコードを大幅に削減できます。実際、現行GLBoostでは相当数のnull/undefinedチェックコードがありプログラムの見通しが非常に悪かったのですが、新バージョンでは劇的に改善しました。
 実行時も、これらのイレギュラー値の混入を始めから抑止できているので、チェック漏れによるエラーで落ちることもありません。

 また他の方法として、nullを入れられる余地のないnumber型を使い、オブジェクトをID整数値で管理するのも一案です。
 もっとも、数値型の場合は、無効な値の扱いとその運用をちゃんと事前に取り決めましょう。TypeScriptでもさすがに値域まではコンパイル時チェックできませんので。

訂正:なんと、TypeScriptでは整数リテラルという型によって、整数の値のコンパイル時チェックが可能のようです。
https://basarat.gitbooks.io/typescript/docs/types/literal-types.html

export default class Primitive extends RnObject {
  private _attributes: Array<Accessor>;
  // ↑この時点で、_attributesに null/undefined は入りようがありません(そうしようとするコードはコンパイル時に弾かれます)。
  // null/undefinedの可能性を認めるには、変数名に?をつけて _attributes? とする必要があるからです。

  private _material: ObjectUID = 0; // 0は無効な初期ID値。有効な番号は1から

  constructor(
    attributes: Array<Accessor>,
    material: ObjectUID
  ) {
    super();
    this._attributes = attributes;
    this._material = material;
  }
}

 null/undefined安全に限らず、こうしたコーディング上の制約はライブラリにおいては強くかけた方が良いです。そうしたコードは利用者側からすれば気軽に扱いづらくなってしまう面もありますが、そこは便利なヘルパ関数の導入などで敷居の高さを緩和しましょう。ライブラリのコア部分は、安定性やメンテナンス性の観点からも堅牢さこそ最重要です。間違ってもコンパイルエラーをただ回避したいだけの理由で変数に?をつけないようにしましょう。そういう安易なことを続けると必ず後悔します。

ライブラリの内部処理で扱うデータ形式は一つに統一しよう

 GLBoostにGeometryというクラスがありまして、これは頂点データを設定する対象となるクラスです。
 これが、頂点データをなんと4種類もの方法で受け付けていました。

  • Float32Array(TypedArray)
  • JavaScriptの一次元配列
  • JavaScriptの二次元配列([[v1.x, v1.y, v1.z], [v2.x, v2.y, v2.z], ...])
  • Vector3といったGLBoost独自のベクトルクラス

 ユーザーへの利便性にこたえる、ということで、要望に応じてどんどん増やしていったのですが、その結果の内部コードがどうなったかは……興味のある方はGithubの当該ファイルをご覧ください。改修したくてももはやここまでになると、案件で使われている状況では、おいそれと手を入れられない状況になってしまいました(しかも、現行GLBoostはテストコードもまだ揃っていない状況です)

 で、理想的にはどうすべきか。もちろん答えは、一種類に絞ることです。しかも、リアルタイム向けCGライブラリであれば、GPUがそのまま扱える形式がベストです。するとこの中ではFloat32Arrayということになりますね。
それだけを考えれば良いとすれば、このGeometryクラスはもちろんのこと、Geometryと協調動作する他のライブラリ内部クラスのコードも自然と整理されていくのは自明のことです。
 できれば、glTFファイルなどから読み込んだバイナリ頂点データをそのまますっとGPUまで流せると最高ですね。

 ユーザーへの利便性は、変換をよしなにやってくれるヘルパ関数で対応しましょう。
 また、テストコードをちゃんと揃えないと、怖くてリファクタリングもできなくなります。それだけは避けるようにしましょう。

 開発中の新作ライブラリの方は、テストがあるおかげで、現行のような冥界とはすっきりおさらばできました。

3D APIのリソースハンドラを、一般クラスが直で持つのはやめよう

GLBoostではこのようなクラスが未だ大半です。

class Geometry {
  vbo : VertexBufferObject; // WebGLリソースを直で持っている。
  ibo : ElementVertexBufferObject; // WebGLリソースを直で持っている。
  indices: Int32Array;
  vertices: Array<Float32Array>;
  constructor() {
    ...
  }
}

 別に絶対ダメというわけではないのですが、今後WebGPUなどの別種類のAPIも登場することなどを考えると、一般的なクラスでWebGLだけを意識したリソースハンドラを直接メンバとして持つのはよくありません。抽象化しましょう。

 その抽象化した3DAPIラッパーにおける、各APIリソースに対応するIDナンバーを発行し、それを保持するなどに発想を切り替えましょう。
 そして、そのIDを3DAPIリソースを管理するリポジトリクラスに渡すことで、実際のリソースを受け取るなどの運用にすると良いでしょう。

 こうすることで、複数のAPI種類への対応が容易になり、また3DAPIとの結合も疎にすることができます。

 開発中の新ライブラリでは、そのように抽象化・疎結合された設計になっています。

循環参照はなくそう

 最近のECMAScriptの仕様でこそ、限定的に循環参照が許容されていたり(ここらへんよく知らない)、Rollup.jsでバンドル化するときもほぼ問題なく処理してくれますが、たいてい警告が出ます。

image.png
rollup.jsが出す循環参照の警告文(黄色のテキスト)

 クラスやモジュール同士が循環参照しているときは、設計からいってあまり良い状況ではありません。最悪の場合、コードが読み込まれていないため未定義の状態のものを利用しようとしてundefinedエラーになってしまうケースもあります。

 理由としては大抵、「このクラスにこのAPIを生やせば便利だろうから」といってつけたメソッドが循環参照を引き起こしています。その機能、そのクラスにもつけてあげる必然性ってありますか? その役割を受け持つ本来のクラスに完全に任せるべきではないでしょうか。というか、その機能自体、冗長で要らないかもね?

 循環参照を減らすようにリファクタリングをしていくと、利用側のコードの書き方としては不便になります。便利なAPIが減るのだから、ある意味当然です。しかし、クラスの役割分担を明確に分ける、ということは長い目で見てライブラリの設計面で必ずメリットをもたらすでしょう。

 また、循環参照はある程度は以下のテクニックで回避可能です。

型注釈のためだけのimportは循環参照としてカウントされない

たとえば、外部モジュールをインポートしても型注釈をつけることにしか利用しておらず、値として一切利用していない場合は、(TypeScriptの型注釈はコンパイルを通して消えてなくなるため)循環参照とはカウントされません(Flowtype におけるimport type文も同様)。 

 そのため、後述する本格的なDI機構を入れずとも、オブジェクトをnewする側が、そのときにコンストラクタ引数に自身のオブジェクト(this)を渡してあげるような簡易DIなコードだけでも循環参照を防ぐことはある程度可能です。

 さて、DIという言葉が出てきました。こうした参照関係をスムーズに解決する抜本的な仕組みが世の中には存在します。
 それが、DI(依存性注入)です。

DI(Dependency Injection:依存性注入)とは

 DI(依存性注入)はあるクラスにおいて、他のクラスへの依存を本来の参照構文でコードに直接記述するのではなく、自身のコンストラクタなどに引数を用意し、そこへ外部から依存対象クラスをオブジェクトとして動的に渡してもらう(注入してもらう)方式により、依存関係を動的に解決する仕組みのことです。これにより、プログラム言語本来の参照依存文をプログラムコードから削減することができ、ソフトウェアを利用ケースや仕様の変更に強い設計にすることが可能です。テスト実施にあたって、一部をモックに差し替える、といったケースでもよく使われます。

 Javaなど業界では、DIコンテナという名前で一時期一世を風靡した技術ワードでもあります。JavaScriptの世界でも、AngularフレームワークのDI機構などは有名ですね。

 「依存性注入」と書きましたが、日本語訳だとそう表記されているケースが大半なのでそれに従ったまでですが、これは非常に意味がわかりにくいです。DIとはDependency Injectionの略であり、このDependencyとは正確には「依存性」ではなく「依存している(クラスの)オブジェクト」のことです。「機能的に依存している外部クラスのオブジェクトを注入(Inject)してくれる仕組み」のことですね。

 AngularのようにTypeScriptのDecorator機能でDIの仕組みを実装しても良いでしょうし、そこまで大掛かりにしなくても、ちょっとしたコード上の工夫で簡単にDIは実現することができます。学びたい方は「DI 依存性注入」でネット検索してください。多くの事例やコードサンプルが見つかるでしょう。

 そうした仕組みを専用に作り込んだDIのためのライブラリを「DIコンテナ」などと呼んだりします。JavaScriptの世界ではInversifyJSなどが有名です。

InversifyJS
http://inversify.io/

 とはいえ、私個人としてはDIに頼りすぎ、動的にとはいえ相互の機能的な依存関係を作りすぎてしまうのは考えものだと思います。依存せざるを得ない場合にのみ依存する、というスタンスでクラス間の関係を設計するのが本来は良いのではないでしょうか。

 そうしたクラスの責任範囲の分離と、ユーザーコードの利便性とのバランスをどうするか……。ここはライブラリ開発者としても頭を悩ませるところかもしれません。

コンポーネントシステムなどの今時の設計を考えよう

 コンポーネントシステムがもはや「今時」といえるのか、については賛否あるかと思いますが。まぁ、大昔のようなクラスの継承だけで機能の多様性をなんとか実現しようというのは、なかなか無理があるというものです。

 よくあるケースとして、子クラスが親クラスを拡張する(is-a関係)としても、子クラスであるAとBが、親クラスCの性質を全てきちんと満足しているか怪しい設計をしているケースなどが多くあります2。単にクラスAとBの共通コードを括り出すためだけに親クラスCを作って、そこに共通コードを移動させるようなやり方もアプローチとしては考え物です。こうしたやり方はそもそも本来の継承プログラミングとしても褒められた物ではありません。

 それに、継承関係だけで、機能の多様性を作るのには無理があります。継承ではなくコンポジション(包含、has-a関係)を活用しましょう。特に、よくゲームエンジンで取られている手法が、「コンポーネントシステム」です。

 コンポーネントシステムでは、ライブラリ/エンジンの各種の機能を「コンポーネント」というソフトウェア部品単位に分割し、さらに3D空間上に存在するモノ(オブジェクト)を「エンティティ」というクラスで表現します。基本的に、エンティティはコンポーネントを入れる「箱」にすぎず、自身に動的に搭載したコンポーネントの種類だけ、それらの能力を獲得し、機能するというものです。

 このシステムの利点は、各種機能をコンポーネントという部品に明確に区分することで、設計がとても綺麗になることです。
 また、単にクラスを継承していくのでは、末端クラスになるほど機能が膨れ上がっていく問題や、複数機能の良いとこ取りをするといったことができない制限がありますが、このコンポーネントシステムでは、エンティティに必要な機能(コンポーネント)を乗せるだけで、過不足のない機能をさせることができます。それも、動的にです。

 多少の設計の違いはあれど、Unity(GameObjectがエンティティに相当しますね)やUnreal Engine(「アクター」がエンティティに相当します)など、現在主流の多くのゲームエンジンで採用されている仕組みですので、皆さんもぜひ参考にされてみてください。私の新ライブラリでも、もちろん設計に取り入れています。

 コンポーネントシステムの詳細については、「ゲームエンジン・アーキテクチャ」という本でもある程度触れられている他、実例の一つとしてUnity社がUnity ECS(Entity-Component-System)の設計について多くの情報を出しています。

Unity ECS
https://unity.com/unity/features/job-system-ECS#entity-component-system-ecs

ライブラリの並列処理対応に今から備えよう

 今や、CPUはメニーコア時代であり、物理コア8個程度は当たり前、ハイエンドにいたっては16コア、AMDのCPUに至っては32コアというお化けのようなCPUが市場に出回るようになってきました。ハイパースレッディングも有効なので64スレッドとか同時に動くわけです。
 そうしたハードウェアで、1スレッドだけでちんたらJavaScriptが動くとかもったいないですね。今後はJavaScriptの分野でも、3DCG系のライブラリは並列化がキーとなってくるでしょう。
 鍵となるのが、WebWorkerとSharedArrayBufferです。WebWorkerはJavaScriptコードをメインスレッドとは別のスレッドで動かす仕組み、SharedArrayBufferはメインスレッドや複数のWorkerからアクセスできるスレッド共通のArrayBufferです。
 SharedArrayBufferについては、以前のCPU Spectre/Meltdown脆弱性問題への一時的な対策で、Chrome以外のブラウザでは標準で無効化されてしまいましたが、あくまでこれは一時的な対策であるとのことで、いずれ標準で有効に戻されることになると思われます。

 さて、ライブラリ設計において、処理対象をコンポーネントシステムなどで細かい粒度に整理することで、その粒度での並列化が可能となります。詳細はこの記事では書ききれないので別の機会に譲りますが、真面目にライブラリ開発に挑戦しようという方で余裕のある人は、少なくとも今のうちから並列プログラミングについての基本的な理解や、クラス設計時の処理対象の粒度についての熟慮、スレッド安全性を妨げるようなコーディングを避けることなど、今から準備をしておくと後々の並列化対応が楽になるでしょう。

 新GLBoostも、すでに並列化対応を見据えての設計になっています。

コードが綺麗かどうか、整理されているかどうか、は人としての性格が出る

 少々書きづらいことではありますが、しかし得られた経験としては一番印象強いことでもあるので、あえて共有します。コードやプロジェクトの様相には、その人としての性格が必ず出ます。これは、乱雑さや無秩序さなどの負の要素についても同様です。
 ですが、強く意識すれば必ずより良いものに変えていくことができます。

* チェック漏れが多かったり、無秩序にあったりなかったりするのは、「うっかり癖」や「考え方の一貫性のなさ」が関係しているかもしれません。
* 複数のデータ形式を安易に受け入れるような設計にしてしまう人は、「なんでもかんでも気安く手を出して、どれも中途半端になりがち」なのかもしれません。「引き算的考え」や「一番重要な物のみ選ぶ潔さ」、そして「それらの考えを一貫させること」を意識しましょう。
* 「console.log('Test Hoge');」のような、いい加減な文言をそのまま放置してGitにコミットし、しかもあろうことかリリースコードにまで混入を許す人は、自分の言動や見た目の不自然さをなんとも思わない人なのかもしれません。自分の言動や服装、乱れていないですか? 無条件に改めましょう。はぁ、そういう人なんだ、と思われても損ですよね。
* やっとの思いで書いたドキュメントが、人から「よく意味がわからない」と言われがちな人は、普段から日本語がいい加減か、よく考えをまとめる前に吐き出してしまう人か、言いたいこと全部入れてしまう、削ることを知らない欲張りなのかもしれません。普段の仕事で、「おまえの文章はわかりにくい」といわれている人は、言われているうちが華なので直せるうちに改善しましょう。小説などをそれなりに書ける人でも、ビジネス文章だと全くうまく書けないパターンも多くあります。
* すでに意味のなくなったコメントや、やろうとしているリファクタリングや、無意味になったプロジェクト設定項目を、そのままずっと放置している人は、整理をおっくうがる人か、整理すること自体を生来そもそも苦手とする人なのかもしれません。自分の部屋、散らかっていないですか? 意識的に改善していくしかありません。
* ドキュメントをいつまでも未整備のままにする人は、人から理解される、理解してもらうことの重要性を軽んじているのかもしれません。ちょっと頑張って整備すれば、チャンスが大きく広がるかもしれないのに……。
* テストコードを書かない人は(以下略

 何を生意気な、と思わないでください。ご心配なく。全部が全部、私emadurandalのことです。
 でも、「一部は自分にも当てはまるかも」というものが皆様にももしかしたらあるかもしれません。もしそうなら、「他人の振り見て我が振り治す」のが知恵者のやり方です。今が改善のチャンスです。
 自分でライブラリを書いていると、そういうことに嫌というほど痛感することになります。

 もちろん企業で、プロの仕事としてライブラリを作られている方にとっては「あほかそんなの気をつけるのは当たり前だよ」と思われるでしょう。企業体なら、当然それなりの品質体制が用意されているはずです。
 しかし、GLBoostは元々は個人的なイキりから始まったオレオレライブラリが出自です。まずいコードややり方を叱ってくれる上長もいなければレビューしてくれる先輩や同僚もいません。そんな気の抜けた状況では、実際に痛い目を見て気づく以外にありませんでした。

そういう意味では、ライブラリ開発は自分の人間としての気づきにもなっています。

 ちなみに今は自戒を続けて、「emadurandal氏、ボクいろいろ改善するよキャンペーン中🎊」なので、多少はマシになっています。お仕事でご一緒する方は「大丈夫かな、コイツ」とか思わないでくださいね(笑)

WebGLよりもCGの理論と実装

 GLBoostも最初はWebGL APIをうまく抽象化する役割が主でしたが、そうした側面は今はむしろ少なくなってきました。
 求められることが増えるにつれ、機能を増やす上で重要なのは、WebGLよりもむしろ「CGの理論と実装」になってきています。

 例えば、シェーダーで良いライティング品質を出すためには、もはや昔のCGの教本に出てくるようなBlinn-Phongだけでは力不足です。 3

 リアルな品質を実現するには、今は物理ベースレンダリング4 が重要だとされています。その為にはBRDFや5 レンダリング方程式6、マイクロファセット7など、なんだか難しそうな概念を調べていかなくてはなりません。

 数学がたくさん出てきますが、私もよく壁に当たって詰まりました。今も現在進行形で詰まります。
 手に入れられる書籍や情報はすべて当たって、食らいついて理解する必要があります。諦めなければ理解できることは着実に増えていきます。昨今では、技術系のフォーラムやSNS、チャットルームなど、人に聞ける場所も増えてきました。

 さて理論の一方で、CGライブラリは実用性も必要です。その一つに処理速度があります。
 GeForce RTX 20xxなど、ものすごい性能のGPUが出てきている昨今ですが、それでも最適化をないがしろにはできません。

 CGにおける最適化の余地は多岐に渡ります。

  • キャッシュ(ジオメトリキャッシュ、シーングラフの変換行列キャッシュ、高度なものだとイラディアンスキャッシュ8など)
  • カリング(GPUが自動的にやってくれないもの。たとえばビューフラスタムカリング9、ポータルシステム10、オクルージョンカリング11など)
  • GPUで計算できるものはGPUにやらせる
  • 処理のマルチスレッド並列化

このあたりは、CGの教科書的な教本にはあまり載っていないことが多いでしょう。技巧的な要素が強いですが、Amazonやネットで探せば、そうしたことの専門に特集する本があります。

WebGL Insight
https://www.kadokawa.co.jp/product/301807000650/

ゲームエンジン・アーキテクチャ第2版(英語版は第3版が出ています)
https://www.sbcr.jp/products/4797377484.html

Computer Graphics Gems JPシリーズ

GPU Gemsシリーズ(少し古いけど、NVIDIAの書籍で日本語版あり)

CEDiL(日本のゲーム開発者向けイベント「CEDEC」で発表された資料のアーカイブサイトです)
http://cedil.cesa.or.jp/

高度かつ英語の資料になりますが

ShaderX/GPU Pro/GPU Zenシリーズ(英語書籍)
http://www.realtimerendering.com/resources/shaderx/

SIGGRAPH Course(世界最大のCG学会のコース資料。Unreal EngineやEAのFrostbiteエンジンなど、トップレベルのリアルタイムCG技術の理論&実装の一端が毎年公開されています)
http://advances.realtimerendering.com/

GDC Vault(世界最大のゲーム開発者イベントの資料にアクセスできます。無料登録で見れるコンテンツもあります)
https://www.gdcvault.com/

 英語が苦手だけど最近のCGの潮流を追いたい、という人はまずCEDiLに登録して日本語の資料をあたり、不足があれば英語の文献にも手を伸ばす、というのでもいいかもしれません。

 こうしてみると、WebGLは単に描画のための道具の一つにすぎないことを感じます。

 UnityやUnreal Engineなど、便利なゲームエンジンが普及して、エンジンベースで物を作れる人は増えました。
 しかしゲーム業界などでは、CGの理論や実装の背景が分かる人材が少なすぎる、という嘆きの声も聞こえてきます。専門学校のみならず、現場ですら最初から最後までUnityでやるようなケースが増えたためです。こうした業界を目指されている方は、スキル上の差別化の一つとして、CGを体系的に学ぶことをされてみるのは非常に有意義だと思います。

これからCGを学んでみたい方へ

 私などが「CGはこう学ぶべし」みたいなことはとても言えないのですが、「振り返るとこういうステップがCG学習に役立ったよ」ということならお伝えできると思います。

抽象度の高いWebGLライブラリで、一度3Dコードを書いてみる。

 いつかはWebGLをぜひ直接書いてみていただきたいのですが、やはり初めての方が挑戦すると、大抵は挫折してしまうようです。
 最初は、WebGLを抽象度の高いAPIで覆った、高級ライブラリを通してコーディングされる方が良いでしょう。WebGLは描画に至るまでに行うべきデータの準備や関数呼び出し手続きの決まりごとが多く、まず絵を表示させるまでが大変です。
 WebGLライブラリであれば、CGの基礎的なお作法に従って、やってほしいことや値を指定していくだけで3Dを作れます。逆にいうと、基本的なCG知識は持っていないと、まだちょっと大変です。

 そういう意味では、CGのイロハがまずわからないよ、という方は最初にCG-ARTS協会の「コンピュータグラフィックス」という書籍で体系的に学ばれると良いでしょう。最近だとなんかこんなコンテンツもあるみたいですね。

日本でいうと、情報量が多いWebGLライブラリはやはりThree.jsなのでしょうか。海外だとMicrosoft勢の方々が作られているBabylonJSなども人気のようです。12

 また、最近では各種WebGLライブラリのプロジェクトが、専用のWebGLビューアー・エディターをリリースしていることも多く、ブラウザ上でその機能を簡単に試すこともできるようになっています。
 GLBoostも専用ビューアーをリリースしています。

image.png

image.png

WebGLを直接使ってコーディングをしてみる

 WebGLといえばおなじみ @doxas さんの wgld.org や、MDNのチュートリアル、WebGLではないですが有志の方によるOpenGL Tutorial JPなどのサイトでWebGLを使った直接コーディングを学びましょう。
 最近ではVulkanやDirect3D 12といった、ハードウェアにかなり近いローレベルAPIが話題を集めていますが、あそこまで低レベルなのは基本的にはGPUの処理パフォーマンスを最大限に引き出すためであり、CG表現的な自由度という点では、WebGLおよびそのベースであるOpenGLが特に劣っているわけではありません。

 VulkanやDirect3D 12は、使いこなせると格好いいイメージがありますが、正直私もまだ本格的に手を出す気になれません(Metalはさわり始めましたが)。
 機能がシンプルなOpenGL ESにしても、ネイティブコードだとエラーハンドリングなどは自分でやらなければなりませんし、C/C++のプログラミングは慣れないと罠にはまります。

 3D API初めて触る、という人は、悪いことは言いません。迷うことなくWebGLにいきましょう。

 GL系のAPIは色々と意見もありますが、GPUをダイレクトに叩け(広義の意味で)、マルチプラットフォームでありつつ、程よい抽象度で様々なリアルタイム3D処理を制限なく記述できる、現状唯一のAPIだと思います。

 つまり、WebGLを学ぶことで、他の3DAPIを学ぶ際の最初のとっかかりにすることができる、ということです。

恐れずライブラリ開発に挑戦してみることの勧め

 ある程度、WebGLなどの3DAPIを書けるようになった方は、ぜひ私のようにライブラリやヘルパ関数集を作ってみることをおすすめします。必ずしも最初からGithubで晒さなくても、ただローカルで密かに書き溜めていくだけでも良いと思います。

 私個人の考えにすぎませんが、この自作ライブラリ開発以上にCG開発スキルを骨身に染みさせるものはないと思うからです。概念としてなんとなくわかっているつもりでいるのと、実際に実装でき、しかも利用者に向けてAPI化できるというのは、雲泥の差です。実装しようとすると、動作させるのに必要な知識が全然足りていなかった、あるいは誤解していた、ということはざらにあります。
 逆に、実装してAPI化までできたのなら、完全に習得できたことを自らに証明できたことになります。

 そうして蓄積した機能は、さらに大きなCG機能を構成する部品として使っていくことができます。大掛かりなCG理論は、実装して検証するのに、ゼロから作るにしては工数がかかりすぎます。こうした部品を積み上げて実現していくことになります。

glTFはいいぞ(勉強に)

 ……元glTF警察の癖が出てしまった(今はそれほど警察名乗れるほどのことしてませんが)。
 どういうことかというと、リアルタイムCGのデータが一般的にはどういう構成になっているのか。という概要を学ぶのに良いのです。

 次の図を見てください。

image.png

有志による日本語版もあります)

 glTF2.0フォーマットの構成図です。これを見ると、頂点データの構成(Mesh, Primitive, Attribute)、データのメモリ上のレイアウト(Buffer, BufferView, Accessor)、アニメーションデータの構成(スケルタルアニメーション、ブレンドシェイプ(頂点モーフィング))や再生方法など、多くのことが理解できます。
 
 実際glTF2フォーマットは、その前身となるCollada、glTF0.8、glTF1.0を経て、長い議論と運用を重ねて練られたものであり、再生用のデータ形式としてはよくできています。現行GLBoost時代はクラス設計面などで、実際のCGデータの保持にあたって冗長すぎる作りをしてしまったり、ちぐはぐだったりしたのですが、新GLBoostではglTF2.0のデータ構成を意識するようにした結果、かなり設計面をすっきりさせることができました。

 ライブラリを作りはじめたものの、どうやってデータ設計しよう……と悩んでいる方は、参考事例としてまずはglTFフォーマットを調べることをおすすめします。

他ライブラリのソースコードを読んでみる

 これもおすすめです。ライブラリ設計面はもちろん、言語の高度な使いこなし方、プロジェクトの構成の仕方など、多くのことを学べます。あまり大きすぎるプロジェクトは、持て余してしまい読み切れないので、自分の肌感に合うプロジェクトを選ぶといいでしょう。

 Three.jsは一番メジャーなWebGLライブラリですが、古くからの歴史あるライブラリですので、まだECMAScript2015のクラス構文の導入が見送られていたりと、ややコードが読みづらい側面があります。とはいえ、一番知見やサンプルが豊富に集まっているライブラリであることは間違いありません。サンプルコードのディレクトリなど、部分部分で参考になるところはたくさんあるはずです。

 他には、Babylon.jsはTypeScriptなので比較的読みやすいですね(規模は大きいですが)。

 あとは、これはJavaScript WebGLライブラリではないですが、Googleが公開しているC++製のFilamentというライブラリもお勧めです。このライブラリは物理ベースレンダリングへの高度な対応を始め、Clustered Forwardレンダリングという大量のライトを処理できる機構など、その設計・実装面はかなり優れていそうで、とても勉強になりそうです。

WebGPU/WebGL2+ の足音が聞こえる

WebGL1はすでに十分と言っていいほど普及し、今年はさらにWebGL2の普及も少し進みました。AppleのSafariブラウザ、以前からテクノロジープレビュー版で一応WebGL2対応でしたが、GLSLシェーダーはなぜか1.0止まりでした。しかしつい最近、WebGL2.0で必要となるGLSL ES 3.0に対応。対応を残すメジャーブラウザはEdgeのみとなりました。

とはいえ、WebGL2(=OpenGL ES3.0相当)ですら、機能性としてはもはや10年以上前のAPIです。現在の強力なGPUの先端機能の大半を使えずにおり、それがWebにおける3D表現の隆盛の足かせとなっていました。

その状況がもう数年で大きく変わりそうです。WebGPUという、新しいWeb3D向けの標準API規格が近い将来のリリースのために策定中です。
 これはもともとAppleが自身のMetal APIをベースにWebAPI化した仕様提案から始まりました。最初はGoogleやMozillaもそれぞれ新世代Web3D APIの提案を出していましたが、結局WebGPUに業界の動きが収斂してきたようです。もっとも、現在は大筋ではMetalベースでありながらも、より他の要素を補いながらバランスのとれたものにすべく、策定が進んでいるようです。シェーダ言語もMetal Shading Languageではなく新たにWHLSL(Web High Level Shading Language)というHLSLベースの言語が採用される予定です。
 このWebGPUはすでに、試験実装をSafari Technology Preview版で試すことができます。

 (ちなみにMetalにより近い元のApple提案ベースのAPIは、WebMetalという名前でSafari Technology Previewに残されており、こちらも開発者オプションで有効にすることができます。)

国内でも、早くも熱心に試して素晴らしい記事にされている方がいらっしゃいますね(WebGL2についての記事もあるようです)

「次世代のWebGPUの可能性 – WebGLと比較して理解する描画機能の違い」 (ICS MEDIA)

WebGPU世代になれば、汎用計算が行えるコンピュートシェーダーなどの、現在のGPUの機能の多くをWebでも利用することができ、活用分野・可能性が大きく広がります。すでに、CGだけではなく、機械学習などにも応用が試験されているようです。

 ちなみに、WebGPU APIはAppleやGoogleの人たちが主導している面が強く、現状のWebGLを策定しているCG分野の業界標準化団体Khronosはあまり積極的でないようです(一応、各種公開資料ではその存在についてペラ1枚で言及するなどはしているようですが……)。
 Khronosとしては、WebGL2の普及を推進するとともに(MicrosoftがEdgeエンジンを捨て、Chromiumを採用するというニュースもありましたね)、WebGL2に新たな拡張仕様を追加することで存在感を維持しようという思惑があるようです(?)。

SIGGRAPH 2018では、KhronosからWebGL2やglTFについても、状況アップデートがありました。
https://www.khronos.org/developers/library/2018-siggraph

今後、WebGL2に対して検討されている主な拡張機能

詳しくはこちらのBOFをご覧ください。

特に気になるのは次の2つでしょうか。

  • コンピュートシェーダー(WebGL2 Compute):頂点/フラグメント(ピクセル)シェーダー以上の汎用的な演算ができます。OpenGL ES 3.1の仕様を持ってこようとしているようです。
  • WebVR向け最適化(WEBGL_multiview extension):現状は右目・左目それぞれに2回描画する必要があり、高負荷を強いられていますが、それを最適化する拡張です。

WebGL1時代は、各Web3Dライブラリも、できることが似たり寄ったりな部分がありましたが、WebGPU/WebGL2+世代のGPU機能の幅の広さであれば、それをどこまで活用できるかで、各ライブラリの機能の差が顕著に現れるようになると思います。

付録

GLBoostの商業採用事例

今までの事例を紹介します。なお、クライアント企業様の採用先製品やサービスについては、かなり表現をぼかしています。

2016年末の頃:Webサービス用の3Dファイルビューアーとしての採用

 ある企業向けWebサービスにおける、3Dファイルのビューアーとして採用されました。現在もサービス稼働中です。商業レベルの機能を実装しなければならないスケジュールはハードでしたが、この時に得た経験は、何者にも代え難いものとなり、後に続く他の商業採用にも繋がっていきました。

2017年:3Dコンテンツ配信における描画エンジンとしての採用

 前項の商業案件で鍛えられたためか、当時の他の選択肢よりも高精度にアニメーション再生をしてのけたのが決め手となり、GLBoostが再生エンジンとして選ばれました(最初は、ただの業務委託プログラマとして入っただけなのですが・・・)。
その後は、GLBoostと3Dに関する総合的なアドバイザとして、採用いただいたクライアント企業様をサポートするために現在も参画を続けています。

2018年: 3つめの案件採用

 こちらは、いろいろ事情もありまだ内容は伏せさせていただきます。来年あたりは何か書けるようになるのかな……。

最後に

 GLBoostの現在と、試行錯誤の過程、これからの予定。そして、ライブラリ開発のススメ、という流れで記事を書かせていただきました。
 この呼びかけ、意外とやってみるもので、「自分もやってみよう」と独自のWebGLソリューションを作られる方も実際におられます。声高にこうしたことを主張するのも実は内心ハラハラするのですが、こうした例を拝見すると「言ってみるものだな」と思えるので、またここでも似たようなことを書かせていただきました。
 今年のアドベントカレンダーを見てみると(別に私の呼びかけに関係なく始められた方がほとんどでしょうが)、少しずつ増えてきてとても楽しい感じです。

 来年はさらに仲間が増えたりするでしょうか……。


  1. 直前のコミット値なら可能ですが、それだと1つずれてしまい分かりづらくなります。 

  2. 「リスコフの置換原則」という単語で調べてみましょう。 

  3. 念のために言えば、写実的なリアルさを売りにする最近のタイトルでも、Blinn-Phongを使っているケースは今でもまれにあります。特性をわかっている上で使うのであれば、必ずしもダメというわけではありません。 

  4. エネルギー保存則などの物理法則の考慮や、マテリアルの反射率などに数学的幾何モデルや現実の実測値を用いるなど、物理的根拠に極力基づこうというレンダリングアプローチ。 https://ja.wikipedia.org/wiki/%E7%89%A9%E7%90%86%E3%83%99%E3%83%BC%E3%82%B9%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0 

  5. 双方向反射率分布関数。 https://ja.wikipedia.org/wiki/%E5%8F%8C%E6%96%B9%E5%90%91%E5%8F%8D%E5%B0%84%E7%8E%87%E5%88%86%E5%B8%83%E9%96%A2%E6%95%B0 

  6. 物体間の反射による光の輸送を再帰的に扱うレンダリングのための方程式。現在のCG理論の基礎の一つ。 [Kajiya1986] James T. Kajiya - "THE RENDERING EQUATION", 1986 

  7. 物体表面を微小な平面の集まりと考え、その平面の向きやそれによる光の遮蔽率などを数学モデルとして捉える考え方、またその微小平面のこと。 https://en.wikipedia.org/wiki/Specular_highlight#Microfacets 

  8. 放射照度(イラディアンス)という光量の単位で、周りから入射してくる光の量をキャッシュしておき、次のレンダリングで光の反射の計算に用いるキャッシュシステムのこと。 

  9. カメラの視界の外にある3Dオブジェクトについては、描画命令を発行しないようにCPU側で処理対象から取り除く(カリング)すること。 

  10. 室内シーンなどにおいて、部屋(ポータル)の配置情報を事前にマップに埋め込んでおくことで、描画処理の最適化を行うシステムのこと。 

  11. 深度テストを有効にした状態で最初にオクルーダー(テスト用の簡易オブジェクト)を描画し、実際に描画に成功したピクセルが存在するオブジェクトのみ、本番のオブジェクトを描画する最適化手法のこと。オクルーダーの分の描画負荷が一定分増えるため、それを補って余るほどの処理時間の削減が発生しなければ、むしろ遅くなってしまう場合もあり、実装が割と難しい。 

  12. 最近ですと、BabylonJSとBISHAMON(エフェクトツールです)を組み合わせたWebゲームの事例もありました。 BabylonJSは単なるライブラリというよりも、若干ゲームエンジン的な部分も見越して開発されているように見えます。 

30
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
17