lens-flare
lens-flare
LENS-FLARE — the one accent tag that is not a mesh, and what it is FOR.
Read the rig's notes
Scene 01 is coverage.promo's cv07 moved here unchanged (ids renamed cv07-* →
lf01-*, haze match keys kept): the tag plus its four optic variants on an
empty stage. Scenes 02-11 are the part cv07 could not carry inside a
42-scene registry sweep — the flare doing its actual job, BEHIND things:
behind a device, behind a laptop lid, behind <ui3d> panels, behind a flat
<ui> billboard, on a device's own glass, and finally the same composition
twice, once without and once with.
Scenes 12-16 are the ATTACHED form, and the attribute they demo is new:
every anchor in 02-11 is a hand-solved world coordinate, which is fine until
the subject moves. 12-16 name a corner of another node instead.
16 scenes, 90s of scene plus fourteen 0.7s match-move gaps — 99.8s on
the wall. The three lobes hz0/hz1/hz2 ride every scene, so the
reel is one continuous room. Scene starts, for capturing beats:
lf01 0 · lf02 3.0 · lf03 9.2 · lf04 14.4 · lf05 19.6 · lf06 25.3 ·
lf07 30.5 · lf08 36.2 · lf09 42.4 · lf10 49.6 · lf11 56.3 · lf12 64.0 ·
lf13 72.7 · lf14 80.4 · lf15 87.1 · lf16 93.8.
01 variants prism · aperture · orbs · anamorphic (= cv07)
02 not occluded THE rule: the core over the subject erases it
03 behind a phone one blade + one prism off the silhouette
04 behind a lid two anchors rimming a yawing macbook
05 behind ui3d flares in the GAPS between three real panels
06 behind ui a flat <ui> page raked from off its corner
07 on the glass a tiny flare parked on a screen as a specular hit
08 edge fade three anchors at ndc 1.07 / 1.46 / 1.78
09 the modules spokes · ghosts · halo · hex · streak, isolated
10 live channels intensity keyframes, size does not
11 without / with the payoff — one composition, the light switched on
12 attach·corner offset=0 on all four corners, then offset=18
13 rides the rig attached vs hand-solved, through the same 42° yaw
14 bounds·screen an iMac's stand vs its glass
15 offset·depth 0 / 70 / 70 + behind, on a laptop lid
16 any node three <ui3d> panels, three glints, three attributes
WHAT THE TAG ACTUALLY IS. The node is only an ANCHOR: a transform that can
be positioned, keyframed and match-moved like anything else. The picture is
a FULLSCREEN additive overlay whose core is placed each frame by projecting
the anchor's world position through the live camera (layer-adapters/
lens-flare.ts). Four anchors therefore give four flares in one frame at no
extra pass, and the whole optic rides the camera for free.
THE FIVE THINGS THAT BITE, in the order they bite:
1. IT IS NEVER OCCLUDED. blending=Additive, depthTest=false,
renderOrder=9990 — the overlay is drawn last, over everything, and no
geometry can hide it. "Behind the phone" is therefore a statement about
where the ANCHOR sits, never about draw order. Park the core ON a
subject and you erase that subject (scene 02 does it on purpose); park
it just OFF the silhouette and the blade, ghosts and halo rake across
it, which is the entire look. Scene 02 is the A/B.
2. THE CHAIN MIRRORS THROUGH SCREEN CENTRE. Ghosts, hex ghosts and the
orb chain are all placed along the light→centre axis. An anchor at dead
centre collapses every one of them onto the core, so the flare reduces
to a sun. Off-centre is not a taste call — it is what makes the optic
visible at all (scene 09 anchors off-centre for exactly this reason).
3. THE ANCHOR MAY LEAVE THE FRAME, and should. Edge fade is
clamp(1 - (|ndc| - 1.1) / 0.7) per axis: full inside 1.1, half at ~1.45,
gone by 1.8. So an anchor a frame-width out of shot still lays a tail
across the picture with no core anywhere — the cheapest cinematic
upgrade in the vocabulary, and scene 08 measures it.
4b. IT PAINTS THE IMAGE, NOT THE SCENE. The overlay is added after shading,
so by itself a flare puts no highlight on the casing it sits beside —
which is most of why a bright flare over an unlit product reads as a
sticker. Two opt-in knobs close that, both off by default, both riding
the same keyframed `intensity` as the optic, and both demoed in scene 12:
light="true" a PointLight on the anchor in the flare's own colour
(light-intensity / -distance / -decay / -color). Defaults
are decay=0 + a 600mm cutoff because three's falloff is
1/d^decay in WORLD units, and in a millimetre world a
decay=2 light near a phone needs intensity in the tens of
thousands — the same d² trap that makes the corpus's
`type=point intensity=1.8` lights no-ops.
veil="0..1" the whole-frame lift a real barrel scatters, tinted by
`color`. This is precisely what the soft-clip curve was
shaped to PREVENT (stray tails greying a dark scene), so
it is an explicit term you scale, not that bug back.
4. `size` IS NOT A CHANNEL AND `scale` DOES NOTHING. `size` zooms the whole
optic but is read once at build; the anchor's `scale` is ignored outright
(the overlay's vertices are clip-space). The two things that move are
`intensity` (userData.setLensFlareIntensity) and `opacity` (read back off
the material every frame). Scene 10.
5. THE CORE IS UNCONDITIONAL. `spokes=0` does not remove the sun — it
removes the 12-spoke shimmer and the starburst frill only; the core term
and its bloom cushion always draw. Scene 09 shows each module isolated
and that residual core is why every panel there still has a bright dot.
AND ONE THAT DOES NOT BITE: a lens-flare cannot be nested inside a device
tag. <iphone>/<macbook>/<ipad>/<imac>/<chrome> only recurse into <ui> and
<ui3d overlay> children — anything else in a device body is dropped. Put the
anchor in the scene next to the device and keyframe the two together, which
is what scenes 03, 04 and 07 do — or `attach` it, which is what 12-16 do and
what you should reach for now.
`attach` + `corner` — THE ANSWER TO ALL OF THE ABOVE
lens-flare#glint(attach=hero corner=top-right offset=26 depth=-40)
`attach` names any node in the scene and `corner` names a corner of ITS OWN
bounding box, measured in ITS OWN local frame. The anchor mounts as a child
of that node, so the target's position, rotation and scale carry it and the
author never writes a world coordinate — which matters because a hand-solved
coordinate is not wrong when you write it, it just stops being right the
moment the subject moves, and nothing in the document says so (scene 13 is
that A/B). It is deliberately the target's LOCAL box, not a world AABB: a
world AABB of a yawed phone is bigger than the phone and its "top-right" is
a corner of empty space.
corner the `origin` vocabulary — top-left · top · top-right · left ·
center · right · bottom-left · bottom · bottom-right (+ tl/tr/…)
offset SIGNED mm along that corner's own diagonal. THE craft knob:
positive is outward, so one number means "clear of the
silhouette" whichever corner was named, i.e. rule 1 as an
attribute. At offset=0 the anchor IS the corner — scene 12's
first beat parks four pinpoints on a phone's four corners to
show exactly that, because "18" only means something once you
can see what it is 18 out from. (A bounding box around a rounded
body sits a hair outside the visible silhouette, so a zero-offset
core reads as touching the corner, not as inset from it.)
NEGATIVE pulls inward instead, toward the box centre, which is
how you park a small dim core just INSIDE a corner so it reads as
a reflection on the glass rather than a light beside the device —
scene 07's specular idiom, without the hand-solved coordinate.
It is a true distance and nothing clamps it, so a negative larger
than the half-diagonal walks out the opposite side; and on
corner=center the direction vector is zero, so offset does
nothing at all.
depth mm along the target's local z — negative parks the anchor behind
it, which is scene 04's whole idea without the arithmetic
bounds "union" (default, the whole subtree) or "screen" (the live
display mesh only). Not cosmetic: an OPEN laptop's union box is
the deck's full width by the lid's full height, so its top-left
is a corner of air. Scene 14 measures an iMac both ways.
mesh / mesh_contains narrow the measured box to named GLB meshes
offset/depth are the target's own local mm, so they scale with it — a
phone at scale 2 carries a 26mm offset 52mm out, keeping the glint in the
same place relative to the phone. The flare's own position/x/y/z stay live
as a further nudge in that same frame, keyframes included, because the
corner is carried by an intermediate group rather than baked into the node.
Implementation: parsed in core/xml-scene.ts promoLensFlareNode → `flare_anchor`,
solved in engine/lib/flare-anchor.ts, mounted by promo-runtime's
_anchorLensFlares — a deferred post-loop pass, like <decal> and surface
`attach`, so the target's GLB has loaded whatever the authoring order.
Palette (coverage's, so cv07 still looks like itself): background top
#0b1020 → bottom #05070f; lobes #24406e / #3a2a6e / #1b5a68 at 0.34-0.46.
Ink #eef3ff, mute #93a4c8, accent #66e0ff, warn #ffb454, ok #7dffc4,
bad #ff7a8a. Variant tints are the adapter's own defaults: prism #a47edd,
aperture #ffc98a, orbs #9fd8ff, anamorphic #86b7ff.
Frame geometry: every scene is fov=42 at z≈765 and no camera pushes closer
than 760, i.e. ±522 x ±294 mm at z=0. Titles ride y=234, subs y=202, beat
labels y=-240 — far enough in that a dolly cannot clip them. Devices are
true physical size scaled by `scale`, so a phone at 2.0 is 319mm and fills
~54% of frame; a <macbook> is only 237.8mm tall but its DECK runs forward in
z, so it eats far more frame than its height suggests — 0.85 is a hero, not
1.5, and anything larger parks its keyboard over the beat label.
Preview: /packages/runtime/index.html?project=promos/primitives/lens-flare/lens-flare.promo- Topic
- lights
- Scenes
- 16
- Kind
- reference rig
//
LENS-FLARE — the one accent tag that is not a mesh, and what it is FOR.
Scene 01 is coverage.promo's cv07 moved here unchanged (ids renamed cv07-* →
lf01-*, haze match keys kept): the tag plus its four optic variants on an
empty stage. Scenes 02-11 are the part cv07 could not carry inside a
42-scene registry sweep — the flare doing its actual job, BEHIND things:
behind a device, behind a laptop lid, behind <ui3d> panels, behind a flat
<ui> billboard, on a device's own glass, and finally the same composition
twice, once without and once with.
Scenes 12-16 are the ATTACHED form, and the attribute they demo is new:
every anchor in 02-11 is a hand-solved world coordinate, which is fine until
the subject moves. 12-16 name a corner of another node instead.
16 scenes, 90s of scene plus fourteen 0.7s match-move gaps — 99.8s on
the wall. The three lobes hz0/hz1/hz2 ride every scene, so the
reel is one continuous room. Scene starts, for capturing beats:
lf01 0 · lf02 3.0 · lf03 9.2 · lf04 14.4 · lf05 19.6 · lf06 25.3 ·
lf07 30.5 · lf08 36.2 · lf09 42.4 · lf10 49.6 · lf11 56.3 · lf12 64.0 ·
lf13 72.7 · lf14 80.4 · lf15 87.1 · lf16 93.8.
01 variants prism · aperture · orbs · anamorphic (= cv07)
02 not occluded THE rule: the core over the subject erases it
03 behind a phone one blade + one prism off the silhouette
04 behind a lid two anchors rimming a yawing macbook
05 behind ui3d flares in the GAPS between three real panels
06 behind ui a flat <ui> page raked from off its corner
07 on the glass a tiny flare parked on a screen as a specular hit
08 edge fade three anchors at ndc 1.07 / 1.46 / 1.78
09 the modules spokes · ghosts · halo · hex · streak, isolated
10 live channels intensity keyframes, size does not
11 without / with the payoff — one composition, the light switched on
12 attach·corner offset=0 on all four corners, then offset=18
13 rides the rig attached vs hand-solved, through the same 42° yaw
14 bounds·screen an iMac's stand vs its glass
15 offset·depth 0 / 70 / 70 + behind, on a laptop lid
16 any node three <ui3d> panels, three glints, three attributes
WHAT THE TAG ACTUALLY IS. The node is only an ANCHOR: a transform that can
be positioned, keyframed and match-moved like anything else. The picture is
a FULLSCREEN additive overlay whose core is placed each frame by projecting
the anchor's world position through the live camera (layer-adapters/
lens-flare.ts). Four anchors therefore give four flares in one frame at no
extra pass, and the whole optic rides the camera for free.
THE FIVE THINGS THAT BITE, in the order they bite:
1. IT IS NEVER OCCLUDED. blending=Additive, depthTest=false,
renderOrder=9990 — the overlay is drawn last, over everything, and no
geometry can hide it. "Behind the phone" is therefore a statement about
where the ANCHOR sits, never about draw order. Park the core ON a
subject and you erase that subject (scene 02 does it on purpose); park
it just OFF the silhouette and the blade, ghosts and halo rake across
it, which is the entire look. Scene 02 is the A/B.
2. THE CHAIN MIRRORS THROUGH SCREEN CENTRE. Ghosts, hex ghosts and the
orb chain are all placed along the light→centre axis. An anchor at dead
centre collapses every one of them onto the core, so the flare reduces
to a sun. Off-centre is not a taste call — it is what makes the optic
visible at all (scene 09 anchors off-centre for exactly this reason).
3. THE ANCHOR MAY LEAVE THE FRAME, and should. Edge fade is
clamp(1 - (|ndc| - 1.1) / 0.7) per axis: full inside 1.1, half at ~1.45,
gone by 1.8. So an anchor a frame-width out of shot still lays a tail
across the picture with