Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Instanced Sprite Renderer Example

Discussion in 'Graphics for ECS' started by EBR, Apr 5, 2018.

  1. EBR

    EBR

    Joined:
    Jan 27, 2013
    Posts:
    117
    This project is a simple example of how Unity's new Entity Component System can be used to create a performant instanced sprite renderer.

    How it Works
    By adding SpriteInstanceRenderer to an entity it is rendered using its Position2D and Heading2D as a quad with a texture on it. The SpriteInstanceRender inherits ISharedComponentData meaning any entity using same instance of will be drawn in one draw call. This is possible because of Graphics.DrawMeshInstanced method. In the Example Scene included, 10,000 sprites are drawn. However the before mentioned method only draws a maximum of 1023 instances at once, so it splits up into as many groups necessary to draw all the instances.

    Quick Start
    1. Make sure you have this version of Unity 2018.1.0b12 installed
    2. Make sure the manifest.json file located at .../[PROJECT FOLDER]/Packages/manifest.json looks like this.
    3. Download this .unitypackage file and import it.
    4. Open Example Scene and press play.

    Improvements
    This is a very naive implementation that I threw together, however it does provide fairly good results even with 10,000 entities.

    GitHub Link:
    https://github.com/toinfiniityandbeyond/ecs-instanced-sprite-renderer
     
    Last edited: Apr 6, 2018
  2. Djayp

    Djayp

    Joined:
    Feb 16, 2015
    Posts:
    114
    If you don't need a Mesh, you can go with DrawTexture :
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Unity.Entities;
    3. using Unity.Mathematics;
    4. using Unity.Transforms2D;
    5. using UnityEngine;
    6. using UnityEngine.Experimental.PlayerLoop;
    7. namespace Playtest.Rendering
    8. {
    9.     [ExecuteInEditMode]
    10.     public class SpriteInstanceRendererSystem : ComponentSystem
    11.     {
    12.         List<SpriteInstanceRenderer> m_CacheduniqueRendererTypes = new List<SpriteInstanceRenderer>(10);
    13.         ComponentGroup m_InstanceRendererGroup;
    14.         protected override void OnCreateManager(int capacity)
    15.         {
    16.             m_InstanceRendererGroup = GetComponentGroup(ComponentType.Create<SpriteInstanceRenderer>(), ComponentType.Create<Position2D>());
    17.         }
    18.         protected override void OnUpdate()
    19.         {
    20.             Camera.onPostRender = null;
    21.             EntityManager.GetAllUniqueSharedComponentDatas(m_CacheduniqueRendererTypes);
    22.             Camera.onPostRender += (Camera camera) =>
    23.             {
    24.                 GL.PushMatrix();
    25.                 GL.LoadPixelMatrix(0, Screen.width, 0, Screen.height);
    26.             };
    27.             for (int i = 0; i != m_CacheduniqueRendererTypes.Count; i++)
    28.             {
    29.                 var renderer = m_CacheduniqueRendererTypes[i];
    30.                 m_InstanceRendererGroup.SetFilter(renderer);
    31.                 var positions = m_InstanceRendererGroup.GetComponentDataArray<Position2D>();
    32.                 for (int j = 0; j != positions.Length; j++)
    33.                 {
    34.                     float2 position = positions[j].Value;
    35.                     Camera.onPostRender += (Camera camera) =>
    36.                     {
    37.                         Graphics.DrawTexture(
    38.                         new Rect(position.x,
    39.                                 position.y + renderer.sprite.height,
    40.                                 renderer.sprite.width,
    41.                                 -renderer.sprite.height),
    42.                         renderer.sprite,
    43.                         renderer.material);
    44.                     };
    45.                 }
    46.             }
    47.             Camera.onPostRender += (Camera camera) =>
    48.             {
    49.                 GL.PopMatrix();
    50.             };
    51.             m_CacheduniqueRendererTypes.Clear();
    52.         }
    53.     }
    54. }
    Code (CSharp):
    1. using System;
    2. using Unity.Entities;
    3. using UnityEngine;
    4. namespace Playtest.Rendering
    5. {
    6.     [Serializable]
    7.     public struct SpriteInstanceRenderer : ISharedComponentData
    8.     {
    9.         public Texture2D sprite;
    10.         public Material material;
    11.     }
    12.     public class SpriteInstanceRendererComponent : SharedComponentDataWrapper<SpriteInstanceRenderer> { }
    13. }
     
    thomboy, TakuanDaikon and IsaiahKelly like this.
  3. IsaiahKelly

    IsaiahKelly

    Joined:
    Nov 11, 2012
    Posts:
    418
    Thank you both for sharing. These kinds of examples are always very helpful.

    @Djayp would it be very easy to use DrawTexture with 3D positions? Just wondering how you might calculate the scale based on distance from the camera and if it would be better to just use a mesh when dealing with 3D instead. I'd like to make a custom particle system using the new ECS & job system, since it should be faster and allow for more advanced features like collision detection between particles etc.
     
  4. Djayp

    Djayp

    Joined:
    Feb 16, 2015
    Posts:
    114
    @IsaiahKelly I think I would use Z-position as a factor for width and height.

    Using Unity.Transforms.Position (be careful it's Unity.Transforms2D.Position2D in my sample), it would be something like :

    Code (CSharp):
    1.  
    2. float scaleFactor = math.pow(position.z / myHorizon, math.sign(position.z))
    3. Camera.onPostRender += (Camera camera) =>
    4.                     {
    5.                         Graphics.DrawTexture(
    6.                         new Rect(position.x,
    7.                                 position.y + renderer.sprite.height,
    8.                                 renderer.sprite.width / scaleFactor,
    9.                                 -renderer.sprite.height / scaleFactor),
    10.                         renderer.sprite,
    11.                         renderer.material);
    12.                     };
    13.  
    I didn't test it btw, and you MUST adjust myHorizon to your needs, using position.z as a factor.

    Be aware my sample code is pretty trivial. I think we could use SetPixels to merge textures according to their z-order and materials to reduce draw calls.
     
    IsaiahKelly likes this.
  5. Djayp

    Djayp

    Joined:
    Feb 16, 2015
    Posts:
    114
    Also, you should look at this, they are so much better developpers than me :)

     
  6. IsaiahKelly

    IsaiahKelly

    Joined:
    Nov 11, 2012
    Posts:
    418
    @Djayp Thanks!

    I'm a little confused by Graphics.DrawTexture now. At first I thought it must work just like a GUITexture that is drawn directly to the screen, but it's actually rendered in world space, even though it uses screen coordinates!? So there's no billboarding effect in 3D space. You actually need to use GUI.DrawTexture for direct screen drawing, but that's really slow. So I don't exactly understand the point of this? If you could actually set the 3D position and rotation it would make a lot more sense to me...
     
  7. Rennan24

    Rennan24

    Joined:
    Jul 13, 2014
    Posts:
    38
    In order to do transformations on Graphics.DrawTexture you have to use the GL functions in Unity

    Code (CSharp):
    1.     private void Start()
    2.     {
    3.         Camera.onPostRender += PostRender;
    4.     }
    5.  
    6.     private void PostRender(Camera camera)
    7.     {
    8.         // Pushes the current matrix onto the stack so that can be restored later
    9.         GL.PushMatrix();
    10.  
    11.         // Loads a new Projection Matrix, you can also use other methods like LoadOrtho() or GL.LoadPixelMatrix()
    12.         GL.LoadProjectionMatrix(Matrix4x4.Perspective(90, camera.aspect, -10f, 10f));
    13.  
    14.         // You can also multiply the current matrix in order to do things like translation, rotation and scaling
    15.         // Here I'm rotating and scaling up the current Matrix
    16.         GL.MultMatrix(Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0, 0, 45), new Vector3(2, 2)));
    17.  
    18.         // Draws your texture onto the screen using the matrix you just loaded in
    19.         Graphics.DrawTexture(new Rect(0, 0, 1, 1), Texture);
    20.  
    21.         // Pops the matrix that was just loaded, restoring the old matrix
    22.         GL.PopMatrix();
    23.     }
    This post over at Unity Answers also helped me wrap my head around things a little bit

    Answer by Bunny83 · Dec 08, 2014 at 06:08 PM

    That aren't almost world space coordinates, that are world space coordinates ;) (0,0) is (0,0,0) and (1,1) is (1,1,0).

    OnPostRender is ment to manually draw arbitrary things. It isn't specifically ment to render in screen space. You have to setup a matrix manually. Something like this:

    Code (CSharp):
    1.  
    2. GL.PushMatrix();
    3. GL.LoadOrtho();
    4. Graphics.DrawTexture(new Rect(0, 0, 1, 1), myTexture);
    5. GL.PopMatrix();
    6.  
    Note: 0,0 is the bottom left corner and 1,1 the top right (usual viewport space). Alternativels you can setup a pixel matrix like this:

    Code (CSharp):
    1.  
    2. GL.PushMatrix();
    3. GL.LoadPixelMatrix(0, Screen.width, Screen.height,0);
    4. Graphics.DrawTexture(
    5.      new Rect(0, 0, Screen.width, Screen.height),
    6.      myTexture);
    7. GL.PopMatrix();
    8.  
    LoadPixelMatrix takes 4 parameters: right, left, bottom, top which let you specify any orthographic mapping you want

    GL.LoadPixelMatrix(0, 1, 0, 1); // would equal GL.LoadOrtho();
    GL.LoadPixelMatrix(0, 1, 1, 0); // same as above but y reversed so 0,0 is top left
    edit
    Just in case you want to render something in the local space of another object, you have to do this:

    Code (CSharp):
    1.  
    2. public Texture2D myTexture;
    3. public Transform someObject; // use this object's localspace
    4. private void OnPostRender()
    5. {
    6.      GL.PushMatrix();
    7.      GL.LoadProjectionMatrix(camera.projectionMatrix);
    8.      GL.modelview = camera.worldToCameraMatrix * someObject.localToWorldMatrix;
    9.      Graphics.DrawTexture(new Rect(0, 0, 10, 10), myTexture);
    10.      GL.PopMatrix();
    11. }
    12.  

    So we setup the same projection matrix the camera uses to draw the scene and as modelview matrix we set the usual MV matrix (model and view matrix combined in right to left order). Everything you render now will apprear in local space of "someObject".

    I have found many downsides using Graphics.DrawTexture though! For one you can't see it in the Scene view, Which in my use cases is very annoying. Another thing is that whenever I have used it, It is a lot less efficient than just doing Graphics.DrawMesh (It seems to be calling GUITexture.Draw in the profiler). Another thing is that it has to be done OnPostRender which is pretty limiting.

    I would suggest just using Graphics.DrawMesh, and Graphics.DrawMeshInstanced ;)
     
    5argon and IsaiahKelly like this.
  8. IsaiahKelly

    IsaiahKelly

    Joined:
    Nov 11, 2012
    Posts:
    418
    @Rennan24 Thanks for the info. This would seem to answer my question about using it for 3D then. No point in doing all that extra work to calculate scaling if it's not even faster. Seems to be optimized for screen space scaling anyway, so I'd kind of be fighting it's whole purpose. My goal is really just to find the fastest way to draw many billboarded sprites in 3D with as few batches as possible.
     
  9. Djayp

    Djayp

    Joined:
    Feb 16, 2015
    Posts:
    114
    Ouch... didn't know that...
     
  10. Rennan24

    Rennan24

    Joined:
    Jul 13, 2014
    Posts:
    38
    Currently there is no way to draw more than 1023 sprites per batch when using Graphics.DrawInstance, and you have to use Matrix4x4[] instead of NativeArray<float4x4> so you can't jobify it until Unity updates their API. If I were you it might be worth trying to use the Unity particle system to spawn particles in manually through code and manipulate them there, especially since they support billboarding, and they can be done in 3D.

    Yeah, I was hoping that DrawTexture() would be faster than DrawMesh(), sadly that's not the case :(
     
    Djayp and IsaiahKelly like this.
  11. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Hi,
    first thanks for the great asset! I used in in one of my projects.
    But I noticed that going with the regular MeshInstanceRenderer that has a material with the "Sprite Instanced" shader yields very similar results.

    Things I noticed: I can set the rotation with the MeshInstanceRenderer but not with your InstancedSpriteRenderer
    With you InstancedSpriteRenderer I have the posibility to adjust scale and pivot of the texture to be drawn. (although I think all pivots remain at (0.5, 0.5) for my game)

    I have a question: There are these requirements for my game and I would like to know if it is possible to achieve them with either your component system or with the MeshInstanceRenderer.
    If not, I have to use regular SpriteRenderers again.

    So in my game I want to:
    - set position of a sprite
    - set the rotation of a sprite
    - set the scale of a sprite, uniform - one value for both axes
    - set the color of my sprite.

    One way I tried to do it was to render with batches of 1 and change the mesh for the size and the material for the color each time.
    As you can imagine, performance was abysmal.

    So do you have a hint how to go about this?
    I might have to resort to regular gameObjects for rendering again.

    Kind regards
     
  12. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    The GitHub Repository up top already utilizes Position and Rotation with Position2D and Heading2D components, you can also easily add a float for the rotation angle and then rotate the TransformMatrix in the RenderSystem.

    I've quickly added scaling and coloring support by just making a few modifications. I'm also just using a built-in shader.
    If you want individual scaling and coloring you can't use SharedComponentData like in the example. You could also add Color and Scale components that you then utilize in the RenderSystem instead of it all being in the SpriteInstanceRenderer. But you won't be able to take advantage of some of the batch rendering.

    I'm mostly just playing around with it to learn the Unity ECS myself.
    The following code just shows the areas with relevant modifications:

    SpriteInstanceRendererComponent.cs:
    Code (CSharp):
    1.     [Serializable]
    2.     public struct SpriteInstanceRenderer : ISharedComponentData
    3.     {
    4.         public Texture2D sprite;
    5.         public int pixelsPerUnit;
    6.         public float2 pivot;
    7.         public Color color;
    8.         public float uniformScale;
    9.         public SpriteInstanceRenderer(Texture2D sprite, int pixelsPerUnit, float2 pivot, Color color, float uniformScale)
    10.         {
    11.             this.sprite = sprite;
    12.             this.pixelsPerUnit = pixelsPerUnit;
    13.             this.pivot = pivot;
    14.             this.color = color;
    15.             this.uniformScale = uniformScale;
    16.         }
    17.     }
    SpriteInstanceRenderSystem.cs:
    Code (CSharp):
    1.                 Mesh mesh;
    2.                 Material material;
    3.                 var size = math.max(renderer.sprite.width, renderer.sprite.height) / (float) renderer.pixelsPerUnit * renderer.uniformScale;
    4.                 float2 meshPivot = renderer.pivot * size;
    5.                 if (!meshCache.TryGetValue(renderer, out mesh))
    6.                 {
    7.                     mesh = MeshUtils.GenerateQuad(size, meshPivot);
    8.                     meshCache.Add(renderer, mesh);
    9.                 }
    10.  
    11.                 if (!materialCache.TryGetValue(renderer, out material))
    12.                 {
    13.                     material = new Material(Shader.Find("Legacy Shaders/Transparent/Diffuse"))
    14.                     {
    15.                         enableInstancing = true,
    16.                         mainTexture = renderer.sprite
    17.                         color = renderer.color;
    18.                     };
    19.                     materialCache.Add(renderer, material);
    20.                 }
    SpriteRendererSceneBootstrap.cs:
    Code (CSharp):
    1.             var renderers = new[]
    2.             {
    3.                 new SpriteInstanceRenderer(animalSprites[0], animalSprites[0].width, new float2(0.5f, 0.5f), Color.white, 1),
    4.                 new SpriteInstanceRenderer(animalSprites[1], animalSprites[1].width, new float2(0.5f, 0.5f), Color.cyan, 0.5f),
    5.                 new SpriteInstanceRenderer(animalSprites[2], animalSprites[2].width, new float2(0.5f, 0.5f), Color.red, 2),
    6.             };
     
    Last edited: Jun 5, 2018
    FM-Productions and Afonso-Lage like this.
  13. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Hey that's awesome, thanks for your solution!
    I already figured out a way to adjust scaling in my game. There is a thread I found about it here:
    https://forum.unity.com/threads/transformmatrixcomponent-and-scaling.524054/

    The solutions for the color will only work for the same InstanceRenderer and material I guess? I think there is no possibility to have an individual color for each element (as that would make it impossible to batch the draw calls).
     
  14. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    Yes, I'm redoing the whole thing with a separate UniformScaleComponent and a ColorComponent. At least on the 10000 sprites scale it gets extremely slow giving each sprite its own color. I'm still playing around with optimizations though.
     
    FM-Productions likes this.
  15. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Glad to hear that!
    I tried it with rendering the meshes one by one, but performance was abysmal, even better to use 1000 gameObjects with sprite renderers in this case, at least from what I've experienced.
    I think I simply accept that I can only change the color for the whole Renderer, but that's not that big of a deal, but it would have opened certain possibilities like using color change to visualizing a damage effect for example.
     
  16. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    FM-Productions likes this.
  17. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Oh, that's just what I needed, good find!

    I didn't know much about Material Property Blocks, but the API reference says this:

    MaterialPropertyBlock is used by Graphics.DrawMesh and Renderer.SetPropertyBlock. Use it in situations where you want to draw multiple objects with the same material, but slightly different properties. For example, if you want to slightly change the color of each mesh drawn. Changing the render state is not supported.


    Sound exactly like the feature I was looking for.
     
  18. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    So, in the current design, in order to have material property blocks utilized is to partially rework the InstancedMeshRenderer, and pass the prop block to the draw call. The caveat: is that basically breaks MOST of the batching that is done, so while it is neat for a few objects an environment with 100k+ cubes running at 120+fps dropped down to about 20 fps pretty quickly. They are still working on the graphics portion at the moment so it almost seems too hacky (at least to me) to try to design work arounds.
     
  19. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Why using prop block break the batch? Is it a bug? Because I thought the point of it is to have variations in the same material without breaking the draw call. (Not using it then break the batch since every little color adjustment on the material would make a new material)
     
  20. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    In the case of the ECS, the MeshInstancedRenderSystem batches the transform matrices and combines them into an array. You then have the OPTION to pass in a material property block but for that array set. So if you consider again 100k cubes all having different transforms, this call handles it no problem, but if you then say have 100k cubes with different property blocks, you would then need to call this method PER cube rather than per batch of cubes

    Code (CSharp):
    1. while (beginIndex < transforms.Length) {
    2.     int length = math.min(m_MatricesArray.Length, transforms.Length - beginIndex);
    3.     CopyMatrices(transforms, beginIndex, length, m_MatricesArray);
    4.     Graphics.DrawMeshInstanced(renderer.mesh, renderer.subMesh, renderer.material, m_MatricesArray, length, null, renderer.castShadows, renderer.receiveShadows);
    5.     beginIndex += length;
    6. }
    "null" currently is the prop block parameter.

    Again, you CAN hack a solution in at the moment to get prototyping working; however, at a major loss of performance. One consideration is to then group prop blocks into batches; however, then consider if 100k cubes all had varying colors by just one of the 4 vector components. Then it breaks down again.
     
    5argon and FM-Productions like this.
  21. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    I did get it to work last night.

    10000 Sprites, individual random scale and color, 31 Batches. Can't really speak for the Performance in general because I've got a monster machine. I'll upload a public repository when I get home this evening since it's easier than just copy and pasting part of the code.
     
    FM-Productions likes this.
  22. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Awesome! Can't wait.
    I have a notebook from late 2012 that had high end hardware at the time (2,7ghz i7, gtx 680m). It can still run everything and I actually prefer having a little bit of a lower end hardware for testing and optimization purposes.
    Curious about your implementation!
     
  23. chanfort

    chanfort

    Joined:
    Dec 15, 2013
    Posts:
    641
    Just for curiosity, did you tried using Graphics.DrawMeshInstancedIndirect with positions and rotations passed through the shader? I tried the documentation example and it is working nicely with cubes. It also seems to get rid of "1023" limit and no grouping needed.

    I also noticed that DrawMeshInstancedIndirect is quite popular in Nordeus demo, so could be worthwhile exploring it a bit further :) .
     
    Rennan24 likes this.
  24. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Did you mean me? I didn't try it, but I could imagine that it will still get batched to a 1023 or 500 limit internally, but I don't really know much about draw operations in Unity. It is all working nicely, but I'm looking for a way to apply colors to each object individually and I don't think Graphics.DrawMeshInstancedIndirect will help me with that.

    for all my other needs, forum members were already helpful enough to provided a solution or point me into the right direction.
     
    Last edited: Jun 6, 2018
    chanfort likes this.
  25. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    FM-Productions likes this.
  26. Rennan24

    Rennan24

    Joined:
    Jul 13, 2014
    Posts:
    38
    Graphics.DrawMeshInstancedIndirect is exactly what you need if you make some tweaks with the shader.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class DrawMeshInstancedIndirect : MonoBehaviour
    4. {
    5.    public int instanceCount = 100000;
    6.    public Mesh instanceMesh;
    7.    public Material instanceMaterial;
    8.    public int subMeshIndex = 0;
    9.  
    10.    private int cachedInstanceCount = -1;
    11.    private int cachedSubMeshIndex = -1;
    12.    private ComputeBuffer positionBuffer;
    13.    private ComputeBuffer colorBuffer;
    14.    private ComputeBuffer argsBuffer;
    15.    private uint[] args = new uint[5] {0, 0, 0, 0, 0};
    16.  
    17.    private void Start()
    18.    {
    19.       argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
    20.       UpdateBuffers();
    21.    }
    22.  
    23.    private void Update()
    24.    {
    25.       // Update starting position buffer
    26.       if (cachedInstanceCount != instanceCount || cachedSubMeshIndex != subMeshIndex)
    27.          UpdateBuffers();
    28.  
    29.       // Pad input
    30.       if (Input.GetAxisRaw("Horizontal") != 0.0f)
    31.          instanceCount = (int) Mathf.Clamp(instanceCount + Input.GetAxis("Horizontal") * 40000, 1.0f, 5000000.0f);
    32.  
    33.       // Render
    34.       Graphics.DrawMeshInstancedIndirect(instanceMesh, subMeshIndex, instanceMaterial, new Bounds(Vector3.zero, new Vector3(100.0f, 100.0f, 100.0f)), argsBuffer);
    35.    }
    36.  
    37.    private void OnGUI()
    38.    {
    39.       GUI.Label(new Rect(265, 25, 200, 30), "Instance Count: " + instanceCount.ToString());
    40.       instanceCount = (int) GUI.HorizontalSlider(new Rect(25, 20, 200, 30), (float) instanceCount, 1.0f, 5000000.0f);
    41.    }
    42.  
    43.    private void UpdateBuffers()
    44.    {
    45.       // Ensure submesh index is in range
    46.       if (instanceMesh != null)
    47.          subMeshIndex = Mathf.Clamp(subMeshIndex, 0, instanceMesh.subMeshCount - 1);
    48.  
    49.       // Positions
    50.       positionBuffer?.Release();
    51.       positionBuffer = new ComputeBuffer(instanceCount, 16);
    52.  
    53.       // Colors
    54.       colorBuffer?.Release();
    55.       colorBuffer = new ComputeBuffer(instanceCount, 16);
    56.  
    57.       Vector4[] colors = new Vector4[instanceCount];
    58.       Vector4[] positions = new Vector4[instanceCount];
    59.       for (int i = 0; i < instanceCount; i++)
    60.       {
    61.          float angle = Random.Range(0.0f, Mathf.PI * 2.0f);
    62.          float distance = Random.Range(20.0f, 100.0f);
    63.          float height = Random.Range(-2.0f, 2.0f);
    64.          float size = Random.Range(0.05f, 0.25f);
    65.          positions[i] = new Vector4(Mathf.Sin(angle) * distance, height, Mathf.Cos(angle) * distance, size);
    66.          // Sets a random color in the Color buffer to be used in the shader
    67.          colors[i] = new Vector4(Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f));
    68.       }
    69.      
    70.       // Sets the ComputeBuffers data and set's them in the shader
    71.       colorBuffer.SetData(colors);
    72.       positionBuffer.SetData(positions);
    73.       instanceMaterial.SetBuffer("colorBuffer", colorBuffer);
    74.       instanceMaterial.SetBuffer("positionBuffer", positionBuffer);
    75.  
    76.       // Indirect args
    77.       if (instanceMesh != null)
    78.       {
    79.          args[0] = (uint) instanceMesh.GetIndexCount(subMeshIndex);
    80.          args[1] = (uint) instanceCount;
    81.          args[2] = (uint) instanceMesh.GetIndexStart(subMeshIndex);
    82.          args[3] = (uint) instanceMesh.GetBaseVertex(subMeshIndex);
    83.       }
    84.       else
    85.       {
    86.          args[0] = args[1] = args[2] = args[3] = 0;
    87.       }
    88.  
    89.       argsBuffer.SetData(args);
    90.  
    91.       cachedInstanceCount = instanceCount;
    92.       cachedSubMeshIndex = subMeshIndex;
    93.    }
    94.  
    95.    // Make sure to release the buffers, like a good programmer!
    96.    private void OnDisable()
    97.    {
    98.       positionBuffer?.Release();
    99.       positionBuffer = null;
    100.  
    101.       colorBuffer?.Release();
    102.       colorBuffer = null;
    103.  
    104.       argsBuffer?.Release();
    105.       argsBuffer = null;
    106.    }
    107. }
    108.  
    Code (CSharp):
    1. Shader "Instanced/InstancedShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Blend SrcAlpha OneMinusSrcAlpha
    10.         Tags {"LightMode"="ForwardBase"}
    11.         Pass
    12.         {
    13.             CGPROGRAM
    14.  
    15.             #pragma vertex vert
    16.             #pragma fragment frag
    17.             #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
    18.             #pragma target 4.5
    19.  
    20.             #include "UnityCG.cginc"
    21.             #include "UnityLightingCommon.cginc"
    22.             #include "AutoLight.cginc"
    23.  
    24.             sampler2D _MainTex;
    25.  
    26.             // Here we take the command buffers into the graphics card as structured buffers
    27. #if SHADER_TARGET >= 45
    28.             StructuredBuffer<float4> positionBuffer;
    29.             StructuredBuffer<float4> colorBuffer;
    30. #endif
    31.  
    32.             struct v2f
    33.             {
    34.                 float4 pos : SV_POSITION;
    35.                 float2 uv : TEXCOORD0;
    36.                 float3 ambient : TEXCOORD1;
    37.                 float3 diffuse : TEXCOORD2;
    38.                 float4 color : TEXCOORD3;
    39.                 SHADOW_COORDS(4)
    40.             };
    41.  
    42.             void rotate2D(inout float2 v, float r)
    43.             {
    44.                 float s, c;
    45.                 sincos(r, s, c);
    46.                 v = float2(v.x * c - v.y * s, v.x * s + v.y * c);
    47.             }
    48.  
    49.             v2f vert (appdata_full v, uint instanceID : SV_InstanceID)
    50.             {
    51.                 // Make sure that our shader supports structured buffers in the first place and get the one based on the instance ID
    52. #if SHADER_TARGET >= 45
    53.                 float4 data = positionBuffer[instanceID];
    54.                 float4 color = colorBuffer[instanceID];
    55. #else
    56.                 float4 data = 0;
    57.                 float4 color = 1;
    58. #endif
    59.  
    60.                 float rotation = data.w * data.w * _Time.y;
    61.                 rotate2D(data.xz, rotation);
    62.  
    63.                 float3 localPosition = v.vertex.xyz * data.w;
    64.                 float3 worldPosition = data.xyz + localPosition;
    65.                 float3 worldNormal = v.normal;
    66.  
    67.                 float3 ndotl = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz));
    68.                 float3 ambient = ShadeSH9(float4(worldNormal, 1.0f));
    69.                 float3 diffuse = (ndotl * _LightColor0.rgb);
    70.  
    71.                 v2f o;
    72.                 o.pos = mul(UNITY_MATRIX_VP, float4(worldPosition, 1.0f));
    73.                 o.uv = v.texcoord;
    74.                 o.ambient = ambient;
    75.                 o.diffuse = diffuse;
    76.                 o.color = color;
    77.                 TRANSFER_SHADOW(o)
    78.                 return o;
    79.             }
    80.  
    81.             fixed4 frag (v2f i) : SV_Target
    82.             {
    83.                 fixed shadow = SHADOW_ATTENUATION(i);
    84.                 fixed4 albedo = tex2D(_MainTex, i.uv);
    85.                 float3 lighting = i.diffuse * shadow + i.ambient;
    86.                 fixed4 output = fixed4(albedo.rgb * i.color * lighting, albedo.w);
    87.                 return output;
    88.             }
    89.  
    90.             ENDCG
    91.         }
    92.     }
    93. }
    94.  

    This allows you to pretty much have them all be different colors, positions, sizes and whatever your heart desires. As long as your graphic cards support compute buffers!
     
    FM-Productions and chanfort like this.
  27. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    Thanks Necromantic for the update, you're great! Will test it later today.
    And Rennan24, I couldn't figure that out by reading the API page for the function, I have not really worked with shaders yet, so that comment was really helpful!
     
  28. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    You could also use MaterialPropertyBlock like I am doing.
     
  29. Rennan24

    Rennan24

    Joined:
    Jul 13, 2014
    Posts:
    38
    The problem with doing the MaterialPropertyBlock way is that you limit yourself to that block when you call Graphics.DrawMeshInstanced, meaning if I wanted 1,000,000 cubes with unique colors I would need 1,000,000 MaterialPropertyBlocks which would take at worst 1,000,000 draw calls, with Graphics.DrawMeshInstancedIndirect and CommandBuffer's not only can you assign unique colors to each index, but it also gets past Graphics.DrawMeshInstanced 1023 batch size, In my testing I was able to draw 1,500,000 cubes with unique colors, positions and sizes at a consistent 60fps that took only 3 draw calls using a GTX1080
     
  30. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    No, you wouldn't. You'd only really need one as I have demonstrated in my example code repository.
    I guess the 1023 batch limit can be a problem but you can even use MaterialPropertyBlock with DrawMeshInstancedIndirect if you really wanted to. Which is actually weird because they say the reason DrawMeshInstanced is limited to 1023 instances is that MaterialPropertyBlock Arrays can only go up to that in length.
    In general I guess DrawMeshInstancedIndirect is probably better because of the limit just the ComputeBuffer requirement is just of a bummer, even though I guess nowadays it really shouldn't matter.
     
    Last edited: Jun 6, 2018
  31. Rennan24

    Rennan24

    Joined:
    Jul 13, 2014
    Posts:
    38
    Ohhhh I see, you taught me something new! :)
    I did not realize that you could pass a VectorArray of colors into a MaterialPropertyBlock and have them be indexed individually when calling Graphics.DrawMeshInstanced, I figured you'd have to keep on changing the property block's color and drawing a whole new batch. So if you want to support graphics cards without compute shaders, I assume your MaterialPropertyBlock method is fine at the cost of a few extra drawcalls but it still ends up being not that much since each batch takes 1023 entities, unless you want more than 1,000,000 which is crazy in it's own right!
     
  32. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    I'm mostly thinking about mobile phone support which can be a bit problematic when it comes to shaders.
     
  33. FM-Productions

    FM-Productions

    Joined:
    May 1, 2017
    Posts:
    72
    You guys are awesome! I finally managed to have my rendering system the way I want to with a combined solution from Necromantic's modified Instanced Sprite Renderer and the rendering solution posted here:
    https://forum.unity.com/threads/transformmatrixcomponent-and-scaling.524054/

    I guess you can set any additional properties for the shader in the MaterialPropertyBlock too? So I could adjust additional properties as long as the shader supports them? That would be neat.

    You can see an example of the system here:
     
    Rennan24 likes this.
  34. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    Yes, but you have to adjust the shader so the variables are per instance id, like I did with _Color in mine.
     
    FM-Productions likes this.
  35. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    I've rebuilt the sprite rendering to make use of the actual Sprite structure. This can improve performance further because you can potentially save on texture allocations.

    Edit:
    I've also played around with DrawMeshInstancedIndirect. Funnily enough I get the same average FPS, no matter the amount of Entities, despite the batching. In some cases slightly lower and in some slightly higher.
     
    Last edited: Jun 9, 2018
  36. BadFoolPrototype

    BadFoolPrototype

    Joined:
    Sep 7, 2013
    Posts:
    14
    Hey Necromantic, thanks for sharing the code!
    I was curious on how to ECS and a custom renderers would work.

    Is your system also culled by Unity own culling mechanism ?

    Thank you!
     
  37. gilley033

    gilley033

    Joined:
    Jul 10, 2012
    Posts:
    1,181
    I suck at shaders. How can I modify ECSSprite.shader to make use of lighting? Is it possible to modify SpriteInstanceRendererSystem to use lighting culling?