レンダリングエンジン進捗まとめ
前回のポエムが長くなり過ぎたので二回に分けた。
現在、レンダリングエンジンに対するコミット数は104。
簡単に振り返ってみる。
1日目あたり
最初の方のコミットは、なんというか、コピペコードばかりだ。酷過ぎる。とりあえず動くところ見るためとはいえ、よそ様のブログのコードをコピペって動かしているのがよくわかる。一応は自分なりに考えてレンダラ、シーン、ビューのクラスを作って雰囲気的に設計を考えてるようだが、かなりお粗末だ。
まず最初に、メッシュという概念を定義して、その派生クラスとして三角形プリミティブメッシュを作ったようだ。そして、それを描画するための一通りの初期化コードと、コンパイルしたシェーダを読み込むためのバイナリローダ、およびリソースハンドラを作っている。リソースの所有権を誰が持つかは当時まったく決めていなかったようで、なぜかレンダラがシェーダの所有権を持っている。
このレベルだと3Dですらない。画面上に三角形を出すだけだ。シェーダも、頂点シェーダは頂点情報をピクセルシェーダに渡すだけだし、ピクセルシェーダも頂点カラーを出力しているだけである。まあ、初期のレンダリングエンジンの足掛かりなんてこんなものだろう。
2~5日目あたり
一つのオブジェクトにつき一つ作られるドローエレメントという描画用クラスを定義したり、シーンクラスを真面目につくったり、少しずつ地盤を固めている。
そして、そろそろテクスチャを描画したくなったのか、libpngライブラリを導入し、pngローダを作成した。どうやらこの時期にようやくカメラという概念を導入し、ビュー変換、パースペクティブ変換を実装し、3D的な描画ができるようになったみたいだ。といっても、2Dポリゴンを傾けるようなレベルだが。
この次辺りでなぜかファイルキャッシュの仕組みを作っている。他にやるべきことがあるだろうに。
6日目あたり
ここでようやく、インデックスバッファを使った描画に対応し始めた。
これ以降からやっとレンダリングエンジンらしい開発を始めている。これまでは、レンダリングエンジンというか、ただのDirectXチュートリアルみたいな状態だった。
7日目あたり
3D描画のテストのためには、描画用のモデルローダを作る必要がある。最も多くの無料モデルが配布されていると言っても過言ではない、PMDモデル(MikuMikuDanceモデル)に着目し、最初にローダを実装した。現在の主流はFBXだが、アレは仕様が複雑怪奇すぎるので、いまだに実装をしていない。Autodeskが配布しているライブラリに依存するのも嫌なので、後回しにしている。
とりあえず頂点とインデックスリストだけを読み込むだけなら、30分程度で作れた記憶がある。意外と簡単にモデルのディテールが表示されて、謎の達成感に包まれた。なにせ、やっと3Dの世界にきたという実感があったからだ。(一応、立方体の描画とかはやっていたが)
7日目あたり
次に、やっとライティングに手をつけた。まずディレクショナルライトを追加し、最も簡単なランバート拡散反射を実装している。正規化頂点法線と正規化光源ベクトルの内積で、そのなす角が求まるので、直接光が当たってるかどうかわかるよねってだけのアルゴリズムだ。ポイントライトも追加しているが、ディレクショナルライトに距離減衰と光源位置を追加しただけのものなので、拡張は楽だった。
8~11日目あたり
最初の挫折はここで味わった記憶がある。ディレクショナルライトのシャドウの実装だ。驚くほど動かなかった。
アルゴリズムとしては、シーンを覆える視野のライトカメラを仮定して、ライトカメラから見えるシーンすべてのオブジェクトの深度を、ステンシルビューを使って描画する。あとは、実際のレンダリングで、対象の頂点とライトカメラの距離と、深度バッファの保存してある深度を比較し、必要ならシャドウをつける。
GPU処理の基本、すべてのデータをテクスチャに焼きこんで、複数のパスで共有して使う。その最初の一歩だった。テクスチャには16ビット型無しフォーマットとして深度を描画し、それをシェーダーリソースとしてR8G8符号なしテクスチャとして読み込む。またビュー座標系からテクスチャ座標系へのマッピングも忘れてはならない。
他にも、カメラから深度バッファを書き込むときはもちろん、実際のレンダリングのときにも、再度頂点をカメラの位置からビュー変換しなくてはならないので、定数バッファにライトの数だけライトのビュー・パースペクティブ行列を格納しなければならない。
今まで雰囲気でやっていたシェーダ処理に、途端に厳密な実装を要求され、手探りでいろいろと試していた記憶がある。結果的に3~4日かかってしまった。動いたときはそれはもう感動したものだ。
14日目あたり
今更ポイントライトの修正をしたり、あとはマウス操作によるカメラ移動を実装している。インタラクティブな要素を入れた途端、リアルタイムレンダリング感が出た。
15日目あたり
DirectXリソースのメモリリークを解消している。実はこれをするまで、尋常じゃないくらいメモリリークを起こしていて、Warningを吐いていた。全部無視していたが、やっと観念して修正したようだ。
そして、PMDモデルの材質リストに着手するとともに、一つのメッシュが複数のマテリアルを持つという当たり前のことが想定できていなかったせいで、大幅なレンダラのリファクタリングを強いられている。PMDモデルのテクスチャは基本BMPなので、BMPローダの実装も行った。無圧縮フォーマットなので実装は楽勝だった。やっと色付きの初音ミクの表示ができたのである。
17~20日目あたり
鏡面反射を実装している。ここで、ようやく本格的な物理ベースレンダリングのコードを描き始めたことになる。基本的にはクック・トランスの反射モデルを採用し、今まで通り直接光しか計算していない。材質のメタリック値を上げると、スペキュラが3Dモデルに現れるようになった。割と、現実に近い挙動をする。真面目な物理モデルを実装したので。だが、実際のそのアルゴリズムを紹介するには、ここの余白は狭すぎる。というか、自分が参考にしたQiita記事が最強だったので、DirectXの実装的な補足を加えつつその記事を紹介する記事を書きたいものだ。
21日目あたり
PMDモデルだけでは限界を感じてきたので、.xモデルのローダも作成した。バイナリフォーマットは面倒くさそうだし、MSDNの資料も微妙だったので、テキストフォーマットにだけ対応することにした。テキストフォーマットなら、仕様は実際のファイルから読み取ることができる。ダーティーなハックになるが、いろんなファイルをかき集めて、ロードに失敗するたびにファイルの中身を確認し、想定漏れの仕様等を毎回カバーする形で実装していけば、いずれ完全なローダになる。
C++のクソみたいな文字列処理で頑張って実装してもよかったのだが、いい機会だったのでパーサコンビネータを作成した。Boost.Spirit.Qiを採用したが、最終的なアウトプットはまあそこそこ美しい反面、あまりにも開発が苦痛だった。なにせちょっと構文や型を間違えるだけで尋常じゃない量のコンパイルエラーを吐くからだ。人類には早すぎるライブラリな気もする。ただ、ユーザー定義構造体をタプルにバインドして、美しく型安全な構文ルールを定義し、パースする仕組み。なるほど、うまく使えればとても気持ちが良い。私自身はTMPが大好きなので、正直使ってて興奮した。
22日目~35日目
リアルが立て込んだせいで日付が一気に飛んでいるが、主にひたすら.xモデルの仕様網羅と最適化をしていた。
.xモデルの仕様として、頂点リスト-面リスト-材質リストという順に定義されている。面リストは、一つの面につき複数の頂点への参照を持つ。つまりインデックスだ。そして、材質リストは、面リストすべてのマテリアルを定義する。つまり、面一つ一つが独自のマテリアルを持つことが可能(仕様上)であり、そのまま愚直に実装すると、ドローコールが面の数だけ発生してしまう。
描画最適化のために、まず面リストを同種のマテリアルでグループ化し、グループ化された面リストを一つのインデックスリストに展開するようにした。こうすれば、ドローコールはマテリアルの数だけで済む。PMDモデルと一緒だ。ただし、面の順序を変える形になるので、面が連続ではなくなる。そのため、ストリップ描画をすると、離れた面へ描画していくとき、その軌跡に謎の三角形が生じてしまう。なので、頂点数4以上の面は、複数の三角形に分解し、三角形リストとして描画することにした。描画が爆速になった。
あとは、モデルによっては頂点法線がデータに入っていないことがあったので、その際の計算も行うようにした。頂点が所属するすべての面の法線を合成した正規化ベクトルが頂点法線になる。ただし、三角形の面の法線はある点から他の二点への位置ベクトルの外積で求まるが、四角形はそうはいかない。対角ベクトル二本の外積を求める必要がある。五角形以上は知らん。
現在
環境キューブマップの実装をしている。描画に使う、6面の周囲情報をベイクしたテクスチャのことだ。これを使うと、ポイントライトの全方位シャドウや、リフレクションが実装可能になる。ポイントライトのシャドウは結構面倒なのだ。また、リフレクションが実装できれば、鏡面に周りのオブジェクトの映り込みが実現できるので、おそらく更に”それっぽさ”が向上するだろう。
一か月ちょっとを駆け抜けてみた。結構いろんなことをやったようで、まだ何もしてないようにも思える。シャドウの品質を上げるためのキャスケードシャドウや、間接光計算実現のためのフォトンマッピング、アンビエントオクルージョン、そもそもディファードレンダリングすら実装できていない。そのほか、ライトの色情報や、その輝度だったり、そもそもライトが面積を持つことも想定していない。というか、こういう細かいことを挙げていたらキリがない。まともな見栄えのレンダリングができるまで、少なく見積もってもあと半年はかかると思っている。
最終的な目標は、国産のPMX・PMDデフォルト対応の高品質レンダリングエンジンだが、まあ、数年がかりになるだろうし、そこまで到達できるとは思っていない。
あくまで、今は自分が楽しんで、学ぶために開発している。そして、それがほかのレンダリングに興味のある人間の刺激になればなおよい。
振り返りはここまで。次からは、ちゃんとした開発日記をつけていこうと思う。
レンダリングエンジンの開発を始めた。
レンダリングエンジンの開発を始めた。
今は割とやる気があるので、このやる気が継続するよう、開発日記をつけるためにブログの開設をした。
私はゲームプログラマであるので、レンダリングとは一般的なほかのエンジニアよりも縁が深い立ち位置だが、今まではレンダリングという技術を雰囲気でしかわかっていなかった。いや、真剣に学び直した今考え直してみると、実は1ミリもわかっていなかったようにも思う。
そもそもレンダリングエンジンは、ゲームエンジンをレイヤー分けした時、割と低レイヤーな機能に属する。勿論最も高いレイヤーの機能はマウス操作で完結するエディターだ。
例えば、Unityが外部アセットから読み込んだポリゴンを内部でどのように展開して描画してるのかとか、どんなライティングアルゴリズムを使っているのかとか、そんなことを気にしなくてもゲームは作れる。
ライトをたくさん置くと死ぬほど重くなるし、リフレクションやシャドウとかはなるだけ静的にベイクしないと死ぬ。半透明オブジェクトはディファードレンダリングで描画する有力な手段がないのでコストが高い。画面外のオブジェクトはカリングされるので描画コストについては考慮しなくて良い。
(余談だが、一部モバイル端末のグラフィックスAPIはMRTに対応していないらしく、Unityモバイル向けビルドではディファードレンダリングが使えないらしい。正気か?)
こういったことはどのゲームエンジンでも共通で、皆経験則的に理解している。だが、実装の詳細まで把握している人間は一体どれだけいるのだろうか。優秀なゲームエンジンが多くある今なら尚更だ。あるいは、コンシューマ全盛期にまで遡ったとして、その時期は固定機能シェーダが基本だったのではないだろうか。自力でバーテックスシェーダやピクセルシェーダを使い、物理ベースレンダリングを実装したことのある人間はゲームエンジニア人口のどれほどになるのだろう。
少なくとも、私はその他大勢の人間だった。リアルタイムレンダリングという技術への理解が浅いまま、cocos2dxやUnityを使いゲームを開発していた。
だが、ある時仕事の都合で、UnrealEngine4のソースコードを読む機会があった。主にレンダリングのコードをだ。その時、私の中に衝撃が走った。リアルタイムレンダリングという技術に、ただただ感嘆した。cocos2dxもオープンソースで、プルリクを送った経験があるくらいにはエンジンソースコードを読んでいたが、その時とは比にならない感動を覚えた。
率直に言って、レンダリングフローの流れを掴み、どのクラスでどんなことをしているのかは分かっても、その実際の処理の詳細まで理解することは叶わなかった。知識が圧倒的に足らなかった。
オープンソースのエンジンを使っていながら、その中身を理解せぬままに上部だけをすくい取り開発をすることが、果たしてエンジニアにとって正しい姿なのだろうか。少なくとも私は違うと思っている。
なにはともあれ、リアルタイムレンダリングの正しい知識を身につけるべきだと思った。
そう思い、私はフルスクラッチでレンダリングエンジンを開発することにした。
三角形ポリゴンを表示するところからだ。ちなみにDirectX11を使っている。本来ならばグラフィクスドライバAPI部分は抽象化して、クロスプラットフォームに対応するべきなのだろうが、私はプロダクトが作りたいわけではなく、レンダリングが学べればよいので、Windowsに依存する形にした。あと楽なので。VulkanやMetal、OpenGLなど、それらすべてを網羅するには、本質的ではない煩雑な学習コストがかかる...。
仕事の合間に作業を進めつつ早1ヶ月、簡易的なものなら一通り動いている。だが、道のりは長く険しい。
githubのコミット履歴を見ると、大体書いたコード量はライブラリ等を除くと1万行くらいだ。しかも消したり書いたりしてるので実質残っているコードは5千行くらいな気もする。
開発日記と言いながら、ほとんどポエムになってしまった。
次は、現在までの進捗をまとめることにする。