CRO MeshCluster - Frame Rate Booster

Description

Mesh cluster is a virtualized geometry system that is using an internal mesh format and render technology. The data is highly compressed and there could be differences when switching between mesh clustering and normal rendering.

Benefits

  • You can have a lot of different meshes, a high object counts and a lot of instances.

  • Frame budgets no longer constrained by draw calls, or visibility pass.

Performance Considerations

  • This is a GPU intensive code and the performances will vary depending on the GPU card used. For example the demo scene include with the tool contains over 150 different meshes and over 55.000 game objects. We tested the FPS on two different PCs:

    • First PC

      • CPU Intel I7-9700K @ 3.60GHz​

        • 8 Cores with 8 Logical Cores​

      • GPU NVidia RTX 2080​

      • FPS

        • Mesh Cluster Enable​: 250-350 (with video recording)

          • 290 - 410​ (without video recording)

        • Mesh Cluster Disabled: 45 - 75​

    • Second PC​​​​

      • CPU Intel Xenon E5-2630 @ 2.30GHz​

        • 12 Cores with​ 24 Logical Cores

      • GPU NVidia GeForce GTX 1050 Ti​

      • FPS

        • Mesh Cluster Enabled: ​70-80 (without video recording)

        • Mesh Cluster Disabled: 25-40

  • Dynamic meshes are supported (moved, rotated or scaled every frame), there are some CPU implications on having 50.000 moving objects (getting the transform from Unity is not very fast).

  • This code is heavy on the GPU side, this means on a slow GPU it will perform a worse than the native Unity renderer.

  • If you have a lot of triangles in a scene and you don’t use LOD groups, you will end up with a slower version of the game (faster on CPU, but slower on GPU).

  • To see a performance improvement, you will need a lot of different objects, and a few materials.

  • The number of draw calls is increasing with the number of different materials. We will investigate on how to allow some parameters to change between materials in an attempt to minimize even more the number of draw calls.

  • In Mesh Cluster Settings you are allowed to set the number of triangles per cluster. This is very important and can affect the performances in a bad way. Default we set it to 64 triangles. This means a mesh will always be converted to a multiple of 64 triangles so our rendering method will work. This means if you have a mesh with 4 triangles we will add 60 empty triangles, so the idea is that if you have less than 64 triangles is not necessarily better. Same thing if you have for example 65 triangle, we will create 2 clusters of 64 triangles, so your mesh will have 128 triangles in the end. To be super-efficient with this system it is recommended to have meshes that are multiple of triangles you set in the mesh cluster settings. Other things that you need to take into consideration, if you decrease the number of triangles per mesh, it will be a lot more expensive to do the visibility and not to mention the overhead of adding a bounding volume per every cluster, materials properties and so on, so decreasing it too much will make the game run slower.

Quick setup

CRO Mesh cluster was designed for Unity and at this moment it is working only with URP (Universal Rendering Pipeline).

  • The code was tested with UPR 10.6.0 (latest official version at this moment) and Unity 2020.3.17f1 (LTS).

  • We added support also for Unity 2021.1 (URP 11.0.0)

  • 2021.2 (URP 12.0.0).

    • For URP 12.0.0 we support also Deferred Rendering path that has less restrictions in terms of lights.

For your project safety, make a copy of the project before adding mesh cluster.

After importing the package in your project go to Window -> CRO Mesh Cluster

MeshCluster_01.png

When the project is not setup for CRO Mesh Cluster you will see this screen:

MeshCluster_02.png

After clicking setup, the CRO Mesh cluster window will change:

MeshCluster_03.png

From here you have two options.

  • Open your scene where you want mesh cluster and click on URP -> CRO and Setup current scene and you are done.

  • Open Advanced options

MeshCluster_04.png

and click Setup everything. Doing this we will open all the scene and parse them all and convert everything to CRO Mesh Cluster. Depending on the project size this could take some time.

Now everything is setup and ready to go.

How to remove CRO Mesh Cluster from the project

Open Mesh Cluster Window

Open Advanced options

MeshCluster_05.png

Click on Cleanup everything.

When this is finished you can safely remove CRO_MeshCluster folder from the project.

CRO Mesh Cluster known restrictions

Render tech

We support only URP (Universal Rendering Pipeline)

LOD Group Restrictions

