A light is a part with no shape: named, placed in the tree, carried with its parent, whose aspect is the light it throws. A lantern's light sits where its bulb is, but it is not the bulb: the bulb is the glowing shape (its material's emission), the light is what lights the ground, the walkway and whatever stands near, in their own colors. Lights are placed in one of two ways: at a place (a point or spot light under a lantern) or by a direction (the sun and moons, which walking never moves). This page covers both, light layers, shadows, the world's lamp level and how a light's strength follows it, and what the renderer offers.
Words used on this page:
- Candela
- A light's strength in a direction (a candle is about one). Point and spot lights are given in candela.
- Lux
- Light falling on a surface; the sun's is given in lux (about 100 000 at noon).
- Nits
- A glowing surface's brightness (Materials).
- Range
- How far a point light reaches before it fades to nothing.
- Shadow map
- A picture of the scene drawn from a light, recording how far the light reaches in each direction; a point beyond that is in shadow.
- Exposure
- How much the camera's picture is brightened: like a real camera, it opens up at night, so a bulb that reads bright at night must be far dimmer than at dusk.
How you write it
A light at a place
part lanternHead
part bulb : filament(size), material: materials.lanternBulb, castsShadow: false // the bulb is the light
part light: pointLight(color: color(1, 0.14, 0.01), source: 2 cm, reach: 5 m,
castsShadow: true),
at: (0, 0.335, 0)
part inner: pointLight(color: lampColor, source: 2 cm, reach: 0.35 m,
layers: [interior]),
at: (0, 0.335, 0)
derive(light.intensity, materials.lanternBulb.glow * cdPerNit * gain)
derive(inner.intensity, materials.lanternBulb.glow * 0.006 candelaPerNit * gain)
The lantern's real light, 33.5 cm up at the filament's middle, a source 2 cm in radius, reaching 5 m, in a deep gold, casting shadows, on the world layer (every light's, unless it names others); and a second light at the same point, the lamp's own color, reaching 35 cm, on the interior layer, which lights only the lantern's own frame, roof underside and floor. Each light's intensity is the bulb material's brightness times a fixed number of candela for each nit (cdPerNit, 0.45 on a path lantern), times the lantern's nearness gain. Both start dark.
A spot light
part cottage
part windowLight: spotLight(color: roomColor, source: 0.3 m, reach: 6 m, // a design: no at
aim: (0, -0.5, 1), cone: 1.2 rad, softEdge: 0.7 rad)
field share : number = 1 // own: this window's share
field cdPerNit: candelaPerNit = 3.1 candelaPerNit, common: true
derive(intensity, materials.lanternBulb.glow * cdPerNit * share)
part leftBayLight : windowLight, at: (-1.46, 1.99, 0.12) // just in front of the glass
part rightBayLight: windowLight, at: (1.46, 1.99, 0.12)
part doorLight : windowLight(share: 0.8), at: (0, 2.0, 0.17)
part wingLight : windowLight(share: 1.4), at: (-3.415, 2.0, 0.12) // three tall windows
part cornerLight : windowLight(share: 0.4), at: (-4.67, 2.06, 0.12) // the narrow one
Five spot lights just in front of the cottage's windows, in the cottage's frame, each shining out and down onto the deck or the ground, casting no shadow. Each one's intensity is 3.1 candela for each nit of the bulb's glow, times its window's share: the two bays give all of it, the door's glazing 0.8, the wing's three tall windows together 1.4, and the narrow corner window 0.4. The lit rooms behind the glass glow at 0.4 of the bulb (Materials), a separate number.
A light's parameters
Every argument of pointLight. reach has no default, so it may be given by position (it is declared first) or by name; every other argument has a default and is named. Each is also a field of the light's part, which a derive may follow (derive(light.intensity, ...)).
| Argument | Type, default | What it is | The lantern's light | The window light |
|---|---|---|---|---|
reach | meters | how far it reaches before it fades to nothing; must be given | 5 m | 6 m |
color | color, white | the light's color, linear | color(1, 0.14, 0.01) | roomColor, (1, 0.25, 0.028) |
intensity | candela, 0 candela | its strength; 0 is dark | derived: glow × 0.45 candelaPerNit × gain | derived: glow × 3.1 candelaPerNit × share |
source | meters, 0 m | the radius of the glowing source; softens its highlights and shadows | 2 cm | 0.3 m (the glass) |
castsShadow | bool, false | whether it asks for shadows (Shadows) | true | false |
layers | List[layer] (names), [world] | which surfaces it lights (Light layers) | [world] | [world] |
spotLight takes all of these, and three more (cone has no default, so it may be given by position after reach):
| Argument | Type, default | What it is | The window light |
|---|---|---|---|
cone | angle | the angle from the axis at which the light ends; must be given | 1.2 rad (69°) |
aim | direction, down | the way its axis points, in its part's frame; any length | (0, -0.5, 1): out of the window and down |
softEdge | angle, 0 deg | how far inside the cone's edge it begins to fade: full strength out to cone - softEdge, none beyond cone | 0.7 rad: full within 0.5 rad (29°) |
The world's lamp level
field lampLevel: fraction = 0, within: 0 ..= 1, mutable: true // the world's: lit windows follow it too
on(dusk, { start(transition(lampLevel, to: 1, over: world(15 min), ease: linear,
from: current, begin: dusk.due)); })
on(dawn, { start(transition(lampLevel, to: 0, over: world(15 min), ease: linear,
from: current, begin: dawn.due)); })
on(created, { // start: a world arriving after dusk
let h = hour(now(world));
if h >= sky.duskHour || h < sky.dawnHour { set(lampLevel, 1); } else { set(lampLevel, 0); }
})
field bulbBrightness: curve = curve( // a bulb's radiance at each evening hour
points: [(16.975, 3600 nits), (17.475, 2800 nits), (17.725, 1600 nits), (17.975, 800 nits),
(18.475, 160 nits), (18.975, 24 nits), (19.475, 3 nits), (19.975, 1.3 nits)],
between: logarithmic)
fn eveningHour(h: number) number { // a morning hour mirrored about noon:
if h < 12 { 24 - h } else { h } // the sun stands as high at 7:00 as at 17:00
}
derive(materials.lanternBulb.glow, lampLevel * bulbBrightness.at(eveningHour(hour(now(world)))))
The lamps come on at dusk and go off at dawn, each fading over a quarter of an hour of the world's day, world(15 min): on a world whose day lasts 24 minutes, as the lanterns' world's does, that is 15 seconds of the scene's clock (Values: the day length). Each fade is timed from the time the message was for, not the time it was read: begin: dusk.due starts the fade in the past if the message is read late. Each switch starts a linear transition on the level at the moment the switch was for; the newest takes over from where the level has got to. The level is the world's, not the lanterns', because the lit windows follow it too. A world arriving after dusk or before dawn starts with the lamps on; the hours are the sky's, sky.duskHour and sky.dawnHour, worked out from the world's date and latitude (Sky and weather). The bulb's brightness then follows the hour: about 2800 nits at dusk, falling to 1.3 nits from 20:00, held in a curve of eight evening hours blended on a log scale (bulbBrightness), because the exposure rises about 5000-fold over the evening; a morning hour is read as the evening hour mirrored about noon, when the sun stands as high. eveningHour is the world's own two-line function for that mirror: hour(m) gives a number of hours from 0 to 24 (Values: moments), and an hour before noon, 7.0, becomes 24 - 7 = 17.0. When a changed glow is actually sent to the renderer is the engine's business (Materials: delivery). One value lights every lamp: every light reads the one glow.
Nearness
part pathLantern: lanternHead // the head holds nearGain (1.45) and gain
field near: bool = false, lives: client // each visitor's own (derived: not mutable)
part post: stake(), material: materials.lanternBlack
area(self, sphere(radius: 4 m, at: (0, 0.2, 0)))
derive(near, occupied(post, by: me))
on(created, {
start(chase(gain, toward: if near && lampLevel > 0 { nearGain } else { 1 },
speed: rate(nearGain - 1, per: sim(0.4 s))));
})
Come within 4 m of a lit path lantern and its glass, its bulb and both its lights brighten to 1.45 times over 0.4 s; walk away and they dim back. The chase is started for each lantern as it is created, its goal moved when nearness or the lamp level changes it.
Whose nearness. The brightening is each visitor's own: occupied(post, by: me) is true on a visitor's machine while that visitor's being is within the area (Physics: areas), and near, computed per visitor, has lives: client. So each visitor sees the lantern beside them brighten, on their own machine, and nothing is sent. Written occupied(post), it would be true while any being is inside, and every visitor would see a lantern brighten whoever stood by it.
A light by direction: sun and moons
part sun : sunLight(direction: sunDirection(latitude: latitude, day: date, hour: hour(now(world))),
color: sky.sunColor, lux: sky.sunLux * weather.sun)
part moon: sunLight(direction: moonDirection(sun: sun.direction), color: color(0.80, 0.92, 1.10),
lux: sky.moonLux * weather.sun) // a separate part
The sun and the moons are separate parts placed by a direction that may change over time; walking never moves them, and two suns may light a scene together. (The stars turn about the pole with the camera at their center, so they are placed round the camera, at: around(camera, rotation: world); Frames.) The names a world writes are sunDirection(latitude:, day:, hour:) and moonDirection(sun:); they bind the analyticdaylight package's sunDirectionAt(latitude, declination, hour) and moonDirectionFor(sun). The sky answers the sun's color and light and the moon's light from its model of the air (sky.sunColor, sky.sunLux, sky.moonLux); the weather's sun share dims them. The sun's light and the sun's disc in the sky are two parts, and so are the moon's. The sky's drawing, the discs and the numbers are on Sky and weather.
What it means
A light is a part
A light is placed, parented, carried and removed like any part; only its aspect differs from a shaped part's. Its description is a kind, place, direction, color, candela, range, radius, cone, layers and shadow; sent again, it changes the light. Whether a light is a part, an aspect of the glowing part ("the bulb is a light of 2 cm, reaching 5 m"), or derived from a glowing material is open.
Light layers
A light reaches a surface only when they share a layer. world is the built-in layer: every light and every material is on it unless it says otherwise. A light names its layers with layers: [...], and a material with the modifier litByLayers([...]) (Materials); either replaces world with the list, so a list that wants world as well names it. Any other name used in layers: or litByLayers(...) is a layer of this world: it needs no declaration, and the same name means the same layer everywhere in the world.
part inner: pointLight(color: lampColor, reach: 0.35 m, layers: [interior]) // interior only part lanternHead: [shiny(...), litByLayers([world, interior])] // lit by both part lanternMetal: shiny(...) // world only, by default
So the lantern's interior light lights the head's frame, roof and floor and not the grass a meter away, which is on world alone; the bracket, of the same bronze but on world alone, is not lit from inside. The sun lights every layer. The browser maps the names to its own bits (the renderer offers 24); content never states a bit.
Shadows
Two switches, one on each side. castsShadow: true is an argument of a light: the light casts shadows (point and spot lights cast none unless asked). castsShadow: false is an option of a drawn part: the part throws no shadow from any light (every drawn part casts one unless it says so).
part light: pointLight(color: color(1, 0.14, 0.01), reach: 5 m, castsShadow: true) part bulb : filament(size), material: materials.lanternBulb, castsShadow: false // the bulb is the light part plate: tracery(size), material: materials.lanternHead, castsShadow: false // so the light inside reaches the ground
The bulb casts none, so as not to shadow its own light; nor does the lantern's tracery, whose shadow from a point light a few centimeters from the bars was a hard sunburst on the ground. Point and spot lights share a budget of shadow maps: "A map holds 'maxViews' layers (24 at most): a spot light takes one and a point light six", ranked by range squared over one plus distance squared, with a bonus for a light shadowed last frame; "a light that does not get one lights unshadowed". The sun's shadows are cascades, 4 of 2048 at the high tier, out to 120 m. That budget is the engine's: castsShadow: true asks for shadows, and the nearest lights get them.
Light color, not surface color
The light's color is the mix of wavelengths it gives off; what is seen is that times the surface's reflectance. In the lantern example the ground light is a deeper gold than the lamp (1, 0.14, 0.01 against 1, 0.397, 0.095, about 2700 K), because grass reflects mostly green and a light as green as the glass made a yellow-green pool. The content compensates for the grass and the tone map; whether it should state the physical light and let the engine answer for the look is open (Materials).
Where a light's values are worked out
A light's intensity is a formula of the clock, the lamp level and a lantern's gain. Every browser could work it out for itself. The nearness gain is a chase, which remembers where it got to, so two machines that saw the condition change at slightly different times would disagree slightly. The lantern says whose it is: its near has lives: client, each visitor's own (Whose nearness), so the gain is worked out on each visitor's machine and never sent. The threshold for sending anything that is sent is the engine's.
The sun's light and the exposure
The renderer has one directional light, the sun's kind, and the ambient light. The directional light is the sun while its light is the greater, else the moon; the ambient is the sky's average; and the exposure follows the sun's height: EV 15 with the sun high, falling to EV -1 at night. In a place with no sun, such as a labyrinth lit by braziers and lamps, the exposure is fixed (EV 6). Two suns at once, as the model allows, is not yet possible: the renderer has one directional light.
The exposure curve is the engine's. It is not content, so a world does not write it, but its numbers are these. The exposure is a camera's, set in EV (exposure value; each step up halves the light the camera takes in) from the sine of the sun's height above the horizon, straight between these points:
| The sun's height | -18 deg or lower | -12 deg | -6 deg | 0 deg | 10 deg | 30 deg or higher |
|---|---|---|---|---|---|---|
| EV | -1 | 4 | 8 | 12 | 14 | 15 |
Half a stop is taken off the EV (the picture is exposed half a stop brighter than the meter, so the tone mapping's roll-off keeps a sunlit white from going gray), and the weather's ev opens the camera by that many stops (the exposure is multiplied by 2 to the power ev): a dull sky has a larger ev so the picture is not left dark. On top, an automatic exposure adjusts slowly within a band of 1.5 stops darker to 0.75 stops brighter, aiming at the day's own look by the same height, and counting 0.4 of the weather's ev. At night the light is the moon's, but the exposure still follows the sun's height, not the moon's, so the night stays dark.
The render package's lights
| Kind | Description |
|---|---|
| Sun (directional) | a direction, a color as irradiance in lux, an ambient radiance in nits |
| Point | addPoint(position, color, candela, range) |
| Spot | addSpot(position, direction, color, candela, range, innerAngle, outerAngle) |
| Changes | move, aim, setColor, setIntensity, setRange, setSourceRadius (a bulb: 0.015 m), setCone, setLayers, setCastsShadow (off to start) |
Lights are shaded by clustered forward shading (the view divided into 16 by 9 by 24 cells, each listing the lights that reach it), with a windowed inverse-square falloff, up to 1024 lights, 64 in a cell. There is no area light. Fog does not scatter the point lights. The effectsfx package adds short-lived lights for effects (a muzzle flash) into the same set.
How it connects
- Materials: emission, light layers on materials, light and surface color.
- Sky and weather: the sky's drawing, the weather's dimming of the sun.
- Animators:
transition,chase,beginin the past. - Events:
dusk,dawn,created. - Frames: placed and by-direction kinds.
- The lanterns, written out.
Open questions
- A light: a part, or an aspect of the bulb? A part of its own; an aspect of the glowing part; or derived from a glowing material, its strength from the glow.
- Layers as requirements. Layers are now named in content (Light layers, proposal); whether a requirement ("the frame is lit from within") should one day replace them, the engine choosing the layers.
- A light bound to an effect: whether a brazier's light may follow its flame's flicker.
- Physical or perceived color for lights.
- Several suns: the model allows them; the renderer has one directional light.
← Textures and surface patterns | Contents | Sky and weather →