Skip to content

kids.kapish.atoms

unity

GPU-instanced sprite rendering system for Unity.

Architecture

The package provides a lightweight, GameObject-free rendering pipeline for large numbers of sprites. Instead of creating individual SpriteRenderer components, you create plain C# Atom objects managed by an AtomSystem that issues batched draw calls every frame.

Core Flow

  1. An AtomSystem<T> holds a List<T> of atoms (where T : Atom).
  2. Each frame, AtomSystem.Update() iterates all enabled atoms, calls their ManagedUpdate() lifecycle hook, recomputes the TRS matrix, and groups them by CachedHash.
  3. The hash is derived from the atom's sprite and mesh via AtomicCache, so atoms sharing the same sprite are grouped into a single instanced draw call.
  4. Graphics.DrawMeshInstanced is used when available (up to 1023 instances per batch). Otherwise, the system falls back to Graphics.DrawMesh per atom via AtomSingle.Render.

Render Modes

AtomDefaultRenderMode provides four built-in modes:

ModeRender QueueUse Case
Opaque2499Standard opaque sprites
OpaqueEmissive2499Emissive opaque sprites
TransparentDefaultAlpha-blended sprites
DepthOnlyDefaultDepth pre-pass / shadow-only

All four default to Sprites/Default shader. Override with Atom.SetDefaultShader() before creating systems.

Hashing and Batching

AtomicCache is a static dictionary-backed cache that maps Sprite and Mesh references to stable hash codes. The composite hash (sprite * 397 ^ mesh) determines batch grouping -- atoms with the same sprite and mesh are drawn together in a single instanced call.

The cache is intentionally lazy: hashes are computed on first access and stored. The Atom.CachedHash property marks itself outdated whenever the sprite changes.

Lifecycle Hooks

Subclass Atom and override these virtual methods:

MethodCalled When
ManagedCreate()Atom is added to a system via Add()
ManagedDestroy()Atom is removed via Remove() or Clear()
ManagedReset()Atom is added (before ManagedCreate) or Reset() is called
ManagedUpdate()Each frame during AtomSystem.Update(), before matrix recomputation

Editor Support

In UNITY_EDITOR, the system also renders into SceneView.lastActiveSceneView.camera, so atoms are visible in the Scene view during play mode.

Assembly

  • Assembly name: Carrot.Atoms
  • Root namespace: Carrot.Atoms
  • References: Carrot (base package providing SpriteHelper, PrimitiveHelper)

Key Design Decisions

  • No MonoBehaviour dependency. Atoms are plain objects. The system can be driven from any update loop.
  • Automatic instancing detection. AtomSystem checks SystemInfo.supportsInstancing and Material.enableInstancing at construction time; no manual toggle needed.
  • 1023-instance batch limit. This is Unity's hard cap for DrawMeshInstanced. The system handles chunking automatically.
  • Per-instance color. Colors are passed via _Color vector array in the MaterialPropertyBlock, supporting per-instance tinting even in instanced mode.

Usage Guide

Installation

Add to your Unity project's Packages/manifest.json:

json
"kids.kapish.atoms": "0.1.0"

Requires kids.kapish 0.1.0 as a dependency.

Quick Start

Rendering a batch of sprites

csharp
using Carrot.Atoms;
using UnityEngine;

public class BulletManager : MonoBehaviour
{
    [SerializeField] private Sprite bulletSprite;

    private AtomSystem bulletSystem;

    private void Start()
    {
        // Create a system with the opaque render mode
        bulletSystem = new AtomSystem(AtomDefaultRenderMode.Opaque);
        bulletSystem.Camera = Camera.main;

        // Add some atoms
        for (int i = 0; i < 500; i++)
        {
            var atom = new Atom();
            atom.Sprite = bulletSprite;
            atom.Position = new Vector3(Random.Range(-10f, 10f), Random.Range(-10f, 10f), 0f);
            atom.Color = Color.yellow;
            bulletSystem.Add(atom);
        }
    }

    private void Update()
    {
        // This updates all atoms and issues the draw calls
        bulletSystem.Update();
    }
}

Rendering a single sprite (immediate mode)

For one-off rendering without managing a system:

csharp
using Carrot.Atoms;
using UnityEngine;

public class Cursor : MonoBehaviour
{
    [SerializeField] private Sprite cursorSprite;

    private void Update()
    {
        Vector3 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        pos.z = 0f;
        AtomSingle.Render(cursorSprite, pos, Color.green);
    }
}

Custom Atom Subclasses

Override lifecycle methods to add per-atom behaviour:

csharp
using Carrot.Atoms;
using UnityEngine;

public class Particle : Atom
{
    public Vector3 Velocity { get; set; }
    public float Lifetime { get; set; }
    private float age;

    public override void ManagedReset()
    {
        age = 0f;
        Enabled = true;
    }

    public override void ManagedUpdate()
    {
        age += Time.deltaTime;

        if (age >= Lifetime)
        {
            Enabled = false;
            return;
        }

        Position += Velocity * Time.deltaTime;
        Color = new Color(1f, 1f, 1f, 1f - (age / Lifetime));
    }
}

Then use it with a typed system:

csharp
var system = new AtomSystem<Particle>(AtomDefaultRenderMode.Transparent);
system.Camera = Camera.main;

var particle = new Particle();
particle.Sprite = mySprite;
particle.Velocity = Vector3.up * 2f;
particle.Lifetime = 3f;
system.Add(particle);

Object Pooling

AtomSystem has built-in support for reusing disabled atoms:

csharp
// Try to reclaim a disabled atom before allocating
if (!system.TryGetAtom(out Particle p))
{
    p = new Particle();
    p.Sprite = bulletSprite;
    system.Add(p);
}

p.Enabled = true;
p.Position = spawnPos;
p.Velocity = direction * speed;
p.Lifetime = 2f;

TryGetAtom returns the first atom where Enabled == false. GetAtom() does the same but returns null on failure.

Custom Shaders and Materials

Override the default shader for a render mode

Call before creating any AtomSystem:

csharp
Shader myShader = Shader.Find("Custom/MyInstancedSprite");
Atom.SetDefaultShader(AtomDefaultRenderMode.Opaque, myShader, renderQueue: 2499);

Use a specific material

csharp
Material mat = new Material(Shader.Find("Custom/MyShader"));
mat.enableInstancing = true;

var system = new AtomSystem(mat);

GPU instancing is auto-detected from Material.enableInstancing. If the material or hardware does not support instancing, the system falls back to individual draw calls transparently.

Layers

csharp
system.Layer = LayerMask.NameToLayer("Bullets");
// or
system.LayerName = "Bullets";

Transform Helpers

csharp
atom.ScaleOne = 2f;           // Uniform scale (2, 2, 2)
atom.ScaleX = 0.5f;           // X-axis only
atom.FlipX();                 // Toggle X flip
atom.FlipX(true);             // Force flipped
atom.Euler = new Vector3(0, 0, 45f);  // Rotation via euler angles
atom.Quaternion = someQuat;            // Direct quaternion

Bounds Queries

csharp
float left   = atom.GetXMin();
float right  = atom.GetXMax();
float bottom = atom.GetYMin();
float top    = atom.GetYMax();

These account for sprite bounds and current scale.

Debugging

csharp
system.ShowDebug();

Logs GPU instancing status, total/enabled atom counts, batch group count, and average atoms per group.

Carrot