Renderer · function
packIesAtlas
planes rows of intensity per profile, normalised to the profile's peak, indexed by angle.
Explained in Hello world.
function packIesAtlas(profiles: readonly PhotometricProfile[], width?: number):import { packIesAtlas } from '@driftengine/core';Parameters
| Parameter | Type | Description |
|---|---|---|
profiles | readonly PhotometricProfile[] | |
width? | number |
In depth
Resampled to a regular grid here rather than in the reader, because that is a decision about a texture and not about a file: LM-63 is dense where a distribution changes and sparse where it does not, and a shader cannot sample an irregular table without carrying it.
Asymmetric fixtures keep their horizontal planes as of 2026-08-27. This function flattened to the first plane and said in its own comment what to reach for instead — "a row per horizontal plane with the light carrying a plane count" — and that is what this is. A street light and a wall washer are the fixtures that are not axially symmetric, and they are exactly the fixtures whose whole point is that they throw more one way than another.
One plane count for the atlas rather than one per profile, and every profile is padded to
it. The shader's row is profile * planes + plane; a per-profile count would need a second
number in a light's record to divide by, and a light's record is where space is expensive.
Padding a symmetric profile repeats its single row, which costs width floats a plane — half a
kilobyte — against the image an IES profile replaces. A set with nothing asymmetric in it
comes out at one plane and is bit-identical to what this produced before planes existed, which
is what keeps the common case free.
The planes are resampled onto a regular 0-to-360 arc, for the reason the vertical angles are resampled onto a fixed one: the shader maps an azimuth to a plane by arithmetic and carries no per-profile angle list to search. A file measuring 0 and 180 packed into four planes has its measured values at planes 0 and 2 and the interpolation between them at 1 and 3, which is what a fixture symmetric about its own axis actually throws.
A profile occupies planes + 1 rows, and the extra one repeats the first. That is what lets
the shader do the whole azimuth blend in one fetch, with the texture's own linear filter
between adjacent rows: without the wrap row the plane after the last would have to blend back to
the first, which a fractional row cannot express — it would reach into the next profile's
first plane instead. The alternative was two explicit fetches and a mix, which was written,
measured at +18,315 gzipped bytes on core-only across the sixteen fragment permutations,
and replaced by this. One row a profile is 512 bytes.
Normalised to each profile's own peak because the shader multiplies it into a light's colour, which already carries the intensity a consumer chose. A profile that also carried absolute candela would multiply the two and make every IES light either black or blinding. The peak is the profile's, not the plane's: normalising each plane to itself would make every plane reach 1 and throw away the asymmetry this exists to carry.