Lighting a scene
Orblit lights are stated in real photometric units, and the camera has a real exposure. That’s more to learn than an arbitrary 0–1 brightness, and it pays for itself the first time a value transfers in from somewhere else.
The four kinds
Section titled “The four kinds”return OrblitLight( key: 1, kind: OrblitLightKind.directional, direction: Vector3(-0.4, -1, -0.6)..normalize(), intensity: 100000, // lux);| Kind | Unit | A realistic value |
|---|---|---|
directional |
lux | 100,000 for full daylight, 400 for heavy overcast |
point |
lumens | 1,600 for a bright domestic bulb, 450 for a dim one |
spot |
lumens | As above, concentrated into a cone |
area |
lumens | As above, off one face of a rectangle |
Filament honours one directional light per scene. A second one gets
reported back through onSceneNotes rather than silently dropped, so a scene
that has quietly lost its second sun will say so.
An area light is a window, a softbox or a strip in a ceiling. width and
height are its size in metres, direction is the face it lights from, and
tangent is the edge width runs along, since a strip light on its side is a
different light. Filament has no area light, so Orblit shades this one in the
surface material. It doesn’t count against the 256 point and spot lights a
view shades, but a view shades 16 rectangles at most, and the scene notes say
when there are more. Only the first rectangle that asks to castShadows gets
a shadow, and below Filament’s feature level 3 none do. The
Panel shadows example has one.
Exposure
Section titled “Exposure”The camera is a camera:
return OrblitCamera( position: Vector3(4, 3, 6), target: Vector3.zero(), aperture: 16, // f/16 shutterSpeed: 1 / 125, // seconds sensitivity: 100, // ISO);Those defaults are the “sunny 16” rule, and they’re correct for a scene lit at 100,000 lux. Between them they decide how bright the image is, exactly as they would on a real camera.
The practical upshot: a black scene has two ends you can fix it from. Either the lights are too dim, or the exposure is set for daylight and you’re lighting a room. An interior at 300 lux wants ISO 800 and f/2.8, not a sun turned down to 300.
Point and spot detail
Section titled “Point and spot detail”return OrblitLight( key: 2, kind: OrblitLightKind.spot, position: Vector3(0, 4, 0), direction: Vector3(0, -1, 0), intensity: 1600, falloffRadius: 10, // metres, beyond which it contributes nothing innerConeAngle: 0.5, // radians, full brightness inside this outerConeAngle: 0.6, // radians, nothing outside it. The gap is the // soft edge between the two.);falloffRadius is a culling distance as much as a physical one. It’s what
lets the renderer decide a light can’t affect an object without shading it, so
setting it far larger than the light actually reaches costs you performance
and buys you no picture.
sourceRadius controls how soft the shadow is. A physically larger source
casts a softer shadow, which is why a strip light’s shadows are soft and a
bare bulb’s are hard. Only area and soft shadows read it, and
the two drew the same picture on Filament 1.76, which hasn’t been measured
again on 1.77. The default, sharp, ignores it.
Image-based lighting
Section titled “Image-based lighting”A single sun on a black background looks like a single sun on a black background. What makes a render look photographed is all the light coming from everywhere else: the sky, the ground, the walls.
return OrblitScene( camera: OrblitCamera(position: Vector3(4, 3, 6), target: Vector3.zero()), objects: const [], environment: const OrblitEnvironment( radiance: '/path/to/env_ibl.ktx', skybox: '/path/to/env_skybox.ktx', intensity: 30000, // lux rotation: 0.5, // radians, to turn the environment ),);Both files are baked from an equirectangular .hdr or .exr by Filament’s
cmgen. The engine’s tool/bake_environment.sh runs it at the sizes the
renderer itself would choose:
tool/bake_environment.sh env.hdr outThat writes out/env_ibl.ktx and out/env_skybox.ktx.
Straight from the picture
Section titled “Straight from the picture”If you’d rather not bake, name the picture and the renderer filters it while the scene runs:
return OrblitScene( camera: OrblitCamera(position: Vector3(4, 3, 6), target: Vector3.zero()), objects: const [], environment: const OrblitEnvironment.fromImage( '/path/to/env.hdr', // or .exr, or a name given to OrblitResources intensity: 30000, ),);That costs something once per picture. On an M4 Pro with a 2K picture, the
decode takes about 70 ms for an .hdr and 150 ms for a ZIP-compressed .exr,
off the drawing thread, then there’s one frame of a few milliseconds to upload
it and one frame of 20 to 55 ms to filter it. In a browser the decode runs on a
Web Worker. Naming the same picture again filters nothing. Compared with a
cmgen bake of the same picture, the frame is a third of a level out on
average.
Until it’s ready, the scene is lit by its flat ambient, and anything wrong with
the picture (missing, damaged, too big for the device, not twice as wide as it
is tall) turns up in the scene notes under environment and skybox. A device
that can’t filter on the GPU filters a smaller one on the CPU.
So which? Bake what ships: it’s fixed before anyone runs it, and a launch reads two small files and filters nothing. Name the picture when you can’t know it in advance, such as an HDR a player picked, or while you’re still trying environments on.
Shadows
Section titled “Shadows”Shadows are per-light, and off by default on populations, because a hundred thousand shadow casters is rarely what anyone had in mind.
OrblitLight(..., castShadows: true)OrblitObject(..., castShadows: true, receiveShadows: true)Quality is set on the scene’s pipeline rather than per light. kind chooses
the filter, from sharp, soft, area or variance, and cascades splits
the directional light’s shadow map by distance, so more of it gets spent on
what’s near the camera:
return OrblitScene( camera: OrblitCamera(position: Vector3(4, 3, 6), target: Vector3.zero()), objects: const [], pipeline: OrblitPipeline( shadows: OrblitShadows( kind: OrblitShadowKind.soft, cascades: 4, // more of the shadow map spent near the camera mapSize: 2048, ), ),);Authoring in artist units
Section titled “Authoring in artist units”orblit_light exists for the other direction. An artist states a fitting in
watts, metres and degrees, and it converts that to the lumens, lux and radians
the renderer takes.
Use it whenever a light is authored rather than computed. Doing the conversion separately in the editor and in the runtime is how the two end up disagreeing, which is why both of them call this package instead of each doing the sum.
