Sprite rendering with ECS in Unity

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.

Simple Sprite Generator.
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. ๐Ÿ˜‰

Effect of Simple Sprite Generator.

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.

Sprites on multiple materials.
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. ๐Ÿ˜‰

Effect with sprites and multiple materials.

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?

Single material with atlas and list of sprites.
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? ๐Ÿ˜ƒ

Look very similar but we have to do less work ๐Ÿ˜‰

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! ๐Ÿค“

0 0 vote
Article Rating
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Patricio
Patricio
8 months ago

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?