この記事は Rust 2 Advent Calendar 2020 - Qiita 20日目の記事です!
RustとWebGPUを扱えるwgpuを使って、3DCGに挑戦しました!ので、その記録になります
ここにもGif画像をあげたかったのですが大きすぎた...
- リポジトリ: https://github.com/anotherhollow1125/obj-viewer
- お試しページ: https://www.namnium.work/obj-web Firefox Nightlyでのみ動作確認しています。重いです。マウス操作等は受け付けません。
- 姉妹記事(ぜひ読んで!): 3DCGの基礎知識 Objファイルを描画するまで
動機とRustの良さ
Rustを知るまではUnity C#でゲーム制作を行っていたのですが、Rustの魅力的な機能を知るとC#を書きたくなくなってしまい、Unityアレルギーになってしまいました。Rustには以下のような特徴があります。
- 所有権やライフタイムによるメモリ管理: NullReferenceError回避
- ガベージコレクションを使わないメモリ管理: 上記と被りますが、こちらは速度向上の意味で
- トレイト: 継承とか考えなくてよい上にわかりやすい
- わかりやすいパッケージシステム: Cargo.tomlによる管理は、他の言語の依存ライブラリ管理よりはるかにわかりやすいです
- 幅広いコンパイルターゲット: クロスプラットフォームを実現しやすいといえます。Rustによってゲームボーイアドバンスのソフトを作ったりもできるそうです!
そこで、ゲーム制作の未来を担うモダン言語Rustでゲーム制作を行うために3DCGをやってみました!本記事で、Rust&wgpuによる実装についてまとめます!
3Dゲームは9割が3DCGができるか否かにかかっていると思います(言い過ぎ?)。ともかく、3Dモデルを読み込んで表示できれば、Unityに固執する必要はなくなるでしょう。Unityで3Dモデルをロードする方法(あるいはBlenderで3Dモデルをエクスポートする方法)はいろいろありますが、その中でも扱いやすいObjファイルを使用して3Dモデルを描画できれば、この目標を達成できたことになります。
また、 今回取り組むにあたり必要となった理論的知識は、3DCGの基礎知識 Objファイルを描画するまでという姉妹記事のほうにまとめました 。そのためこちらの記事では3DCGの基礎知識に関して詳しくは取り扱いません。適宜こちらの記事を参照していただけると幸いです。( このアドベントカレンダー担当記事を優先するべきところをそっちのけで頑張って書いた力作なので是非読んでマサカリを投げてほしいです() )
使用したライブラリ - wgpu
Rustでシェーダーを触れるライブラリは複数ありますが、今回は最終的にwgpuというライブラリを使用しました。
Rustで3DCGが行えればなんでも良いと考えていたため、最初はgfxというライブラリを使用していたのですが、複数の光源を設定しようとしたところShader Storage Buffer Objectが使えないために回りくどい書き方しかできないことに気づき、またバックエンドのOpenGLもだんだんとレガシー化しているということを知り、gfxライブラリ以外に使えるライブラリを探していたところ、これから策定されようとしているWebGPUというものがあることを知りました。
WebGPUが登場した背景には各社のGPUの設計が似通ってきたことがあります。従来のOpenGL (WebGL) は「各社グラフィックボードでの共通部分をAPIとしてまとめたもの」であり、抽象度が高くオーバーヘッドが大きいです。OpenGLと比べると、より低レベルから書ける各社のAPIのほうが当然性能が高いのですが、結局低レベルでのAPI設計も似通ってきたために、新たに共通なAPIとしてWebGPUを策定しようという運びになったらしいです。以下の記事の受け売りなので詳しくはそちらを見てください()
そしてそのWebGPUのRust用の実装として存在していたのがwgpu-rsでした。というわけで、紹介していきます!
ちなみにWebGPUはWebGL同様にブラウザで動かすものですが、wgpuはWebGPU専用のライブラリというわけではなく、ローカルでバックエンドの指定なしでビルドすると使用できるバックエンドを勝手に選ぶそうです。自分の環境では Vulkan
でした。WebGPUを用いてブラウザで実行するプログラムまで書いてみたかったのですが、アドベントカレンダー担当日には間に合わないので、後日できたら追加しようと思っています。(2021/1/3 追加しました!)
2021/1/3追記
https://www.namnium.work/obj-web にてWebGPU版をデプロイしてみましたが、はっきりいって全くまともに動いていない状況です。wgpu版のWebGPUは、Firefox Nightlyのみでしか動作確認ができませんでした。その上、フレームレートがかなり死んでいます。
WebGPU関連のリンク:
- [忙しい人向け] 100行から始めるWebGPU(WGSL対応版) - Qiita
- WebGPU 実装状況のメモ; Firefox-80.0a1(nightly), Chrome-86.0.4191.0(canary) なう - C++ ときどき ごはん、わりとてぃーぶれいく☆
- Implementation Status · gpuweb/gpuweb Wiki
- WebGPU Samples
- WebGPU 対応
誰かwgpuのWebGPUについてもう少し検証等していただけるとありがたいです(><) (他力本願)
もといライブラリやブラウザがもう少し成熟するのを待った方が良いかもしれません。
以降についてTL;DR
wgpuに関する日本語の情報はほとんどなかったのですが、英語のチュートリアルサイトでとても素晴らしいものを見つけました!wgpuだけではなく3DCGの基礎知識も適宜しっかりと教えてくれるので非常におすすめです。
このチュートリアルを見つける前からObjファイルを描画することが目標だったのですが、なんとネタ被りしており、Objファイルを描画するプログラムが登場しています!チュートリアル終了後にかなり書き直しはしましたが、実装した内容は基本的にこのチュートリアルの内容です。(ぶっちゃけこの記事の目的の9割がこのチュートリアルの紹介です)
最近も更新されている新しいチュートリアルですので、wgpuに興味を持たれた方はぜひこのチュートリアルをやってみると良いと思います。
実装した内容
かるーく、今回取り組んだ内容を紹介していきます!
1. Objファイルの読み込みと表示
tobjというライブラリを使用してObjファイルを読み込み、表示させました!
Unityではオブジェクトをインスタンスとして管理するので、それに倣い、オブジェクトファイルを読み込んだうえでインスタンスを作成し表示させる方式にしました。そのため同じ物体を複数置いたりなどもできます。
Objファイルを表示させるまでの基礎知識は始めのほうで述べた通り3DCGの基礎知識 Objファイルを描画するまでの方を参照していただければと思います。
2. 光源の表現
Phongの反射モデルをもとに光源を設置してその影響をオブジェクトに与えるようにしました。
またUnityでは複数の光源を設置することができます。wgpuではShader Storage Buffer Objectが使用できるので、これで光源オブジェクトを動的配列として渡すことができ、複数の光源を設定することができます。(ちなみにチュートリアルにあるのはSSBOの紹介までです)
(実際の実行画面では問題ないのですがなぜかGif画像がうまく行ってない...)
3. シャドウマッピング
やはり立体に見せるためには影が必要だろうと思い、今回一番頑張ったところです。
実装は wgpu-rs/examples/shadow at master · gfx-rs/wgpu-rs を参考に行いました。テクスチャやシェーダを複数用意して入れ替えて...という作業は、振り返ってみるとwgpuだからこそ楽に行えたように感じます。
3DCGの基礎知識 Objファイルを描画するまでの最後の節でシャドウマッピングに関しても軽く解説しているので、良かったら覗いてみてください。
wgpuを触ってみていくつか気になった点
LayoutDescriptor
詳しくは述べませんがwgpuが持つ要素はどれも
LayoutDescriptor
Layout
- 実体 (
BindGroup
など)
の三要素からなることがとても印象的でした。
自前で用意した構造体で new
みたいなコンストラクタの役割を果たす関数に引数を渡す際、設定項目が多いとどの引数がどの引数かわからなくなってしまったのですが、そうなったとき LayoutDescriptor
の存在は一つの解を与えてくれたように思えます。すなわち、引数を構造体としてまとめて渡せば、設定項目について"ラベル"を明記しなければならないので、結果的に可読性が上がるだろうという解法です。
この方法を使うと、先ほど述べた通り引数項目が何であるのかを明記しなければいけないことに加え、std::default::Defaultトレイトを用いることで デフォルト引数に近いものを設定できる というメリットがあります!
実際にこのような性質を利用し"引数構造体"を定義してみるということは結局しなかったのですが、今後コンストラクタに渡す引数が多くなりそうなときは使ってみたいと思っています。
#[repr(C)]
とbytemuck
今回、Rustの構造体をシェーダーに渡すために用いたのが #[repr(C)]
とbytemuckライブラリでした。
#[repr(C)]
アトリビュートを指定すると、構造体のメモリレイアウトがC言語準拠になります。C言語準拠にすることでシェーダー側が解釈できるレイアウトになります。
この話題に関しては、Rustの構造体メモリレイアウト - ryochack.blogというページが詳しかったです。
また、これに関連して、シェーダーが解釈できるようにするために "パディング" を構造体に入れなければならなかったのですが、これの仕様がよくわかりませんでした。 ユニフォームブロックのメモリレイアウト @ゲームプログラマの小話[開発:グラフィックス] - Qiita などの記事を読み色々調べたり実験したりした結果、次の図のようなルールなのかと解釈したのですが、合っていますでしょうか...?
誰か詳しい人がおりましたら合っているか教えていただきたいです。m(_ _)m
あとがき
最後まで読んでいただきありがとうございました!本記事でRustでのゲーム制作について可能性を示すことができたならば幸いです。
記事投稿までに間に合いませんでいたが、せっかくWebGPUが使えるライブラリを使用しているので、近いうちにWebAssembly化したデモを作りたいと思います! (→ 作りました! )
コメント、編集リクエスト等お待ちしております!