LOD’s don’t have local transform saved. This means in all lods must have the Position and Rotation set to 0,0,0 and the Scale set to 1,1,1

The first LOD (lod0) will give the visibility and shadow casting mode for the lod group. The rest of the lods are not checked and the visibility and shadow casting.

Lighting restrictions
  • Forward rendering, for the moment, supports only one directional light. There is no easy access to the internal unity list of lights so we will need to make a workaround until that data will be available from the engine. We will try to fix this in a later release. Deferred rendering path does not have this issue.

  • We don’t support dynamic lights at this moment (in a later release we could allow one or two dynamic lights, but this will be after we fix the first part where we need to support more than one light)

  • Light probes are computed for location 0, 0, 0. This means the probe occlusion and SH coefficients are not correct. We will try to rectify this in a later release, if possible, also we will need to see at what level of accuracy we can do it (This should be done after we support more than one light).

  • Baked lighting – not working. We need to investigate Unity support for this feature.

  • Only one directional shadow is supported. Adding the support for additional shadows will be investigated for later releases.

Camera restrictions
  • We didn’t test AR camera

  • VR has limited support for the moment. Two passes work as expected but it is very expensive in terms of rendering. It seems that we render everything 8 times instead of 3 times in our test scene. We will try to fix this issue as soon as possible.

Shader/Material restrictions
  • Mesh cluster is coming with the following shaders support:

    • CRO_Lit that is a copy of the Universal Render Pipeline/Lit shader. It has all the properties but not everything is supported (check the known restrictions for more details, in particular lighting)

    • CRO_Simple Lit, as the previous it is a copy of the Universal Render Pipeline/Simple lit shader.

    • CRO_Unlit is a copy of Universal Render Pipeline/Unlit shader.

  • Custom shaders

    • Can be created, but it is not simple task. We will explain how to do a custom shader in a separate tutorial.

  • We added support (automatic conversion to CRO shaders) for Universal Render Pipeline/Lit, Universal Render Pipeline/Simple Lit and Universal Render Pipeline/Unlit.

  • We don’t support changing the materials at run time (we will investigate to see if we can support this and release it in a later version)

  • Skinned object – not supported (we need to investigate and see if we can support them)

  • Particles – not supported

  • Transparency – not correctly sorted. We don’t sort geometry at this moment, so we cannot render transparency back to front. A later version will support per material and per cluster sorting. This is still not correct but it will work a lot better.

  • No support for HDR colour as vertex input

Depth texture feature
  • Partially supported if occlusion culling is used. At this moment we render to the depth buffer only the objects that are visible taking into account the previous depth buffer. This means a few objects will not exist in the new generated depth buffer. We will try to fix this in a future release.

Mesh Renderer
  • At this time mesh cluster data and the mesh renderer data are loaded in memory. We don’t have a solution for this problem as some Unity feature require mesh renderer data to be in memory and CPU accessible. We will investigate possible solution for removing the data that when you don’t need it.

Platforms supported
  • PC version:

    • DX11 fully only supported

    • DX12 crashes inside Unity code (there is nothing we can do for this)

    • Vulkan version is really slow (we will investigate and if it is from our side, we will fix it)

    • We didn’t test OpenGL versions but we won’t support them officially.

  • Android version:

    • Working with Vulkan but it is very slow (maybe the video card is not fast enough for mesh cluster)

  • IOS version:

    • Metal version is crashing inside Unity code (there is nothing we can do for this)

  • Consoles:

    • We didn’t test on any, but in theory they should work unless there is another crash inside Unity code.

  • We didn’t test Linux or MacOS, so we cannot say if the code is working or not

Unity versions supported
  • We tested with Unity 2020.3. Just to let everyone know we will officially support only the LTS versions of Unity. If a LTS version is remove we will remove official support for it as well.

  • Unity 2021.1 (you will need to manually unpack the 2021.1 version for shaders and materials)

  • Unity 2021.2 (same as 2021.1, needs manually unpack of the shaders and materials)

Disclaimer
  • There could be other restrictions that we didn’t find yet. If you buy this product and you encounter an unlisted restriction, please let us know so we can try to fix it if possible.

  • We are not responsible if you encounter errors that are impossible to fix (crash inside Unity or other packages that are not related to ours). We will still try to help you as much as we can and try to find for you a work around the error/issue.

