GPU Instancingの使用による画面の「ちらつき」問題

今回の話題：
1）GPU Instancingの使用による画面の「ちらつき」問題
2）AssetBundleのサイズを最適化する方法
3）GPU Skinningでパフォーマンスを向上させる方法
4）iOSのShaderでのtex2Dサンプリングオフセットの問題
5）内蔵深度図を管理および破棄する方法

---
##GPU Instancing

**Q：型番：Meizu MX5
Unityバージョン：2019.1.5f1
レンダリング設定：OpenGL ES3、Dynamic Batching、GPU Instancing
問題：シーンが突然崩れて、任意にちらつきなります。排除してから、ツリーをレンダリングする時に発生したとわかります（ツリーのマテリアルはGPU Instancingが選択されています。レンダリングしない限り、ちらつきになれません）。**
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2c492493-c832-ffc1-54ba-35948e6a5918.png)
*レンダリング設定を変更してみました。動的バッチ処理とGPU Instancingのどちらをオフにすると、ちらつきなりません。両方が同時に存在する場合、画面はちらつきなります。
これがUnityのバグなのか、Meizu機種の問題なのかわかりません。
添付ファイル：[Demoプロジェクト](https://answer.uwa4d.com/question/5dce374d14ec712eefaf01ae)（リンクで確認します）
>A：問題主が提供してくれた情報に基づいて、以下の試みを行いました。
**テストデバイス**：Meizu 5; **対照デバイス**：Mi 6
**テストシーン**：お客様から提供
**テスト現象**：
Meizu 5でDynamic和Instancingが有効になっている場合、ちらつきなります。UBOアレイの長さが2になり、ログエラーが発生します。
Mi6とOPPOK1はどちらもちらつきならなく、UBOアレイの長さは128で、ログエラーは報告されません。
**RenderDocのMeizu5のデータ**：
GPU Instancingがオンになっている場合、Vertex Shaderには2つのUBO（Uniform Buffer Object）があります。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/86987305-f80d-ef16-2187-6efcd6739bcc.png)
それぞれの内容は次のとおりです。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/9b9e3ba3-3c0d-227d-22ac-541940ef1c7f.png)
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/9d0db8fa-cdeb-eb76-8537-c5e1562a2238.png)一番目の写真はSH関数の係数を記録していて、二番目の写真は各Instanceの変換マトリックスを示しています。
Meizu 5では、アレイの長さは2です。これは、Meizu5がInstanceをサポートしていないことを表しています。Mi6およびOPPOK1では、次の図に示すように、128のInstanceがサポートされています。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/7d96358b-ab57-25af-a09e-b85dc3e1fe78.png)
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/d89cbedd-d7af-cca2-0dc5-531470d49681.png)
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/9c87aa51-43a3-9328-f973-39b7f23b0f48.png)
モバイルプラットフォームでは、Bufferのサイズの上限は16KBであり、 InstanceはObjectToWorldとWorldToObjectのマトリックスを記録しています。合計は16x4x2 = 128 Byteであるため、 Instanceの合計上限は16×1024 Byte/128 Byte = 128になります。
RenderDocの情報から、実際の長さが128であるアレイは、Meizu5では2つしかありません。
また、Meizu 5でテストシーンを実行すると、Logcatからわかるように、ログエラーが発生します。
Log：
GLSL: unexpected struct parameter ‘unity_Builtins2Array1.unity_SHCArray’
GLSL:unexpected struct parameter ‘unity_Builtins0Array1.hlslcc_mtx4x4unity_WorldToObjectArray[
この2つのLogは、Instancingに必要なUBOのデータです。
もう1つの注意点は、テストシーンでは、239個の三角形のMeshが使用されており、Meizu5ではちらつきなります。Dynamic Batchingが有効になっているかどうかに関係なく、上記のエラーが発生するため、本質的には、**Meizu5が Instancingをサポートしていない**ことが原因です。
シーンを作成し、シーンに3つの同一のツリーがあり、 Instancingを開始すると、OpenGLで使用されるインターフェイスがglDrawElementsであることがわかります。
![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/b9b33d13-7824-071d-eacd-72edc3990a2d.png)
Xiaomi Mi 6で呼び出されるインターフェースは、以下に示すようにglDrawElementsInstanncedです。
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/42dab75e-841a-7b75-93b4-6aa2c0b23a67.png)
シミュレーターはテストされていません。理論的には、状況は同じです。

