Recently, I’ve encountered a problem with rendering sprites with ECS in Unity. And yes, I know, ECS is still in preview but why wouldn’t share some of my experience with it? ?
ECS is a new way of programming in Unity, and I made a post with the introduction to it recently. You can check it here if you didn’t see it already. ?
Back to today’s subject! You might ask, what is the problem with sprites in ECS?
Well… There is nothing like SpriteRenderer which we have now in Unity. This reminds me of the old days when we didn’t have any 2D components… Yeah, it was long ago… ?
So how we did it back then?
We used quads! It was the easiest way to render a 2D object in a 3D environment.
As you know the secret, now let’s jump into code, because there are some other things there! ?
Rendering sprite with single
Let’s start with something easy. Let’s create an entity with sprite embedded in the material.
Our generator will look like that.
using UnityEngine; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; /// <summary> /// Example of spawning Sprites using quad mesh and material. /// </summary> public class SimpleSpriteGenerator : MonoBehaviour { // Amount of entities to spawn. [SerializeField] private int entitiesToSpawn = 10; // Reference to the sprite mesh - quad. [SerializeField] private Mesh spriteMesh; // Reference to the material with sprite texture. [SerializeField] private Material spriteMaterial; /// <summary> /// Unity method called on the first frame. /// </summary> void Start() { GenerateEntities(); } /// <summary> /// Generating example. /// </summary> public void GenerateEntities() { // Storing reference to entity manager. var entityManager = World.Active.EntityManager; // Creating temp array for entities. NativeArray<Entity> spriteEntities = new NativeArray<Entity>(entitiesToSpawn, Allocator.Temp); // Creating archetype for sprite. var spriteArchetype = entityManager.CreateArchetype( typeof(RenderMesh), typeof(LocalToWorld), typeof(Translation) ); // Creting entities. entityManager.CreateEntity(spriteArchetype, spriteEntities); // Creating Randomness. var rnd = new Unity.Mathematics.Random((uint)System.DateTime.UtcNow.Ticks); // Looping over entities for (int i = 0; i < entitiesToSpawn; i++) { var spriteEntity = spriteEntities[i]; // Assigning values to the renderer. entityManager.SetSharedComponentData(spriteEntity, new RenderMesh { mesh = spriteMesh, material = spriteMaterial }); // Assigning random position. entityManager.SetComponentData(spriteEntity, new Translation { Value = rnd.NextFloat3(new float3(-5, -3, 0), new float3(5, 3, 0)) }); } // Clearing native array for entities. spriteEntities.Dispose(); } }
Nothing scary. ?
Rendering sprites with few materials
Next step would be rendering multiple sprites on the scene, with few materials.
It won’t be that hard, as we have to define a list of materials to put on the entities.
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; /// <summary> /// Example of spawning Sprites using quad mesh and different materials. /// </summary> public class MultipleSpriteGenerator : MonoBehaviour { // Amount of entities to spawn. [SerializeField] private int entitiesToSpawn = 10; // Reference to the sprite mesh - quad. [SerializeField] private Mesh spriteMesh; // References to materials with sprite texture. [SerializeField] private List<Material> spriteMaterials; /// <summary> /// Unity method called on the first frame. /// </summary> void Start() { GenerateEntities(); } /// <summary> /// Generating example. /// </summary> public void GenerateEntities() { // Storing reference to entity manager. var entityManager = World.Active.EntityManager; // Creating temp array for entities. NativeArray<Entity> spriteEntities = new NativeArray<Entity>(entitiesToSpawn, Allocator.Temp); // Creating archetype for sprite. var spriteArchetype = entityManager.CreateArchetype( typeof(RenderMesh), typeof(LocalToWorld), typeof(Translation) ); // Creting entities. entityManager.CreateEntity(spriteArchetype, spriteEntities); // Creating Randomness. var rnd = new Unity.Mathematics.Random((uint)System.DateTime.UtcNow.Ticks); // Looping over entities for (int i = 0; i < entitiesToSpawn; i++) { var spriteEntity = spriteEntities[i]; // Assigning values to the renderer. entityManager.SetSharedComponentData(spriteEntity, new RenderMesh { mesh = spriteMesh, material = spriteMaterials[rnd.NextInt(spriteMaterials.Count)] }); // Assigning random position. entityManager.SetComponentData(spriteEntity, new Translation { Value = rnd.NextFloat3(new float3(-5, -3, 0), new float3(5, 3, 0)) }); } // Clearing native array for entities. spriteEntities.Dispose(); } }
Still simple. ?
Rendering sprites from atlas?
How about spicing things up? How about rendering a single sprite from an atlas using the same material?
Maybe SpriteRenderer is still not in ECS, but how easy it would be if you could assign a sprite to the entity?
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; /// <summary> /// Example of spawning Sprites using quad mesh, material and list of sprites. /// </summary> public class SingleMatSpriteGenerator : MonoBehaviour { // Amount of entities to spawn. [SerializeField] private int entitiesToSpawn = 10; // Reference to the sprite mesh - quad. [SerializeField] private Mesh spriteMesh; // Sprites from the atlas texture assigned in spriteMaterial. [SerializeField] private List<Sprite> sprites; // Reference to the material with sprite atlas texture. [SerializeField] private Material spriteMaterial; /// <summary> /// Unity method called on the first frame. /// </summary> void Start() { GenerateEntities(); } /// <summary> /// Generating example. /// </summary> public void GenerateEntities() { // Storing reference to entity manager. var entityManager = World.Active.EntityManager; // Creating temp array for entities. NativeArray<Entity> spriteEntities = new NativeArray<Entity>(entitiesToSpawn, Allocator.Temp); // Creating archetype for sprite. var spriteArchetype = entityManager.CreateArchetype( typeof(RenderMesh), typeof(LocalToWorld), typeof(Translation), typeof(NonUniformScale) ); // Creting entities. entityManager.CreateEntity(spriteArchetype, spriteEntities); // Creating dictionary with sprite materials. var spriteMaterials = new Dictionary<Sprite, Material>(); // Generating sprite materials. for (int i = 0; i < sprites.Count; i++) { var spriteMat = Material.Instantiate(spriteMaterial); // Assigning sprite texture offset. spriteMat.mainTextureOffset = SpriteECSHelper.GetTextureOffset(sprites[i]); // Assigning sprite texture size. spriteMat.mainTextureScale = SpriteECSHelper.GetTextureSize(sprites[i]); // Assigning material name - you can skip it. spriteMat.name = sprites[i].name; // Adding material to dictionary. spriteMaterials[sprites[i]] = spriteMat; } // Creating Randomness. var rnd = new Unity.Mathematics.Random((uint)System.DateTime.UtcNow.Ticks); // Looping over entities for (int i = 0; i < entitiesToSpawn; i++) { var spriteEntity = spriteEntities[i]; // Choosing random sprite to assign to entity. var sprite = sprites[rnd.NextInt(sprites.Count)]; // Assigning values to the renderer. entityManager.SetSharedComponentData(spriteEntity, new RenderMesh { mesh = spriteMesh, material = spriteMaterials[sprite] }); // Assigning random position. entityManager.SetComponentData(spriteEntity, new Translation { Value = rnd.NextFloat3(new float3(-5, -3, 0), new float3(5, 3, 0)) }); // Assigning sprite size with pixels per unit measure. entityManager.SetComponentData(spriteEntity, new NonUniformScale { Value = SpriteECSHelper.GetQuadScale(sprite) }); } // Clearing native array for entities. spriteEntities.Dispose(); // Clearing material dictionary. spriteMaterials.Clear(); } }
using UnityEngine; using Unity.Mathematics; /// <summary> /// Little helper class for getting data for Sprite rendering in ECS. /// </summary> public static class SpriteECSHelper { /// <summary> /// Returns Texture Offset for provided sprite. /// </summary> /// <returns>The texture offset.</returns> /// <param name="sprite">Sprite.</param> public static Vector2 GetTextureOffset(Sprite sprite) { Vector2 offset = Vector2.Scale(sprite.rect.position, new Vector2(1f / sprite.texture.width, 1f / sprite.texture.height)); return offset; } /// <summary> /// Returns Texture Size for provided sprite. /// </summary> /// <returns>The texture size.</returns> /// <param name="sprite">Sprite.</param> public static Vector2 GetTextureSize(Sprite sprite) { Vector2 size = Vector2.Scale(sprite.rect.size, new Vector2(1f / sprite.texture.width, 1f / sprite.texture.height)); return size; } /// <summary> /// Returns quad scale for provided sprite. Uses Pixels per Unit measure. /// </summary> /// <returns>The quad scale.</returns> /// <param name="sprite">Sprite.</param> public static float3 GetQuadScale(Sprite sprite) { return new float3(sprite.rect.width / sprite.pixelsPerUnit, sprite.rect.height / sprite.pixelsPerUnit, 1); } }
Yay! We created our custom ECS SpriteRenderer! How awesome it is? ?
ESC is still in preview, and it’s missing a lot of things, but existing elements already looks promising ?
What do you think about it? What other components do you miss in ECS? Let me know in the comment section below!
If you want to get notified on future content, sign up for the newsletter!
As always, the whole project is available at my public repository. ?
And I hope to see you back soon! ?
Hi Patrik:
Thanks for sharing your solution.
I’d like to apply it to my code, but I want to set different color to each Sprite.
Do you want it is possible with the same shared Material?
Sure! But you will have to create a different material.
If you apply changes to the material which is shared, the change will be visible on all the sprites.
This is why in my solution I’m creating a dictionary with new instances of the base material with different sprites.
This is much slower than sprite renderer, since each Mesh is making RenderMeshSystemV2 call and it slows things down really fast
I’m not up to date with newer releases, you might be right 🙂
Would it be possible to use sprite masks with this solution?
Hmm… If you make a different material, then it may work with masks 🙂
Thanks, after a bit of research I used ZMasking (using quads) and it works perfectly 🙂