Skip to content

Rendering

Drawing shapes and textures in a node’s space, render targets, texture atlases, tile batches and the choice of renderer.

Files: primitives.odin, render_target.odin, render_backend.odin, atlas.odin, tile_batch.odin.

Texture atlases: many small images packed into one picture, so the game loads one texture and draws them all from it (fewer textures to switch between, one file to ship).

An atlas is an asset of kind “Atlas”, a .atlas.xml file beside its packed picture:

<atlas formatVersion="13" image="atlases/items.png" filter="Pixel" padding="2">
<region name="coin" source="assets/coin.png" x="0" y="0" w="16" h="16"/>
<region name="gem" source="assets/gem.png" x="18" y="0" w="12" h="20"/>
</atlas>
  • Packing is Atlas_Pack (rectangles of the images’ sizes into the smallest power-of-two picture, tallest first along shelves, padding pixels apart); editors pack the pixels and write both files (the Crossfire Editor’s atlas inspector: Pack).
  • Using one: a Sprite whose texture is the atlas file and region a region’s name draws that region (scene files: &lt;node kind="Sprite" texture="atlases/items.atlas.xml" region="coin"/>). From code: Sprite_SetRegion(sprite, ATLAS_ITEMS, "coin"), or Game_AtlasRegion for the texture and the rectangle to draw with DrawTexture.
  • Game_LoadAtlas reads each file once (Game.atlases, the game’s arena; editors put theirs there before building, like animation sets). The picture is an ordinary texture of the resource manager, filtered as the atlas says.

Tiles drawn in one batch (TileMap layers: tilemap.odin): quads of one texture, each its tile’s part of it with Aseprite’s flips (ASEPRITE_TILE_FLIP_X, _Y, _D), drawn through the draw transform and tint as one RenderGeometry, only those the screen shows.

SDL’s software renderer (the editor’s viewport) is handled as the LDtk tile layers are (ldtk_nodes.odin says why): the whole batch is drawn once into a texture of its size, then the part that shows is copied each frame; and triangles never share vertices there.

RenderTarget :: struct {
// What was drawn into it: draw it like any texture
texture: ^sdl.Texture,
// Its size in pixels
size: [2]f32,
// The renderer target RenderTarget_Begin replaced, put back by RenderTarget_End
previous: ^sdl.Texture,
// The renderer viewport before RenderTarget_Begin, put back by RenderTarget_End
previousViewport: sdl.Rect,
// The logical presentation size before RenderTarget_Begin, put back by RenderTarget_End
previousLogical: [2]i32,
// The logical presentation mode before RenderTarget_Begin, put back by RenderTarget_End
previousMode: sdl.RendererLogicalPresentation,
}

Render targets: textures to draw into instead of the screen. Draw a scene into one (a minimap, a picture-in-picture, a split-screen half, a transition snapshot) or draw anything with the Draw* helpers between RenderTarget_Begin and RenderTarget_End; then draw its texture like any other (DrawTexture, a Sprite’s texture).

Example

// Given: level: ^e.Scene
// A minimap: the level, drawn small every frame, shown in a corner by a HUD global
minimap := e.RenderTarget_Create(game, {320, 180})
// In the HUD's draw:
e.Scene_DrawToTarget(level, minimap, e.Transform2D{x = {0.1, 0}, y = {0, 0.1}})
e.DrawTexture(renderer, minimap.texture, {960, 20, 320, 180})

struct. Source: render_target.odin:32

RenderBackend :: enum {
// SDL's choice for the platform
Auto,
OpenGL,
OpenGLES2,
Vulkan,
// SDL's GPU API (Vulkan, Direct3D 12 or Metal underneath)
GPU,
Direct3D11,
Direct3D12,
Metal,
// On the CPU: works everywhere, slowest
Software,
}