---
##Asset
**Q：AssetBundleが3.8Gを超えて分類しましたが、アセットが多すぎるために合計サイズが3.8Gを超え、多くのプレーヤーがインストールできません。解決策はありますか？
>A1：ハードウェアの発展に伴い、画質への要求も厳しくなります。基本的に、パッケージはますます大きくなることは避けられません。ポイントは、妥当かどうかということです。基本的に、以下の点に従ってチャックできます。
（1）まず、UWAのアセット検出ツールを使用して、AssetBundleに重複するアセットが多すぎるかどうかを確認します。あった場合、検出結果に応じて調整し、UWAテストを定期的に送信します。
（2）アセット分析ツールと合わせ、エディターで人工的プロジェクトのアセットを見通して、冗長なアセットがあるかどうかを確認します。ゲーム開発バージョンの更新により、以前に行われたことが覆されたり、大幅に変更されたりするようになることがよくあります（プランナーやアーティストのため）。そして、一部のアセットは使用されていないが、プロジェクトに残ったままになります。パッケージ化時にそれを削除しないと、バージョンに無駄なアセットが出現するようになります。
（3）アセットのインポートが妥当かどうかを確認します。例：スタンプが正しい圧縮形式を使用しているかどうか、アニメーションファイルが適度に圧縮されているかどうか、アトラスが妥当かどうか、アトラスに多くの無駄があるかどうかなど。
（4）検査してもまだ進捗がない場合は、プロジェクトには本当に大きなアセットパックが必要であるとしか言えません。ユーザーがより良いゲーム体験を楽しめるようにするために、一部のアセットを起動した後にダウンロードすることを計画します。製品のニーズと組み合わせて、起動後に1回限りのダウンロードにすることも、ゲームのプロセスに応じてパーツごとにダウンロードすることもできます。

>A2：アセットの重複について、見落としやすい点があります。つまり、同じアセットは名前が変更され、別の場所に配置されて、様々なアセットに引用されることがよくあります。実際のテストした後、重複の量は予測を上回っています。

---
##アニメーション
**Q：こちらのゲームはUWA Blogの記事[《GPU Skinning加速骨骼动画》](https://blog.uwa4d.com/archives/Sparkle_GPUSkinning.html)を参照していますが、実機環境のフレーム数の向上は著しくありません。Profilerに接続すると、GPU Skinningをオン/オフにしても、合計バッチ数はあまり変わらないとびっくりしました。かえてSetPass Callsの変化は著しく、エディターでAndroidプラットフォームに切り替えると、合計バッチ数が100以上増えることをテストしました。しかし、Androidの実機になると、変更がないのはちょっとおかしいなと思います。
 
PS：元のキャラクターアニメーションAnimator + SkinMeshRendererは、GPU Skinningを使用してから、MeshRendererに置き換えられ、Animatorも選択されていません。
>A：その記事のGPU SkinningはDraw Callを減らすではなく、GPU Instancingと組み合わせてさえ、Draw Callを減らすことができます。 GPU Skinningが削減するのは、Animators.UpdateとMeshSkinning.Updateの時間です。使用後にこれらの2つの値が変更されていないか、最適化が著しくない場合は、おそらく使用方法が間違っています。
**GPU Skinningソリューション**
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/34bf9822-923c-0c06-b7c7-44f658eb33b0.png)
この方法は、UnityエンジンのMeshSkinningとAnimatorモジュールを完全に破棄し、スキンメッシュを単独でサンプリングし、ボーンノードのマトリックス情報をテクスチャの形式で保存してから、GPUで頂点の計算を完了して直接レンダリングする方法です。利点は、SkinnedMesh.UpdateとAnimator.UpdateのCPU使用率を大幅に削減できることです。ボーンノード情報はテクスチャを介して保存されるため、スキーム２と比較してデータ量が大幅に削減されます。
**オープンソースライブラリのダウンロードリンク：**
[https://lab.uwa4d.com/lab/5bc6f85504617c5805d4eb0a](https://lab.uwa4d.com/lab/5bc6f85504617c5805d4eb0a)
**テストシーン：**
同じケースを作成し、シーンモデルの数はそれぞれ50と200で、それぞれ1000フレームをテストし、Walkアニメーションを再生します。
**結果：**
このソリューションのテスト効率を次の図に示します。Camera.Render以外、MeshSkinning.UpdateおよびAnimator.Updateは削除されましたが、GPUSkinning.StartおよびGPUSkinning.Update関数が追加されています。**分析の結果、マルチスレッドレンダリング機能を有効にしたRedmi Note2デバイスでは、テストフレーム数は合計1000フレームであり、50モデルの平均CPU時間は0.6ms、200モデルの平均CPU時間は1.6msであることがわかります。**
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/4234d98e-674c-1530-cec8-1d5b65fcfb83.png)
図1：GPU Skinningを使用した後、RedmiNote2のTop10CPUの使用状況


