Appearance
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
- An
AtomSystem<T>holds aList<T>of atoms (whereT : Atom). - Each frame,
AtomSystem.Update()iterates all enabled atoms, calls theirManagedUpdate()lifecycle hook, recomputes the TRS matrix, and groups them byCachedHash. - 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. Graphics.DrawMeshInstancedis used when available (up to 1023 instances per batch). Otherwise, the system falls back toGraphics.DrawMeshper atom viaAtomSingle.Render.
Render Modes
AtomDefaultRenderMode provides four built-in modes:
| Mode | Render Queue | Use Case |
|---|---|---|
Opaque | 2499 | Standard opaque sprites |
OpaqueEmissive | 2499 | Emissive opaque sprites |
Transparent | Default | Alpha-blended sprites |
DepthOnly | Default | Depth 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:
| Method | Called 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 providingSpriteHelper,PrimitiveHelper)
Key Design Decisions
- No MonoBehaviour dependency. Atoms are plain objects. The system can be driven from any update loop.
- Automatic instancing detection.
AtomSystemchecksSystemInfo.supportsInstancingandMaterial.enableInstancingat 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
_Colorvector array in theMaterialPropertyBlock, 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 quaternionBounds 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.