隠されているものを削除します。モバイルゲームの3Dシーンを最適化します。 従業員のヒントPlarium Krasnodar

モバイルゲームの作成の初期段階では、詳細なモデルがポータブルデバイスに大きな負荷をかけるため、特に弱いデバイスではフレームレートが低下することに注意してください。 視覚的な品質を損なうことなく、3次元モデルのリソースを経済的に使用する方法は? Under the cut-クラスノダールスタジオプラリウムの専門家が見つけたソリューション。



画像



ここで説明する方法は、大規模な計算を必要とし、シーンの準備にのみ適しています。



ゲーム「ターミネータージェニシス:フューチャーウォー」には、カメラを使用してさまざまな側面から見ることができるユニット(人、ロボット、車)の3次元のミニチュアがあります。 ただし、その概要はソフトウェアによって制限されており、モデルの特定の部分は常にユーザーの目には見えません。 そのため、そのようなサイトを見つけて削除する必要があります。



不可視部分は2つのカテゴリに分類されます。



  1. 後部モデル。
  2. 他の部品と重複しています。


最初のカテゴリのパーツは、非表示の三角形を削除する標準的な方法を使用して簡単に処理できます。 2番目のカテゴリでは、すべてがそれほど明白ではありません。



最初に、非表示の三角形を削除する段階を決定する必要があります。 3Dエディターでユニットと環境オブジェクトのモデルを作成し、Unityでシーン、カメラ、照明設定の最終アセンブリを実行しました。 3Dエディターでのモデルジオメトリの最適化では、3Dパッケージごとに追加のツールを開発する必要があるため、Unityで最適化を実行することにしました。



不可視の三角形を決定するために、単純なアルゴリズムを開発しました:



  1. シーン内のオブジェクトの可視性に影響を与えないエフェクトをオフにします。
  2. チェックを実行する位置とカメラの角度を設定します。 指定した位置が多数あると、結果はより正確になりますが、最適化プロセスが遅くなります。 数十のポジションを使用しました。
  3. シーン内のすべてのオブジェクトにシェーダーを割り当て、オブジェクトメッシュの頂点の色を表示します。 デフォルトでは、頂点は黒く塗られているため、このフォームのシーンはMalevichの有名な絵のように見えます。
  4. 最適化されたオブジェクトの1つのメッシュ三角形をすべて通過します。


4.1。 各ステップで、メッシュから現在の三角形を切り取り、別の一時的なメッシュに保存し、それに応じて、ステージ上に別のオブジェクトを取得します。 同時に、そのピークを赤でペイントします。 その結果、小さな赤い三角形の黒いシーンが得られます。



4.2。 最初に記録されたすべての位置とカメラアングルを調べます。



4.2.1。 現在のカメラ位置で、シーンの写真を撮ります。 良好な画像解像度は結果をより正確にしますが、最適化プロセスを遅くします。 4K解像度を使用しました。



4.2.2。 結果の画像では、赤色を探しています。 画像のすべてのピクセルを通過しないように、チェックされた三角形が位置する画像の領域を計算します。 これを行うには、現在の位置とカメラの角度を考慮して、シーン空間の三角形の頂点の座標を画面座標に変換します。 テスト対象の領域に赤いピクセルが見つかった場合は、すぐに次の手順に進むことができます。



4.2.3。 赤いピクセルが見つかった場合、他のカメラの角度と位置の詳細な検証は省略できます。 次の三角形を確認して、ステップ4.1に戻ります。



4.2.4。 次のカメラ位置とステップ4.2.1に進みます。



4.3。 すべてのステップを経てここにたどり着いた場合、撮影した写真のいずれにも赤色が見つからなかったことを意味します。 三角形を削除して、ステップ4.1に進むことができます。



5.利益! オブジェクトの1つを最適化しました。 他のオブジェクトについては、ステップ4に進むことができます。



6.シーンが最適化されます。



