Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

ambientOcclusion system

Screen-space ambient occlusion — the contact darkening in corners, creases and where objects meet.

Without it, every concave region is lit exactly like a flat one: a box resting on a floor has no seam, a corner is as bright as the wall, and an untextured scene reads as plastic. Occlusion is the cheapest shading cue there is per millisecond, because it needs no extra geometry pass — only the depth and normal the frame already produced.

Three techniques are shipped. All three answer the same question — what fraction of the hemisphere above this pixel is visible — and all three cost the same taps to answer it.

Using it

The scene's occlusion is a component — author it once and it persists with the scene:

local id = entity.spawn("ambient_occlusion").id
entity(id).component.add("AmbientOcclusion", {
    technique = "gtao",   -- ssao | hbao | gtao
    intensity = 0.9,      -- how far the darkening goes, 0..1
    radius = 1.5,         -- world-space reach
    quality = "high",     -- low | medium | high | ultra
})

Every field is live: edit one in the inspector and the shading follows. Setting intensity to 0 turns it off without losing the rest.

For code that adjusts occlusion at runtime — a quality slider, a cutscene — reach for the module directly (same options, no component):

local ao = require("@builtin::systems.ambientOcclusion.ao")

ao.set({ technique = "hbao", intensity = 0.9 })  -- any subset; the rest is kept
ao.get()                                         -- the settings in force
ao.techniques()                                  -- the three, each with what it does
ao.qualityLevels()                               -- the sampling shape each level costs
ao.active()                                      -- is the pass running
ao.clear()                                       -- off, settings kept

The three techniques

techniquewhat it measures
ssaoTaps scattered over a disk, each weighed on its own: how far the neighbour rises above the tangent plane, over how far away it is.
hbaoOne horizon per direction — a ray marched outward until it finds the steepest thing along it — averaged as sines.
gtaoThe cosine-weighted arc between the two horizons of a slice, in closed form. This is the visibility the other two estimate.

They are scaled to agree. A point in a right-angle corner sees exactly half the hemisphere, and each technique is normalised so it reads that corner as half:

techniqueflat planeright-angle cornerocclusion
ssao1.0000.5660.434
hbao1.0000.5920.408
gtao1.0000.5940.406

That is what lets intensity mean the same thing whichever technique is selected, so switching one for another changes how the estimate is made rather than how dark the scene gets.

What the knobs do

fieldeffect
techniqueWhich estimator runs: ssao | hbao | gtao.
intensityHow far the darkening goes, in [0, 1]. 0 turns it off and releases the pass.
radiusWorld-space reach — roughly "how big is a crease worth darkening". Constant in world space, so it does not swell as the camera approaches.
powerContrast curve on the visibility term. Above 1 deepens the dark end.
thicknessWorld-space thickness assumed for an occluder, so a thin foreground object stops occluding everything behind it out to the full radius.
biasHow far a neighbour must rise above the surface before it counts as an occluder rather than as the surface itself, in [0, 1]. Raise it if a flat surface shades itself in patches; lower it if fine creases disappear.
qualitySampling shape. See the table below.
qualityslices × stepstaps per pixelresolution
low2 × 312half
medium3 × 424half
high4 × 648full
ultra6 × 896full

Every technique spends the same budget: slices × steps × 2 taps. gtao walks each slice both ways, hbao treats each of the two directions as its own ray, and ssao takes that many independent taps of the disk.

The lower two levels compute occlusion on a half-resolution grid — a quarter of the pixels, each still costing its level's taps. Occlusion is low-frequency almost everywhere, and the filter that removes the sampling noise lifts the coarse grid back to the frame in the same pass, weighting each texel by the depth where it sits so silhouettes survive the upsample. What a half-resolution grid loses is the innermost pixel of a sharp crease; everything wider than a couple of pixels comes back the same.

How it renders

One compute pass after lighting writes the per-pixel visibility, a second filters it and multiplies it into the frame, and a blit puts the result on screen. The filter is depth-aware: a plain blur would bleed occlusion across silhouettes, so each tap is weighted by how close its depth is to the centre pixel's.

Every technique jitters its sampling by a per-pixel offset, which trades banding for noise; the filter is the half of that trade that removes the noise.

Depth, normal and camera come from the render target being drawn (@scene.depth, @scene.normal, @frame.camera), so the result is correct in offscreen captures and render-to-texture cameras at their own resolutions, not only the live viewport. Sky pixels are passed through — there is no surface to occlude.

The normal buffer is the deferred G-buffer, so this needs the deferred path (the default).

Seeing the occlusion on its own

While occlusion is running, ambientOcclusion is a capture view: the filtered visibility with no scene colour, white where the hemisphere is open and dark where it is closed.

capture { pass = "ambientOcclusion" }

It is the fastest way to tell a shading problem from an occlusion one, and to compare two techniques or two radius values without the lighting in the way.

Files

ambientOcclusion.package/
  AmbientOcclusion.component/   -- the scene's occlusion, authored and persisted
  ao.module/                    -- ao.set / .get / .techniques / .qualityLevels / .active / .clear
  aoShared.module/              -- the prelude, filter and passes the techniques share
  ssao.renderFeature/           -- taps weighed on their own
  hbao.renderFeature/           -- one horizon per direction
  gtao.renderFeature/           -- the arc between two horizons