Rendering: 2D lights, shadows and post-processing
Lit 2D scenes in Balaur: light2d and occluder2d build a light map every sprite, polygon and tile is multiplied by, plus camera ambient and screen-space post.
På denne side
A 2D scene draws unlit until it has a light2d. One light2d starts a light
map at the camera's resolution. Everything drawn is multiplied by that map
plus the camera's ambient_color. Sprites, polygons and tile maps are lit with no
material.

A light
[[nodes]]
id = "n_lamp"
name = "Lamp"
[nodes.transform]
position = [0, 2.5, 0]
[nodes.light2d]
kind = "point" # point | directional
color = "#ffd28a"
range = 8.0 # world units; a point light fades to nothing here
intensity = 1.6
shadow_enabled = true
- The node's
transformplaces a light and its rotation aims it. At rest alight2dshines along −y; the node's z rotation turns it. - A point light fades quadratically to zero at
radius. - A directional light ignores
radiusand lights the whole view. intensityabove 1 pushes past white, which bloom picks up.
Ambient
[nodes.camera2d]
pixels_per_unit = 60.0
ambient_color = "#181c22" # what an unlit surface is multiplied by
- A scene with no
light2dcosts no extra pass and no extra draw. - With a light, everything the lights do not reach falls to the camera's
ambient_color, black by default. - White
ambient_coloris the unlit look.
Occluders and shadows
[nodes.occluder2d]
# mesh = "meshes/wall.toml" # points in order; empty derives the outline
# closed = true # the last point joins the first
- A light with
shadow_enabled = trueis blocked by any node with anoccluder2d. Each edge is extruded away from the light into a polygon that masks the light. - Left empty, the outline follows the node's
collider2d, else its circle, capsule, rect or sprite shape. meshnames an outline of its own. A polyline or polygon node needs one.- Every edge casts, so an occluder stands in its own shadow. A crate that
should stay lit needs a smaller outline than its sprite, or a light with
shadow_enabled = false. - A directional light's shadows are parallel.


- The Polygon tool traces an
occluder2din Points mode. Triangles, UVs and weights modes are not offered. - The Lights chip in the Scene workspace draws every light's reach and every occluder's resolved outline, derived ones included.
Post-processing
[nodes.camera]
kind = "2d"
post = ["bloom"]
bloom_threshold = 0.9 # brightness a pixel has to pass
bloom_intensity = 0.7 # how much is added back
bloomis the one that applies in 2D; the light map gives it bright pixels to bleed.ssao,ssranddofare the other three, 3D only.postspans both dimensions (ambient_colordoes not). The last current camera of either kind sets it.
Custom passes
post runs in list order. An unknown name is a material drawn over the
whole frame.
[nodes.camera]
post = ["fog", "tonemap", "bloom", "materials/grade.toml"]
- The engine's own passes run at fixed points:
ssaoandssrfeed shading,bloomrides the tonemap. Naming one switches it on. - Order applies to materials. Before
tonemapa pass reads linear light and its output blooms. After it a pass reads the tonemapped picture. - A list without
tonemaphas it at the head: a plain list of materials runs over the finished frame. - A pass that fails to compile is reported and left out.
See Shaders for what a post material imports.
Light map coverage
- The multiply covers sprites, polygons, tile maps, and a 3D scene under a 2D one.
- Debug lines and particles draw after it and stay unlit.
- Lights never touch the simulation. A lit game and a headless one tick identically.
Lights in 3D
[[nodes]]
name = "Key"
[nodes.transform]
position = [4, 7, 4]
rotation_euler = [-0.87266, 0.69813, 0] # radians: -50° and 40°
[nodes.light3d]
kind = "spot"
color = "#fff2e0"
intensity = 6
range = 30
inner_angle_degrees = 25
outer_angle_degrees = 45

- The node's
transformplaces it and its rotation aims it.kindisdirectional,pointorspot. - A scene with no
light3dkeeps the engine's default key light. The firstlight3dreplaces it. visibleon the node turns it off.shadow_enabledsays whether it casts.light_layersis a bitmask; ameshorshape3dis lit when it shares a bit.
The scene's atmosphere
[nodes.environment]
sky = "skies/studio.hdr"
ambient_color = "#20242a"
tonemap = "neutral"
exposure = 1.1
fog_mode = "exponential"
fog_color = "#9fb4c8"
fog_density = 0.012
environmentcarries scene-wide settings: sky, ambient light, fog, exposure, tonemap, colour grading, shadow budget.- The last
currentone in tree order wins. skyis a project-relative equirectangular image:.hdr,.exror.png. It draws behind the scene.sky_enabled = falsehides it; orientation and intensity still apply.- Image-based lighting from the sky is not bound yet. A scene is lit by its
light3dnodes and its ambient. - The editor's Library dock ships three gradient skies.
A physically based material
A 3D material imports package::pbr for metallic, roughness, emissive and
reflectance. The default is Lambert.
struct Params { metallic: f32, roughness: f32, reflectance: f32 }
@group(3) @binding(0) var<uniform> params: Params;
@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
var s = default_surface(in);
s.metallic = params.metallic;
s.roughness = max(params.roughness, 0.045);
s.reflectance = params.reflectance;
return shade_pbr(in, s);
}
- A
[params]key named for one of six texture slots and given an image path binds that slot:albedo,normal,metallic_roughness,occlusion,emissive,height. - A slot left out gets a one-pixel neutral texture.
- The editor's library ships four materials over one such shader.
Drawing only when asked
[window] low_processor = true makes a window draw a frame only when something asks for one. Input asks, and so do ui.request_repaint() and a repaint scheduled with ui.request_repaint(#{ after: seconds }).
Work that finishes on another thread wakes it when it reports, and so do a log line and a changed script. Between frames no script runs and nothing is drawn, and a browser tab checks every 16 ms. It suits a tool or a turn-based game. The editor runs this way; a game that moves every frame leaves it off.
[window]
low_processor = true
Limits
- A frame draws at most 64 2D lights. Each shadow-casting one costs a pass.
- A scene past that is told once in the log.
- The 3D contract carries sixteen lights in its frame uniforms.