Known issues
  • We compress the data aggressively and there are some differences between Unity Renderer and Mesh Cluster rendering (Render Quality).

  • We emit different draw calls for shadows even if the shadow parameters of different materials are identical (Rendering Performance).

  • We emit different draw calls for depth pre-pass even if the depth pre-pass parameters of different materials are identical (Rendering Performance).

  • When creating a new shader or assigning a new material to a mesh we don’t correctly detect it.

    • Workaround: Open advanced settings and press Cleanup export stats, then press setup/export current scene or setup everything

CRO Mesh Cluster window

MeshCluster_03.png
Material setup
URP -> CRO

When pressing this button, we will attempt to convert all known URP shaders to CRO. For the moment we support only Lit, Simple Lit and Unlit shader. There is convention for converting materials from URP to CRO. We are looking at the name of the shader and we are trying to find a shader with the same name and with the prefix CRO_. If a shader with this pattern is found we simply swap the shaders in the material.

CRO -> URP

We will convert all CRO shaders to the correspondent URP shader. Same convention will be used when converting shaders. We will try to find a shader with the same name but without the prefix CRO_.

Current scene setup
Setup current scene

When you press this button, we will setup CRO mesh cluster for this particular scene. We will parse all game objects and when we find a visual object, we will add the CRO_MeshCluster monobehaviour to it. We will parse the mesh filters and convert it to our internal format. We will parse also the material and prepare it for runtime usage. This will work even if the object is inside a prefab, if the prefab is used in another scene everything will work as expected. The export time will vary depending on the scene complexity. For example, if you have a scene with a lot of prefabs in prefabs it will take a much longer time to process comparing to the same scene where all the prefabs are un-packed completely. The reason for this is that we will attempt to parse all the prefabs and try to make sure you don’t have parameters that are not applied to the root prefab.

Cleanup current scene

As you might guessed we will clean the current scene by removing the CRO_MeshCluster from all prefabs in the scene and from all objects in that particular scene.

Export current scene

This is for advanced usage. Pressing this button we will create a new scene for you, same name as the original but with the suffix _CRO_EXPORTED. From this scene we will remove all the visual components and mesh filters from the game objects and add them to our own manager directly. From the original scene we will keep all the dynamic objects and all the other components. This is useful when you have on the visual object a collider, or a trigger. The exported scene can be added to the build list instead of the original. Please test this feature and let us know how you like it.

In a future release we will be using this feature to allow you to have huge world loaded and streamed in based on the camera position.

Mesh Cluster settings

In here you can adjust the runtime parameters of the mesh cluster feature. After setting one or more parameters you need to press Apply to set the new properties.

Occlusion shader

By default, CRO Mesh Cluster come with its own shader for doing different tasks, Frustum Culling, Occlusion Culling, HiZ generator and so on. You should not change this parameter, if by mistake you changed it, you can find it in: CodeRoadOne/CRO_MeshCluster/Shaders/Common/CRO_OcclusionCulling.

Screen percentage culling size

This parameter will adjust when an object will not be rendered anymore as it is too small to be seen. If you set it as 1 it will mean that if the object has the size smaller than 1% of the screen width or 1% of the screen height it will not be rendered anymore. This will not overwrite the parameters in the LOD Group, unless the LOD group doesn’t have a cull size.

Vertex Buffer Size(MB)

Comparing to the default usage mode in Unity, CRO Mesh Cluster is putting all mesh filters in one singe buffer. This includes the vertex buffer, index buffer, object transform, material information. Here you control how much memory you reserve for that buffer. Make sure you have a big enough buffer that will allow you to load the biggest scene in your project.

Max Number of Cluster Meshes

Every single mesh filter is cut in small pieces for Number of Triangles per Cluster. Let’s say that Number of Triangles per Cluster is 64, this means that for a mesh with 65 triangles will use 2 clusters, a mesh with 128 triangles will still use 2 clusters. For the code and memory usage to be optimal it is recommended that you have meshes close to multiple of 64 (or the value that you choose for Number of Triangles per Cluster). If you have less than 64 triangles using this system let’s, say a billboard that has 2 triangles) is not necessarily faster than a mesh with 64 triangles.

Max Number of Dynamic Meshes

This value controls what is the maximum number of moving objects that you could have in a scene. Moving objects are considered objects that you cannot move after they are placed in the world. If you move an object that is not marked as dynamic in editor you will receive a message and the object will move, but at runtime the object will not move. Having a large number of dynamic objects can be costly, for example moving 50.000 objects by getting unity transform will slow down your game a lot. If you have a faster way to compute the object transform the update cost on GPU will be minimal.