Which SDL renderer a game draws with: a project setting (project.xml’s renderer, format 11), overridden by the player’s --renderer <name>. Auto lets SDL choose (OpenGL on Linux, Direct3D 11 on Windows, Metal on macOS). One that this machine’s SDL doesn’t have, or that fails to start, falls back to SDL’s choice with a warning. The browser always uses WebGL.

Example

// Given: window: ^sdl.Window
renderer := e.Application_CreateRenderer(window, .Vulkan)

enum. Source: render_backend.odin:16

RenderBackendInfo :: struct {
// For SDL_CreateRenderer (nil: SDL's choice)
driver: cstring,
// The name shown to people (the editor's Project Settings)
label: string,
// The platforms it exists on, as text for people
platforms: string,
}

A backend’s SDL driver name, how it’s shown, and where it exists

struct. Source: render_backend.odin:32

AtlasData :: struct {
// The packed picture, relative to the project folder
image: string,
// How it's filtered (asset_import.odin; Default: the project's)
filter: TextureFilter,
// Pixels between regions (packing)
padding: int,
// The regions, in the order they were packed
regions: [dynamic]AtlasRegion,
}

An atlas: its packed picture and the regions in it (ParseAtlas)

struct. Source: atlas.odin:40

AtlasRegion :: struct {
// What code and scene files call it (unique in the atlas)
name: string,
// The image it was packed from, relative to the project folder (for packing it again)
source: string,
// Where it is in the picture: x, y, width, height in pixels
rect: [4]int,
}

One image packed into an atlas

struct. Source: atlas.odin:52

DrawRectangle :: proc(renderer: ^ sdl.Renderer, rect: ^sdl.FRect, color: sdl.Color)

Draws a filled rectangle.

A rotated draw transform draws it as a rotated quad.

Inputs

  • renderer: The renderer to draw with
  • rect: The rectangle, in local coordinates (x, y is its top-left corner)
  • color: The color (multiplied by the node’s tint)

Example

rect := sdl.FRect{10, 10, 200, 40}
e.DrawRectangle(renderer, &rect, {40, 40, 60, 255})

procedure. Source: primitives.odin:36

DrawRectangleOutline :: proc(renderer: ^sdl.Renderer, rect: ^sdl.FRect, color: sdl.Color)

Draws a rectangle’s 1-pixel outline.

Inputs

  • renderer: The renderer to draw with
  • rect: The rectangle, in local coordinates (x, y is its top-left corner)
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:58

DrawPoint :: proc(renderer: ^sdl.Renderer, p: sdl.FPoint, color: sdl.Color)

Draws a single pixel.

Inputs

  • renderer: The renderer to draw with
  • p: The point, in local coordinates
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:79

DrawLine :: proc(renderer: ^sdl.Renderer, a, b: sdl.FPoint, color: sdl.Color)

Draws a 1-pixel line (1 pixel wide at any scale; DrawLineThick for wider lines).

Inputs

  • renderer: The renderer to draw with
  • a: The start, in local coordinates
  • b: The end, in local coordinates
  • color: The color (multiplied by the node’s tint)

Example

e.DrawLine(renderer, {0, 0}, {100, 50}, {255, 255, 255, 255})

procedure. Source: primitives.odin:97

DrawLineThick :: proc(renderer: ^sdl.Renderer, a, b: sdl.FPoint, thickness: f32, color: sdl.Color)

Draws a line with a thickness, as a quad. The thickness scales with the node’s scale and the camera’s zoom. Nothing is drawn when a and b are the same point.

Inputs

  • renderer: The renderer to draw with
  • a: The start, in local coordinates
  • b: The end, in local coordinates
  • thickness: The width, in local units
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:115

DrawTriangle :: proc(renderer: ^sdl.Renderer, a, b, c: sdl.FPoint, color: sdl.Color)

Draws a filled triangle.

Inputs

  • renderer: The renderer to draw with
  • a: The first corner, in local coordinates
  • b: The second corner
  • c: The third corner
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:139

