Skip to content

Rendering

Shmellyorc edited this page Aug 30, 2026 · 2 revisions

Rendering

Void's rendering system uses batched rendering to achieve high performance. Instead of drawing each sprite or primitive individually, the system collects all draw commands, sorts them efficiently, and sends them to the GPU in as few draw calls as possible.

SpriteBatcher

The SpriteBatcher handles rendering sprites, textures, and text. Create one instance and reuse it throughout your game's lifetime.

In your OnEnter method:

_batcher = new SpriteBatcher();

In your OnDraw method:

_batcher.Begin(SortMode.BackToFront);
_batcher.Draw(texture, new Vect2(100, 100), Color.White);
_batcher.Draw(texture, new Rect2(200, 200, 64, 64), Color.Red, 0.5f, 
    new Vect2(2, 2), new Vect2(32, 32), TextureEffects.None, 0.5f);
_batcher.End();

In your OnExit method:

_batcher?.Dispose();

Important: Never create a new batcher per draw call. Create one, reuse it, and dispose it when your game exits.

Draw vs DrawBypassAtlas

Use Draw for textures loaded through the AssetManager. These textures are automatically packed into the atlas for optimal performance.

Use DrawBypassAtlas for textures created at runtime, such as render targets, procedurally generated textures, or any texture not loaded through the AssetManager. Drawing these through the standard Draw method will fill up the atlas manager quickly and cause unnecessary evictions.

var renderTarget = RenderTarget.Get(256, 256);
// ... render to target ...
var texture = renderTarget.GetTexture();

_batcher.DrawBypassAtlas(texture, position, Color.White);

PrimitiveBatcher

The PrimitiveBatcher handles shapes like rectangles, circles, lines, and polygons.

var primitiveBatcher = new PrimitiveBatcher();

Draw shapes:

primitiveBatcher.Begin(SortMode.BackToFront);
primitiveBatcher.DrawRect(new Vect2(100, 100), new Vect2(200, 150), Color.Red);
primitiveBatcher.DrawCircleOutline(new Vect2(300, 300), 50, Color.Blue, 32);
primitiveBatcher.DrawLine(new Vect2(0, 0), new Vect2(100, 100), Color.Green);
primitiveBatcher.End();

Cameras

Cameras control what part of the game world is visible. They handle position, zoom, and bounds.

var camera = new Camera();
camera.Position = new Vect2(100, 100);
camera.Zoom = 2.0f;
camera.Bounds = new Rect2(0, 0, 1000, 1000);

Cameras are cheap to create and switch between. Creating a new Camera instance or switching cameras mid-frame has minimal performance impact. You can use multiple cameras for split-screen effects, UI rendering, or minimaps without worrying about overhead.

Pass the camera to Begin:

batcher.Begin(SortMode.BackToFront, BlendMode.Alpha, camera);

Convert between screen and world coordinates:

var worldPos = camera.ScreenToWorld(mouseScreenPos);
var screenPos = camera.WorldToScreen(entityPosition);

Texture Atlasing

Void automatically packs textures into atlas pages to reduce draw calls. This happens transparently when you draw sprites.

The atlas system:

  • Packs textures into pages (default: 2048x2048)
  • Uses Skyline or Guillotine packing algorithms
  • Defragments automatically when fragmentation exceeds threshold
  • Evicts least recently used textures when pages are full

You can configure the atlas:

GameSettings.Instance
    .SetAtlasPageSize(2048)
    .SetAtlasPageCount(4)
    .SetAtlasDefragThreshold(0.3f)
    .SetAtlasDefragMovesPerFrame(10);

Atlas defragmentation is spread across multiple frames to avoid frame rate hitches. Call ProcessPendingDefragMoves each frame:

AtlasManager.Instance.ProcessPendingDefragMoves(10);

If you prefer automatic handling, the AtlasManager processes defrag moves automatically when you call Begin on your SpriteBatcher. The number of moves processed per frame is controlled by the atlas defrag moves setting in GameSettings:

GameSettings.Instance.SetAtlasDefragMovesPerFrame(10);

The default value is 10 moves per frame, which provides a good balance between defragmentation speed and performance. Values between 5 and 20 are typical. Higher values complete defragmentation faster but may cause small frame hitches. Lower values spread the work across more frames.

GameSettings.Instance
    .SetAtlasDefragMovesPerFrame(5);   // Conservative, minimal frame impact
    .SetAtlasDefragMovesPerFrame(20);  // Faster defrag, slight frame impact
    .SetAtlasDefragMovesPerFrame(100); // Maximum allowed, use only if you know what you're doing

You can also call ProcessPendingDefragMoves manually at specific times, such as during loading screens or scene transitions, to complete defragmentation faster without affecting gameplay.

This gives devs both options and explains the trade-offs.

Shaders

Load shaders through the asset manager:

var shader = AssetManager.Instance.Load<Shader>("shaders/glow.shader");

Apply a shader to your batcher:

batcher.SetShader(shader);
batcher.Begin();
// All draws use the shader
batcher.End();

Set uniform values:

shader.SetUniform("uTime", 1.5f);
shader.SetUniform("uColor", Color.Red);
shader.SetUniform("uProjection", projectionMatrix);

Post-Processing

Apply post-processing effects using the PostProcessor class:

var bloomShader = AssetManager.Instance.Load<Shader>("shaders/bloom.shader");
var postProcessor = new PostProcessor(bloomShader, new Vect2(1920, 1080));

In your render loop:

postProcessor.Apply(sceneTarget, camera);
var result = postProcessor.GetResultTexture();

batcher.DrawBypassAtlas(result, Vect2.Zero, Color.White);

Blend Modes

Void provides several built-in blend modes:

batcher.Begin(blendMode: BlendMode.Alpha);      // Standard transparency
batcher.Begin(blendMode: BlendMode.Add);        // Additive blending (glow)
batcher.Begin(blendMode: BlendMode.Multiply);   // Multiplicative blending
batcher.Begin(blendMode: BlendMode.None);       // No blending (opaque)

Create custom blend modes:

var custom = BlendMode.Create(
    colorSrc: BlendFactor.SrcAlpha,
    colorDst: BlendFactor.One,
    colorEq: BlendEquation.Add,
    alphaSrc: BlendFactor.One,
    alphaDst: BlendFactor.One,
    alphaEq: BlendEquation.Add
);

batcher.Begin(blendMode: custom);

Back to Home

Clone this wiki locally