Rendering
Brine2D's rendering system gives you everything you need for 2D games - from basic sprites to advanced particle systems and post-processing effects.
Quick Start
public class RenderingScene : Scene
{
private ITexture? _playerTexture;
private readonly IAssetLoader _assets;
public RenderingScene(IAssetLoader assets) => _assets = assets;
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_playerTexture = await _assets.GetOrLoadTextureAsync("assets/images/player.png", cancellationToken: ct);
}
protected override void OnRender(GameTime gameTime)
{
// Renderer available automatically!
Renderer.ClearColor = Color.Black;
// Draw texture
if (_playerTexture != null)
{
Renderer.DrawTexture(_playerTexture, 100, 100, 64, 64);
}
// Draw shapes
Renderer.DrawRectangleFilled(200, 200, 50, 50, Color.Red);
Renderer.DrawCircleFilled(300, 300, 25, Color.Blue);
// Draw text
Renderer.DrawText("Hello, Brine2D!", 10, 10, Color.White);
}
}
Topics
Getting Started
| Guide | Description |
|---|---|
| Rendering Architecture | Interfaces, GPU drivers, headless mode |
| GPU Renderer | Render targets, scissor rects, blend modes, text |
| Sprites & Textures | Load and draw images |
| Drawing Primitives | Shapes, lines, and basic graphics |
Intermediate
| Guide | Description |
|---|---|
| Cameras | Camera movement, zoom, and rotation |
| Texture Atlasing | Optimize draw calls with sprite packing |
Advanced
| Guide | Description |
|---|---|
| Particles | Particle systems for visual effects |
| Post-Processing | Off-screen rendering and effects |
Key Concepts
Renderer Architecture
Brine2D has a single GPU-accelerated renderer built on the SDL3 GPU API:
graph TB
IRenderer["IRenderer / IDrawContext<br/>(Interfaces)"]
GPU["SDL3Renderer<br/>(Vulkan/Metal/D3D12)"]
Headless["HeadlessRenderer<br/>(Servers & Testing)"]
IRenderer --> GPU
IRenderer --> Headless
GPU --> Vulkan["Vulkan"]
GPU --> Metal["Metal"]
GPU --> D3D12["Direct3D 12"]
style IRenderer fill:#2d5016,stroke:#4ec9b0,stroke-width:3px,color:#fff
style GPU fill:#1e3a5f,stroke:#569cd6,stroke-width:2px,color:#fff
style Headless fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,color:#fff
Learn more in Rendering Architecture
Render Loop
Understanding the render loop:
sequenceDiagram
participant GL as Game Loop
participant W as World (Systems)
participant S as Scene
participant R as Renderer
loop Every Frame (~60 FPS)
GL->>S: OnUpdate(gameTime)
S->>S: Update game logic
GL->>R: BeginFrame()
GL->>W: World.Render(renderer, gameTime)
W->>W: Render systems (e.g. SpriteRenderingSystem)
GL->>S: OnRender(gameTime)
S->>R: DrawTexture(...)
S->>R: DrawRectangle(...)
S->>R: DrawText(...)
GL->>R: ApplyPostProcessing()
GL->>R: EndFrame()
R->>R: Present to screen
end
Pattern: Update game state, then render. The framework manages BeginFrame / EndFrame automatically. World.Render() always runs before Scene.OnRender() — every render system (sprites, particles, debug overlay) draws before your scene's own OnRender code, so anything you draw in OnRender layers on top of system output by default. This matters if you're doing a mid-frame render-target trick (e.g. a minimap): the target's push/pop state is reset at the start of BeginFrame, so any target you push must be popped within the same frame — see Render Targets for the full pattern.
Framework Property
The Renderer property is automatically set by the framework:
public class GameScene : Scene
{
// ✅ No injection needed!
// Renderer is a framework property
protected override void OnRender(GameTime gameTime)
{
// Renderer available automatically
Renderer.ClearColor = Color.Black;
Renderer.DrawText("Hello!", 10, 10, Color.White);
}
}
Pattern: Matches ASP.NET's ControllerBase.Request - framework-provided properties you don't inject.
Common Tasks
Draw a Sprite
private ITexture? _sprite;
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_sprite = await _assets.GetOrLoadTextureAsync("assets/sprite.png", cancellationToken: ct);
}
protected override void OnRender(GameTime gameTime)
{
if (_sprite != null)
{
Renderer.DrawTexture(_sprite, x: 100, y: 100, width: 64, height: 64);
}
}
Full guide: Sprites & Textures
Draw Shapes
protected override void OnRender(GameTime gameTime)
{
// Filled rectangle
Renderer.DrawRectangleFilled(100, 100, 50, 50, Color.Red);
// Circle outline
Renderer.DrawCircleOutline(200, 200, 25, Color.Blue);
// Line
Renderer.DrawLine(300, 300, 400, 400, Color.Green, thickness: 2);
}
Full guide: Drawing Primitives
Camera Movement
private Camera2D _camera = new();
protected override void OnUpdate(GameTime gameTime)
{
// Follow player
_camera.Position = _playerPosition;
_camera.Zoom = 2.0f;
// Apply camera transform
Renderer.Camera = _camera;
}
protected override void OnRender(GameTime gameTime)
{
// All drawing now relative to camera
Renderer.DrawTexture(_worldTexture, 0, 0);
}
Particle Effects
var entity = World.CreateEntity("ParticleEmitter");
entity.AddComponent<ParticleEmitterComponent>();
var emitter = entity.GetComponent<ParticleEmitterComponent>();
emitter.EmissionRate = 100f;
emitter.ParticleLifetime = 2f;
emitter.StartColor = new Color(255, 200, 0, 255);
emitter.EndColor = new Color(255, 50, 0, 0);
emitter.Shape = EmitterShape.Cone;
emitter.BlendMode = BlendMode.Additive;
Texture Atlasing
// Load individual textures
var textures = new List<ITexture>();
for (int i = 0; i < 10; i++)
{
textures.Add(await _assets.GetOrLoadTextureAsync($"assets/sprite{i}.png", cancellationToken: ct));
}
// Build atlas at runtime
var atlas = await AtlasBuilder.BuildAtlasAsync(
Renderer,
textures,
padding: 2,
maxSize: 2048
);
// Draw using atlas (1 draw call instead of 10!)
var region = atlas.GetRegion(textures[0]);
Renderer.DrawTexture(atlas.AtlasTexture, region.SourceRect, destRect);
Performance Tips
Reduce Draw Calls
Problem: Each texture switch is expensive.
Solution: Use texture atlasing to batch sprites.
// ❌ Bad - each sprite may use a different texture, causing many draw calls
for (int i = 0; i < 100; i++)
{
Renderer.DrawTexture(_sprites[i], x[i], y[i]);
}
// ✅ Good - all sprites in one atlas, batched automatically
var atlas = await AtlasBuilder.BuildAtlasAsync(Renderer, sprites);
Renderer.DrawTexture(atlas.AtlasTexture, ...); // Batched!
Minimize Texture Loads
Problem: Loading textures is slow.
Solution: Load once in OnLoadAsync, cache references, or use AssetManifest for parallel loading.
// ✅ Good - load once
private ITexture? _sprite;
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_sprite = await _assets.GetOrLoadTextureAsync("assets/sprite.png", cancellationToken: ct);
}
protected override void OnRender(GameTime gameTime)
{
Renderer.DrawTexture(_sprite, ...); // Use cached texture
}
// ❌ Bad - loads every frame!
protected override void OnRender(GameTime gameTime)
{
var tex = await _assets.GetOrLoadTextureAsync("assets/sprite.png"); // NO!
Renderer.DrawTexture(sprite, ...);
}
Monitor Performance
Use built-in performance monitoring:
**Press F3** to toggle overlay showing:
- FPS
- Frame time
- Draw calls
- Memory usage
[:octicons-arrow-right-24: Learn more: Performance Monitoring](../performance/monitoring.md)
---
## Best Practices
### ✅ DO
1. **Load textures in `OnLoadAsync`** - keep `OnRender` fast
2. **Use texture atlasing** - batch sprites that render together
3. **Set `ClearColor` once** - don't change every frame
4. **Cache texture references** - don't reload textures
5. **Use `AssetManifest`** - parallel loading with ref counting
```csharp
// ✅ Good pattern
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
await _assets.PreloadAsync(_manifest, cancellationToken: ct);
}
protected override void OnRender(GameTime gameTime)
{
Renderer.ClearColor = Color.Black;
Renderer.DrawTexture(_manifest.Background, 0, 0);
}
❌ DON'T
- Don't inject
IRenderer- use theRendererframework property - Don't load assets in
OnRender- causes lag - Don't change
ClearColorevery frame - unnecessary - Don't draw thousands of individual sprites - use atlasing
// ❌ Bad pattern
protected override void OnRender(GameTime gameTime)
{
// Don't load in render!
var sprite = await _assets.GetOrLoadTextureAsync("sprite.png"); // NO!
// Don't draw many individual sprites without atlasing
for (int i = 0; i < 1000; i++)
{
Renderer.DrawTexture(_sprites[i], ...); // Use atlas instead!
}
}
Troubleshooting
"Texture is null" Error
Symptom: NullReferenceException when drawing.
Solution: Ensure the texture loaded successfully in OnLoadAsync.
protected override void OnRender(GameTime gameTime)
{
if (_sprite != null) // Always check!
{
Renderer.DrawTexture(_sprite, 100, 100);
}
}
Nothing Renders (Black Screen)
Checklist:
- ✅ Is
OnRender()being called? (Add logger) - ✅ Is texture loaded? (Check
_sprite != null) - ✅ Are coordinates on screen? (Try
x=0, y=0) - ✅ Is texture size non-zero? (Check
Width,Height) - ✅ Is alpha > 0? (Check color alpha channel)
protected override void OnRender(GameTime gameTime)
{
Logger.LogDebug("OnRender called");
if (_sprite == null)
{
Logger.LogWarning("Sprite is null!");
return;
}
// Draw at 0,0 to test
Renderer.DrawTexture(_sprite, 0, 0, 64, 64);
}
Poor Performance
Check:
- Draw call count - Press F3 to see overlay
- Goal: < 100 draw calls per frame
-
Solution: Use texture atlasing
-
FPS - Should be 60 (or monitor refresh rate)
- If low: Profile with performance monitor
-
Solution: Reduce draw calls
-
Memory usage - Check for texture leaks
- Solution: Use
AssetManifestfor automatic lifecycle management
Related Topics
- Sprites & Textures - Load and draw images
- Cameras - Camera movement and zoom
- Performance Optimization - Improve rendering speed
- Fundamentals: Architecture - Understand rendering architecture
Ready to render? Start with Sprites & Textures!