DrawTriangleOutline :: proc(renderer: ^sdl.Renderer, a, b, c: sdl.FPoint, color: sdl.Color)

Draws a triangle’s 1-pixel outline.

Inputs

  • renderer: The renderer to draw with
  • a: The first corner, in local coordinates
  • b: The second corner
  • c: The third corner
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:154

DrawCircle :: proc(renderer: ^sdl.Renderer, center: sdl.FPoint, radius: f32, color: sdl.Color)

Draws a filled circle (a polygon with more segments the larger it is on screen).

Inputs

  • renderer: The renderer to draw with
  • center: The center, in local coordinates
  • radius: The radius, in local units
  • color: The color (multiplied by the node’s tint)

Example

e.DrawCircle(renderer, {0, 0}, 16, {120, 200, 255, 255})

procedure. Source: primitives.odin:171

DrawCircleOutline :: proc(renderer: ^sdl.Renderer, center: sdl.FPoint, radius: f32, color: sdl.Color)

Draws a circle’s 1-pixel outline.

Inputs

  • renderer: The renderer to draw with
  • center: The center, in local coordinates
  • radius: The radius, in local units
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:184

DrawEllipse :: proc(renderer: ^sdl.Renderer, center: sdl.FPoint, rx, ry: f32, color: sdl.Color)

Draws a filled ellipse, its axes along the local x and y axes.

Inputs

  • renderer: The renderer to draw with
  • center: The center, in local coordinates
  • rx: The horizontal radius, in local units
  • ry: The vertical radius, in local units
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:198

DrawEllipseOutline :: proc(renderer: ^sdl.Renderer, center: sdl.FPoint, rx, ry: f32, color: sdl.Color)

Draws an ellipse’s 1-pixel outline, its axes along the local x and y axes.

Inputs

  • renderer: The renderer to draw with
  • center: The center, in local coordinates
  • rx: The horizontal radius, in local units
  • ry: The vertical radius, in local units
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:213

DrawPolygon :: proc(renderer: ^sdl.Renderer, points: []sdl.FPoint, color: sdl.Color)

Draws a filled convex polygon (filled as a triangle fan from the first point; concave shapes draw wrong).

Inputs

  • renderer: The renderer to draw with
  • points: The corners in order, in local coordinates (at least 3). Not modified
  • color: The color (multiplied by the node’s tint)

Example

points := []sdl.FPoint{{0, -20}, {15, 15}, {-15, 15}}
e.DrawPolygon(renderer, points, {255, 200, 0, 255})

procedure. Source: primitives.odin:231

DrawPolygonOutline :: proc(renderer: ^sdl.Renderer, points: []sdl.FPoint, color: sdl.Color)

Draws a closed 1-pixel outline through the points (the last joins the first).

Inputs

  • renderer: The renderer to draw with
  • points: The corners in order, in local coordinates (at least 2). Not modified
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:243

DrawRoundedRectangle :: proc(renderer: ^sdl.Renderer, rect: ^sdl.FRect, radius: f32, color: sdl.Color)

Draws a filled rectangle with rounded corners.

Inputs

  • renderer: The renderer to draw with
  • rect: The rectangle, in local coordinates (x, y is its top-left corner)
  • radius: The corners’ radius, in local units (clamped to half the shorter side)
  • color: The color (multiplied by the node’s tint)

Example

button := sdl.FRect{20, 20, 160, 48}
e.DrawRoundedRectangle(renderer, &button, 8, {60, 60, 90, 255})

procedure. Source: primitives.odin:260

DrawRoundedRectangleOutline :: proc(renderer: ^sdl.Renderer, rect: ^sdl.FRect, radius: f32, color: sdl.Color)

Draws the 1-pixel outline of a rectangle with rounded corners.

Inputs

  • renderer: The renderer to draw with
  • rect: The rectangle, in local coordinates (x, y is its top-left corner)
  • radius: The corners’ radius, in local units (clamped to half the shorter side)
  • color: The color (multiplied by the node’s tint)

