Rendering
Most of how a scene looks comes from its content — meshes, materials, and lights. This guide is about the scene-wide controls on top of that: lighting, post-process effects, and the renderer's global…
Lighting
The lighting toolbox is the scene's light environment — sun, ambient, sky, and point/spot lights, each with modify-or-spawn semantics. When you're setting a scene up interactively from execute(), reach for the tools; a component or runtime script that adjusts lighting each frame uses the modules.api.engine.lighting capability directly (same opts, no tool call):
-- authoring the scene's lighting from execute():
tools.use("lighting", "setSun", { direction = { -0.5, -1, -0.3 }, intensity = 3, color = { 1, 0.95, 0.8 } })
tools.use("lighting", "setAmbient", { intensity = 0.2 })
tools.use("lighting", "setSky", "sunset") -- preset word, material word, or "none"
tools.use("lighting", "addLight", "torch", { x = 2, y = 3, z = 0 }, { intensity = 5 })
tools.use("lighting", "setLight", "torch", { intensity = 8 })
tools.use("lighting", "removeLight", "torch")
tools.use("lighting", "setup", opts) -- sun + ambient + sky + clearColor at once
tools.use("lighting", "get") -- full lighting + sky state, feeds back into setup
Lighting is real: with every source removed the scene goes dark. Lights also exist as a component (entity.component.add("Light", { intensity = 5 })) for ones that move with an entity — the lighting surface is the scene-environment one, the Light component is the per-entity one.
The sky is an explicit component on an entity — ProceduralSky for the procedural day/night look, or Skybox for a material sky or kind = "none" to turn it off. A scene with no sky entity renders the engine's fallback sky and carries a missing-sky warning; tools.use("lighting", "setSky", ...) creates (or reconfigures) the scene's sky entity and clears the warning.
Global illumination — baked lightmaps & light probes
The lights above are direct light. Indirect light — bounce, soft ambient, contact shadows — comes from the @builtin::systems.globalIllumination system, which bakes global illumination offline so static scenes get realistic indirect lighting at no per-frame cost. Two flavours:
-
Lightmaps — baked indirect light stored per-entity, for static geometry. Add the
Lightmapcomponent, then bake:tools.use("lightmap", "attach", entity) -- add the Lightmap component (sets bake params) tools.use("lightmap", "bakeAll") -- bake every entity in the scene that carries a Lightmap tools.use("lightmap", "bake", entity) -- re-bake just one; clear frees its resources -
Light probes — baked indirect light sampled through a volume, for dynamic entities moving through a baked space. Author a
VolumeProbe, bake it, and bind movers (which carry aLightProbeReceiver):tools.use("volumeProbe", "create", opts) -- spawn a probe-volume entity tools.use("volumeProbe", "bakeAll") -- bake every VolumeProbe in the scene tools.use("volumeProbe", "bind", entity) -- a dynamic entity samples indirect light from the volume
The full system (bake settings, denoising, UV requirements, the math) is its own package — guides / asset.inspect("@builtin::systems.globalIllumination") and lsp.methods("lightmap") / lsp.methods("volumeProbe") cover it in depth. ReflectionProbe (per-entity specular reflections) is a sibling component.
Post-process
Full-screen effects run after the scene renders — bloom, tonemap, colour grading, your own. Each is a shader (the shaders guide) registered as a named pass:
postprocess.add("bloom", bloomShaderSource, opts)
postprocess.setEnabled("bloom", true)
postprocess.setParam("bloom", slot, values)
postprocess.list() -- also setTexture / setSampler / remove
Render features — custom GPU passes (outlines, x-ray, wireframe, decals)
A render feature inserts your own GPU passes into the frame, every frame — the
generic capability behind mesh outlines, x-ray, custom wireframe, decals, and
any effect the built-ins don't cover. A feature is a .renderFeature asset whose
render(ctx) hook calls ctx.enqueue{...} to queue passes; the engine runs them
at a named pipeline phase. Three pass kinds: "fragment" (full-screen shader),
"compute" (dispatch), and "geometry" (re-draw scene meshes with your pipeline).
A geometry pass is the one for mesh effects — it redraws scene render-objects with a material you construct, and can target specific entities:
ctx.enqueue {
kind = "geometry",
material = myMatKey, -- a material from renderer.material.create
geometry = "@geometry.opaque", -- "@geometry.opaque" | "@geometry.transparent" | "@geometry.all"
select = { entityA, entityB }, -- OPTIONAL: only these entities (refs); omit = all
phase = "afterLighting", -- lit scene + valid depth
}
select restricts the draw to the given entities — what turns a whole-scene pass
into a chosen-meshes effect. The material decides how those meshes are drawn,
and you control three things:
- Render-state.
renderer.material.createtakes arenderblock —cull(back/front/none),depthWrite,blend— so the pass rasterizes differently from the surface pass.propertiesseeds the shader's declared uniforms (a flat{ name = value }map, matching the names inproperties.yaml; a colour is a{r,g,b,a}array).texturesbinds its declared texture slots. There is nocolors/floatssplit — that is the mat.yaml authoring shape, not this runtime call:renderer.material.create({ shader = myShaderKey, -- a .shader you authored (identity, not guid) render = { cull = "none", depthWrite = false }, properties = { my_color = { 1, 0.45, 0, 1 } }, -- seeds material.my_color }, "my_pass_mat") - The shader. A geometry
.shaderis a surface shader:fn vertex(v: VertexData) -> VertexDatamoves vertices (v.position/v.normal, object space);fn fragment(...) -> vec4<f32>is the colour. - Its properties. A shader reads ONLY the uniforms it declares in
properties.yaml, viamaterial.<yourProp>— and bothvertex()andfragment()can read them (the generatedmaterialuniform is bound for both stages). Referencing one it didn't declare (e.g.material.base_color) fails to compile and renders magenta; the compile error in the engine log names the field and the fix (declare it, or use a constant). (See the shaders guide.)
What effect that produces — outline, x-ray, wireframe, highlight — is yours to design from those pieces; the engine names none of them.
The legacy
visuals.setOutline/Model:setOutline/sc.outline(and theEntityVisuals.outlinefield) are deprecated — they predate render features and draw nothing. A mesh outline is a render-feature geometry pass.
The full feature contract (setup / render / teardown, ctx, every phase,
the @scene.* / @frame.camera / @geometry.* names) is in the renderFeature
assetType README. Reserved inputs resolve per render target, so a screen-space
feature that reconstructs world space from depth via @frame.camera is correct in
the live viewport AND in offscreen captures (capture(source = "position"/"entity"))
from a different camera.
Scene-wide renderer settings
The global pipeline switches — shadows, deferred shading, order-independent transparency, culling, debug channel — live in the world's .world_settings file under [render], not in a runtime API. Edit them there (the engine/development guides cover world settings):
[render]
shadows = true
deferred = true
oit = true
culling_mode = "gpu"
debug_channel = 0 # > 0 visualises normals / depth / etc. while diagnosing
Per-camera overrides live on the Camera component — debugChannel and the renderLayers spec (a space-separated list of layer names, e.g. "all !ui", where ui / sky / postProcess / EditorUI are built-in layers) decide what that camera draws (the render-textures guide uses this for secondary views).
Finding the rest
lsp.methods("lights") and lsp.methods("postprocess") list those surfaces in full; asset.inspect("@builtin::components.Camera") shows the camera's render fields. For rendering into a texture (mirrors, minimaps, in-UI 3D) see the render-textures guide; for the shaders these passes run, the shaders guide. The model to hold: content makes the look, lights/postprocess tune the scene at runtime, and .world_settings [render] sets the pipeline.