How To Get Started With DOTS (第 5 篇)

本文探讨了Unity的DOTS渲染系统中动态批处理的工作原理,指出只有拥有相同材质和网格的实体才能进行动态批处理。当网格不同但材质相同时,即使顶点和三角形相同,也会产生额外的绘制调用。作者建议使用GPU实例化以提高性能,并提供了创建独特实例的方法,包括利用ComputeBuffer和DrawMeshInstancedIndirect。此外,还提到了SRP批处理器的作用和限制,以及如何利用HybridRenderer V2实现更快的渲染。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

/////////////////////////////////////////////////////

Instantiated Entities Dynamic Batching in DOTS  

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.

  1. Entity boxEntity = CreateUniqueBoxEntity(blockSize, new float3(-2, 0, 0), blockMass, blockMaterial, blockVelocity);

  2.        

  3.         for (int j=0; j<rows; j++)

  4.         {

  5.             for (int i = 0; i < columns; i++)

  6.             {

  7.                 float3 blockPosition = new float3(j + 1.5f, i * 1f, 0);

  8.                 if (uniqueBlockMeshes)

  9.                 {

  10.                     // GENERATE A UNIQUE MESH

  11.                     CreateUniqueBoxEntity(blockSize, blockPosition, 2, blockMaterial, blockVelocity);

  12.                 }

  13.                 else

  14.                 {

  15.                     // INSTANTIATE

  16.                     var tmpEntity = entityManager.Instantiate(boxEntity);

  17.                     entityManager.SetComponentData(tmpEntity, new Translation() { Value = blockPosition });

  18.                 }

  19.              

  20.             }

  21.         }