procedure. Source: primitives.odin:275

DrawTexture :: proc(
renderer: ^sdl.Renderer,
texture: ^sdl.Texture,
dest: sdl.FRect,
rotation: f32 = 0,
src: Maybe(sdl.FRect) = nil,
tint := sdl.Color{255, 255, 255, 255},
flip := sdl.FlipMode.NONE,
)

Draws a texture (or part of one) into a rectangle, with optional rotation, tint and flip.

dest is in the drawing node’s local coordinates; the node’s world transform and the camera move, scale and rotate the texture. Prefer this over sdl.RenderTexture, which bypasses them.

Inputs

  • renderer: The renderer to draw with
  • texture: The texture to draw. Nothing is drawn if nil
  • dest: Where to draw, as a rectangle in local coordinates
  • rotation: Degrees, clockwise around the center of dest
  • src: Part of the texture to draw, in texture pixels (e.g. one sprite sheet frame). nil draws the whole texture
  • tint: Multiplies the texture’s colors and alpha. White leaves it unchanged
  • flip: Mirror the texture horizontally and/or vertically (a draw transform with a negative scale, like a sprite node with scale.x -1, mirrors it too)

Example

// Given: rm: ^e.ResourceManager, pos: [2]f32, heading: f32, sheet: ^sdl.Texture, x: f32, y: f32
ship := rm->loadTexture("ship.png")
e.DrawTexture(renderer, ship, {pos.x, pos.y, 64, 64}, rotation = heading)
// Second 32x32 frame of a sprite sheet, faded to half transparency
e.DrawTexture(renderer, sheet, {x, y, 32, 32}, src = sdl.FRect{32, 0, 32, 32}, tint = {255, 255, 255, 128})

procedure. Source: primitives.odin:305

RenderTarget_Create :: proc(game: ^Game, size: [2]f32, smooth := false) -> ^RenderTarget

Makes a render target of size pixels, cleared to transparent.

Inputs

  • game: The game (its renderer)
  • size: Width and height in pixels
  • smooth: Linear filtering when it’s drawn scaled (otherwise nearest, for pixel art)

Returns

Example

minimap := e.RenderTarget_Create(game, {320, 180})
defer e.RenderTarget_Destroy(minimap)

procedure. Source: render_target.odin:62

RenderTarget_Destroy :: proc(target: ^RenderTarget)

Frees a render target and its texture.

Inputs

  • target: The target (nil does nothing)

procedure. Source: render_target.odin:86

RenderTarget_Begin :: proc(renderer: ^sdl.Renderer, target: ^RenderTarget, clear := false, clearColor := sdl.Color{0, 0, 0, 0})

Draws into a target from now on (until RenderTarget_End): the Draw* helpers and SDL calls land in its texture, in its own pixels (the game’s logical presentation is off meanwhile). Calls can nest across different targets; don’t begin the same target twice before ending it.

Inputs

  • renderer: The renderer that made it
  • target: The target
  • clear: Clear it to clearColor first
  • clearColor: The color to clear to (transparent by default)

Example

// Given: target: ^e.RenderTarget
e.RenderTarget_Begin(renderer, target, clear = true)
e.DrawCircle(renderer, {160, 90}, 40, sdl.Color{255, 200, 0, 255})
e.RenderTarget_End(renderer, target)

procedure. Source: render_target.odin:109

RenderTarget_End :: proc(renderer: ^sdl.Renderer, target: ^RenderTarget)

Goes back to drawing where it was before RenderTarget_Begin (the screen, or another target), with the renderer’s viewport and logical presentation as they were.

Inputs

procedure. Source: render_target.odin:132

Scene_DrawToTarget :: proc(scene: ^Scene, target: ^RenderTarget, view: Maybe(Transform2D) = nil, clearColor := sdl.Color{0, 0, 0, 0})

