Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

MaterialPropertyBlock support in MeshInstanceRendererSystem

Discussion in 'Data Oriented Technology Stack' started by bac9-flcl, Sep 28, 2018.

  1. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    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:

    Code (csharp):
    1. materialPropertyBlock.SetVector (shaderID_Color, colorVector);
    2. materialPropertyBlock.SetVector (shaderID_Damage, damageVector);
    3. 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.
     
    Last edited: Sep 28, 2018
    mkracik, deus0, Antypodish and 2 others like this.
  2. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    1,587
    With MeshInstanceRenderer you must also use culling (WorldMeshRenderBounds) and LODs (MeshLODComponent , MeshLODGroupComponent) for gain performance
     
  3. vitautart

    vitautart

    Joined:
    May 3, 2018
    Posts:
    29
    Problem with meshrenderer and materialpropertyblock is really annoying. But it is hybrid, so i think it will be rewrited in a future, as it was a few weeks ago, when they added chunk iteration. And I think they even will rewrite Graphics.DrawMeshInstanced or will create analog so it can takes float4x4 instead Matrix4x4.

    A time ago I do some hacks with MeshInstanceRenderingSystem with approach as you describe with MaterialPropertyBlock.SetVectorArray, and that somehow works. What I did.
    0. Prepared special shader.
    1. Forked MeshInstanceRenderingSystem, MeshInstanceRenderer, RenderingSystemBootstrap.
    1. Created TeamColor : IComponentData. It is a container for color value for each entity.
    2. Then in modified MeshInstanceRenderingSystem I just did same stuff with TeamColor like developers did with VisibleLocalToWorld component (Collect data from chunk, and rearrange data array to array of values). Except I used for-loop (that causes some performance regress maybe) instead unsafe but quick memcpy to copy values from ComponentData to array.
    3. And Then I had consistent array that coresponds to entities pretty well that I can push to MaterialPropertyBlock.SetVectorArray and then to DrawMeshInstanced.

    I tested it with different colors, and it works well.

    But yeah, it is very hacky and unmodular thing, and when they will change MeshInstanceRenderingSystem, I must also update my RenderingSystem. So we definitely need more modular rendering API with per-instance properties.
     
  4. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    Thanks, that's very valuable information! Could you, by any chance, post the code for the step where you collect the data from a chunk and perform a memcpy? I've never used a memory copy functionality before, so having a reference would be much appreciated!

    On another note - yeah, I think Unity would move away from Matrix4x4 use in this system in the future - the TODO comment in the unsafe CopyTo function even references that. :)
     
  5. vitautart

    vitautart

    Joined:
    May 3, 2018
    Posts:
    29
    So am I. That's why i used dirty for-loop:). About chunk iteration you can read there https://github.com/Unity-Technologi...ster/Documentation/content/chunk_iteration.md . MemCpy is used in function that you mention:
    Code (CSharp):
    1. static unsafe void CopyTo(NativeSlice<VisibleLocalToWorld> transforms, int count, Matrix4x4[] outMatrices, int offset)
    2.         {
    3.             // @TODO: This is using unsafe code because the Unity DrawInstances API takes a Matrix4x4[] instead of NativeArray.
    4.             Assert.AreEqual(sizeof(Matrix4x4), sizeof(VisibleLocalToWorld));
    5.             fixed (Matrix4x4* resultMatrices = outMatrices)
    6.             {
    7.                 VisibleLocalToWorld* sourceMatrices = (VisibleLocalToWorld*)transforms.GetUnsafeReadOnlyPtr();
    8.                 UnsafeUtility.MemCpy(resultMatrices + offset, sourceMatrices, UnsafeUtility.SizeOf<Matrix4x4>() * count);
    9.             }
    10.         }
    You can take this sources, it is almost the same like vanila unity sources, but with some heavy tweaks in VMeshRendererSystem. I didn't test it enough, but for now it seams ok for my stuff.
     

    Attached Files:

    GilCat likes this.
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,732
    What vitautart is writing is definitely the right approach. Ultimately i would love to make it so that

    struct TeamColor : IComponentData, IInstanceRenderProperties { public float4 Color; }

    And it automatically gets bound to the material via MaterialPropertyBlock. Extra cherry on top would be to have shader graph generate the C# struct.

    There is nothing preventing you from doing this today by forking the MeshInstanceRendererSystem code. And we will add something like this at a later point.
     
  7. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    Thanks for confirming this is the right way forward!

    The pattern you outlined would certainly be great to have.

    Oh, and looks like I misunderstood the role of SetVectorArray and other array methods on MaterialPropertyBlock. Using them with Graphics.DrawMeshInstanced works like using SetVector with MeshRenderers. This simplifies the implementation, I won't need custom shaders then. :)
     
  8. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    I got the basic implementation working and I'd appreciate any advice on improving it! So far, I got support for up to 8 arbitrary vectors per entity working. None of the other types MaterialPropertyBlock supports setting are really essential - any floats can just be packed into vectors, any colors are vectors etc. Whole setup works like this:

    _______________

    First, we introduce a new data component called MeshInstanceMaterial and attach it to our entities along with MeshInstanceRenderer:

    Code (csharp):
    1. public struct MeshInstanceMaterial : IComponentData
    2. {
    3.     public int propertyVectorCount;
    4.     public MaterialPropertyVector propertyVector0;
    5.     public MaterialPropertyVector propertyVector1;
    6.     public MaterialPropertyVector propertyVector2;
    7.     public MaterialPropertyVector propertyVector3;
    8.     public MaterialPropertyVector propertyVector4;
    9.     public MaterialPropertyVector propertyVector5;
    10.     public MaterialPropertyVector propertyVector6;
    11.     public MaterialPropertyVector propertyVector7;
    12. }
    At first I assumed you could just use an array there, similar to how Materials and MeshRenderers do it, but I forgot that data components can't contain non-blittable types like arrays. Looks dirty, but I'm not yet familiar with any other way to hold this on a data component - please tell me if I missed some sort of an ECS component friendly collection type.

    The MaterialPropertyVector is a simple struct like this, mirroring material properties on traditional MaterialPropertyBlock.

    Code (csharp):
    1. public struct MaterialPropertyVector
    2. {
    3.     public int id;
    4.     public Vector4 data;
    5.  
    6.     public MaterialPropertyVector (int id, Vector4 data)
    7.     {
    8.         this.id = id;
    9.         this.data = data;
    10.     }
    11. }
    To assign material properties to an entity, you construct the MeshInstanceMaterial like this (this matches what you usually do with MaterialPropertyBlock.SetVector relatively closely):

    Code (csharp):
    1. MeshInstanceMaterial mim = new MeshInstanceMaterial
    2. {
    3.     propertyVectorCount = 3,
    4.     propertyVector0 = new MaterialPropertyVector (propertyID_A, propertyValue_A),
    5.     propertyVector1 = new MaterialPropertyVector (propertyID_B, propertyValue_B),
    6.     propertyVector2 = new MaterialPropertyVector (propertyID_C, propertyValue_C),
    7. };
    8. entityManager.SetComponentData (entity, mim);
    I should probably wrap this in a convenience method that stops you from assigning an incorrect integer to propertyVectorCount and stops you from writing to incorrect properties, as if you were using MaterialPropertyBlock. But this works for now.

    Next, in custom instanced renderer system, we add the following fields in the beginning:

    Code (csharp):
    1. private MaterialPropertyBlock m_PropertyBlock = new MaterialPropertyBlock ();
    2. private Dictionary<int, Vector4[]> m_PropertiesVector = new Dictionary<int, Vector4[]> ();
    I'm not sure if using a traditional dictionary is a good idea in a system, but I'm not yet familiar with any other types that can be used for a similar setup - I'd appreciate any pointers here!

    We also modify the query the system performs to include the new data component. Next, now that we have these fields and material data components are grabbed, we can modify the Update*InstanceRenderer methods like this, making a new call right after CopyTo:

    Code (csharp):
    1. unsafe void UpdateDynamicInstanceRenderer ()
    2. {
    3.     ...
    4.     var meshInstanceMaterialType =
    5.         GetArchetypeChunkComponentType<MeshInstanceMaterial> (false);
    6.     ...
    7.     for (int i = 0; i < packedChunkCount; i++)
    8.     {
    9.         ...
    10.         var materials = chunk.GetNativeArray (meshInstanceMaterialType);
    11.         ...
    12.         CopyTo (visibleTransforms, activeCount, m_MatricesArray, batchCount);
    13.         CollectMaterialProperties (activeCount, batchCount, materials);
    14.         ...
    15.     }
    16.     ...
    17. }
    The CollectMaterialProperties method collects the vectors from MeshInstanceMaterial data components and puts them into 1023 entries long arrays which can later be used with MaterialPropertyBlock.SetVectorArray method. It's tempting to add convenience methods to MeshInstanceMaterial allowing to iterate through its properties as if it was a real array, but that would require a ton of branching repeated by number of ultimately utilized properties, so that's not a good idea. Instead, I just attempt to read all properties in a row and bail out if an index goes over the property count from the data component. Maybe this can be improved further - and again, maybe individual fields are not even necessary if there is a way to put collections into data components.

    Code (csharp):
    1. void CollectMaterialProperties (int activeCount, int batchCount, NativeArray<MeshInstanceMaterial> materials)
    2. {
    3.     MeshInstanceMaterial mim;
    4.     int shiftedIndex;
    5.  
    6.     for (int i = 0; i < activeCount; i++)
    7.     {
    8.         mim = materials[i];
    9.         shiftedIndex = batchCount + i;
    10.  
    11.         CheckProperty (mim.propertyVectorCount, 0, mim.propertyVector0, shiftedIndex);
    12.         CheckProperty (mim.propertyVectorCount, 1, mim.propertyVector1, shiftedIndex);
    13.         CheckProperty (mim.propertyVectorCount, 2, mim.propertyVector2, shiftedIndex);
    14.         CheckProperty (mim.propertyVectorCount, 3, mim.propertyVector3, shiftedIndex);
    15.         CheckProperty (mim.propertyVectorCount, 4, mim.propertyVector4, shiftedIndex);
    16.         CheckProperty (mim.propertyVectorCount, 5, mim.propertyVector5, shiftedIndex);
    17.         CheckProperty (mim.propertyVectorCount, 6, mim.propertyVector6, shiftedIndex);
    18.         CheckProperty (mim.propertyVectorCount, 7, mim.propertyVector7, shiftedIndex);
    19.     }
    20. }
    21.  
    22. // This method is only there to reduce boilerplate, allowing 1 line per property
    23. void CheckProperty (int propertyCount, int propertyIndex, MaterialPropertyVector property, int shiftedIndex)
    24. {
    25.     // If this check fails, that property was not used
    26.     // (is beyond the length of our make-believe array)
    27.     if (propertyCount <= propertyIndex)
    28.         return;
    29.  
    30.     // This condition would only be satisfied whenever the system
    31.     // encounters a new property ID, so it won't cause array allocations often
    32.     if (!m_PropertiesVector.ContainsKey (property.id))
    33.         m_PropertiesVector.Add (property.id, new Vector4[1023]);
    34.  
    35.     m_PropertiesVector[property.id][shiftedIndex] = property.data;
    36. }
    After all this, we end up with the dictionary full of shader property ID keys, with a 1023 vectors attached to each key (in an array). Each vector correctly corresponds to ever-shifting position of entities within the instanced batches. The dictionary is never cleared, and arrays are never discarded - new arrays are created whenever new shader property ID is encountered, but that's it.

    Now, we can finally modify the RenderBatch method to put this to use, by calling MaterialPropertyBlock.SetVectorArray for each array in our dictionary and plugging the resulting MaterialPropertyBlock into the argument of the Graphics.DrawMeshInstanced method.

    Code (csharp):
    1. void RenderBatch (int lastRendererIndex, int batchCount)
    2. {
    3.     ...
    4.     if (renderer.mesh && renderer.material)
    5.     {
    6.         m_PropertyBlock.Clear ();
    7.         foreach (KeyValuePair<int, Vector4[]> vectorProperty in m_PropertiesVector)
    8.             m_PropertyBlock.SetVectorArray (vectorProperty.Key, vectorProperty.Value);
    9.  
    10.         if (renderer.material.enableInstancing)
    11.         {
    12.             Graphics.DrawMeshInstanced (renderer.mesh, renderer.subMesh, renderer.material,
    13.                 m_MatricesArray, batchCount, m_PropertyBlock, renderer.castShadows,
    14.                 renderer.receiveShadows, 0, ActiveCamera);
    15.         }
    16.         else
    17.         {
    18.             for (int i = 0; i != batchCount; i++)
    19.             {
    20.                 Graphics.DrawMesh (renderer.mesh, m_MatricesArray[i], renderer.material, 0,
    21.                     ActiveCamera, renderer.subMesh, m_PropertyBlock,
    22.                     renderer.castShadows, renderer.receiveShadows);
    23.             }
    24.         }
    25.     }
    26. }
    This gives me reasonable parity with MaterialPropertyBlocks I used on traditional MeshRenderers previously. Sure, I can't assign more than 8 vector properties and can't use anything other than vectors, but I wouldn't call going over those limits reasonable.

    The setup is perfectly functional, but again, I'd really appreciate any advice on improving it. :)
     
    Last edited: Sep 30, 2018
    Squize and vitautart like this.
  9. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Isn't this a perfect usecase for IBufferElementData?
     
  10. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    You are right, I rewrote it yesterday using buffers! I also made the system store properties based on shader identifier since batches would never contain more than one shader and filling a property block with properties originating from more than shader is a waste.

    On another subject, I wonder if it would be possible to move the whole system to Graphics.DrawMeshInstancedIndirect, since what I use the system for is a relatively static level which would really benefit from creating the draw buffer just once instead of per frame.
     
  11. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,732
    Graphics.DrawMeshInstancedIndirect requires custom shaders. But yes, its definitely faster if all data is already uploaded to the GPU. You then have to build an index list based on the culling results. Or do culling on the GPU.

    It is all possible of course.
     
  12. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    Thanks for clarifying that!

    By the way, is this ECS renderer intersecting what SRP (or HDRP in particular) are doing, in any way? I'm not very familiar with SRP code, but I remember many related posts mentioning that pipelines bring improved culling and faster rendering (which is what this system tries to do too). If HD rendering pipeline or any pipeline in general already have a similar system as new default (as in, do fast culling and submit meshes for rendering through Graphics.DrawMeshInstanced), then this whole system is obviously redundant if I were to move the project to HDRP.
     
  13. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,732
    SRP goes through normal culling codepath and support Graphics.DrawMeshInstancedIndirect & Graphics.DrawMeshInstanced so ECS render code works in both legacy and SRP's
     
  14. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Attached you find an early prototype of per instance properties more like @Joachim_Ante suggested in hopes that it will be easy to switch to what Unity will release in the future.

    It currently doesn't use Unity's MeshInstanceRendererSystem as it made prototyping simpler but it should be possible to integrate it within an hour.

    To add a new property just add a component derived from IRenderProperty<T>. Only float, float4 and float4x4 are supported, what the MaterialPropertyBlock supports. Should probably get replaced by derived interfaces eg FloatRenderProperty...
    Code (CSharp):
    1. public struct HightlightColorProperty : IRenderProperty<float4>
    2. {
    3.     public float4 color;
    4. }
    The property name is generated by cutting 'Property' from the component name and adding an '_' before it. So the above example will set '_HighlightColor' Vector4Array in the MaterialPropertyBlock. A property is only set in the block if at least one entity has it overwritten using the component.

    Instances having this component will get batched together with the ones not having it. The ones not having it will get the material's value for the property.

    It should already be pretty fast. There is a very expensive, very unoptimized mesh spawner in there. Just ignore it :)

    Edit: New version below.
     
    Last edited: Oct 11, 2018
    bac9-flcl likes this.
  15. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    New version, now supporting non-instanced properties. MaterialPropertyBlock is basically fully supported now.
    2018.3b5 required (C# 7.3)

    Example Properties:
    Code (CSharp):
    1.  
    2.     public struct ColorInstancedProperty : IFloat4InstancedRenderProperty
    3.     {
    4.         public float4 color;
    5.     }
    6.  
    7.     public struct MainTexProperty : ITextureRenderProperty
    8.     {
    9.         public Texture texture;
    10.         public Texture Value{ get => texture; }   // <-- currently required to prevent boxing for value types
    11.     }
    12.  
    Usage:
    Code (CSharp):
    1. buffer.AddComponent(entity, new ColorInstancedProperty { value = new float4(1,0,0,1)});
    2. buffer.AddSharedComponent(entity, new MainTexProperty { texture = someTexture});
    EDIT: Now uses fork of Unity's MeshInstanceRendererSystem. Frozen are not tested but implemented.
     

    Attached Files:

    Last edited: Oct 11, 2018
    Code-T, Deadcow_, j-stash and 5 others like this.
  16. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Update: Now uses fork of MeshInstanceRendererSystem (0.0.12-preview.17).

    Untitled.png
     
    Last edited: Oct 11, 2018
  17. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    Wow, that's incredible work, thanks a lot for looking into this! Out of curiosity, what exactly did you mean by the following comment in the fork (I'm not sure what "cache supported" means in that context)?

    Code (csharp):
    1. // todo: cache supported properties per material
     
  18. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Currently for each chunk the material is checked for properties that have RenderProperty components defined and only these properties are assigned to the MaterialPropertyBlock. Currently this is just checked each time and this could be cached. Will probably only have a real effect when there are ALOT of components, but I type this stuff done when they come to my mind.

    Actually there is a problem in this implementation. Graphics.DrawMeshInstanced seems to merge batches under certain circumstances but ignores that some MaterialPropertyBlocks contain an overwrite and some don't resulting in the ones without getting the overridden property too. I added a '// todo: report to unity' for that. :) Currently it just writes all material defaults to the MaterialPropertyBlock which isn't that great and throws an exception when the material has eg a _MainTex field set to null, as MaterialPropertyBlock may not set textures/buffers to null. In our actual project MeshInstanceRendererSystem draws to CommandBuffers, which do not have this issue and just let me only set the properties actually set in the chunk so I didn't optimize further.

    Probably check for null in _Apply of Texture and ComputeBuffer for now to prevent exception.
     
    Last edited: Oct 11, 2018
  19. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Reported: (Case 1090093) Graphics.DrawMeshInstanced merges incompatible batches when overriding material properties using MaterialPropertyBlock

    Unfortunately I don't have a way to escalate it as support just ran out :'(
     
  20. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    MaterialPropertyBlocks are the only thing holding me back from using a completely pure ECS solution at the moment.

    The work here is amazing, but this bug makes it unusable, get changing colors everywhere =(

    side note

    // not optimized... who uses non instanced materials?


    me =( much better performance on unique meshes.
     
  21. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Really? I didn't profile this as nearly everything is rendered multiple times in our project but I know that Unity already issues non instanced DrawIndexed calls instead of DrawIndexedInstanced when instance count is 1 so I would expect it to be equally fast.
     
  22. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    I'm heading to bed but I'll profile again tomorrow as I haven't actually checked since may but back then instance draw calls on 256 unique meshes halved my fps.
     
  23. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    Hmm, I can no longer replicate (at least in my smallish scale test) the performance issues I faced months ago; pretty much identical fps instanced or not.

    Good to know.
     
    julian-moschuering likes this.
  24. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    @julian-moschuering thanks again for the source.

    I managed to take it and modify it a bit and got my pixel perfect selection system going.

    As a requirement to get to work what I wanted, I had to move my replacement material support to within the MeshInstanceRendererSystem instead of modifying the LWRP directly because the material properties were skipped with how it's setup. However this is good, it should be easier for me to maintain in the future (LWRP upgrades have broken it multiple times.)

    For anyone who comes across thread and wanted to add support for replacement shaders / materials in LWRP or HDRP, an easier way is with a custom mesh instance renderer. Just camera lookup and material replace.

    Code (CSharp):
    1.  
    2.         void IndexSharedComponents()
    3.         {
    4.             meshInstanceRenderer.Clear();
    5.             localToGlobalRendererMap.Clear();
    6.             EntityManager.GetAllUniqueSharedComponentData(meshInstanceRenderer, localToGlobalRendererMap);
    7.  
    8.             // REPLACEMENT MATERIAL
    9.             if (ReplacementMaterials.TryGetValue(this.ActiveCamera, out var material))
    10.             {
    11.                 for (int i = 0; i < this.meshInstanceRenderer.Count; i++)
    12.                 {
    13.                     var instance = this.meshInstanceRenderer[i];
    14.                     instance.material = material;
    15.                     this.meshInstanceRenderer[i] = instance;
    16.                 }
    17.             }
    18.             // END REPLACEMENT MATERIAL
    19.  
    20.             // ...
    21.  
     
    Last edited: Oct 25, 2018
    bac9-flcl, TZ- and julian-moschuering like this.
  25. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    252
    I have tried to use IFloat4InstancedRenderProperty, VS does not find this interface. What do I need to do to use it?
     
  26. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Did you add the code from this post? IFloat4InstancedRenderProperty is included in namespace
    Realmforge.BraveNewWorld.Rendering.RenderProperties.
     
  27. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    Just something to note for anyone using this and linear rendering.

    Took me a while to realize I had to convert my Color to linear space. This was previous handled automatically by MaterialPropertyBlock.SetColor so I didn't consider it but you'll need to convert it yourself with Color.linear before converting to float4.
     
  28. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    Oh, yeah :) Took me a while to realize back than, that SetColor takes a Color with 4 floats and thus no really requiring gamma space but actually does a gamma conversion! I think Color32 should always be in gamma space and Color should always be linear.
    I'm happy that that is gone. But good you mentioned it for everyone!
     
  29. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    103
    Would it be possible to use this with a Shader with RenderMode: Fade?

    I'm trying to render lots of planes (with images for shapes) whilst adjusting their alpha-values so that they fade in/out.. I'm terrible at shaders, and haven't been able to adjust your shader to work so far..
     
  30. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    367
    This works with any shader. The property you are changing must be an instanced property (https://docs.unity3d.com/Manual/GPUInstancing.html) when using I...InstancedRenderProperty and must not be an instanced property when using I...RenderProperty
     
  31. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    103
  32. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    781
    Looks like standard artifact of transparent rendering mode (lack of z-sorting).
     
  33. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    103
    Last edited: Nov 13, 2018
  34. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    957
    with preview 23, what is currently the best solution for setting per entity color? For a stress test, I would like to display colliding entities in a different color (without moving chunks / change the shared component data)
     
  35. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    457
    Any easy way for using this with RendeMeshSystemV2 and BatchRendererGroup?
    Btw @julian-moschuering Great work you've done with Render Property Prototype.
     
  36. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    I tried today for a little while but I could not find an easy way. Passing a property block to the BatchRendererGroup seemed to have no effect and I don't really want to spend a lot of time figuring it out so I'm just sticking to a version of V1 for now. Also in my case I can easily do replacement materials in V1 but V2 while MaterialPropertyBlocks might be possible, there will be no way to do replacement materials. I'm not sure how I'm going to handle this into the future. Probably a custom pass in the SRP.

    Hopefully at some point we at least get official support for MaterialPropertyBlocks.

    I'm not sure what others are currently doing as from what I can tell the version posted here does not seem to work with culling (at least p23). From what I can tell, basically if a chunk has 4 entities 1, 2, 3, 4 and 1, 2 are culled then 3,4 will get 1,2 property.

    I've got an updated version that fixes this, but it only supports InstanceProperties as I never bothered adding the others as I don't use them. If anyone needs it let me know and I'll go about stripping it out.
     
    GilCat likes this.
  37. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    957
    I am also waiting for official support, until then I use a completely stripped down custom version (no culling, no support for frozen, etc.)

    It's basically just the call to DrawMeshInstanced with the matricesArray and a MaterialPropertyBlock (fed by an Icomponentdata for color) in 1023 chunks.
     
    GilCat likes this.
  38. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    457
    Ok. Though it was just me that couldn't figure this out :p
    I'm using the version posted here with some changes but i'm sure in the future this will officially be addressed.
     
  39. Rotary-Heart

    Rotary-Heart

    Joined:
    Dec 18, 2012
    Posts:
    483
    I'll like to see how you handle this. I have my own version (didn't know this thread existed) and it has the culling issue you said there.
     
  40. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,833
    So how Unity's RenderMeshSystem culls entities is it adds a pseudo array to each chunk by adding a VisibleLocalToWorld component to (entities within) each chunk.

    Then the CullLODToVisible job copies the matrix4x4 to this 'array' not caring what entity it is associated with and passes that to the Graphics.DrawMesh[Instanced]. I just added an extra component, VisibleIndex

    Code (CSharp):
    1.     public struct VisibleIndex : IComponentData
    2.     {
    3.         public int Value;
    4.     }
    which holds the index of the copied matrix. So when unity does the cull and copies the matrix to the VisibleLocalToWorld array, I also just store the index of that entity and use this to get the correct material block properties. My add data function (from julian-moschuering original work) becomes

    Code (CSharp):
    1.         public override void AddData(in ArchetypeChunk chunk, in NativeArray<VisibleIndex> visible, int instanceStart, int copyCount)
    2.         {
    3.             var chunkData = chunk.GetNativeArray(this.ArchetypeType);
    4.             if (chunkData.Length == 0)
    5.             {
    6.                 return;
    7.             }
    8.  
    9.             if (this.lastWrite < instanceStart)
    10.             {
    11.                 this.Fill(this.lastWrite, instanceStart - this.lastWrite);
    12.             }
    13.  
    14.             this.lastWrite = instanceStart + copyCount;
    15.  
    16.             Assert.IsTrue(copyCount <= math.min(this.Data.Length - instanceStart, chunkData.Length));
    17.        
    18.             var src = new NativeSlice<T>(chunkData).SliceConvert<float>();
    19.             var dest = new NativeSlice<float>(this.Data, instanceStart, copyCount);
    20.  
    21.             new CopyJob
    22.                 {
    23.                     Visibile = visible,
    24.                     Source = src,
    25.                     Dest = dest,
    26.                 }
    27.                 .Schedule().Complete();
    28.         }
    So it now takes a NativeArray<VisibleIndex>, and the copy job is a simple

    Code (CSharp):
    1.         [BurstCompile]
    2.         protected struct CopyJob : IJob // is this worth making parallel?
    3.         {
    4.             [ReadOnly]
    5.             public NativeArray<VisibleIndex> Visibile;
    6.  
    7.             public NativeSlice<TU> Source;
    8.             public NativeSlice<TU> Dest;
    9.  
    10.             /// <inheritdoc />
    11.             public void Execute()
    12.             {
    13.                 for (var index = 0; index < this.Dest.Length; index++)
    14.                 {
    15.                     this.Dest[index] = this.Source[this.Visibile[index].Value];
    16.                 }
    17.             }
    18.         }
    So it now only gets the properties from the correct entities. In comparison it used to do something like this

    Code (CSharp):
    1.             var dest = new NativeSlice<float>(this.Data, instanceStart, copyCount);
    2.             UnsafeUtility.MemCpy(dest.GetUnsafePtr(), src.GetUnsafeReadOnlyPtr(), src.Length * UnsafeUtility.SizeOf<T>());
    Just taking the X number of elements from the start. So if elements were culled in this section the wrong properties were taken.

    Put all my updated code here: https://github.com/tertle/com.bovinelabs.rendering
     
    Last edited: Feb 19, 2019
    Rotary-Heart and GilCat like this.
  41. Rotary-Heart

    Rotary-Heart

    Joined:
    Dec 18, 2012
    Posts:
    483
    Isn't that what is already happening since it uses the CopyTo for the matrix? Why does that one work correctly?
     
  42. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    17
    So, does anyone know, has unity implemented GPU Instanced variables with its render mesh system yet? :)
     
  43. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    1,587
    Not yet. But you can write your own or use compute buffers.
     
    deus0 likes this.
  44. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    2,013
    Any news on this one? I'm almost tempted to write my own rendering system.
     
    Antypodish likes this.
  45. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    457
    You should wait for another week. Unity team said they will be releasing a new version of the DOTS packages (i suppose the Rendering package will also be included) early this week.
     
    xVergilx likes this.
  46. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    457
  47. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    2,013
    No support in this one. FeelsBadMan
     
  48. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    457
    Yeah. Well i guess you will have to write your own after all. Like @eizenhorn said you should go with Compute Buffers rather than Material Property Blocks.
    There are some threads about it in the forum already, that can guide you + some Unity examples that will help you for sure.
     
    Last edited: Jul 31, 2019
    xVergilx likes this.
  49. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    2,013
    Problem with compute buffers is that they use shader model 4.5. Which means its a high-end solution.

    I think I'll just try to hack a solution for the current hybrid render for the moment.
    Like swapping shared materials on the RenderMesh or something similar.

    Render system already uses a shared material (and has a support for it).
    Its pretty silly not to include a blob reference field on the component to supply it to the system itself upon rendering.

    I guess it will be added in the future.

    (Although OP message was from sept of 2018, and its august of 2019 now, almost a year has passed! And I think at that point in time blobs were not even available.)
     
    GilCat likes this.
  50. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    457
    In that case, this prototype uses Material Property Blocks and might give you a head start.