Does Dynamic Batching in DOTS only work for Instantiated Entities?

It seems that dynamic batching only works for instantiated entities. Entities with the same material in their RenderMeshes but with different Meshes do not seem to dynamically batch.

Is this expected behavior? Am I missing something?

I am hoping to create block structures using many different shapes but all the same Material.

Here are images from my test rig:

The top image shows the case where each block has a uniquely-generated Mesh. The second shows the same stacking of blocks using instantiation of a source entity.

Basically the only difference is this:

 Entity boxEntity = CreateUniqueBoxEntity(blockSize, new float3(-2, 0, 0), blockMass, blockMaterial, blockVelocity);
       
        for (int j=0; j<rows; j++)
        {
            for (int i = 0; i < columns; i++)
            {
                float3 blockPosition = new float3(j + 1.5f, i * 1f, 0);

                if (uniqueBlockMeshes)
                {
                    // GENERATE A UNIQUE MESH
                    CreateUniqueBoxEntity(blockSize, blockPosition, 2, blockMaterial, blockVelocity);
                }
                else
                {
                    // INSTANTIATE
                    var tmpEntity = entityManager.Instantiate(boxEntity);
                    entityManager.SetComponentData(tmpEntity, new Translation() { Value = blockPosition });
                }
              
            }
        }

Here is the full code:

using UnityEngine;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;

using Unity.Entities;
using Unity.Transforms;
using Unity.Rendering;
using UnityEngine.Rendering;
using Unity.Mathematics;
using Unity.Physics;
using Collider = Unity.Physics.Collider;

using Unity.Physics.Extensions;


public class TestEntityGenerator : MonoBehaviour
{
    public Mesh mesh;
    public UnityEngine.Material blockMaterial;
    public UnityEngine.Material projectileMaterial;

    public float    rows            = 10;
    public float    columns         = 10;
    public float    blockMass       = 2;
    public Vector3  blockSize       = Vector3.one;

    public float    projectileMass  = 10;
    public Vector3  projectileSize  = Vector3.one;

    public bool uniqueBlockMeshes = true;


    EntityManager entityManager;

    EntityArchetype blockArchetypeype;




// Start is called before the first frame update
    void Start()
    {
        // ENTITY MANAGER
        entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

        // BLOCK_ARCHETYPE
        ComponentType[] dynamicComponentTypes = new ComponentType[11];
        dynamicComponentTypes[0] = typeof(RenderMesh);
        dynamicComponentTypes[1] = typeof(RenderBounds);
        dynamicComponentTypes[2] = typeof(Translation);
        dynamicComponentTypes[3] = typeof(Rotation);
        dynamicComponentTypes[4] = typeof(LocalToWorld);
        dynamicComponentTypes[5] = typeof(PhysicsCollider);
        dynamicComponentTypes[6] = typeof(PhysicsVelocity);
        dynamicComponentTypes[7] = typeof(PhysicsMass);
        dynamicComponentTypes[8] = typeof(PhysicsDamping);
        dynamicComponentTypes[9] = typeof(Damage);
        dynamicComponentTypes[10] = typeof(OriginalTransform);
        blockArchetypeype = entityManager.CreateArchetype(dynamicComponentTypes);



        // CREATE STACKED BLOCKS
        PhysicsVelocity blockVelocity = new PhysicsVelocity();

        Entity boxEntity = CreateUniqueBoxEntity(blockSize, new float3(-2, 0, 0), blockMass, blockMaterial, blockVelocity);
       
        for (int j=0; j<rows; j++)
        {
            for (int i = 0; i < columns; i++)
            {
                float3 blockPosition = new float3(j + 1.5f, i * 1f, 0);

                if (uniqueBlockMeshes)
                {
                    // GENERATE A UNIQUE MESH
                    CreateUniqueBoxEntity(blockSize, blockPosition, 2, blockMaterial, blockVelocity);
                }
                else
                {
                    // INSTANTIATE
                    var tmpEntity = entityManager.Instantiate(boxEntity);
                    entityManager.SetComponentData(tmpEntity, new Translation() { Value = blockPosition });
                }
              
            }
        }


        // CREATE PROJECTILE
        float3          projectileLaunchPoint   = new float3(-120, 40, 0);
        PhysicsVelocity projectileVelocity      = new PhysicsVelocity() { Linear  = new float3(80f, 0f, 0f) };

        CreateUniqueBoxEntity(projectileSize, projectileLaunchPoint, projectileMass, projectileMaterial, projectileVelocity);
    }




