πŸ“œ ⬆️ ⬇️

Remove what is hidden: optimization of 3D scenes in the mobile game. Tips employees Plarium Krasnodar

Already at the initial stage of the creation of mobile games, it should be borne in mind that the detailed models heavily load a portable device, and this leads to a drop in the frame rate, especially on weak devices. How to economically use the resources of three-dimensional models without losing visual quality? Under the cut - a solution found by the specialists of the Krasnodar studio Plarium.

image

The method described here requires large calculations and is suitable only for the preliminary preparation of scenes .

The game Terminator Genisys: Future War has three-dimensional miniatures of units (people, robots, cars) that can be viewed from different sides with the help of the camera. However, its review is limited by software, and certain parts of the models always remain hidden from the eyes of users. So, it is necessary to find and delete such sites.
')
Invisible parts fall into two categories:

  1. Located behind the model.
  2. Overlapped by other parts.

Parts of the first category are fairly easy to process using the standard method of removing invisible triangles. With the second category, things are not so obvious.

First you need to determine at what stage to remove hidden triangles. We created models of units and environment objects in 3D editors, and final assembly of the scene, camera and lighting settings were carried out in Unity. Since the optimization of model geometry in the 3D editor requires the development of additional tools for each 3D package, we decided to perform optimization in Unity.

To define invisible triangles, we developed a simple algorithm:

  1. Turn off the effects that do not affect the visibility of objects in the scene.
  2. We set the positions and camera angles with which to check. A large number of specified positions will make the result more accurate, but will slow down the optimization process. We used several dozen positions.
  3. All objects in the scene are assigned a shader, which displays the color of the vertices of the object meshes. By default, the vertices are painted black, so the scene in this form will be similar to the famous picture of Malevich.
  4. We pass through all the triangles of the mesh of one of the objects being optimized.

4.1. At each step, we cut the current triangle out of the mesh, save it into a separate temporary mesh and, accordingly, obtain a separate object on the stage. At the same time, paint its vertices red. As a result, we have a black scene with a small red triangle.

4.2. We pass through all the initially recorded positions and camera angles.

4.2.1. In the current position of the camera take a picture of the scene. Good image resolution will make the result more accurate, but will slow down the optimization process. We used 4K resolution.

4.2.2. On the resulting picture we are looking for red. We calculate the region of the image in which the checked triangle is located so as not to pass through all the pixels of the image. To do this, we translate the coordinates of the triangle vertices from the scene space to the screen coordinates, taking into account the current position and camera angle. If we find a red pixel in the checked region, then we can immediately proceed to the next step.

4.2.3. If we find a red pixel, then further checking for other angles and camera positions can be omitted. Check the next triangle, returning to step 4.1.

4.2.4. Moving on to the next camera position and to step 4.2.1.

4.3. If we went through all the steps and were here, then we did not find the red color on any of the pictures taken. The triangle can be deleted and go to step 4.1.

5. Profit! We optimized one of the objects. You can proceed to step 4 for other objects.

6. The scene is optimized.

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); } } 

image

We decided not to dwell on trimming the geometry and tried to save free texture space. To do this, we returned the optimized model models to the modellers, and they recreated the texture sweeps in the 3D package. Then we added models with new textures to the project. It remains only to re-calculate the lighting in the scene.

image

With the help of the created algorithm we managed:


As a result, in the models we managed to remove up to 50% of polygons and reduce textures by 10–20%. It took three to five minutes to optimize each scene consisting of several objects.

We hope that these findings will make your further work more convenient and more pleasant.

Source: https://habr.com/ru/post/348494/


All Articles