Builtin Library
README.md
postprocess Module
Public Luau surface over the __postprocess Internal FFI namespace —
fullscreen post-process effect registration and control.
Purpose
Wrap the raw __postprocess.* FFI namespace in a typed Luau table
that gets auto-injected as _G.postprocess via the prelude. add,
setProperty and setTexture report what the chain did with the call —
the engine applies the mutation on the caller's own stack, and the
boolean is its answer; the compiler's message and the reason for a refusal
are in the engine log. A call made from inside queue() or batch() is
applied later and answers for the queued request.
add is the one call that puts a pass into the frame. A post-process
.shader asset defines an effect; registering it here is what makes it render,
and remove takes it back out while leaving the asset in place. Editing a
registered shader's body recompiles its pass in place — same enabled state,
priority, layer, tuned property values and bound textures, new pixels.
The display pass
"__display__" is always in the chain, between the scene layer and the
all layer — status() lists it with layer = "display". It carries two
properties: curve (0 linear clamp, 1 Reinhard, 2 ACES, 3 Khronos PBR
Neutral — the default) and exposure (default 1, a multiplier on the
scene's light before the curve runs). Read it with
postprocess.describe("__display__") and write it with
postprocess.setProperty("__display__", "curve", 2); remove("__display__")
and setEnabled("__display__", false) are both refused, so a scene's picture
always reaches the screen through some exposure and some curve. A
PostProcess component's display field is the load-time authoring of the
same two properties.
Usage
local vignette = asset.resolve("@builtin::shaders.post.vignette", "shader")
-- No layer: the vignette reads light, so the chain runs it on `scene`, over
-- the rendered picture before the UI reaches it.
postprocess.add("vignette", vignette, { priority = 200 })
postprocess.setProperty("vignette", "intensity", 0.6)
-- Later
postprocess.setEnabled("vignette", false)
postprocess.remove("vignette")
-- Inspect
for _, name in ipairs(postprocess.list()) do print(name) end
-- Read one effect in full: its chain state, the schema it declared,
-- and the value each of those properties holds right now.
local e = postprocess.describe("vignette")
print(e.enabled, e.priority, e.layer, e.values.intensity)
for _, p in ipairs(e.properties) do print(p.name, p.type, p.min, p.max) end
WGSL text is accepted in place of an asset reference, and then opts.properties
declares the whole property schema; this form is deprecated and will be
removed, and logs a WARN naming the effect each time it is used — author the
effect as a .shader asset and pass that reference instead:
postprocess.add("fade", [[
// @domain: post-process
fn fragment(in: PostInput) -> vec4<f32> {
let c = zero_sample_scene(in.uv);
return mix(c, vec4<f32>(0.0, 0.0, 0.0, c.a), material.amount);
}
]], { properties = { { name = "amount", type = "float", default = 0.0 } } })
add's options
opts is optional; every field of it is:
| Field | Default | What it does |
|---|---|---|
priority | 100 | Chain order — lower runs first. |
enabled | true | false registers the effect dormant, for a later setEnabled. |
layer | the layer the effect's space belongs to — light → "scene", display → "all" | Which composited image the effect grades. "scene" runs it before the UI is drawn, over the scene's own light in a float target, so it grades the rendered picture and leaves every widget on screen as authored; "all" runs it after the UI has landed, over the picture the display transform fitted to the screen, so the interface is graded along with the picture. A threshold, a blur or a grade reads the scene's highlights only on "scene" — on "all" nothing stands above 1.0. Naming "all" is what an effect that must grade the interface says, and on a world several authors share it grades everyone else's screens too. |
space | the asset's own space ("light" where the asset declares none) | "light" for a threshold, blur or grade that works on the scene's linear radiance; "display" for a lookup table, dither ladder or authored overlay that works on the __display__ pass's code values. Overrides what the shader asset declares for this one registration; zero_sample_scene (and its alias zero_scene_light) answers in whichever space this names, and the chain converts at both ends where the target it runs against holds the other space. |
properties | the asset's own schema | { { name, type, default?, min?, max?, textureDefault? } }. With a .shader asset this layers over the schema the asset declares — an entry naming a property the shader declares replaces it, one naming a new property adds it. With WGSL text it is the whole schema. |
passes | the asset's own passes (empty where the asset declares none) | { { name, entry, scale?, inputs? } }. Each pass names an entry in the shader, the fraction of the view it draws at (absent on the last pass, which draws at the view's size), and what it reads ("scene" — the chain's input for this effect — or the name of an earlier pass). With a .shader asset this defaults to what that asset's getPasses() declares; with WGSL text it is the whole declaration; a single-pass effect, the common case, leaves this empty. |
shader | — | The .shader asset key the source came from. add fills this in itself when it is handed an asset reference, so editing that shader recompiles the effect in place. |
textureDefault is what a type = "texture" property samples while nothing is
bound to it: "white" (1,1,1,1 — the default), "black" (0,0,0,1), "normal"
(0.5,0.5,1,1) or "transparent" (0,0,0,0). An effect that lays its texture over
the scene wants "transparent", so the frame is untouched until setTexture
binds a texture that exists.
Reading the chain back
status() is the survey — every registered effect in the order the chain runs
them, with the state that decides whether each one reaches the frame.
describe(name) is the deep read of one of them: the same record, plus
properties (the schema the effect declared) and values (what each of those
properties currently holds — the value the last setProperty wrote, or the
schema default where nothing has written one). A scalar comes back as a number
and a wider value as the array, which is what setProperty takes, so a property
read here is written straight back.
setProperty answers whether the uniform took the value; describe answers what
the effect holds now. That is the pair a pass driving its properties every frame
needs, and it is what tells a mistyped property name from an effect that is not
grading. values is the engine's own record of the effect — every write the
chain accepted into its uniform, layered over the schema's defaults, the same
record /runtime/fx/<name>/meta.json is serialized from. A write the chain
refused is not in it.
Before an effect is registered its schema lives on the .shader asset it will
render — asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties()
names what that shader declares, which is the same set describe reports once
the effect exists.
Exports
postprocess.add(name, shader, opts?) -> boolean, string?— false and the compiler's message when the chain refused the registrationpostprocess.remove(name) -> booleanpostprocess.setEnabled(name, enabled) -> booleanpostprocess.setProperty(name, prop, value) -> booleanpostprocess.setTexture(name, prop, path) -> booleanpostprocess.setSampler(name, opts) -> booleanpostprocess.list() -> { string }postprocess.status() -> { PostprocessStatus }postprocess.describe(name) -> PostprocessDescription?
PostprocessStatus is { name, enabled, priority, layer, space, passes, error?, shader?, pending, textures, pendingTextures }. PostprocessDescription is that
record with properties and values on it. passes names the passes the
effect draws, in order — empty for a single-pass effect.