MeshInstanceRendererSystem is a wonderfully optimized bonus you can find in the Entities package, allowing you to render objects with MeshInstanceRenderer shared data components without having to deal with culling, LOD and other concerns associated with rolling Graphics.DrawMeshInstanced calls directly.
Unfortunately, that system has a huge piece of the puzzle missing, and that is support for MaterialPropertyBlocks. It simply passes null to the MaterialPropertyBlock argument of Graphics.DrawMeshInstanced.
With traditional GameObjects using instanced shaders, MaterialPropertyBlocks provide an unparalleled flexibility: you can render whole level with tens of thousands of blocks using just a few drawcalls without sacrificing the ability to customize materials of every instance separately. The process is simple:
- Reuse a single MaterialPropertyBlock
- Assign new values to it for every instance
- Apply the MaterialPropertyBlock to MeshRenderer of a given instance
- Move to next instance
For a simple example, this be called on every block based on block data, making every part of the level able to take on different damage values and colors:
materialPropertyBlock.SetVector (shaderID_Color, colorVector);
materialPropertyBlock.SetVector (shaderID_Damage, damageVector);
blockRenderer.SetPropertyBlock (materialPropertyBlock)
For a start, lets create a fork of Unity.Rendering part of the package. I love how simple that is - just copy a few classes out of the package, create a new assembly definition, reference Unity.Rendering there and youâre all ready to create your custom rendering system.
Now, letâs look into our options. The first obvious idea is to simply extend the MeshInstanceRenderer component data class, allowing it to pack a MaterialPropertyBlock reference along with Mesh and Material references that are sitting there originally.
Then, going back to where Graphics.DrawMeshInstanced calls are done in the MeshInstanceRendererSystem, letâs just plug that new MaterialPropertyBlock reference into the RenderBatch method that was previously stuffing nulls into the corresponding arguments.
Problem is, weâre getting nowhere: these changes do not allow you to request per-entity material properties. Since we are reusing a MaterialPropertyBlock instance over all Entities holding a MeshInstanceRenderer, there is no per-instance data to speak of. That is, unless you use the array methods on MaterialPropertyBlock and write custom shaders that know the right array index and utilize array reads to get to their properties.
While itâs a pain to rewrite shaders and modify MaterialPropertyBlock handling code to set these arrays, itâs doable, and according to the previous threads on the subject, itâs the recommended way of getting per-instance values to instances rendered with Graphics.DrawMeshInstanced. Except this solution is not possible here for one simple reason.
Your code is not in control of the instancing batches, MeshInstanceRenderingSystem is. Your level manager code knows the material values for each entity comprising the level, but it canât stuff the arrays of a MaterialPropertyBlock object with those values because the exact order and number of batches is never up to your level code, just like the ultimate calls to Graphics.DrawMeshInstanced - thatâs totally up to the instanced rendering system and depends on interplay of culling and other factors. So, you canât predict which entity would end at which array index of which batch, making MaterialPropertyBlock.SetVectorArray and other similar methods quite useless in this case.
How was the old MeshRenderer based approach allowing us to ignore this complication? I guess the magic sauce of the old MeshRenderer approach was calling MeshRenderer.SetPropertyBlock, which let each MeshRenderer grab a copy of material data. I have no idea how that particular method is implemented, because it is not managed at all - according to Unity C# code reference repository, it just invokes an internal non-managed method. But I guess what it does is fill a struct with a copy of MaterialPropertyBlock data passed into it.
Subsequently, that copy of material data was picked up in the native code and used with a lower level counterpart of Graphics.DrawMeshInstanced, which allowed rendering from an array while allowing shaders to keep traditional non-array based properties. We obviously have no access to such a rendering method, so weâll have to make Graphics.DrawMeshInstanced work for us.
I guess that the best way to get parity with MeshRenderers on MeshInstanceRendererSystem would be to do the following:
-
Drop the MaterialPropertyBlock use on the level manager side, since its ultimately a disposable container which canât be usefully passed to Graphics.DrawMeshInstanced call happening in the instanced rendering system
-
Introduce a new non-shared data component, MeshInstanceMaterial, which would hold a set of property values instead of a property block reference. This data canât exist on MeshInstanceRenderer, since that component is shared and canât have per-instance values. In a way, that material data component would be similar to position/rotation/scale components
-
Set that struct per entity, mirroring what happens when you call MeshRenderer.SetPropertyBlock
-
For each batch constructed in the customized MeshInstanceRendererSystem, construct a MaterialPropertyBlock with property arrays holding 1023 entries corresponding to properties stored in MeshInstanceMaterial data component. Again, this would mirror what happens with scale/position/rotation, where a 1023 entries long array of matrices is constructed at the same batch preparation stage
-
Submit the per-batch MaterialPropertyBlock in DrawMeshInstanced argument
-
Write custom shaders reading arrays instead of traditional properties (no way around that without magic sauce Unity has for per-MeshRenderer instanced properties, as far as I see)
Please correct my thinking if you see any issues in my conclusions. Iâd also appreciate any info from Unity developers on whether an official support for MaterialPropertyBlock+MeshRenderer like workflow is planned for MeshInstanceRendererSystem in the future.