Search Unity

Resolved Tiled Unity quad MeshCombiner to save draw calls

Discussion in 'General Graphics' started by Eugenio, Sep 1, 2020.

  1. Eugenio

    Eugenio

    Joined:
    Mar 21, 2013
    Posts:
    197
    Hello forum !! :)
    New day new challenges, :p
    Today I have kind of a simple question that I'm not entirely sure it may have a simple answer.
    Please note: the images included in the post are just meant to be a quick and dirty example.

    Want to make a 3D game (not 2D using sprites).
    I'm trying to create a flat terrain made by tiles.
    Each tile is going to be a Unity quad.

    The textures to apply to each tile are combined into an atlas.

    Seen that the Unity quads will not be UV mapped on that atlas, I had to create 1 material per tile that I can assign to a prefab.

    I instantiated the needed prefabs in my scene and I created a simple example.

    Then I wrote a MeshCombiner script to combine all the instantiated quads in a single mesh.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class MeshCombiner : MonoBehaviour
    4. {
    5.     [SerializeField]
    6.     private Material _material = null;
    7.  
    8.     void Start()
    9.     {
    10.         MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
    11.         CombineInstance[] combine = new CombineInstance[meshFilters.Length];
    12.         int i = 0;
    13.         do
    14.         {
    15.             combine[i].mesh = meshFilters[i].sharedMesh;
    16.             combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
    17.             i++;
    18.         } while (i < meshFilters.Length);
    19.         var go = new GameObject();
    20.         go.name = "Tiles";
    21.         var filter = go.AddComponent<MeshFilter>();
    22.         var mesh = filter.mesh = new Mesh();
    23.         mesh.name = "Combined";
    24.         mesh.CombineMeshes(combine);
    25.         var renderer = go.AddComponent<MeshRenderer>();
    26.         renderer.sharedMaterial = _material;
    27.  
    28.         Destroy(gameObject);
    29.     }
    30. }
    31.  
    All good, but then I realized that the result is not going to be as expected, because the meshes don't share the same material and they are not UV mapped on the atlas.


    Is there a way to achieve the combining without the need of having the quads made from a 3D artist that will map the correct tile from the atlas?

    An example project has been added to this thread, please, feel free to take a look if you are curious. ;)

    Thank you for your help.
     

    Attached Files:

  2. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    To share the same mesh and material you'll need to use vertex UVs instead of tiling and offset. It would take less memory and less processing to generate the mesh yourself instead of using the combination approach. That said..

    As you've already got the data you need per quad (the tiling and offset values), it shouldn't be too difficult. I'm assuming your quads all share the same quad mesh, so keep in mind that you'll need to instantiate that so as not to replace UVs of the shared one (or worse, the actual built-in quad). Also be sure to Destroy the instantiated meshes after they've been combined.
     
  3. Eugenio

    Eugenio

    Joined:
    Mar 21, 2013
    Posts:
    197
    Thank you very much for your reply :)
    Yes, I was fearing that the answer would have been to actually build the needed mesh and create the UV coordinates for the vertices. The think is, I don't really know where to start to achieve this. I suppose going through all the vertices of the prefabs, copy them and change their UV maps it's a start... I'll try to make a new script and see how it goes ;)

    Thanks again.
     
  4. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    You have this line:
    Code (CSharp):
    1. combine[i].mesh = meshFilters[i].sharedMesh;
    What you want to do is clone the mesh, set the UVs, assign that to the combine instance
    Code (CSharp):
    1. Mesh tmp = Instantiate<Mesh>(meshFilters[i].sharedMesh);
    2. Vector2[] uvs = tmp.uv;
    3. // todo here: set the 4 corner UVs based on the offset and tiling.. ie. uvs[0].Set(u, v);
    4. tmp.uv = uvs;
    5. combine[i].mesh = tmp;
    Then when you're all done combining, where you call Destroy(gameObject), also destroy the clones
    Code (CSharp):
    1. for (i = 0; i < combine.Length; i++) Destroy(combine[i].mesh);
     
  5. Eugenio

    Eugenio

    Joined:
    Mar 21, 2013
    Posts:
    197
    Thank you very much polemical !!!
    To be honest, after your first reply, I thought to go through all the vertices of all the meshes to literally build the final one manually... this is why I wasn't really sure how to do it, but, after your last reply, I was able to make a scripts that works !! :)

    Again, thanks.

    Here's the script:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class MeshCombiner : MonoBehaviour
    4. {
    5.     [SerializeField]
    6.     private Material _material = null;
    7.  
    8.     void Start()
    9.     {
    10.         // get all the meshes from the blueprint.
    11.         MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
    12.         // compile a list of mesh to combine.
    13.         CombineInstance[] combine = new CombineInstance[meshFilters.Length];
    14.         for (int i = 0; i < meshFilters.Length; i++)
    15.         {
    16.             Mesh tmp = Instantiate(meshFilters[i].sharedMesh);
    17.             tmp.uv = GetUVs(tmp.uv, meshFilters[i].GetComponent<Renderer>().sharedMaterial);
    18.             combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
    19.             combine[i].mesh = tmp;
    20.         }
    21.         // create a new object and combine all the meshes.
    22.         var go = new GameObject();
    23.         go.name = "Tiles";
    24.         var filter = go.AddComponent<MeshFilter>();
    25.         var mesh = filter.sharedMesh = new Mesh();
    26.         mesh.name = "Combined";
    27.         mesh.CombineMeshes(combine);
    28.         // create a renderer and assign the proper material.
    29.         var renderer = go.AddComponent<MeshRenderer>();
    30.         renderer.sharedMaterial = _material;
    31.         // destroy the blueprint.
    32.         Destroy(gameObject);
    33.         // destroy the combine list meshes.
    34.         for (int i = 0; i < combine.Length; i++)
    35.         {
    36.             Destroy(combine[i].mesh);
    37.         }
    38.     }
    39.  
    40.     private Vector2[] GetUVs(Vector2[] uv, Material material)
    41.     {
    42.         Vector2 tiling = material.mainTextureScale;
    43.         Vector2 offset = material.mainTextureOffset;
    44.         // bottom left.
    45.         uv[0] = new Vector2(offset.x, offset.y);
    46.         // bottom right.
    47.         uv[1] = new Vector2(uv[0].x + tiling.x, uv[0].y);
    48.         // top left.
    49.         uv[2] = new Vector2(uv[0].x, uv[0].y + tiling.y);
    50.         // top right.
    51.         uv[3] = new Vector2(uv[1].x, uv[2].y);
    52.         return uv;
    53.     }
    54. }
    55.  
    as well as the final version of the test project attached to this message.

    Cheers :)
     

    Attached Files:

    adamgolden likes this.