Rendering¶
Learn everything about 2D graphics rendering in Brine2D - 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 | Difficulty |
|---|---|---|
| Choosing a Renderer | GPU vs Legacy renderer comparison | ⭐ Beginner |
| GPU Renderer | Modern GPU-accelerated rendering (default) | ⭐ Beginner |
| Sprites & Textures | Load and draw images | ⭐ Beginner |
| Drawing Primitives | Shapes, lines, and basic graphics | ⭐ Beginner |
Intermediate¶
| Guide | Description | Difficulty |
|---|---|---|
| Cameras | Camera movement, zoom, and rotation | ⭐⭐ Intermediate |
| Texture Atlasing | Optimize draw calls with sprite packing | ⭐⭐ Intermediate |
Advanced¶
| Guide | Description | Difficulty |
|---|---|---|
| Particles | Particle systems for visual effects | ⭐⭐⭐ Advanced |
| Post-Processing | Screen shaders and effects | ⭐⭐⭐ Advanced |
Key Concepts¶
Renderer Architecture¶
Brine2D supports multiple rendering backends:
graph TB
IRenderer["IRenderer<br/>(Interface)"]
GPU["SDL3GPURenderer<br/>(Vulkan/Metal/D3D12)"]
Legacy["SDL3Renderer<br/>(SDL_Renderer)"]
IRenderer --> GPU
IRenderer --> Legacy
GPU --> Vulkan["Vulkan"]
GPU --> Metal["Metal"]
GPU --> D3D12["Direct3D 12"]
Legacy --> D3D11["Direct3D 11"]
Legacy --> OpenGL["OpenGL"]
style IRenderer fill:#2d5016,stroke:#4ec9b0,stroke-width:3px,color:#fff
style GPU fill:#1e3a5f,stroke:#569cd6,stroke-width:2px,color:#fff
style Legacy fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,color:#fff
Default: GPU renderer (faster, modern features)
Learn more in Choosing a Renderer
Render Loop¶
Understanding the render loop:
sequenceDiagram
participant GL as Game Loop
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->>R: Clear(color)
GL->>S: OnRender(gameTime)
S->>R: DrawTexture(...)
S->>R: DrawRectangle(...)
S->>R: DrawText(...)
GL->>R: EndFrame()
R->>R: Present to screen
end
Pattern: Update game state, then render. Renderer handles frame management automatically.
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)
{
_sprite = await LoadTextureAsync("assets/sprite.png", 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.DrawCircle(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");
var emitter = entity.AddComponent<ParticleEmitterComponent>();
emitter.EmissionRate = 100f;
emitter.ParticleLifetime = 2f;
emitter.StartColor = new Color(255, 200, 0, 255); // Orange
emitter.EndColor = new Color(255, 50, 0, 0); // Fade to transparent
emitter.EmitterShape = EmitterShape.Cone;
emitter.BlendMode = BlendMode.Additive; // Fire effect
Texture Atlasing¶
// Load individual textures
var textures = new List<ITexture>();
for (int i = 0; i < 10; i++)
{
textures.Add(await LoadTextureAsync($"assets/sprite{i}.png", 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 DrawTexture() call is a separate draw call (expensive!)
Solution: Use texture atlasing to batch sprites
// ❌ Bad - 100 draw calls
for (int i = 0; i < 100; i++)
{
Renderer.DrawTexture(_sprite, x[i], y[i]);
}
// ✅ Good - 1 draw call with atlas
var atlas = await AtlasBuilder.BuildAtlasAsync(Renderer, sprites);
Renderer.DrawTexture(atlas.AtlasTexture, ...); // Batched automatically!
Result: 90-99% fewer draw calls, 10x+ better performance
Use GPU Renderer¶
GPU renderer (default) is 2-3x faster than legacy renderer:
// GPU renderer (default)
builder.Services.AddBrine2D(options =>
{
options.Backend = GraphicsBackend.GPU; // Default
});
Advantages: - Automatic sprite batching - Hardware acceleration - Modern shader support - Post-processing effects
Minimize Texture Loads¶
Problem: Loading textures is slow
Solution: Load once in OnLoadAsync(), cache texture references
// ✅ Good - load once
private ITexture? _sprite;
protected override async Task OnLoadAsync(CancellationToken ct)
{
_sprite = await LoadTextureAsync("assets/sprite.png", 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. **Use GPU renderer** (default) for best performance
2. **Load textures in OnLoadAsync()** - Keep OnRender() fast
3. **Use texture atlasing** - Batch sprites that render together
4. **Set clear color once** - Don't change every frame
5. **Cache texture references** - Don't reload textures
```csharp
// ✅ Good pattern
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_sprites = await LoadSpritesAsync(ct);
_atlas = await BuildAtlasAsync(_sprites, ct);
}
protected override void OnRender(GameTime gameTime)
{
Renderer.ClearColor = Color.Black; // Set once
Renderer.DrawTexture(_atlas.AtlasTexture, ...); // Cached
}
❌ DON'T¶
- Don't inject IRenderer - Use the framework property
- Don't load assets in OnRender() - Causes lag
- Don't change clear color every frame - Unnecessary
- Don't draw thousands of individual sprites - Use atlasing
- Don't forget to unload textures - Memory leaks
// ❌ Bad pattern
protected override void OnRender(GameTime gameTime)
{
// Don't load in render!
var sprite = await LoadTextureAsync("sprite.png"); // NO!
// Don't change clear color based on game state
Renderer.ClearColor = _isNight ? Color.Black : Color.Blue; // Avoid
// Don't draw many individual sprites
for (int i = 0; i < 1000; i++)
{
Renderer.DrawTexture(_sprites[i], ...); // Use atlas instead!
}
}
Troubleshooting¶
"Texture is null" Error¶
Symptom: NullReferenceException when drawing
Solution: Make sure texture loaded successfully
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"); // Debug
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, use GPU renderer
-
Memory usage - Check for texture leaks
- Solution: Unload textures in
OnUnloadAsync()
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!