Draws a scene into a render target (cleared first): through view when given, else through its camera scaled from the game’s size to the target’s (the whole game view, stretched when the aspect ratios differ). Its nodes don’t update; the scene that’s playing can draw another one this way, or itself.

Inputs

  • scene: The scene to draw
  • target: Where
  • view: World to target pixels; nil for the camera’s view, scaled to the target
  • clearColor: What’s under the scene (transparent by default)

Example

// Given: level: ^e.Scene, minimap: ^e.RenderTarget
e.Scene_DrawToTarget(level, minimap)

procedure. Source: render_target.odin:155

RenderBackend_Available :: proc(backend: RenderBackend) -> bool

Whether this machine’s SDL has the backend’s driver (Auto always). It doesn’t say the driver starts: a missing GPU driver only shows when the renderer is created.

Inputs

  • backend: The backend

Returns

  • true if SDL was built with its driver

procedure. Source: render_backend.odin:64

RenderBackend_Parse :: proc(name: string) -> (RenderBackend, bool)

The backend named by name: its enum name (“Vulkan”) or SDL driver name (“vulkan”), any case.

Inputs

  • name: The name

Returns

  • The backend and true, or .Auto and false for an unknown name

Example

if backend, ok := e.RenderBackend_Parse("opengl"); ok do log.infof("using %v", backend)

procedure. Source: render_backend.odin:83

Application_CreateRenderer :: proc(window: ^sdl.Window, backend: RenderBackend) -> ^sdl.Renderer

Creates the window’s renderer with a backend, falling back to SDL’s choice (with a warning) when it isn’t there or doesn’t start. Logs the renderer in use. In the browser the backend is ignored (WebGL).

Inputs

  • window: The window to draw in
  • backend: The backend to try first

Returns

  • The renderer, nil only if none at all could be created (SDL’s error is logged)

procedure. Source: render_backend.odin:102

Atlas_IsFile :: proc(path: string) -> bool

Whether a path is an atlas file

procedure. Source: atlas.odin:62

ParseAtlas :: proc(text: string, allocator := context.allocator) -> (AtlasData, ProjectError)

Reads an atlas file.

Inputs

  • text: The file’s contents
  • allocator: For the atlas’s strings and regions

Returns

procedure. Source: atlas.odin:76

FormatAtlas :: proc(atlas: AtlasData, allocator := context.allocator) -> string

Writes an atlas file.

Inputs

  • atlas: The atlas
  • allocator: For the text

Returns

  • The file’s text

procedure. Source: atlas.odin:124

AtlasData_Clone :: proc(atlas: AtlasData, allocator := context.allocator) -> AtlasData

A copy of an atlas (in allocator)

procedure. Source: atlas.odin:139

AtlasData_Destroy :: proc(atlas: ^AtlasData, allocator := context.allocator)

Frees what ParseAtlas or AtlasData_Clone made

procedure. Source: atlas.odin:146

AtlasData_Equal :: proc(a, b: AtlasData) -> bool

Whether two atlases are the same

procedure. Source: atlas.odin:157

Atlas_Find :: proc(atlas: ^AtlasData, name: string) -> (AtlasRegion, bool)

A region of an atlas by name.

Returns

  • The region, and whether the atlas has it

procedure. Source: atlas.odin:169

Atlas_Pack :: proc(sizes: [][2]int, padding := 0, maxSize := 0) -> (positions: [][2]int, size: [2]int, ok: bool)

Packs rectangles into the smallest picture that holds them: power-of-two sides (the narrower side first tried as wide as the wider), the tallest rectangles first, left to right along shelves, padding pixels apart.

Inputs

  • sizes: Each rectangle’s width and height
  • padding: Pixels between rectangles (and none at the picture’s edges)
  • maxSize: The largest side allowed (ATLAS_MAX_SIZE when 0)

Returns

  • Each rectangle’s x and y (same order as sizes; temp allocated), the picture’s size, and false when they don’t fit in maxSize