GPU Skinningを使用した後、RedmiNote2のCamera.Renderの時間コスト。
![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/863fac5f-f965-1a2e-f206-5501d9a3b2ed.png)
図2：50モデル
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/65248bb0-2fe5-9328-d74a-c19cd2a4c08d.png)
図3：200モデル
GPU Skinningを使用した後、RedmiNote2で50モデル時に、Camera.Renderの時間コストは以下のように示しています。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/acf5590c-6ecc-18f0-8529-9979c1ba613d.png)
図4
同時に、図5に示すように、GPU Skinning.StartとGPUSkinning.Updateの時間コストは、ゲームの実行中にも少ないです。
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/ebe48145-3db1-c89e-8248-57342ce8a5e2.png)
![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/62f737ab-f4af-14e6-ccb7-f1865d3b01dd.png)
図5：GPUSkinning.StartおよびGPUSkinning.Update時間コストはゲーム実行中でのCPU時間
**まとめ：**
（1）この方案は、メモリ使用量が少ない一方で、Animators.UpdateおよびMeshSkinning.UpdateのCPU時間を大幅に削減できます。r_gunmanを例にして、すべてのアニメーションファイルの長さは8秒です。サンプリング率が30fpsの場合、テクスチャを通して記録すると、128×128のテクスチャのみでより詳細なアニメーションデータを取得できます。
（2）この方案はGPUに大きなプレッシャーをかけるため、開発者はGPUへのプレッシャーをさらに検討する必要があります。

---
##Render
**Q：ワールド座標を使用して、Filter ModeがPointであるテクスチャをサンプリングしました。PCとAndroidでのサンプリングは正しく、MacとiPhone7 / 7Plusでのサンプリング結果はオフセットされています。**
 
**PCのスクリーンショット（それぞれUV出力とサンプリング結果出力）：**
![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/7eddda76-20c3-0082-2268-eaa0bb0239f6.png)
![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/a4f16f5c-1e2c-5241-fdc8-eb8a91a9a61c.png)
**Macでのスクリーンショット：**
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/96108f9b-6dda-1b7f-113c-a5674935cc43.png)



**メッシュ線の比較に注意してください。Macのサンプル図はオフセットされていますが、UVは正しいです（観察の便宜のために、UVはスケーリングされ、サンプリングはスケーリングされていないUVで行われます）。**
![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/b78e7caf-dd5b-fa1c-b967-bc7afbb2baf3.png)
そのような問題は、どのように対処しますか？
>A1：根本的な原因は特定されていませんが、現在、トリッキーな方法でこの問題を避けます。
元の要件は、ワールド座標を使用してFilter Mode がPointであるテクスチャをサンプリングすることです。そのため、Shaderの中にサンプリングポイントを碁盤化します。擬似コードは以下のように。

>```
float2 uv = floor(worldPos.xz / rectSize) / texSize.xy + 1.0 / (3.0 * texSize.xy);
```
（rectSize は碁盤の目のサイズで、texSize はサンプル図のサイズです）
最後に追加された値（1.0 /（3.0 * texSize.xy））がない場合、PCのサンプリング結果がMacから逸脱するため、3分の1のユニットのオフセットを追加します。


>A2：この質問に似ているはずですが、half pixel offsetはbilinear filterがある時に出現したものです。プラットフォームが違ければ、サンプリング結果が異なる場合もあります。
https://docs.microsoft.com/zh-cn/windows/win32/direct3d9/nearest-point-sampling
https://docs.microsoft.com/zh-cn/windows/win32/direct3d9/bilinear-texture-filtering?redirectedfrom=MSDN
上記の2つのリンクが役立てるかもしれません。

---


##RenderTexture

 
**Q：同じシーンで、さまざまなカメラとさまざまな角度を使用して深度を撮影しているので、複数の異なる深度１が取得されたはずです。Unityがこれらの複数の深度図をどのように管理するか知りたいですが。例えば、それらの一枚だけを保持し、他の深度図は捨てたいなら、どうすればよいですか？**
>A：Unityのデフォルトのレンダリングパイプラインは、複数のカメラがRenderTextureを共有する方法です。つまり、複数のカメラが共通のCameraDepthRenderTextureを使用して1つずつ描画します。各カメラがClearであるかどうかは、すべてのカメラで描画深度がオンになっているかどうかに関係します。全てをオフする場合に、RenderTextureが解放されます。一枚を保存して次のフレームに使用したいなら、別処コピーすることをお勧めします。それなら、次のフレームで他のカメラの使用に影響を与えません。

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

今なら、UWA GOTローカルツールが**15日間に無償試用**できます！！
よければ、ぜひ！

UWA公式サイト：https://jp.uwa4d.com
UWA GOT OnlineレポートDemo：https://jp.uwa4d.com/u/got/demo.html
UWA公式ブログ：https://blog.jp.uwa4d.com