Here is Full Code:

  1. using UnityEngine;

  2. using Unity.Collections;

  3. using Unity.Collections.LowLevel.Unsafe;

  4. using Unity.Entities;

  5. using Unity.Transforms;

  6. using Unity.Rendering;

  7. using UnityEngine.Rendering;

  8. using Unity.Mathematics;

  9. using Unity.Physics;

  10. using Collider = Unity.Physics.Collider;

  11. using Unity.Physics.Extensions;

  12. public class TestEntityGenerator : MonoBehaviour

  13. {

  14.     public Mesh mesh;

  15.     public UnityEngine.Material blockMaterial;

  16.     public UnityEngine.Material projectileMaterial;

  17.     public float    rows            = 10;

  18.     public float    columns         = 10;

  19.     public float    blockMass       = 2;

  20.     public Vector3  blockSize       = Vector3.one;

  21.     public float    projectileMass  = 10;

  22.     public Vector3  projectileSize  = Vector3.one;

  23.     public bool uniqueBlockMeshes = true;

  24.     EntityManager entityManager;

  25.     EntityArchetype blockArchetypeype;

  26. // Start is called before the first frame update

  27.     void Start()

  28.     {

  29.         // ENTITY MANAGER

  30.         entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

  31.         // BLOCK_ARCHETYPE

  32.         ComponentType[] dynamicComponentTypes = new ComponentType[11];

  33.         dynamicComponentTypes[0] = typeof(RenderMesh);

  34.         dynamicComponentTypes[1] = typeof(RenderBounds);

  35.         dynamicComponentTypes[2] = typeof(Translation);

  36.         dynamicComponentTypes[3] = typeof(Rotation);

  37.         dynamicComponentTypes[4] = typeof(LocalToWorld);

  38.         dynamicComponentTypes[5] = typeof(PhysicsCollider);

  39.         dynamicComponentTypes[6] = typeof(PhysicsVelocity);

  40.         dynamicComponentTypes[7] = typeof(PhysicsMass);

  41.         dynamicComponentTypes[8] = typeof(PhysicsDamping);

  42.         dynamicComponentTypes[9] = typeof(Damage);

  43.         dynamicComponentTypes[10] = typeof(OriginalTransform);

  44.         blockArchetypeype = entityManager.CreateArchetype(dynamicComponentTypes);

  45.         // CREATE STACKED BLOCKS

  46.         PhysicsVelocity blockVelocity = new PhysicsVelocity();

  47.         Entity boxEntity = CreateUniqueBoxEntity(blockSize, new float3(-2, 0, 0), blockMass, blockMaterial, blockVelocity);

  48.        

  49.         for (int j=0; j<rows; j++)

  50.         {

  51.             for (int i = 0; i < columns; i++)

  52.             {

  53.                 float3 blockPosition = new float3(j + 1.5f, i * 1f, 0);

  54.                 if (uniqueBlockMeshes)

  55.                 {

  56.                     // GENERATE A UNIQUE MESH

  57.                     CreateUniqueBoxEntity(blockSize, blockPosition, 2, blockMaterial, blockVelocity);

  58.                 }

  59.                 else

  60.                 {

  61.                     // INSTANTIATE

  62.                     var tmpEntity = entityManager.Instantiate(boxEntity);

  63.                     entityManager.SetComponentData(tmpEntity, new Translation() { Value = blockPosition });

  64.                 }

  65.              

  66.             }

  67.         }

  68.         // CREATE PROJECTILE

  69.         float3          projectileLaunchPoint   = new float3(-120, 40, 0);

  70.         PhysicsVelocity projectileVelocity      = new PhysicsVelocity() { Linear  = new float3(80f, 0f, 0f) };

  71.         CreateUniqueBoxEntity(projectileSize, projectileLaunchPoint, projectileMass, projectileMaterial, projectileVelocity);

  72.     }

  73.     /// <summary>

  74.     /// Creates a box entity by generating a simple cube Mesh and a RenderMesh .

  75.     /// </summary>

  76.     /// <returns>The box entity.</returns>

  77.     /// <param name="size">Size.</param>

  78.     /// <param name="trans">Trans.</param>

  79.     /// <param name="mass">Mass.</param>

  80.     /// <param name="material">Material.</param>

  81.     /// <param name="physicsVelocity">Physics velocity.</param>

  82.     private Entity CreateUniqueBoxEntity(Vector3 size, float3 trans, float mass, UnityEngine.Material material, PhysicsVelocity physicsVelocity)

  83.     {

  84.         // CREATE A UNIQUE MESH

  85.         mesh = BoxMesh.CreateBoxMesh(size.x, size.y, size.z);

  86.         RenderMesh renderMesh = new RenderMesh()

  87.         {

  88.             castShadows     = ShadowCastingMode.On,

  89.             layer           = 1,

  90.             material        = material,

  91.             mesh            = mesh,

  92.             receiveShadows  = true,

  93.             subMesh         = 0

  94.         };

  95.         BlobAssetReference<Unity.Physics.Collider> boxCollider = Unity.Physics.BoxCollider.Create(new BoxGeometry()

  96.         {

  97.             BevelRadius     = 0f,

  98.             Center          = new float3(0, size.y/2, 0),

  99.             Orientation     = quaternion.identity,

  100.             Size            = new float3(size.x, size.y, size.z)

  101.         }, CollisionFilter.Default);

  102.         return CreateEntity(renderMesh, boxCollider, mass, trans, physicsVelocity);

  103.     }

  104.     /// <summary>

  105.     /// Creates an entity from scratch.

  106.     /// </summary>

  107.     /// <returns>The entity.</returns>

  108.     /// <param name="renderMesh">Render mesh.</param>

  109.     /// <param name="_collider">Collider.</param>

  110.     /// <param name="mass">Mass.</param>

  111.     /// <param name="trans">Trans.</param>

  112.     /// <param name="physicsVelocity">Physics velocity.</param>

  113.     unsafe Entity CreateEntity(RenderMesh renderMesh, BlobAssetReference<Collider> _collider, float mass, float3 trans, PhysicsVelocity physicsVelocity)

  114.     {

  115.      

  116.         Unity.Entities.Entity entity = entityManager.CreateEntity(blockArchetypeype);

  117.         entityManager.AddSharedComponentData    (entity, renderMesh);

  118.         entityManager.SetComponentData          (entity, new RenderBounds     { Value = renderMesh.mesh.bounds.ToAABB() });

  119.         entityManager.AddComponentData          (entity, new Translation      { Value = trans });

  120.         entityManager.SetComponentData          (entity, new Rotation         { Value = Quaternion.Euler(0f, 0f, 0f) });

  121.         entityManager.SetComponentData          (entity, new PhysicsCollider  { Value = _collider });

  122.         Collider* colliderPtr = (Collider*)_collider.GetUnsafePtr();

  123.         entityManager.SetComponentData(entity, PhysicsMass.CreateDynamic(colliderPtr->MassProperties, mass));

  124.         entityManager.SetComponentData(entity, physicsVelocity);

  125.         entityManager.SetComponentData(entity, new PhysicsDamping()

  126.         {

  127.             Linear = 0.01f,

  128.             Angular = 0.05f

  129.         });

  130.         return entity;

  131.     }

  132. }

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.

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).
 

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?

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

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 Unity - Scripting API: ComputeBuffer.SetData 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.

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:

  1. [MaterialProperty("_uv0_st", MaterialPropertyFormat.Float4)]

  2. public struct MaterialUV0TileOffset : IComponentData

  3. {

  4.     public float4 tileOffset;

  5. }

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.

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).

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.

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:

  1. using System;

  2. using Unity.Entities;

  3. using Unity.Mathematics;

  4. using Unity.Rendering;

  5. [Serializable]

  6. [MaterialProperty("_StoneColor", MaterialPropertyFormat.Float4)]

  7. public struct StoneColor : IComponentData

  8. {

  9.     public float4 Value;

  10. }

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)) });

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.

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?

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.

///////////////////////////////

////////////////////////////////////////

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值