
【cocone TECH TALK VOL.5・後編】AvatarMakerを支える技術【イベントレポート】
-
2022年4月28日
こんにちは!cocone tech blog編集長のYです!
今回は2022年2月25日に行われた「cocone TECH TALK vol.5 ~ cocone x cluster ~」でお話した内容のうち、後編としてクラスター株式会社@yutoppさんに発表していただいた内容になります。
後編のテーマは「AvatarMakerを支える技術」となります。
前編をまだ読んでいない方はこちらからどうぞ!
【cocone TECH TALK VOL.5・前編】Unity Dotsを使ってみた【イベントレポート】
クラスター株式会社でソフトウェアエンジニアをやっております、@yutoppといいます。
よろしくお願いします。
本日はclusterのAvatarMakerという機能を実装した際に得られたUnity開発の知見について、どのような前提条件があって、どのような調査・試行錯誤をしたか、ということを中心にお話できればなと思います。
AvatarMakerとは
Avatar Makerとはcluster(VRからモバイルまで楽しめるメタバースプラットフォーム)内でアバターを作る事ができる機能となります。
Windows(Desktop/VR)、Mac、Android、iOS、Meta Quest2といった様々なプラットフォームで使う事ができ、自由度の高いアバターを作成する事が可能となります。
AvatarMakerの要件
Avatar Makerの要件として最初にあげたのは
- cluster内でアバターを作成できること
- 服装・髪型・色を切り替えられること
- 顔の造形・パーツを変更できること
- 社内外のクリエイターの方が素体の部品をアップロードできること
となっており、まとめると
- どこでも動く
- 大体いじれる
- 社内に閉じない制作ワークフロー
を目指して制作しました。
clusterの設計
Avatar Makerを追加する以前の設計はこのようになっており、ユーザーの方がウェブサイトからVRMをアップロードしておき、clusterアプリを利用する際にVRMをダウンロードして表示するという形を撮っていました。
Avatar Maker追加後の設計はこのようになっており、少し複雑になりました。
Avatar Maker追加後は社内外のクリエイターの方がglTFをサーバーにアップロードできるようになりました。そしてclusterアプリ利用時にglTFをダウンロードして組み立てて表示し、組み立てたパーツをVRMとしてサーバーにアップロードする、という仕組みとなりました。
これによりVRMを作る、という口を作成しました。
Clusterの技術構成
ではここでClusterの技術構成についてご紹介します。
クライアントはUnity2019.4.22fを使っているのですが、社内で新しく更新したいという話が出ています。
おおよそMVPパターンで設計していて、使用しているライブラリはZenjectやUniRX/UniTaskなどがあります。
サーバーサイドはGoで実装されており、VRMやglTFを最適化する機構をたくさん搭載しています。
このあたりの詳しい話はクラスター Advent Calendar 201の
メタバースプラットフォームのclusterのserverについて(2021応用編)
OSSのglTFライブラリにPullRequestを出した件について
を参照していただければと思います。
以上を踏まえて先ほどのclusterの設計を振り返ると、大きい機能追加ですがシンプルに構築する事ができました。
大事だなと思うのは、機能の依存関係の設計の維持を頑張ると建て増しする際に整理しやすいので、変更しやすい状態を維持することを意識すると良いのかなと開発していて思いました。
ZenjectやUniRx/UniTaskなどを使い設計を維持するとメンテナンスもしやすくしつつ、大きな機能開発ができるというメリットがあると思っています。
全体実装
公開部分を含むAvatar Makerライブラリが中心にあり、その下に開発実験アプリやcluster、パーツ動作確認アプリがぶら下がっているような感じで開発しました。
今回は実験用アプリとclusterを別で作り、組み込む形としました。
今回最初からclusterには組み込まずにライブラリと試験実装アプリとして開発しました。
そして途中からインターフェイスの微調整などをしつつライブラリのみcluster本体に組み込みました。
なぜこのようなことをしたのかというと、前提条件として社内外で使うというのがあったため、いきなり組み込んでしますと引き離すのが大変なのである程度外に出すという前提で境界を引く必要があったからです。
今回の実装方法でのメリットとしては
- 疎結合な設計を強制できる & asmdefを切りやすい
- 単体アプリとして動作確認のイテレーションを高速にできる
といったものがありました
デメリットとしては、どこまで最適化するかの塩梅が難しいがちで、組み込んでわかるリソースの条件やプラットフォームごとの差異などが結構ありました。
なので高速で開発してある程度形ができたら組み込んでフィードバックを得る、というループをたくさん回すのが大事だと今回の開発を通して感じました。
glTF 2.0
glTF 2.0はKhronos Groupが開発している3Dモデルを扱うファイル形式で中身はjsonとbinary blobです。
公式サイトによると「glTF is the “JPEG of 3D”」ということで、3DモデルやSceneファイルのエンコードするようなファイル形式です。
VRMはみなさん結構知っているかもしれませんが、VRMはglTFの上に構築されています。
Avatar Makerではglb(binary)形式を利用しています。
glTFの拡張領域にclusterの拡張を定義し、そこにアバターの部品データのメタデータを格納しています。
なので元データ自体はブラウザでも読み込めます。(cluster上では難読化して配信しています)
glTFを採用するメリットとしては
- AssetBundleのように各プラットフォームに向けて何度もビルドする必要がない
- 標準的な規格なので、多言語のライブラリからでも読める相互運用性の高さ
があげられると思います。
プラットフォームごとのデータの最適化はバックエンド側で担保できるので、マルチプラットフォームで使うには最高です。
逆にデメリットとしては、glTFの拡張領域を自由にライブラリから使うのが難しい、というのがありました。
このデメリットの解決策として、UnityでglTFを扱うライブラリを丸ごと作り解決しました。
clusterではglTFのimport/exportとVRMのexportで利用しています。
コードを見るとimportersの後にMaterials.AddHookとあるように、各々の拡張に対してフックをつけることによって読み込み時にフックを介して拡張をうまく機能と結びつけることができます。
OSSで公開しているので、よければ使ってもらえると嬉しいです。
GitHub : yutopp/VGltf
アバターの合成実装
次にアバターの合成実装についてお話します。
これは何かというと、複数のglTFパーツをダウンロードし、クライアントで1つのHumanoidモデルに合成して表示する部分になります。
スライドのようにトップスとボトムス、その他いろいろな3Dモデルを組み合わせて右側のHumanoidモデルを作る事ができます。
この実装をする際に出たボツ案として、「複数のglTFパーツは単体でもHumanoidなので、合成せずにAnimatorを同じ位置に重ねて表示する」がありました。
これのメリットとしては、パーツの合成処理をVRM出力時に1度だけ行えば良いということで軽量です。
しかし、パーツ切り替え時にAnimationのタイミングを全て合わせるのが非常に面倒だったり、必須のComponentを全てのパーツにつけて同期するのも大変でした。
なぜこうしたのかというと、パーツの合成処理でSkeltonの付け替えが必要になるのですが、このコストが高いと思っており最後の1回だけ実行すれば良いと思っていました。
しかし、いざ試してみると実行時のコストはそこまで高くありませんでした。
VRで試してみてもフレーム落ちも目立たないくらいでした。
なのでメリットがなくなってしまい、実装を変える事になりました。
現行ではglTFパーツを切り替えるたびに実行時に1つのHumanoidに合成し直す、という形になっています。
ボツ案とは違い、1つのAnimatorの中にrigとその他3Dモデルが入っています。
この実装のメリットはいくらglTFパーツが増えても、操作するアバターとしては1つのHumanoidモデル扱いができる点です。
そしてデメリットは特にありません!
Bone合成
次にBone合成に関するTips的な話なのですが、Avatar MakerのglTFパーツはHumanoidのBoneの基礎的な部分に関しては仕様を統一しているので、切り替え時にAnimatorのAvatarのHumanDescriptionの変更はしていません。
基本的にはSkinnedMeshRendererのSkeltonの付け替えのみです。
Humanoid関連のBoneを組み直す場合は、以下の値を編集したAvatarをanimationにセットする必要があります。
- HumanDescription.human
- HumanDesctiption.skelton
また、Avatar.isHumanがfalseを返さないように気をつける必要があります。
こちらも重たい処理だと思っていたのですが、先ほど同様にあまり負荷が高くないものだったため、ユーザーの入力に合わせてこのようなものを再生成するというアプローチでも可能だと考えています。
Texture合成シェーダ
次にTextureの合成シェーダについてお話しします。
Textureや色を合成して上書きする部分なのですが、AvatarMakerでは瞳や眉などのTextureが全てバラバラになっています。
それらに色情報を加えて最終的なレンダリングをします。
こちらはマテリアルごとに複数のTextureを1枚にベイクする処理です。
ユーザーの方がcluster上で見ているpreviewの表示とVRMとして出力する部分で同じデータを使えるようにリアルタイムで合成しています。
なぜベイクする必要があるのかというと、瞳や眉などでバラバラになっているものを全てモデルデータに入れてしまうとデータが非常に大きくなってしまいます。
なので、重なり合うように1枚にまとめて最終的な出力結果とするようにしています。
ではこれを実装する時に出たボツ案からご紹介します。
UnityにはCustomRenderTextureというものがUnity2017あたりから入っています。
公式サイトによると「CustomRenderTextureはRenderTextureの拡張機能で、これを使うと簡単にシェーダー付きのTextureを作成できます」とのことです。
この実装をするメリットとしては、今どきであることがあげられます。
ただ、デメリットとしてAndroid端末で異常な結果を返し続ける現象に出会ってしまいました。
Unityのフォーラムにも出ているのですが謎のworkaroundが紹介されており、しかもそれを試しても解決しないということで途方に暮れました。
では現行のものはどのような実装なのかというと、1×1bのメッシュに通常のShaderで描画したものを並行投影で撮影したRenderTextureを使っています。
ExecuteCommandBufferを使い実際に描画したTextureのデータを抜き取って使っています。
この実装のメリットとしてはどの環境でも安定して動くとてつもない安心感を得る事ができます。
一方でこの技術は枯れた技術なのかな、とも思います。
IL2CPP
次にIL2CPPについてお話しします。
IL2CPPはモバイルにおいて影響を受けがちな印象があると思うのですが、WindowsでバックエンドをIL2CPPビルドにしても再現できるものが多かったです。
例えばcheckedというC#の機能があり、これは値がオーバーフローした時に例外を飛ばしてくれるという機能なのですが、IL2CPPビルドを介すと何故かsignedがunsignedに変換されてしまい自前でif文を書く対応が必要になりました。
また、Type/ TypeInfoのGetCustomAttributesで返ってくるインスタンスがキャッシュされていたりしたため、毎回クローンする機能を入れる必要がありました。
最後に、code strippingによって静かに実行時エラーが発生します。
これは”[Preserve]”属性をつければ解決するのですが、今回の実装ではPreserveAttributeというものを自前で定義してそれを使いました。
ここで自前定義•••?と思われた方いると思います。
Unityに”UnityEngine.Scripting.PreserveAttribute”が実はドキュメントに書いてあるのですが、PureC#のコード辺で最適化の抑制を行いたかったのでUnityEngineへの参照を持ちたくない、というものがありました。
しかしドキュメントをよくよく読むと、PreserveAttributeという名前であればどこに定義していても最適化を抑制できるAttributeとして扱われるらしく、ありがたいけどなんだそれは•••という気持ちで自前で定義して最適化などを行いました。
様々な問題が起きたので、開発終盤は(Windows, MacOS, iOS, Android, Linux) × IL2CPPビルドで動作確認するような用心深さになっており、CIで回せるようになったら嬉しいなぁという気持ちでした。
まとめ
今回の話をまとめると
- 設計を維持するのは最終的に速い
- 頑張って共通の規格を使っておくといろいろ相乗りできて便利
- 開発イテレーションを早く回して罠をたくさん踏み抜くのが大事
となります。
特にインターフェースの切り方やTexture・IL2CPPのトラブルシュートなどは終盤に牙を剥いてくるのでイテレーションを早く回すと良いと思います。