Search Unity

Combine textures and meshes (reduce draw calls)

Discussion in 'Scripting' started by JohnnyA, Dec 28, 2011.

  1. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    5,041
    This script takes an array of GameObjects that have mesh renderer and mesh filter components attached. It combines their textures into a single texture sheet which is then used by a material shared by each object. Finally it combines the meshes for any objects marked as static.

    Handy for saving a few draw calls on iOS without having to go to your artist.

    License is MIT ... you can do whatever you want with this, If you don't want to have to include the MIT license send me a PM and I will waive that requirement. Links or credits to www.jnamobile.com are appreciated but not required.

    Note this is pretty rough (an hour or two of messing around), it doesn't check for conditions it can't handle, has limited comments, some poorly named variables, etc. Feel free to post improvements.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class CombineMeshesAndTextures : MonoBehaviour {
    6.    
    7.     public GameObject[] objectsToCombine; // The objects to combine, each should have a mesh filter and renderer with a single material.
    8.     public bool useMipMaps = true;
    9.     public TextureFormat textureFormat = TextureFormat.RGB24;
    10.    
    11.     void Start () {
    12.         Combine();
    13.     }
    14.    
    15.     /*
    16.      * Combines all object textures into a single texture then creates a material used by all objects.
    17.      * The materials properties are based on those of the material of the object at position[0].
    18.      *
    19.      * Also combines any meshes marked as static into a single mesh.
    20.      */
    21.     private void Combine() {
    22.  
    23.         int size;
    24.         int originalSize;
    25.         int pow2;
    26.         Texture2D combinedTexture;
    27.         Material material;
    28.         Texture2D texture;
    29.         Mesh mesh;
    30.         Hashtable textureAtlas = new Hashtable();
    31.        
    32.         if (objectsToCombine.Length > 1) {
    33.             originalSize = objectsToCombine[0].renderer.material.mainTexture.width;
    34.             pow2 = GetTextureSize(objectsToCombine);
    35.             size =  pow2 * originalSize;
    36.             combinedTexture = new Texture2D(size, size, textureFormat, useMipMaps);
    37.  
    38.             // Create the combined texture (remember to ensure the total size of the texture isn't
    39.             // larger than the platform supports)
    40.             for (int i = 0; i < objectsToCombine.Length; i++) {
    41.                 texture = (Texture2D)objectsToCombine[i].renderer.material.mainTexture;
    42.                 if (!textureAtlas.ContainsKey(texture)) {
    43.                     combinedTexture.SetPixels((i % pow2) * originalSize, (i / pow2) * originalSize, originalSize, originalSize, texture.GetPixels());
    44.                     textureAtlas.Add(texture, new Vector2(i % pow2, i / pow2));
    45.                 }
    46.             }
    47.             combinedTexture.Apply();
    48.             material = new Material(objectsToCombine[0].renderer.material);
    49.             material.mainTexture = combinedTexture;
    50.            
    51.             // Update texture co-ords for each mesh (this will only work for meshes with coords betwen 0 and 1).
    52.             for (int i = 0; i < objectsToCombine.Length; i++) {            
    53.                 mesh = objectsToCombine[i].GetComponent<MeshFilter>().mesh;
    54.                 Vector2[] uv = new Vector2[mesh.uv.Length];
    55.                 Vector2 offset;
    56.                 if (textureAtlas.ContainsKey(objectsToCombine[i].renderer.material.mainTexture)){
    57.                     offset = (Vector2)textureAtlas[objectsToCombine[i].renderer.material.mainTexture];
    58.                     for (int u = 0; u < mesh.uv.Length;u++) {
    59.                         uv[u] = mesh.uv[u] / (float)pow2;
    60.                         uv[u].x += ((float)offset.x) / (float)pow2;
    61.                         uv[u].y += ((float)offset.y) / (float)pow2;
    62.                     }
    63.                 } else {
    64.                     // This happens if you use the same object more than once, don't do it :)
    65.                 }
    66.                
    67.                 mesh.uv = uv;
    68.                 objectsToCombine[i].renderer.material = material;
    69.             }
    70.            
    71.             // Combine each mesh marked as static
    72.             int staticCount = 0;
    73.             CombineInstance[] combine = new CombineInstance[objectsToCombine.Length];
    74.             for ( int i = 0; i < objectsToCombine.Length; i++){
    75.                 if (objectsToCombine[i].isStatic) {
    76.                     staticCount++;
    77.                     combine[i].mesh = objectsToCombine[i].GetComponent<MeshFilter>().mesh;
    78.                     combine[i].transform = objectsToCombine[i].transform.localToWorldMatrix;               
    79.                 }
    80.             }
    81.            
    82.             // Create a mesh filter and renderer
    83.             if (staticCount > 1) {
    84.                 MeshFilter filter = gameObject.AddComponent<MeshFilter>();
    85.                 MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();           
    86.                 filter.mesh = new Mesh();
    87.                 filter.mesh.CombineMeshes(combine);
    88.                 renderer.material = material;
    89.                
    90.                 // Disable all the static object renderers
    91.                 for ( int i = 0; i < objectsToCombine.Length; i++){
    92.                     if (objectsToCombine[i].isStatic) {
    93.                         objectsToCombine[i].GetComponent<MeshFilter>().mesh = null;
    94.                         objectsToCombine[i].renderer.material = null;
    95.                         objectsToCombine[i].renderer.enabled = false;
    96.                     }
    97.                 }
    98.             }
    99.            
    100.             Resources.UnloadUnusedAssets();
    101.         }
    102.     }
    103.    
    104.     private int GetTextureSize(GameObject[] o) {
    105.         ArrayList textures = new ArrayList();
    106.         // Find unique textures
    107.         for (int i = 0; i < o.Length; i++) {
    108.             if (!textures.Contains(o[i].renderer.material.mainTexture)) {
    109.                 textures.Add(o[i].renderer.material.mainTexture);
    110.             }
    111.         }
    112.         if (textures.Count == 1) return 1;
    113.         if (textures.Count < 5) return 2;
    114.         if (textures.Count < 17) return 4;
    115.         if (textures.Count < 65) return 8;
    116.         // Doesn't handle more than 64 different textures but I think you can see how to extend
    117.         return 0;
    118.     }
    119. }
    120.  
     
    Last edited: Dec 28, 2011
  2. Tudor_n

    Tudor_n

    Joined:
    Dec 10, 2009
    Posts:
    359
    Surprised no-one said anything. This could be/ is a very useful tool. All it needs is some fancy editor magic and maybe a check for verts (>65.000 = new mesh and atlas resolution). I'll see if I can do something about that in what spare time I'll get during new year's. Thank you Johnny.
     
  3. Dreeka

    Dreeka

    Joined:
    Jul 15, 2010
    Posts:
    507
    This looks interesting, similar to batching tools in the asset store.
     
    Last edited: Dec 29, 2011
  4. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    5,041
    Just a bump, maybe some one is interested ...
     
  5. runonthespot

    runonthespot

    Joined:
    Sep 29, 2010
    Posts:
    305
    Nice one! Definitely needs to be in an editor script for IOS though, to prebake. SetPixel/GetPixel require writable texture, which prevents using any form of texture compression type which in turn causes potential memory issues in IOS.

    Best would be to generate the new texture and set objects accordingly at design time, then we can compress the texture. This then leads to inevitable feature creep (needing borders around textures to prevent bleeding of compression artifacts.
     
  6. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
  7. hellcaller

    hellcaller

    Joined:
    May 19, 2010
    Posts:
    381
    WOW! I have to test it.
     
  8. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    Tried this script but getting error in this line
    Code (CSharp):
    1. combinedTexture.SetPixels((i % pow2) * originalSize, (i / pow2) * originalSize, originalSize, originalSize, texture.GetPixels());
    and error is
    any one getting this error?
     
  9. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    bad thing of this code is manually adding gameobject which can be improved by adding parent child realtion
     
  10. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    5,041
    If you used parent child then you would have to have all your objects in the same hierarchy which would severely limit the usability but regardless you need to make the texture readable.

    Just keep in mind this is a four year old hack written in a an hour or two, so I wound't turn to it as a first point of call.
     
  11. dienat

    dienat

    Joined:
    May 27, 2016
    Posts:
    417
    Tried it and used spheres and it shows all the spheres with their textures but all connected by geometry, instead just showing the spheres without visible connections
     
  12. SlightField

    SlightField

    Joined:
    Dec 30, 2015
    Posts:
    2
    There is a bug in the above code, in a sparse texture enviroment but with a lot of objects the above code will fail because it is using the object index i for the texture atlas positioning. This should be replaced with a separate index that is only incremented when you have a new texture. See the code snippet below that fixes the problem.

    Code (CSharp):
    1.             // Create the combined texture (remember to ensure the total size of the texture isn't
    2.             // larger than the platform supports)
    3.             int index = 0;
    4.             for (int i = 0; i < objectsToCombine.Length; i++)
    5.             {
    6.                 texture = (Texture2D)objectsToCombine[i].GetComponent<MeshRenderer>().material.mainTexture;
    7.                 if (!textureAtlas.ContainsKey(texture))
    8.                 {
    9.                     int x = (index % pow2) * originalSize;
    10.                     int y = (index / pow2) * originalSize;
    11.  
    12.                     combinedTexture.SetPixels(x, y, originalSize, originalSize, texture.GetPixels());
    13.  
    14.                     x = index % pow2;
    15.                     y = index / pow2;
    16.                     textureAtlas.Add(texture, new Vector2(x, y));
    17.                     index++;
    18.                 }
    19.             }