    /// <summary>
    /// Creates a box entity by generating a simple cube Mesh and a RenderMesh .
    /// </summary>
    /// <returns>The box entity.</returns>
    /// <param name="size">Size.</param>
    /// <param name="trans">Trans.</param>
    /// <param name="mass">Mass.</param>
    /// <param name="material">Material.</param>
    /// <param name="physicsVelocity">Physics velocity.</param>
    private Entity CreateUniqueBoxEntity(Vector3 size, float3 trans, float mass, UnityEngine.Material material, PhysicsVelocity physicsVelocity)
    {
        // CREATE A UNIQUE MESH
        mesh = BoxMesh.CreateBoxMesh(size.x, size.y, size.z);

        RenderMesh renderMesh = new RenderMesh()
        {
            castShadows     = ShadowCastingMode.On,
            layer           = 1,
            material        = material,
            mesh            = mesh,
            receiveShadows  = true,
            subMesh         = 0
        };


        BlobAssetReference<Unity.Physics.Collider> boxCollider = Unity.Physics.BoxCollider.Create(new BoxGeometry()
        {
            BevelRadius     = 0f,
            Center          = new float3(0, size.y/2, 0),
            Orientation     = quaternion.identity,
            Size            = new float3(size.x, size.y, size.z)

        }, CollisionFilter.Default);


        return CreateEntity(renderMesh, boxCollider, mass, trans, physicsVelocity);
    }




    /// <summary>
    /// Creates an entity from scratch.
    /// </summary>
    /// <returns>The entity.</returns>
    /// <param name="renderMesh">Render mesh.</param>
    /// <param name="_collider">Collider.</param>
    /// <param name="mass">Mass.</param>
    /// <param name="trans">Trans.</param>
    /// <param name="physicsVelocity">Physics velocity.</param>
    unsafe Entity CreateEntity(RenderMesh renderMesh, BlobAssetReference<Collider> _collider, float mass, float3 trans, PhysicsVelocity physicsVelocity)
    {
      
        Unity.Entities.Entity entity = entityManager.CreateEntity(blockArchetypeype);

        entityManager.AddSharedComponentData    (entity, renderMesh);
        entityManager.SetComponentData          (entity, new RenderBounds     { Value = renderMesh.mesh.bounds.ToAABB() });
        entityManager.AddComponentData          (entity, new Translation      { Value = trans });
        entityManager.SetComponentData          (entity, new Rotation         { Value = Quaternion.Euler(0f, 0f, 0f) });
        entityManager.SetComponentData          (entity, new PhysicsCollider  { Value = _collider });

        Collider* colliderPtr = (Collider*)_collider.GetUnsafePtr();
        entityManager.SetComponentData(entity, PhysicsMass.CreateDynamic(colliderPtr->MassProperties, mass));

        entityManager.SetComponentData(entity, physicsVelocity);
        entityManager.SetComponentData(entity, new PhysicsDamping()
        {
            Linear = 0.01f,
            Angular = 0.05f
        });


        return entity;

    }
}
1 Like

There is no dynamic or static batching for dots rendering. It will use the SRP batcher to make the draw calls very cheap when the materials are the same but one Draw is required per mesh. When the mesh matches too it will use instanced rendering.

2 Likes

Thanks for the response, @julian-moschuering . Not the answer I was hoping to hear, but at least now I know!

Does this mean that if the vertices and triangles are the same on two meshes, but the uvs are different, it will still be an additional draw call?