Max Number of Materials

This is self-explanatory, you set how many different materials you could have in one scene.

Number of Triangles per Cluster

By default, we set this value to 64 triangles per cluster. In Nanite this value is 128, but they are using very high poly models and 128 vertices is more appropriate. Be aware that if you change this value you will need to re-setup everything. So, first you need to open Advanced options, click the Cleanup exports stats and after that click on Setup everything.

Upload GPU Buffer Size (MB)

This is a helper buffer that allow us to combine multiple updates to the GPU in one single command from Unity. Mostly this is used during loading meshes, materials, instances. It can also be used during game play when you hide or unhide a static object. Bigger the size of this buffer less commands to the GPU.

Max Updates per Frame

This will allow you to decrease the number of commands that we send to the GPU during one frame. For example, you decide to hide 5000 objects. Because the objects are not in a continuous memory, we will need to send 5000 commands to the GPU. By setting the value to 500 we will complete your request to hide the 5000 objects in 10 frames. You will see per frame 500 objects changing status.

Use Occlusion Culling

By enabling this feature, we will generate a HiZ texture for the main camera and we will cull objects that are behind other objects and they are not visible. This is still an experimental feature so use it with care. There are still issues with detecting the correct camera for the occlusion culling. Also the depth buffer is generated based on the previous frame visibility. This means that there is a possibility that not all object will exist in the depth buffer. Also, for this feature to work you need to enable Depth Texture in your URP pipeline asset).

Use Deferred Rendering

This should be automatically setup, but unfortunately there is no easy way to access an internal member of the URP, until Unity will give us access to that member, please make sure that this parameter is set when you are using Deferred Rendering path. This option is actually only working on URP 12.

Advanced Settings

In theory you should not go in here very often, but in case there are some issues with materials, or meshes that don’t get correctly updated here is the best place where you can correct those issues.

Setup everything

Pressing this button, we will set everything for you, materials, meshes, all the scenes that you added to the build list. There is also a small thing that you need to take into account. Export everything will export everything that is new, not something that was already exported. If you want to export really everything you will need to press first Cleanup export stats.

Cleanup everything

Pressing this button, will remove all the CRO Mesh Cluster Data, the mesh cluster feature, and revert the shaders to URP.

Cleanup export stats

In theory you should not press this button at all, but we noticed that there are some issues that we still need to address. For example, you change a material from Lit to Simple Lit, this change will not be detected correctly so you will probably need to cleanup the export stats and setup everything again. We will try to fix this as soon as possible.

Remove Mesh Cluster feature

Pressing this will remove the CRO Mesh Cluster feature from URP. This means you cannot enable or disable this feature as this feature is not going to be active. You can use this to test your game without any pollution from our code.

Debugging

Rendering debugging will add the option for you to lock the frustum and check what is sent to the GPU.

For the moment this feature is partially working as it will use the first camera rendered by Unity to lock the frustum and not necessary the main camera. We will need to see what we can do to allow you to debug every single camera if you want to.

How to use/setup the sample scenes

The demo sample scenes are located in CoreRoadOne/CRO_MeshCluster/DemoScene/00_Boot.

To make the scene work as expected you will need to add them to the build settings

MeshCluster_06.png

Make sure you keep 00_Boot as the first scene in the build process as from this scene we will start everything. On screen you will have buttons to move to the next scene or the previous scene. You can add also your scenes here, but you will use our lighting, so maybe is not something that is desired.

Be aware that there are 2 scene that are starting with “03”. First one is 03_CollectedScene and the second that you need to add to build setting is called “03_CollectedScene_CRO_EXPORTED”

Demo scenes

In scene 01_StaticScene you have the normal behavior where everything is setup in Unity editor and no modifications are done to that scene.

02_DynamicScene contains some code that allows to generate from code the scene, this is also in two flavors. One it will create a GameObject for every object that is added, or you can set the No Game Object, and we will not create a game object from the prefab that we receive, but we will add the object to our rendering.

MeshCluster_07.png

