All examples
HDR bloom
With rendering.hdr on, bloom runs on the linear scene colour before tone mapping and reads intensity, radius and an HDR threshold. Coloured strips at full emissive stand in front of cubes up to sixteen times brighter. Turn HDR off to compare Lite's LDR bloom, which works on tone-mapped colour and reads threshold, weight and kernel instead.
Off airLivePaused
HDR bloom
Lighting & GI · 3D

WebGPU: checking…See browser support
Try this
- Raise HDR threshold past one: the strips stop glowing, the bright cubes keep theirs.
- Drag Radius from 0 to 1 and watch the glow go from a tight halo to a wide haze.
- Turn HDR off and compare the LDR bloom on the same scene.
Show source code
Source
import { Camera, Environment, PostProcessStack } from "ignifx";import { bootExample } from "../_kit/boot.ts";import { twoPlaces } from "../_kit/format.ts";import { attachOrbit } from "../_kit/orbit.ts";import { bind, readout, slider, toggle } from "../_kit/panel.ts";import { queryFlag, reloadToggle } from "../_kit/reload.ts";import { createStudioFloor } from "../_kit/stage.ts";import { CLEAR_COLOR, createEmitters, FOCUS, SHOT, START_HDR_BLOOM, START_LDR_BLOOM } from "./scene.ts";import type { PanelGroup } from "../_kit/panel.ts";/** * HDR bloom: the glow is computed on the linear scene colour, before tone mapping, from values * that can be far brighter than white. * * One `bloom` record on the camera's `PostProcessStack` drives two different passes, and * `rendering.hdr` picks which. With HDR on, a downsample/upsample chain over six `rgba16float` mips * runs right before the output pass and reads `intensity`, `radius` and `hdrThreshold`; the glow is * then exposed and tone mapped with the rest of the frame. With HDR off, Lite's LDR bloom runs after * tone mapping, on colour that never exceeds 1, and reads `threshold`, `weight` and `kernel` * instead. `rendering.hdr` is read when the app is created, so the HDR row reloads the frame, and * the panel shows whichever set of fields this load uses. All of them are live. * * With `hdrThreshold` at 0, the default, nothing is cut: every pixel spreads by its own energy and * `intensity` mixes the result back in, so a strip at white glows a little and a cube at sixteen * times white glows a lot. Raise the threshold past 1 and only the cubes keep a glow. *//** Whether this page load runs the HDR path, and therefore the HDR bloom; `?hdr=0` compares. */const HDR = queryFlag("hdr", true);bootExample({ title: "HDR bloom", settings: { rendering: { clearColor: CLEAR_COLOR, hdr: HDR, features: { postProcessing: true } }, }, async setup({ app, panel, afterStart }) { 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.6, maxDistance: 12, }); const grade = app.world.createEntity("Environment").addComponent(Environment); grade.imageProcessing.exposure = 1; grade.imageProcessing.toneMapping = "agx"; createStudioFloor(app, { color: { r: 0.01, g: 0.012, b: 0.016, a: 1 }, roughness: 0.5, fade: true }); await createEmitters(app); const post = eye.addComponent(PostProcessStack); post.bloom.intensity = START_HDR_BLOOM.intensity; post.bloom.radius = START_HDR_BLOOM.radius; post.bloom.hdrThreshold = START_HDR_BLOOM.hdrThreshold; post.bloom.threshold = START_LDR_BLOOM.threshold; post.bloom.weight = START_LDR_BLOOM.weight; post.bloom.kernel = START_LDR_BLOOM.kernel; afterStart((): void => { post.bloom.enabled = true; }); const hdrBloom: PanelGroup = { label: "HDR bloom", controls: [ slider("Intensity", { min: 0, max: 1, step: 0.01, format: twoPlaces }, bind(post.bloom, "intensity")), slider("Radius", { min: 0, max: 1, step: 0.01, format: twoPlaces }, bind(post.bloom, "radius")), slider("HDR threshold", { min: 0, max: 4, step: 0.05, format: twoPlaces }, bind(post.bloom, "hdrThreshold")), ], }; const ldrBloom: PanelGroup = { label: "LDR bloom", controls: [ slider("Threshold", { min: 0, max: 1, step: 0.02, format: twoPlaces }, bind(post.bloom, "threshold")), slider("Weight", { min: 0, max: 1.5, step: 0.05, format: twoPlaces }, bind(post.bloom, "weight")), slider("Kernel", { min: 8, max: 128, step: 4 }, bind(post.bloom, "kernel")), ], }; panel({ title: "HDR bloom", groups: [ { label: "Bloom", controls: [ // The stack's `enabled`, which reads `true` during `setup` while `bloom.enabled` waits // for `afterStart`. toggle("Enabled", bind(post, "enabled")), reloadToggle("HDR", "hdr", HDR), readout("Pass", (): string => (HDR ? "HDR, before tone mapping" : "LDR, after tone mapping")), ], }, HDR ? hdrBloom : ldrBloom, { label: "Grading", collapsed: true, controls: [ // Under HDR the exposure scales the glow with the frame; without it, it moves what // crosses the LDR threshold. slider("Exposure", { min: 0.2, max: 3, step: 0.05, format: twoPlaces }, bind(grade.imageProcessing, "exposure")), ], }, { label: "Frame", collapsed: true, controls: [ readout("Post-process tasks", (): string => String(post.taskCount)), readout("Draw calls", (): string => String(app.renderer.drawCalls)), ], }, ], }); },});/** * The emitters `hdr-bloom` looks at: a row of coloured strips at full emissive, and behind them * Khronos's cubes running from one to sixteen times past white. * * @remarks * Two rows, because the HDR threshold needs both. A strip written in code tops out at an emissive * of 1 (the material's emissive is sRGB and clamped); the cubes go far past it. With the threshold * at 0 every pixel glows by its energy and the strips glow as much as their brightness earns. Raise * it past 1 and only the cubes still glow — a cut the LDR bloom, working on tone-mapped colour that * never exceeds 1, cannot make. */import { Light, MeshAsset } from "ignifx";import { createEmissiveCubes } from "../_kit/emitters.ts";import { paint, place } from "../_kit/props.ts";import type { Point3 } from "../_kit/stage.ts";import type { App, ColorLike } from "ignifx";/** Black, which the frame is cleared to and the floor fades into. */export const CLEAR_COLOR: ColorLike = { r: 0, g: 0, b: 0, a: 1 };/** The point the camera orbits. */export const FOCUS: Point3 = { x: 0, y: 0.6, z: 0.3 };/** The opening shot. */export const SHOT = { fov: 38, yaw: 0, pitch: 8, distance: 5.2 } as const;/** The strips' colours, left to right, in sRGB at full strength. */const STRIPS: readonly ColorLike[] = [ { r: 1, g: 0.32, b: 0.12, a: 1 }, { r: 1, g: 0.75, b: 0.25, a: 1 }, { r: 0.95, g: 0.95, b: 0.95, a: 1 }, { r: 0.25, g: 0.85, b: 1, a: 1 }, { r: 0.7, g: 0.35, b: 1, a: 1 },];/** Metres between strips. */const STRIP_SPACING = 0.55;/** HDR bloom settings the example opens on. */export const START_HDR_BLOOM = { intensity: 0.35, radius: 0.75, hdrThreshold: 0 } as const;/** LDR bloom settings the example opens on, for the comparison with `hdr` off. */export const START_LDR_BLOOM = { threshold: 0.4, weight: 0.6, kernel: 64 } as const;/** * Builds the floor, the strips, the cubes, two lit props and a quiet key light. * * @param app - The app the entities and the assets belong to. */export async function createEmitters(app: App): Promise<void> { const box = MeshAsset.box(app, { size: 1 }); const dark = { r: 0.05, g: 0.05, b: 0.06, a: 1 }; STRIPS.forEach((colour: ColorLike, index: number) => { const x = (index - (STRIPS.length - 1) / 2) * STRIP_SPACING; place(app, `Strip ${String(index + 1)}`, box, paint(app, `bloom/strip-${String(index)}`, dark, { emissive: colour }), { at: { x, y: 0.55, z: -0.2 }, scale: { x: 0.05, y: 1.1, z: 0.05 }, castShadows: false, }); }); // One long horizontal strip on the floor in front, so the glow has a surface to spill over. place(app, "Floor strip", box, paint(app, "bloom/floor-strip", dark, { emissive: { r: 1, g: 0.55, b: 0.2, a: 1 } }), { at: { x: 0, y: 0.015, z: -0.75 }, scale: { x: 2.6, y: 0.03, z: 0.04 }, castShadows: false, }); await createEmissiveCubes(app, { position: { x: 0, y: 1.25, z: 0.9 }, scale: 0.2 }); // Lit props: the glow spreads over them as it does over anything bright enough to keep. const satin = paint(app, "bloom/satin", { r: 0.5, g: 0.5, b: 0.52, a: 1 }, { roughness: 0.35 }); place(app, "Sphere", MeshAsset.sphere(app, { diameter: 0.5, segments: 32 }), satin, { at: { x: -1.6, y: 0.25, z: -0.6 }, }); place(app, "Block", box, satin, { at: { x: 1.6, y: 0.25, z: -0.6 }, scale: { x: 0.5, y: 0.5, z: 0.5 }, rotation: { x: 0, y: 30, z: 0 }, }); const keyEntity = app.world.createEntity("Key light", { position: { x: -3, y: 4, z: -3 } }); keyEntity.transform.lookAt(FOCUS); keyEntity.addComponent(Light, { type: "directional", intensity: 0.9, color: { r: 1, g: 0.96, b: 0.9, a: 1 } }); app.world.createEntity("Sky").addComponent(Light, { type: "hemispheric", intensity: 0.08 });}