The UnityEngine.Object references have to match in order for it to use instancing. Even if the meshes are otherwise identical, if they are separate objects, Unity treats them as such for performance reasons. If you need custom UVs, you should really be looking at customizing the scale and offset of the UVs per instance rather than the full set of UVs. Not only is this a lot less memory to upload to the GPU, but it also can use DOTS instancing so you can specify your scale and offset as an IComponentData and let the Hybrid Renderer take care of it for you.

It is also worth noting that a draw call in SRP Batcher is not the same as a draw call in the built-in renderer. Assuming you aren’t working with a mobile device with crappy graphics drivers, most modern devices can support thousands of draw calls per frame by the purest definition of “draw call”. However, a built-in renderer “draw call” does a bunch of other stuff which prevents it from getting anywhere near the device limits. SRP batcher aims to solve that. It isn’t perfect. But it does a lot better than built-in. But really if you can instance things with custom per-instance properties, that’s way better.

3 Likes

I would go a step further and say that if there’s any particular case you’re worried about, use GPU instancing and be done with it. It’s much easier to manually force GPU instancing (DrawMeshInstancedIndirect)
on one specific case than fighting with the SRP batcher. In my experience, SRP batcher is only good for static geometry, and even then often does more draw calls than it should because of front-to-back sorting/overdraw prevention (which is good if you’re GPU-bound but not so good if CPU-bound).

Yes. If the UVs are just offset from each other or otherwise predictable, you can try doing that in the vertex shader.

3 Likes

This sounds great, @DreamingImLatios ! Any chance you could point me to a resource about scaling and offsetting UVs per instance in the context of using IComponentData for the RenderMesh of an Entity?

Thanks, @burningmime ! This looks very promising. I have just spent the last hour looking into DrawMeshInstancedIndirect. If I went this route, would I not add a RenderMesh component to my Entities? How could I generate the list of matrices for the instances? Would I need to collect the Transform data from the Entities that are being positioned by the simulation?

Yup. You wouldn’t add RenderMesh at all, just add something to do the rendering directly (eg a system in PresentationSystemGroup). The idea would be to extract those transforms into a NativeArray (eg using a job), set them on a compute buffer with https://docs.unity3d.com/ScriptReference/ComputeBuffer.SetData.html and finally call DrawMeshInstanedProcedural or DrawMeshInstancedIndirect .

Now, whether or not that’s a good idea depends on a lot of factors. In particular, this only works with certain shaders, and is a huge pain in the [body part], so only worth it if you know this is a bottleneck.

1 Like

There’s no real resource. It is just combining different pieces of tech together. The first thing you need is a shader graph:
6386403--711687--upload_2020-10-5_18-42-28.png
On the left, you will notice that there is a property named uv0_st. That is the “display name” of the property. You can call it whatever you want for any shader graph. The “Reference” is what really matters. By default, it will be some garbled mess of characters. Name it to something meaningful, as you will use that name in the code.

I have set the default values to (1, 1, 0, 0). As you will see in the graph, the first two values represent the tiling factors which is just inverse scale. The offset are the last two values. And lastly, you will notice a little checkbox that is checked. That’s a checkbox for the Hybrid Renderer. Magic happens there.

The final step is to define an IComponentData which represents this material property. You might do it like this:

[MaterialProperty("_uv0_st", MaterialPropertyFormat.Float4)]
public struct MaterialUV0TileOffset : IComponentData
{
    public float4 tileOffset;
}

Note the name of the string has to match the “Reference” above. That’s what connects the IComponentData to the property. You can name the IComponentData and the members inside it whatever you want. I also believe this would work if you used 4 individual floats instead of a float4 in the struct. The hybrid renderer just memcpys the whole struct without paying much attention to its type.

Anyways, you can attach this component to any entity which has a RenderMesh with a material that uses that “Reference” as a property. The same “reference” and IComponentData can be used for multiple shader graphs. Whatever value is in an instance of the IComponentData when the hybrid renderer executes is what gets used when rendering during that frame.

6 Likes

Just a node on DrawMeshInstancedIndirect

The SRP batcher isn’t removing any draw calls it just makes them a lot cheaper.

