Table of Contents

Renderer Batching

There are a few different ways that Unity can combine meshes together, for optimization:

Note that none of these options actually combine meshes together. They will all result in the same number of graphics API drawcalls. Instead, the purpose of batching is to optimize the order of drawcalls to avoid changing state between calls.

Static Batching

Static Batching in Unity creates a Read-Only mesh named “Combined Mesh (root: <root-game-object>)”, with a Submesh for each batched renderer. Unity invokes a separate draw call for each range of indices within the combined mesh buffer.

Note that this means Unity does not actually render the entire batch in a single draw call. As they mention, truly merging all meshes would make culling vastly less effective.

Instead, the performance comes from issuing fewer material / mesh change state operations (in OpenGL at least). In a lot of ways, this ends up being more similar to SRP Dynamic Batching.

Static batching is done at build-time, so mobile developers should be aware that static batching can unexpectedly increase your build size. Static Batching can result in significantly more mesh data being generated, if many batch meshes are created.

Lightmap UV Coordinates

The lightmap coordinates from Lighting Data Asset are baked into the Combined Mesh UV coordinates when the scene loads3). Afterwards, querying meshRenderer.lightmapScaleOffset will return the old scale and transform, even though it has been baked into the mesh. Technically, in the render pipeline, all of these meshes have a lightmapST (scale/transform) of '(1,1,0,0)'. 4)

Static batching can actually work with manually provided lightmap scale/offsets, not coming from Lighting Data Asset. As renderer.lightmapScaleOffset and renderer.lightmapIndex are not serialized, make sure to always set this value every Awake() (and possibly Start()). This approach is used by the Bakery asset.

Batching and MaterialPropertyBlocks

Static and SRP batching is incompatible with MaterialPropertyBlocks, meaning you can't set per-renderer (not per-material) parameters without breaking batching. However, there is a hacky workaround, at least tested with SRP batching.

This is the data sent by the SRP batcher for each draw call:

 https://blog-api.unity.com/sites/default/files/2019/02/Screen-Shot-2019-02-27-at-3.50.52-PM.png

This data is efficiently managed by the engine and uploaded to the GPU as fast as Unity can. The idea is to abuse some of these built-in values to pass custom data. Good candidates are unity_DynamicLightmapST (Enlighten data, still uploaded even if non-Enlighten lightmaps are used) and unity_RenderingLayer (its value is directly alterable in MeshRenderer UI).

An example, when using SRP batching:

renderer.realtimeLightmapIndex = 0; // forces "real-time lightmapped"" behaviour on the renderer (also not really required on objects using a regular static lightmap)
renderer.realtimeLightmapScaleOffset = ...; // Vector4 of custom data
 
// For custom SRPs:
// (Only relevant for custom SRPs; in existing RPs it should likely just work as is, as they all support lightmapping in the main pass)
drawingSettings.perObjectData = ...; // PerObjectData.Lightmaps or otherStuffYouNeed;

On the GPU you should have a UnityPerDraw cbuffer with float4 unity_DynamicLightmapST in it, and it will be filled with custom data. 5)

1)
See StaticBatchingUtility.Combine to batch meshes at runtime.
2)
I can no longer find a player settings checkbox to even enable this feature
3)
I think this also happens during standalone build, not at runtime, but I'm not sure
4)
It shows the scale and transform it loaded from the LightingDataAsset file for that renderer.
5)
This CBuffer is already present in standard shaders of existing RPs.