All examples

Ambient occlusion

Lighting & GI3D
  • Mouse
  • Touch
  • Gamepad

Ground-truth ambient occlusion (GTAO) estimates how much of the sky each pixel can see and darkens the environment's light by it. Crates stacked into a corner are lit by a studio probe and one sun. Only indirect light is occluded, so the sun is untouched. It works with or without rendering.hdr; the settings are read at start, so changing one reloads.

Off airLivePaused

Ambient occlusion

Lighting & GI · 3D

A corner of two pale walls and a concrete floor with wooden crates stacked against both walls, soft shading where the crates meet the walls and the floor, and three white balls on the floor in front.
Open standalone

WebGPU: checking…

Try this

  1. Turn the sun off, then switch ambient occlusion off and on: the crates float, then settle.
  2. Raise the radius to two metres and watch the shading spread across the walls.
  3. Compare Half and Full quality on the crease where the stacked crates meet the wall.
Show source code
main.ts
import { Camera, Environment } from "ignifx";import { bootExample } from "../_kit/boot.ts";import { metres, twoPlaces } from "../_kit/format.ts";import { attachOrbit } from "../_kit/orbit.ts";import { bind, readout, slider, toggle } from "../_kit/panel.ts";import { queryChoice, queryFlag, queryNumber, reloadSelect, reloadSlider, reloadToggle } from "../_kit/reload.ts";import { loadEnvironment } from "../_kit/stage.ts";import { createCorner, FOCUS, SHOT, START_EXPOSURE } from "./scene.ts";/** * Ground-truth ambient occlusion (GTAO) on the environment's light. * * `rendering.ambientOcclusion` is a project setting. With it on, a compute pass reads the scene's * depth and normals, estimates for each pixel how much of the hemisphere above it is blocked, and * a material plugin multiplies every lit PBR material's `occlusion` by the result. `occlusion` only * scales **indirect** light — the environment's diffuse and specular — so direct lights and * emissive surfaces are untouched, and a scene with no `Environment` cube map shows no difference * at all. That is why this one is lit by the studio probe, with one sun you can switch off. It * works with and without `rendering.hdr`; this example leaves HDR off. * * The record is read once, when the app starts, so every row in its group reloads the frame. * `quality: "half"` traces at half resolution (two slices of four steps a side); `"full"` at full * resolution with three slices of six. `radius` is how far, in metres, an occluder counts, and * `intensity` is an exponent on the visibility: above one the creases darken faster. *//** The ambient-occlusion record, from the query; the defaults are the engine's own. */const AO = {  enabled: queryFlag("ao", true),  quality: queryChoice("quality", ["half", "full"] as const, "half"),  radius: queryNumber("radius", 0.5, 0.05, 3),  intensity: queryNumber("intensity", 1, 0, 4),} as const;bootExample({  title: "Ambient occlusion",  settings: {    rendering: {      clearColor: { r: 0.05, g: 0.055, b: 0.065, a: 1 },      features: { shadows: true },      ambientOcclusion: AO,    },  },  async setup({ app, panel }) {    const eye = app.world.createEntity("Main Camera");    eye.addComponent(Camera, { near: 0.05, far: 100, fov: SHOT.fov });    attachOrbit(app, eye, {      yaw: SHOT.yaw,      pitch: SHOT.pitch,      distance: SHOT.distance,      target: FOCUS,      minDistance: 1.5,      maxDistance: 12,    });    // The probe is what ambient occlusion darkens; without one there is nothing for it to act on.    const studio = loadEnvironment(app, "studio");    await studio.promise;    const sky = app.world.createEntity("Environment").addComponent(Environment, { environment: studio });    sky.imageProcessing.exposure = START_EXPOSURE;    sky.imageProcessing.toneMapping = "aces";    const sun = createCorner(app);    panel({      title: "Ambient occlusion",      groups: [        {          label: "Ambient occlusion",          controls: [            // `rendering.ambientOcclusion` is read once, at start: each row reloads.            reloadToggle("Enabled", "ao", AO.enabled),            reloadSelect("Quality", "quality", ["half", "full"], AO.quality),            reloadSlider("Radius", "radius", { min: 0.1, max: 2, step: 0.05, format: metres }, AO.radius),            reloadSlider("Intensity", "intensity", { min: 0, max: 3, step: 0.1, format: twoPlaces }, AO.intensity),          ],        },        {          label: "Lighting",          controls: [            toggle("Sun", bind(sun, "enabled")),            slider("Exposure", { min: 0.3, max: 3, step: 0.05, format: twoPlaces }, bind(sky.imageProcessing, "exposure")),            slider("Probe rotation", { min: 0, max: 360, step: 5 }, bind(sky, "rotation")),          ],        },        {          label: "Frame",          collapsed: true,          controls: [readout("Draw calls", (): string => String(app.renderer.drawCalls))],        },      ],    });  },});
scene.ts
/** * The corner `ambient-occlusion` looks at: two walls, a floor and a stack of crates pushed against * them, which is to say a scene made of creases. * * @remarks * Ambient occlusion is about how much of the sky a point can see. Out in the open it sees half of * it; in the crease between a crate and a wall it sees a sliver. The environment probe lights every * point as if it saw all of it, which is what makes a scene lit only by a probe look flat and its * objects float. GTAO estimates the sliver from the depth buffer and darkens the probe's light by * it — and only the probe's light: a direct light still reaches into the crease unchanged. */import { Light, MeshAsset } from "ignifx";import { paint, place } from "../_kit/props.ts";import type { Point3 } from "../_kit/stage.ts";import type { App, ColorLike } from "ignifx";/** The point the camera orbits: the corner, a little above the floor. */export const FOCUS: Point3 = { x: -0.6, y: 0.7, z: 0.6 };/** The opening shot. */export const SHOT = { fov: 42, yaw: 32, pitch: 22, distance: 6.4 } as const;/** The exposure the example opens on. */export const START_EXPOSURE = 1.3;/** One crate: where it stands (its base centre), its edge length, and its turn about Y. */interface Crate {  readonly at: Point3;  readonly size: number;  readonly yaw: number;}/** The crates, against the back wall (+Z) and the left wall (-X), some stacked. */const CRATES: readonly Crate[] = [  { at: { x: -1.95, y: 0, z: 1.95 }, size: 1, yaw: 0 },  { at: { x: -1.95, y: 1, z: 1.95 }, size: 0.8, yaw: 8 },  { at: { x: -0.75, y: 0, z: 2.05 }, size: 0.8, yaw: -4 },  { at: { x: -2.05, y: 0, z: 0.75 }, size: 0.8, yaw: 3 },  { at: { x: 0.35, y: 0, z: 2.15 }, size: 0.6, yaw: 12 },  { at: { x: -2.15, y: 0, z: -0.35 }, size: 0.6, yaw: -10 },  { at: { x: 0.2, y: 0, z: 0.6 }, size: 0.5, yaw: 30 },];/** Half the floor's side, and where the two walls stand, in metres. */const ROOM = 2.5;/** How tall the walls are, in metres. */const WALL_HEIGHT = 3.4;/** The crate colours, cycled. */const WOOD: readonly ColorLike[] = [  { r: 0.62, g: 0.46, b: 0.3, a: 1 },  { r: 0.55, g: 0.42, b: 0.3, a: 1 },  { r: 0.7, g: 0.56, b: 0.38, a: 1 },];/** * Builds the corner, the crates, a few round things on the floor, and the sun. * * @param app - The app the entities and the assets belong to. * @returns The sun, which the panel switches. */export function createCorner(app: App): Light {  const box = MeshAsset.box(app, { size: 1 });  const plaster = paint(app, "ao/plaster", { r: 0.82, g: 0.8, b: 0.76, a: 1 }, { roughness: 0.92 });  const concrete = paint(app, "ao/concrete", { r: 0.6, g: 0.6, b: 0.58, a: 1 }, { roughness: 0.85 });  place(app, "Floor", box, concrete, {    at: { x: 0, y: -0.05, z: 0 },    scale: { x: ROOM * 4, y: 0.1, z: ROOM * 4 },    castShadows: false,  });  place(app, "Back wall", box, plaster, {    at: { x: 0, y: WALL_HEIGHT / 2, z: ROOM + 0.05 },    scale: { x: ROOM * 2, y: WALL_HEIGHT, z: 0.1 },  });  place(app, "Left wall", box, plaster, {    at: { x: -ROOM - 0.05, y: WALL_HEIGHT / 2, z: 0 },    scale: { x: 0.1, y: WALL_HEIGHT, z: ROOM * 2 },  });  // A skirting step along both walls, a crease the camera always sees.  place(app, "Step", box, plaster, {    at: { x: 0.6, y: 0.08, z: ROOM - 0.1 },    scale: { x: ROOM * 2 - 1.2, y: 0.16, z: 0.2 },  });  const woods = WOOD.map((colour: ColorLike, index: number) =>    paint(app, `ao/crate-${String(index)}`, colour, { roughness: 0.75 }),  );  CRATES.forEach((crate: Crate, index: number) => {    place(app, `Crate ${String(index + 1)}`, box, woods[index % woods.length] ?? concrete, {      at: { x: crate.at.x, y: crate.at.y + crate.size / 2, z: crate.at.z },      scale: { x: crate.size, y: crate.size, z: crate.size },      rotation: { x: 0, y: crate.yaw, z: 0 },    });  });  const sphere = MeshAsset.sphere(app, { diameter: 1, segments: 32 });  const ceramic = paint(app, "ao/ceramic", { r: 0.86, g: 0.86, b: 0.86, a: 1 }, { roughness: 0.4 });  for (const [x, z, diameter] of [    [0.9, -0.4, 0.7],    [1.5, 0.5, 0.45],    [-0.9, 0.2, 0.35],  ] as const) {    place(app, "Ball", sphere, ceramic, {      at: { x, y: diameter / 2, z },      scale: { x: diameter, y: diameter, z: diameter },    });  }  // A low sun from the open side. Ambient occlusion leaves its light alone, so turning it off is  // the clearest view of what the effect does.  const sunEntity = app.world.createEntity("Sun", { position: { x: 4, y: 6, z: -3 } });  sunEntity.transform.lookAt({ x: -1, y: 0, z: 1 });  const sun = sunEntity.addComponent(Light, {    type: "directional",    intensity: 1.2,    color: { r: 1, g: 0.94, b: 0.84, a: 1 },  });  sun.shadows.enabled = true;  sun.shadows.technique = "pcf";  sun.shadows.mapSize = 2048;  sun.shadows.normalBias = 0.02;  sun.shadows.darkness = 0.2;  return sun;}
Uses
rendering.ambientOcclusionEnvironmentLight.enabledMeshAsset
Assets
Studio environment — CC-BY 4.0, Babylon.js contributors