procedure. Source: atlas.odin:189

Game_LoadAtlas :: proc(game: ^Game, path: string) -> (^AtlasData, bool)

An atlas, read once per game (Game.atlases): the file relative to the project folder.

Returns

  • The atlas (the game’s; nil when it can’t be read, logged), and whether it was read

procedure. Source: atlas.odin:225

Game_AtlasRegion :: proc(game: ^Game, atlas: string, name: string) -> (^sdl.Texture, sdl.FRect, bool)

An atlas region’s picture and where it is in it, to draw with DrawTexture(..., src = rect).

Inputs

  • game: The game
  • atlas: The atlas file, relative to the project folder (ATLAS_X in scripts/assets.odin)
  • name: The region

Returns

  • The texture, the region’s rectangle in it, and whether the atlas has the region

procedure. Source: atlas.odin:240

Sprite_SetRegion :: proc(sprite: ^Sprite, atlas: string, name: string) -> bool

Makes a Sprite draw a region of an atlas (its size the region’s).

Inputs

  • sprite: The Sprite
  • atlas: The atlas file, relative to the project folder
  • name: The region

Returns

  • false (the sprite unchanged, logged) when the atlas has no such region

Example

// Given: sprite: ^e.Sprite
e.Sprite_SetRegion(sprite, "atlases/items.atlas.xml", "coin")

procedure. Source: atlas.odin:259

MAX_CURVE_SEGMENTS :: 128

Upper bound on segments used to approximate curves (circles, ellipses, arcs)

constant. Source: primitives.odin:16

ROUNDED_CORNER_SEGMENTS :: 8

Segments used per corner of a rounded rectangle

constant. Source: primitives.odin:18

ROUNDED_RECT_POINT_COUNT :: 4 * (ROUNDED_CORNER_SEGMENTS + 1)

Perimeter points of a rounded rectangle (four corners of ROUNDED_CORNER_SEGMENTS + 1 points)

constant. Source: primitives.odin:20

RENDER_TARGET_FORMAT :: sdl.PixelFormat.BGRA32

The pixel format of the engine’s render targets (RenderTarget_Create, tile caches): BGRA32, B8G8R8A8_UNORM on Vulkan, the format SDL’s Vulkan swapchains use on nearly every platform.

SDL 3.4’s Vulkan renderer picks its pipelines by the swapchain’s format, not the render target’s (VULKAN_SetDrawState uses surfaceFormat.format), so drawing into a target of another format used pipelines made for an incompatible render pass (Vulkan validation: VUID-vkCmdDraw-renderPass-02684; it drew right on the GPUs tried, but it’s undefined). Other renderers take any format alike. Readers convert (sdl.ConvertSurface): don’t assume RGBA order.

constant. Source: render_target.odin:16

RENDER_BACKENDS := [RenderBackend]RenderBackendInfo{
.Auto = {nil, "Auto (SDL's choice)", "Every platform"},
.OpenGL = {"opengl", "OpenGL", "Linux, Windows, macOS"},
.OpenGLES2 = {"opengles2", "OpenGL ES 2", "Linux, Windows, mobile"},
.Vulkan = {"vulkan", "Vulkan", "Linux, Windows (macOS through MoltenVK)"},
.GPU = {"gpu", "SDL GPU", "Linux, Windows, macOS"},
.Direct3D11 = {"direct3d11", "Direct3D 11", "Windows"},
.Direct3D12 = {"direct3d12", "Direct3D 12", "Windows"},
.Metal = {"metal", "Metal", "macOS"},
.Software = {"software", "Software", "Every platform"},
}

Each backend’s SDL driver name, label and platforms

variable. Source: render_backend.odin:42

ATLAS_EXTENSION :: ".atlas.xml"

An atlas file’s extension

constant. Source: atlas.odin:35

ATLAS_MAX_SIZE :: 8192

The largest picture Atlas_Pack makes, each way

constant. Source: atlas.odin:37