Skip to content

Shaders and screen effects

Shader bundles and materials on nodes, compiling GLSL, and ScreenEffect nodes.

Files: shaders.odin, shader_compile.odin (#+build !js), screen_effects.odin.

Shaders: small GPU programs that change how a node’s pictures are drawn (a hit flash, an outline, dissolving away, swapped colors), and screen effects over the whole scene (CRT, bloom, color grading).

  • On a node (Node_SetShader, a scene file’s shader attribute and <shaderParam> children): the shader colors every pixel the node draws from an image or text (sprites, animated sprites, tilemaps, labels, textured particles), and its children’s, unless a child has a shader of its own (a shader on a character’s body colors the sprite under it). Shapes drawn with lines and fills aren’t affected.
  • On the screen (a ScreenEffect node): the scene is drawn into a picture first, then drawn through the shader. Several ScreenEffects run one after the other.
  • Built-in effects (builtin:hit_flash, builtin:outline, builtin:dissolve, builtin:palette_swap, builtin:crt, builtin:bloom, builtin:color_grade) ship with the engine; your own are GLSL fragment shaders in the project (assets of kind “Shader”, .frag), compiled by the editor into a .frag.shader file beside them (shader_compile.odin).
  • Parameters are the members of the shader’s Params uniform block (std140): floats, vec2, vec3, vec4 (a color when its comment says color). Their comments give a default and a range for editors: float amount; // default=0.5 min=0 max=1. Names time, texelSize and textureSize are filled by the engine: seconds the game has run, 1 / the picture’s size in pixels, the picture’s size. Node_SetShaderParam sets the others, and so can an Animator (shader.<name> tracks).

Where shaders run: SDL’s GPU renderer only (the project’s Renderer set to GPU; Auto picks it when the project has shaders: Game_LoadProject). Shaders are SPIR-V (Vulkan: Linux, Windows) and MSL (Metal: macOS); on Windows the GPU renderer is asked for Vulkan. On other renderers, and on the web, nodes draw as if they had no shader (logged once), except builtin:hit_flash, which draws the picture a second time added in the flash color: it brightens (an approximation).

Params must be used by main for the engine to fill it: the compiler drops a block nothing reads.

A shader written for it (the inputs and outputs SDL’s renderer gives every fragment shader):

#version 450
layout(location = 0) in vec4 v_color; // the vertex color: tint and opacity
layout(location = 1) in vec2 v_uv; // where in the picture, 0 to 1
layout(location = 0) out vec4 o_color;
layout(set = 2, binding = 0) uniform sampler2D u_texture;
layout(set = 3, binding = 0) uniform Params {
float amount; // default=1 min=0 max=1
vec4 tint; // color default=#ff0000
float time; // filled by the engine
};
void main() {
vec4 c = texture(u_texture, v_uv) * v_color;
o_color = vec4(mix(c.rgb, tint.rgb, amount * (0.5 + 0.5 * sin(time * 6.0))), c.a);
}

Example

// Given: hero: ^e.Sprite
e.Node_SetShader(hero, "builtin:hit_flash")
e.Node_SetShaderParam(hero, "amount", 1)

Compiling GLSL fragment shaders into the .frag.shader files games load (shaders.odin): glslang makes SPIR-V (Vulkan), SDL_shadercross MSL (Metal) and the resource counts, and the Params block gives the parameters. Editors call it when a shader is saved, before Play and before exports; tools/compile_shaders compiles the engine’s built-in effects with it.

The tools are built by tools/build_shader_tools.sh into shader_tools/<os>_<arch>/ (ShaderTools_Find); releases ship them beside the editor.

Screen effects: a ScreenEffect node runs its shader (shaders.odin) over the whole scene as drawn: the scene goes into a picture the screen’s size first, then that picture is drawn through each ScreenEffect in turn (in the order they were added), the last one onto the screen.

  • includeScreenLayers (on by default): the effect covers the scene’s screen layers too (a HUD on an old TV); off, it covers the world only and screen layers draw over it untouched.
  • A ScreenEffect that’s hidden, or whose shader can’t run (not the GPU renderer), is skipped.
  • Its parameters are its shader’s (Node_SetShaderParam on the effect node).

Example

tv := e.CreateScreenEffect(scene, "builtin:crt")
e.Node_SetShaderParam(tv, "scanlines", 0.8)
ShaderParamType :: enum {
Float,
Vec2,
Vec3,
Vec4,
// A vec4 shown as a color (its comment says `color`)
Color,
}

A shader parameter’s type

enum. Source: shaders.odin:67

ShaderParam :: struct {
// Its name in the shader
name: string,
// Its type
type: ShaderParamType,
// Its byte offset in the uniform block (std140)
offset: int,
// Its default (unused components 0), and the range editors give it
value: [4]f32,
min, max: f32,
// What editors call it ("" its name)
label: string,
// The engine fills it (`time`, `texelSize`, `textureSize`): editors don't show it
builtin: bool,
}

A shader parameter: a member of its Params block

struct. Source: shaders.odin:77

ShaderStage :: enum {
// Each pixel's color (nodes' and screen effects' shaders)
Fragment,
// Each vertex's position (the engine's own: GPU particles)
Vertex,
}

A shader’s stage: what it computes

enum. Source: shaders.odin:94

ShaderBundle :: struct {
// The format of the file
version: int,
// Its stage
stage: ShaderStage,
// How many storage buffers it reads (vertex shaders: the engine's GPU particles)
storageBuffers: int,
// Its parameters, and the size of its uniform block in bytes (0: none)
params: []ShaderParam,
uniformSize: int,
// How many samplers (textures) and uniform blocks it uses
samplers, uniformBuffers: int,
// The code: SPIR-V and MSL (base64 in the file)
spirv, msl: []byte,
}

A compiled shader: what a .frag.shader (or .vert.shader) file holds

struct. Source: shaders.odin:102

ShaderProgram :: struct {
// Its path ("builtin:outline", or a project path)
path: string,
bundle: ShaderBundle,
// It couldn't be made on a renderer (logged once)
failed: bool,
// The GPU shader made per GPU device
gpu: [dynamic]ShaderOnDevice,
}

A loaded shader (Game_LoadShader), shared by every node using it

struct. Source: shaders.odin:119

ShaderMaterial :: struct {
program: ^ShaderProgram,
// One per parameter, in the program's order
values: [dynamic][4]f32,
// The render state made per renderer (SDL keeps uniform values per state)
states: [dynamic]ShaderState,
}

A node’s shader and its parameter values (Node.shader)

struct. Source: shaders.odin:137

BuiltinShader :: enum {
hit_flash,
outline,
dissolve,
palette_swap,
crt,
bloom,
color_grade,
}

The built-in effects (shaders/builtin/<name>.frag, compiled by tools/compile_shaders)

enum. Source: shaders.odin:155

ShaderTools :: struct {
glslang: string,
shadercross: string,
// The font tool beside them (font_compile.odin; "" when that build has none)
msdfAtlasGen: string,
}

Where the two shader compilers are

struct, only in builds with #+build !js. Source: shader_compile.odin:19

ScreenEffect :: struct {
// The node part: transform, hierarchy, hooks
using node: Node,
// The effect covers the scene's screen layers too
includeScreenLayers: bool,
}

A shader over the whole scene (its shader is the effect)

struct. Source: screen_effects.odin:21

Shader_BuiltinOf :: proc(path: string) -> (BuiltinShader, bool)

A built-in effect’s path from its name, and the name from a path.

Inputs

  • path: “builtin:outline”

Returns

  • The effect, and whether the path is one

procedure. Source: shaders.odin:199

Shader_BuiltinBundle :: proc(b: BuiltinShader) -> (ShaderBundle, bool)

A built-in effect’s compiled bundle (its parameters: what editors show).

Returns

  • The bundle (temp allocated), and false if it can’t be read

procedure. Source: shaders.odin:212

Game_ReleaseShaders :: proc(game: ^Game)

Frees the game’s shaders’ GPU code: call before destroying the renderer when a game isn’t destroyed with Game_Destroy (editors’ previews). Game_Destroy does it.

procedure. Source: shaders.odin:220

Shader_ParseBundle :: proc(text: []byte, allocator := context.allocator) -> (bundle: ShaderBundle, ok: bool)

Reads a compiled shader (a .frag.shader file’s text) into a bundle allocated with allocator.

Returns

  • The bundle, and false when the text isn’t one (or another version)

procedure. Source: shaders.odin:230

Shader_FormatBundle :: proc(bundle: ShaderBundle) -> string

Writes a bundle as a .frag.shader file’s text (temp allocated).

procedure. Source: shaders.odin:261

Shader_ParseParams :: proc(source: string) -> (params: []ShaderParam, size: int, problem: string)

The parameters of a GLSL fragment shader: the members of its uniform block at set 3, binding 0 (Params), with std140 offsets and what their comments say (default=, min=, max=, label=, color). Names the engine fills (time, texelSize, textureSize) are marked builtin.

Inputs

  • source: The shader’s text

Returns

  • The parameters (temp allocated) and the block’s size in bytes; problem names what couldn’t be read (“” when fine)

procedure. Source: shaders.odin:287

Game_LoadShader :: proc(game: ^Game, path: string) -> ^ShaderProgram

Loads a shader for the game (once: later calls return the same one).

Inputs

  • game: The game
  • path: “builtin:<name>”, or a project shader’s path (its compiled <path>.shader is read)

Returns

  • The shader, or nil (logged) when it isn’t there or can’t be read

procedure. Source: shaders.odin:353

Node_SetShader :: proc(node: ^Node, path: string) -> bool

Gives a node a shader (replacing its own), with the shader’s default parameters.

Inputs

  • node: The node
  • path: “builtin:<name>”, a project shader’s path, or “” for none

Returns

  • Whether it has the shader now

procedure. Source: shaders.odin:392

Node_SetShaderParam :: proc{shaderParamFloat, shaderParamVec2, shaderParamVec4, shaderParamColor}

Sets one of a node’s shader parameters.

Inputs

  • node: The node (with a shader)
  • name: The parameter
  • value: A number, a 2, 3 or 4 component vector, or a color

Returns

  • Whether the node’s shader has that parameter

Example

// Given: hero: ^e.Sprite with builtin:hit_flash
e.Node_SetShaderParam(hero, "amount", 1)
e.Node_SetShaderParam(hero, "flashColor", sdl.Color{255, 80, 80, 255})

procedure group. Source: shaders.odin:428

Node_SetShaderParamText :: proc(node: ^Node, name, text: string) -> bool

Sets one of a node’s shader parameters from text (ShaderParam_Parse): what scene files and editors give.

Returns

  • Whether the node’s shader has that parameter and the text is a value of it

procedure. Source: shaders.odin:437

Node_ResetShaderParams :: proc(node: ^Node)

Puts every one of a node’s shader parameters back to the shader’s defaults

procedure. Source: shaders.odin:448

Node_GetShaderParam :: proc(node: ^Node, name: string) -> ([4]f32, bool)

One of a node’s shader parameters (all four components; a float’s is the first).

Returns

  • The value, and whether the node’s shader has that parameter

procedure. Source: shaders.odin:459

ShaderParam_Parse :: proc(type: ShaderParamType, text: string) -> ([4]f32, bool)

A shader parameter’s value from text: “0.5”, “1, 2”, “1, 0, 0, 1”, or a color (“#ff8000”, “#ff800080”).

Returns

  • The four components (missing ones 0; a color’s alpha 1 unless given), and whether it read

procedure. Source: shaders.odin:472

ShaderParam_Format :: proc(type: ShaderParamType, value: [4]f32) -> string

A shader parameter’s value as text (temp allocated): a number, comma-separated components, or a color (#rrggbbaa) for colors.

procedure. Source: shaders.odin:497

Renderer_SupportsShaders :: proc(renderer: ^sdl.Renderer) -> bool

Whether a renderer runs shaders: SDL’s GPU renderer, on a device taking SPIR-V or MSL.

procedure. Source: shaders.odin:513

ShaderTools_Find :: proc(dir: string) -> (ShaderTools, bool)

Finds the shader tools in a folder of builds (<dir>/<os>_<arch>/glslang, shadercross, and the font tool msdf-atlas-gen when it’s there).

Inputs

  • dir: The folder holding the <os>_<arch> folders (the engine’s shader_tools, or a release’s)

Returns

  • The tools (temp allocated paths), and whether both are there

procedure, only in builds with #+build !js. Source: shader_compile.odin:36

Shader_Compile :: proc(tools: ShaderTools, source, name, scratch: string, stage := ShaderStage.Fragment) -> (bundle: ShaderBundle, messages: string, ok: bool)

Compiles a GLSL fragment shader.

Inputs

  • tools: The compilers (ShaderTools_Find)
  • source: The shader’s text
  • name: The file’s name in messages (“assets/fx/water.frag”)
  • scratch: A folder for the compilers’ files (made when missing)

Returns

  • The bundle (its code temp allocated), and the compilers’ messages with the file’s name and line (“assets/fx/water.frag:12: ‘colr’ : undeclared identifier”); ok false when it failed

procedure, only in builds with #+build !js. Source: shader_compile.odin:63

Shader_CompileFile :: proc(tools: ShaderTools, path, name, scratch: string, force := false, stage := ShaderStage.Fragment) -> (messages: string, ok: bool, compiled: bool)

Compiles a shader file into its .shader file beside it (water.frag -> water.frag.shader), when the source is newer than it (or force).

Inputs

  • tools: The compilers
  • path: The .frag file
  • name: Its name in messages
  • scratch: A folder for the compilers’ files
  • force: Compile even when it’s up to date

Returns

  • The messages (“” none), whether it compiled (or was up to date), and whether it compiled now

procedure, only in builds with #+build !js. Source: shader_compile.odin:131

CreateScreenEffect :: proc(scene: ^Scene, shader: string) -> ^ScreenEffect

Creates a ScreenEffect and adds it to the scene (at the start of the next update).

Inputs

  • scene: The scene
  • shader: The effect (“builtin:crt”, a project shader’s path)

Returns

  • The effect node (its shader set; a shader that can’t load leaves it without one, logged)

procedure. Source: screen_effects.odin:38

SHADER_BUNDLE_VERSION :: 1

The file format version Shader_Compile writes

constant. Source: shaders.odin:152

BUILTIN_SHADER_INFO := [BuiltinShader]struct { description: string, screen: bool }{
.hit_flash = {"Flashes the picture toward a color: hits, pickups", false},
.outline = {"Draws a line around the picture's opaque pixels: selection, highlights", false},
.dissolve = {"Eats the picture away in a noisy pattern with a glowing edge: deaths, teleports", false},
.palette_swap = {"Replaces up to four colors with others: team colors, skins", false},
.crt = {"An old TV: scanlines, curved glass, a dark border", true},
.bloom = {"Bright parts glow into their surroundings", true},
.color_grade = {"Brightness, contrast, saturation, tint and a vignette over the scene", true},
}

What each built-in effect does, and whether it’s for the screen (a ScreenEffect) or a node

variable. Source: shaders.odin:166

SHADER_BUILTIN_PREFIX :: "builtin:"

The prefix of a built-in effect’s path (“builtin:outline”)

constant. Source: shaders.odin:188