---
title: "Rendering"
description: "The render bus, sprites, cameras, text, animations, and render features."
canonical_url: "https://docs.ctrl-neo.dev/en/neogamelib/rendering"
---
# Rendering

The render bus draws world-space sprites and text, optionally through one or
more cameras, in a single `SpriteBatch`. Screen-space UI is a separate system
(see [UI](https://docs.ctrl-neo.dev/7.ui)).

## The render bus

`NeoRenderBus` holds three self-registering lists: sprites, texts, and
cameras. Each item registers itself in `OnAwake` via the bus passed to its
constructor, and unregisters on `OnDestroy` — so loading a world brings its
renderers and cameras in, unloading takes them out.

### Draw order

Within one world pass:

1. **Sprites**, sorted by `Depth` descending (higher `Depth` = further back),
ties broken by registration order (earlier added = drawn earlier = behind).
2. **Texts**, same sorting, always drawn after sprites (text sits on top of
the world).

```csharp
// both at Depth 0: later-added draws on top
// to force stacking regardless of add order, use Depth:
backSprite.Depth = 1f;   // behind
frontSprite.Depth = 0f;  // in front
```

<callout color="primary" icon="i-lucide-info">

`Depth` here is a sort key, not a `SpriteBatch` `layerDepth`: **higher =
behind**. The default for everything is 0.

</callout>

### Camera passes

- Every **enabled** camera renders the world stacked by `NeoCamera.Order`
(lower first), each in its own `SpriteBatch` pass with its view matrix.
- `NeoGame.OnDraw` fires exactly once, inside the **main camera's** pass.
- With no enabled camera, the world pass falls back to an identity pass
(pre-camera behavior: (0,0) is the viewport top-left).
- `RenderBus.MainCamera` — the first enabled camera by Order (then
registration). Drives `WorldToScreen`/`ScreenToWorld` and `OnDraw`.
- `RenderBus.HasAnyCamera` — any enabled camera at all.
- `RenderBus.ViewMatrix` — main camera's matrix, or identity.

## NeoSpriteRenderer

Draws a `Texture2D` (or a frame of one) at the parent's transform.

```csharp
NeoObject obj = world.Instantiate("Crystal", new Transform(new Vector2(400, 300)));
NeoSpriteRenderer sprite = obj.AddComponent(new NeoSpriteRenderer(RenderBus, texture));
```

Members:

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Texture
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
    </td>
    
    <td>
      texture to draw; <code>
        null
      </code>
      
       = skipped
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        SourceRectangle
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
    </td>
    
    <td>
      sub-rectangle (animation frame); <code>
        null
      </code>
      
       = whole texture
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Color
      </code>
    </td>
    
    <td>
      <code>
        White
      </code>
    </td>
    
    <td>
      tint
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Depth
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      sort key, higher = behind
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Anchor
      </code>
    </td>
    
    <td>
      <code>
        (0.5, 0.5)
      </code>
    </td>
    
    <td>
      origin as fraction of frame size
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Order
      </code>
    </td>
    
    <td>
      auto
    </td>
    
    <td>
      registration order, the depth tiebreaker (set by the bus on add)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Layer
      </code>
    </td>
    
    <td>
      <code>
        1
      </code>
    </td>
    
    <td>
      bitmask layer for camera culling (below)
    </td>
  </tr>
</tbody>
</table>

### Bounds and screen checks

All are computed from the parent transform each access (rotation is ignored
for `Bounds`; `ScreenBounds` handles it).

- `Bounds` — world-space `Rectangle` of the current frame (absolute,
scale-aware, anchor-aware). This is what `NeoSpriteCollider` uses.
- `ScreenBounds` — the frame projected through the main camera.
- `WithinViewportBounds` — entirely inside the viewport?
- `VisibleInViewportBounds` — any part inside? Use this to skip drawing.
- `IsOverWindowBounds(Direction2D)` — is that edge past the viewport edge?
- `ContainsScreenPoint(Vector2 windowPixels)` — point inside the drawn frame
(handles camera, rotation, scale).
- `IsCursorOnSprite()` — `ContainsScreenPoint` at the current mouse position.

### Layers (camera culling)

`Layer` is a bitmask: a camera only draws the sprite/text when the camera's
`CullingMask` shares at least one bit with it (`(layer & mask) != 0`).

```csharp
sprite.Layer = 1 << 2;                     // layer #2
sprite.Layer = (1 << 3) | (1 << 7);        // layers #3 and #7

camera.CullingMask = 1;                    // only the default layer
camera.CullingMask = (1 << 3) | (1 << 7);  // only #3 and #7
camera.CullingMask = -1;                   // everything (default)
```

### Negative scales

Negative `Transform.Scale` mirrors instead of breaking. The bus converts the
sign into `SpriteEffects` flips and keeps an absolute scale, so:

```csharp
Vector2 scale = Transform.Scale;
scale.X = cursorX > Transform.Position.X ? MathF.Abs(scale.X) : -MathF.Abs(scale.X);
Transform.Scale = scale;   // face the cursor
```

Bounds stay absolute positive rectangles, so colliders keep working.

## NeoCamera

A world-space camera component. Attach to an object; the camera follows the
object's transform every frame.

```csharp
NeoObject camObj = world.Instantiate("cam", new Transform(new Vector2(0, 0)));
NeoCamera cam = camObj.AddComponent(new NeoCamera(RenderBus)); // self-registers on awake
cam.Zoom = 2f;
```

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Zoom
      </code>
    </td>
    
    <td>
      positive multiplier; invalid values (<= 0, NaN, Inf) are rejected silently
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Order
      </code>
    </td>
    
    <td>
      lower renders first
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CullingMask
      </code>
    </td>
    
    <td>
      <code>
        Layer
      </code>
      
       bitmask filter; default <code>
        -1
      </code>
      
       = everything
    </td>
  </tr>
</tbody>
</table>

The view matrix is `Translate(-pos) * Rotate(-rot) * Scale(zoom) * Translate(viewport/2)`:
the camera's position lands on the viewport center, rotation is inverted, zoom
scales the world.

Conversions (viewport-local versions; the bus adds the letterbox offset for
window-pixel versions):

```csharp
Vector2 onScreen = RenderBus.WorldToScreen(new Vector2(100, 50));  // window pixels
Vector2 inWorld  = RenderBus.ScreenToWorld(Input.MousePosition);   // world coords
```

`RenderBus.WorldToScreen`/`ScreenToWorld` go through the main camera (identity
when none) and account for letterbox bars (see
[Systems](https://docs.ctrl-neo.dev/8.systems#letterbox-viewport)).

## NeoTextRenderer

Text works like a sprite: transform, color, depth, anchor, layer.

```csharp
NeoObject label = world.Instantiate("Score");
NeoTextRenderer text = label.AddComponent(new NeoTextRenderer(RenderBus, font));
text.Text = "Score: 0";
text.Anchor = Vector2.Zero;                    // top-left instead of centered
label.Transform.Position = new Vector2(20, 20);
```

Members mirror `NeoSpriteRenderer`: `Font`, `Text`, `Color`, `Depth`,
`Anchor`, `Order`, `Layer`. Empty `Text` or null `Font` skips drawing. Texts
draw after all sprites.

## NeoAnimator + NeoAnimation

Animation = a list of sheet frames + a per-frame duration. The animator sets
`SourceRectangle` on the sprite renderer each tick, so colliders, anchors, and
render features follow the current frame automatically.

```csharp
NeoSpriteRenderer renderer = player.AddComponent(new NeoSpriteRenderer(RenderBus, sheet));
NeoAnimator animator = player.AddComponent<NeoAnimator>();   // requires the renderer

NeoAnimation idle = new(new()
{
    new Rectangle(0,  0, 32, 32),
    new Rectangle(32, 0, 32, 32),
}, 0.2f);                    // 0.2 s per frame, loops

animator.Play(idle);

// attack -> back to idle
animator.OnAnimationFinished = () => animator.Play(idle);
```

`NeoAnimation.FromGrid` walks a grid so you don't list frames by hand:

```csharp
// 6x4 cells of 32x32, left-to-right top-to-bottom
NeoAnimation run = NeoAnimation.FromGrid(new Point(6, 4), new Point(32, 32), 0.1f);

// mirrored walk, with gaps and margins:
NeoAnimation padded = NeoAnimation.FromGrid(
    new Point(6, 4), new Point(32, 32), 0.1f,
    horizontal: Direction2D.Left,
    padding: new Point(2), margin: new Point(4));

// column-first walk, one-shot that clears the sprite at the end:
NeoAnimation vanish = NeoAnimation.FromGrid(
    new Point(4, 6), new Point(32, 32), 0.08f,
    loop: false, holdLastFrame: false);
```

<table>
<thead>
  <tr>
    <th>
      <code>
        NeoAnimation
      </code>
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Frames
      </code>
    </td>
    
    <td>
      ordered <code>
        List<Rectangle>
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        FrameDuration
      </code>
    </td>
    
    <td>
      seconds per frame
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Loop
      </code>
    </td>
    
    <td>
      restart at frame 0 (default true)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        HoldLastFrame
      </code>
    </td>
    
    <td>
      non-looping: keep last frame (true) or clear the sprite (false)
    </td>
  </tr>
</tbody>
</table>

<table>
<thead>
  <tr>
    <th>
      <code>
        NeoAnimator
      </code>
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Play(NeoAnimation)
      </code>
    </td>
    
    <td>
      restart from frame 0; same instance re-Played is a no-op; null/empty throws
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Stop()
      </code>
    </td>
    
    <td>
      stop animating (last frame stays)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OnAnimationFinished
      </code>
    </td>
    
    <td>
      fires when a non-looping animation passes its last frame; callback runs after the end visual is applied, so <code>
        Play
      </code>
      
       inside it wins
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CurrentFrameIndex
      </code>
    </td>
    
    <td>
      current frame
    </td>
  </tr>
  
  <tr>
    <td>
      pausing
    </td>
    
    <td>
      <code>
        animator.Enabled = false
      </code>
      
       pauses; re-enable resumes
    </td>
  </tr>
</tbody>
</table>

## Render features

`NeoSpriteRenderFeature` draws extra layers around the sprite on the same
object — extra passes in the sprite's own render pass, with camera, flip, and
animation frame all applied.

```csharp
public class ShadowFeature : NeoSpriteRenderFeature
{
    public override void OnDrawBehind(SpriteBatch sb, in NeoSpriteDrawInfo info)
    {
        info.Draw(sb, someTexture, Color.Black, new Vector2(4, 4)); // offset drop shadow
    }
}

// attach the sprite renderer FIRST
obj.AddComponent(new NeoSpriteRenderer(RenderBus, texture));
obj.AddComponent(new ShadowFeature());
```

Draw order per sprite: all `OnDrawBehind` → sprite → all `OnDrawFront`
(features in add order). `OnFeatureAwake`/`OnFeatureDestroy` are the setup and
teardown hooks; `Renderer` gives you the sprite. A disabled feature doesn't
draw.

`NeoSpriteDrawInfo` carries the corrected draw values (`Position`, `Origin`,
`Scale`, `Effects`, `Color`, `Depth`, ...). `info.Draw(sb, texture, color, offset)`
draws a texture with the sprite's transform — the texture must share the
sprite's layout since the source rect is the same.

### NeoOutlineFeature (built-in)

Solid-color glow behind a sprite:

```csharp
NeoOutlineFeature outline = obj.AddComponent(new NeoOutlineFeature
{
    Color = Color.Gold,
    Thickness = 2f,            // world pixels
});
outline.Enabled = renderer.IsCursorOnSprite();  // e.g. hover glow
```

Draws a silhouette of the sprite 8 times, offset by `Thickness` in 8
directions. Animation-friendly (follows the current frame).

## Doing your own drawing

`OnDraw` runs inside the main camera's batch (or the identity batch without
cameras). For custom screen-space drawing use `OnDrawUI`. Both receive the
shared `SpriteBatch`, already begun — just draw.


## Sitemap

See the full [sitemap](https://docs.ctrl-neo.dev/sitemap.md) for all pages.