public class MeshData { public Camera Camera; public List<int> Polygons; public MeshFilter Filter; public MeshFilter PolygonFilter; public float ScreenWidth; public float ScreenHeight; public RenderTexture RenderTexture; public Texture2D ScreenShot; } public class RenderTextureMeshCutter { // ..................... //   //     ,        public static void SaveVisiblePolygons(MeshData data) { var polygonsCount = data.Polygons.Count; for (int i = polygonsCount - 1; i >= 0; i--) { var polygonId = data.Polygons[i]; var worldVertices = GetPolygonWorldPositions(polygonId, data.PolygonFilter); var screenVertices = GetScreenVertices(worldVertices, data.Camera); screenVertices = ClampScreenCordinatesInViewPort(screenVertices, data.ScreenWidth, data.ScreenHeight); var gui0 = ConvertScreenToGui(screenVertices[0], data.ScreenHeight); var gui1 = ConvertScreenToGui(screenVertices[1], data.ScreenHeight); var gui2 = ConvertScreenToGui(screenVertices[2], data.ScreenHeight); var guiVertices = new[] { gui0, gui1, gui2 }; var renderTextureRect = GetPolygonRect(guiVertices); if (renderTextureRect.width == 0 || renderTextureRect.height == 0) continue; var oldTriangles = data.Filter.sharedMesh.triangles; RemoveTrianglesOfPolygon(polygonId, data.Filter); var tex = GetTexture2DFromRenderTexture(renderTextureRect, data); //    (  ),      ,    if (ThereIsPixelOfAColor(tex, renderTextureRect)) { data.Polygons.RemoveAt(i); } //       data.Filter.sharedMesh.triangles = oldTriangles; } } //  ,        private static Vector3[] ClampScreenCordinatesInViewPort(Vector3[] screenPositions, float screenWidth, float screenHeight) { var len = screenPositions.Length; for (int i = 0; i < len; i++) { if (screenPositions[i].x < 0) { screenPositions[i].x = 0; } else if (screenPositions[i].x >= screenWidth) { screenPositions[i].x = screenWidth - 1; } if (screenPositions[i].y < 0) { screenPositions[i].y = 0; } else if (screenPositions[i].y >= screenHeight) { screenPositions[i].y = screenHeight - 1; } } return screenPositions; } //    private static Vector3[] GetPolygonWorldPositions(MeshFilter filter, int polygonId, MeshFilter polygonFilter) { var sharedMesh = filter.sharedMesh; var meshTransform = filter.transform; polygonFilter.transform.position = meshTransform.position; var triangles = sharedMesh.triangles; var vertices = sharedMesh.vertices; var index = polygonId * 3; var localV0Pos = vertices[triangles[index]]; var localV1Pos = vertices[triangles[index + 1]]; var localV2Pos = vertices[triangles[index + 2]]; var vertex0 = meshTransform.TransformPoint(localV0Pos); var vertex1 = meshTransform.TransformPoint(localV1Pos); var vertex2 = meshTransform.TransformPoint(localV2Pos); return new[] { vertex0, vertex1, vertex2 }; } //    private static bool ThereIsPixelOfAColor(Texture2D tex, Rect rect) { var width = (int)rect.width; var height = (int)rect.height; //       var pixels = tex.GetPixels(0, 0, width, height, 0); var len = pixels.Length; for (int i = 0; i < len; i += 1) { var pixel = pixels[i]; if (pixel.r > 0f && pixel.g == 0 && pixel.b == 0 && pixel.a == 1) return true; } return false; } //       private static Texture2D GetTexture2DFromRenderTexture(Rect renderTextureRect, MeshData data) { data.Camera.targetTexture = data.RenderTexture; data.Camera.Render(); RenderTexture.active = data.Camera.targetTexture; data.ScreenShot.ReadPixels(renderTextureRect, 0, 0); RenderTexture.active = null; data.Camera.targetTexture = null; return data.ScreenShot; } //     polygonId   triangles private static void RemoveTrianglesOfPolygon(int polygonId, MeshFilter filter) { var newTriangles = new int[triangles.Length - 3]; var len = triangles.Length; var k = 0; for (int i = 0; i < len; i++) { var curPolygonId = i / 3; if (curPolygonId == polygonId) continue; newTriangles[k] = triangles[i]; k++; } filter.sharedMesh.triangles = newTriangles; } //      private static Vector3[] GetScreenVertices(Vector3[] worldVertices, Camera cam) { var scr0 = cam.WorldToScreenPoint(worldVertices[0]); var scr1 = cam.WorldToScreenPoint(worldVertices[1]); var scr2 = cam.WorldToScreenPoint(worldVertices[2]); return new[] { scr0, scr1, scr2 }; } //    Gui  private static Vector2 ConvertScreenToGui(Vector3 pos, float screenHeight) { return new Vector2(pos.x, screenHeight - pos.y); } //    Gui  private static Rect GetPolygonRect(Vector2[] guiVertices) { var minX = guiVertices.Min(v => vx); var maxX = guiVertices.Max(v => vx); var minY = guiVertices.Min(v => vy); var maxY = guiVertices.Max(v => vy); var width = Mathf.CeilToInt(maxX - minX); var height = Mathf.CeilToInt(maxY - minY); return new Rect(minX, minY, width, height); } }
      
      





画像



ジオメトリのトリミングにとどまらず、空きテクスチャスペースを節約しようとしました。 これを行うために、最適化されたユニットモデルがモデラーに返され、3Dパッケージでテクスチャスキャンが再作成されました。 次に、新しいテクスチャのモデルをプロジェクトに追加しました。 シーンの照明を再計算するためだけに残ります。



画像



作成したアルゴリズムを使用して、次のことができました。





その結果、モデル内のポリゴンの最大50%を削除し、テクスチャを10〜20%削減できました。 複数のオブジェクトで構成される各シーンを最適化するには、3〜5分かかりました。



これらの発見があなたの将来の仕事をより便利で楽しいものにすることを願っています。



All Articles