I would refrain from going the DrawMeshInstanced(Indirect/Procedural) route nowadays. The new opt-in HybridRendererV2 is hellishly fast. The full pipeline including culling, sorting, shadow culling is around 2-3 times faster than calling DrawMeshInstanced with known-to-be-visible geometry. It’s simply supported by ShaderGraph, manual shaders are similar easy to write as instanced shaders, it supports most features already (GI, transparency sorting, shadows + separate per instance culling) and will support everything else in the near future (eg occlusion culling is WIP, URP lighting is WIP).

3 Likes

DMI is still relevant if you aren’t using SRP or are having troubles getting URP to play nice atm. Also if you need to prefix your instance data with a compute shader, HRV2 isn’t a good fit yet. But for everything else, HRV2 is holding up strong. Shader Graph instance data is an incredibly powerful graphical feature. If you are clever with it, you can make a world composed of millions of unique-looking objects.

1 Like

@DreamingImLatios , I really appreciate your post! With the example you provided, I was able to get immediate results!

In this test, I varied the color randomly between lighter and darker Color inputs (I will add texture tiling and scaling next). In this image, 5000 blocks are collapsing, each with one of 5000 different shades. In the editor, the frame rate never dropped below 60 and in the build it was well over 120 FPS on a 2018 MacBook Pro. This test is using HybridRendererV1 and URP in 2020.1.

In addition to the ShaderGraph with"_StoneColor" as a parameter, I created an IComponentData:

using System;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;

[Serializable]
[MaterialProperty("_StoneColor", MaterialPropertyFormat.Float4)]
public struct StoneColor : IComponentData
{
    public float4 Value;
}

And added this line to my CreateEntity function from the code above:

      entityManager.AddComponentData(entity, new StoneColor { Value = (Vector4) Color.Lerp(lighterColor, darkerColor, UnityEngine.Random.Range(0.0f, 1.0f)) });

Thanks again for your help, @DreamingImLatios !

And thanks again, @burningmime , for introducing me to DrawMeshInstanceIndirect. I will keep it in mind in the future, but for now I am glad that I will not have to maintain lists of of different meshes (since they will be generated in random sequences) be for being passed to DMII per mesh type.

@julian-moschuering , it is good to hear that HybridRendererV2 is even faster than V1, which, based on this simple test seems to be already a screamer.

3 Likes

I’m working on a a tile based strategy game where all tiles share the same material, but have procedual and therefore completely individual meshes. I thought this should be a use case for DOTS.
When I used regular game objects, dynamic batching reduced the draw calls to somewhere below 10. Now that I use entities, thousands of drawcalls have cut my frame rate in half. @julian-moschuering Will DOTS use SRP automatically and if so what else can I do, to deal with this issue?
@burningmime I have no experience with SRP. Is there any way to compensate for my loss in performance? If possible I would like to recreate static/dynamic batching in some way.

1 Like

Dynamic batching is basically the same as static batching updated each frame for visible objects. As that is pretty expensive it is limited to small meshes. You could replicate that but I wouldn’t do that.

The fastest way to render multiple distinct procedural meshes would be to make the GPU render multiple at once. You would execute one procedural draw with the full primitive count and some buffers containing the data to lookup per tile geometry and instance data. This could be executed extremely fast with DOTS for culling and buffer updates and you would end up with a single draw call per frame for all your tiles.

Out of the box DOTS rendering isn’t well suited for this use case. It will waist a lot of memory as all chunks are nearly empty and have one draw call per mesh. I would expect using BatchRenderGroup directly with SRP batcher still being pretty fast thought.

Is the vertex count the same for all variations?

2 Likes

Thank you very much! The vertex count varies from tile to tile.
I was under the impression, that as soon as I was working with a multitude of similar objects I might get a more tidy result datawise by using DOTS.

The current DOTS rendering using RenderMesh automatically stores objects that can be batched by putting them in chunks by RenderMesh data. That is really really great for most use cases in games but it’s not when each entity has a unique RenderMesh.

DOTS itself isn’t the problem and you can implement good solutions to your problem using it.

1 Like