はじめに
この記事は、ドワンゴ Advent Calendar 2019の11日目の記事です。
ドワンゴではniconicoの課金システムの開発をしています。
課金関係のことに触れられればよかったのですが、普段は趣味でUnity+C#を触っているため、
今回はそちらに関する記事です。
シンプルなglTFローダの作り方などを解説している記事はありますが、複雑なモデル描画まで行っている記事が見当たらず、
かなり苦戦したため書き残しておきます。ようは備忘録です。
Qiita記事はドワンゴに来る前に非公開記事として共有することにしか使ってなかったため、他記事に比べ読みづらいと思いますが生暖かい目で見守ってください。
何故やろうとしたか
VR向けアバターフォーマットであるVRMはglTFをベースとしているため、glTF自体に興味はあったが
触る機会がなく、ふわっとしか理解していなかったので、リファレンスを参考に自分で作ってみようと思いました。
(VRMのロード時間最適化したいといったのもありますが)。
実際にやったこと
GLBの処理
GLBファイルのバイト配列をSpanにし、ヘッダ部分を Slice
して MemoryMarshal
でCastすることで構造体として扱いました。
Unityでも、 System.Memory
をnugetから落としてくれば利用できるためぜひ使ってみてください。
var headerSpan = glbSpan.Slice(0, Marshal.SizeOf<Format.GLBHeader>());
var header = MemoryMarshal.Cast<byte, Format.GLBHeader>(headerSpan)[0];
チャンクを処理する際も、 MemoryMarshal
で処理することでほぼ処理速度を気にせず構造体配列として扱えます。
この時、フォーマットの種類である JSON
or BIN
の判定を、intとして処理することで1byteずつASCIIで判定することなく行えます(つまり、enumで判定できます)ので、フォーマット部はintで行うことをおすすめします。
JSON: 0x4e4f534a
BIN: 0x004e4942
glTFのJSON部処理
mebiusbox/gltf(Github) で公開されている以下の画像を参考にパーサを実装しました。
最初はglTFのjsonスキーマを読みながら実装をしていたのですが、このクイックリファレンスのおかげでかなり楽に行えました。
メッシュ処理
メッシュには Primitive
が配列として格納されているため、それをループで処理します。
Primitive
= Unityで言うサブメッシュを表します。
先にvertex数分確保が必要ですが、Primitive1個目のPosition数*Primitive数で初期化しました。
ここでは、 Position
と Normal
、 TexCoord0
をこの基準で格納しています。
Indices
はPrimitiveの数分のジャグ配列を初期化し、それぞれ格納しています。
実際にUnity上のメッシュにする際は、 subMeshCount
に Primitive
の数をセットし、 SetTriangles
メソッドで Indices
をセットしました。
var umesh = new Mesh
{
vertices = position,
normals = normal,
uv = texcoord0.ToArray(),
subMeshCount = mesh.Primitives.Count
};
for (var i = 0; i < mesh.Primitives.Count; i++)
umesh.SetTriangles(indices[i], i);
そのまま描画しようとすると座標系の違いにより左右反転して表示されるため、変換を行う必要があります。
ここで変換が必要なのは Position
と Normal
です(TangentはVRMの仕様上、含まれないため省いています)
単純にPositionのx座標を反転し、IndicesをVector3として見立てた状態での、X座標とZ座標を入れ替えることで行いました。
困ったこと
右手系座標を左手系座標へ変換する際のSpanのパフォーマンス
Accessorの値配列を得るために、Spanを用いることで高速化しようとしましたが、書き換え時のパフォーマンスがよくありませんでした
(1000ループで約3000ns)
MemoryMarshal.Cast
で float
にキャストした後、該当位置に書き込みなども行いましたが、まったくパフォーマンスがよくならず、頭を抱えました。
結果としては、現在のUnityでの .NET Standard
バージョンが 2.0
であり、ランタイムに最適化が入っていないことによるパフォーマンス低下であり、一度通常の配列として持ち、それをポインタ経由でいじることでパフォーマンスがよくなりました。
そのため、事前に一定のヒープを確保しておき、そこに保持することで高速化することが出来ました。
まとめ
実際に自分の手でローダを作ってみることで、glTF自体や、Unityの描画周りなどを知ることができました。
車輪の再発明ではありますが、パフォーマンスチューニングを含む様々な知見を得られたため、かなりプラスでした。
まだ全体の実装が出来てないため、出来上がったらコード込みの詳細の記事を出そうと思います。