03_CollectedScene_CRO_EXPORTED is a copy of the static scene, but on this scene we choose the option to export the current scene. In this version we remove all the visual meshes, but we keep all the game objects that contains something more that that. For example, we will keep all the game objects with custom scripts, Mesh colliders and so on. This is to make Unity run even faster on CPU by ignoring all the game objects that are empty and not needed.

CRO_MeshCluster as MonoBehavior

MeshCluster_08.png

When a mesh cluster object is added you have a few options:

Is Dynamic

This will specify to the rendering engine in what list we should place this object. If the object is marked as dynamic will be checked every frame to see if he moved. In case it moved it will trigger an update on the rendering code and the object will reflect its new position.

Check for Enable Disable events

By default, we don’t check for this kind of events, but that means that everything that is on the current scene can disappear only if we destroy the object. We can check for enable/disable events and just show/hide the object when requested.

After these options you have some statistics about the selection:

  • How many clusters

  • How many materials

  • Meshes

  • And Lods

CRO_MeshClusterNoMonoBehavior

This is a class that you can use to bypass the MonoBehavior use from Unity. This allow you to quickly add and remove meshes from rendering and move them around by passing directly the transform.

There are a few public functions:

public void Initialise(CRO_MeshCluster meshCluster, Vector3 position, Quaternion rotation, Vector3 scale, bool isDynamic, int layer)

You will need to pass a CRO_MeshCluster to this function. This can be a prefab that can be created before or after you setup a scene. You will need to pass also the position, rotation the scale, if the object is dynamic and the layer.

public void Destroy()

Call this function to remove the mesh instance from the rendering.

public void UpdateTransform(Matrix4x4 localToWorldMatrix, Matrix4x4 worldToLocalMatrix)

public void UpdateTransform(Matrix4x4 localToWorldMatrix, Matrix4x4 worldToLocalMatrix, bool negativeScale)

To move the instance you (if it is dynamic) you need to call the UpdateTransform function. This will allow you to set a new position where you would like this instance to be rendered. For moving objects with physics/nav mesh agents will probably be better to use CRO_MeshCluster instead of CRO_MeshClusterNoMonoBehaviour.

public void Show(bool show)

Call this function in case you want to hide or show the mesh instance. Mesh Instances can remain hidden and it will not affect to much the runtime performance. We will still do a check on the mesh o the GPU to know if we need to render it or not. If the instance is not needed for a very long period of time it will be better to destroy the instance and re-create it later.

How to set a mesh as dynamic

After you setup a scene you can mark different objects as dynamic. Select the mesh/game object in question and tick Is Dynamic check box. Multiple objects can be set all together.

MeshCluster_09.png

How to add/remove a mesh from code

You can find an example for this in CRO_DuplicateInstance.cs (part of our DemoScenes found in CodeRoadOne/CRO_MeshCluster/DemoScen/Scripts/CRO_DuplicateInstance.cs).

    public GameObject CreateInstanceWithGameObject(CodeRoadOne.CRO_MeshCluster.CRO_MeshCluster _prefab, Vector3 position, Quaternion rotation, Vector3 scale, bool isDynamic = false, int layer = 0)

    {

        GameObject instance = null;

        if (_prefab != null)

        {

            _prefab.IsDynamic = isDynamic;

            instance = GameObject.Instantiate(_prefab.gameObject, position, rotation);

            if (instance != null)

            {

                instance.transform.localScale = scale;

                instance.layer = layer;

            }

        }

 

        return instance;

    }

 

    public CodeRoadOne.CRO_MeshCluster.CRO_MeshClusterNoMonoBehaviour CreateInstance(CodeRoadOne.CRO_MeshCluster.CRO_MeshCluster _prefab, Vector3 position, Quaternion rotation, Vector3 scale, bool isDynamic = false, int layer = 0)

    {

        CodeRoadOne.CRO_MeshCluster.CRO_MeshClusterNoMonoBehaviour instance = new CodeRoadOne.CRO_MeshCluster.CRO_MeshClusterNoMonoBehaviour();

        if (instance != null)

        {

            instance.Initialise(_prefab, position, rotation, scale, isDynamic, layer);

        }

 

        return instance;

    }

 

    public void DestroyInstance(CodeRoadOne.CRO_MeshCluster.CRO_MeshClusterNoMonoBehaviour instance)

    {

        if (instance != null)

        {

            instance.Destroy();

        }

    }

 

If you created a mesh instance with a game object this will automatically be destroyed when the object is destroyed so no need for extra code.