sampleLight function
SampleLight.
Returns null for a light that contributes nothing, which is the n_dot_l <= 0 early-out in AccumulateLights.
Implementation
LightSample? sampleLight(ShaderBindings bindings, int index, Surface s) {
// A light past the eighth comes from the frame's light list — `gfx-74n`. The
// row holds the same four vectors the slot arrays do, in the same order, so
// everything below this reads one shape.
final fromList = index >= kMaxLights;
final slot = index - kMaxLights;
// `L6`: from the cell, when the draw reads one; a light the slots already
// hold is skipped by its intensity.
final fromCell = fromList && clustered(bindings);
final row = !fromList
? -1
: fromCell
? clusterRow(bindings, s.world, slot)
: lightListRow(bindings, slot).round();
final position = fromList
? lightListTexel(bindings, row, 0)
: bindings.vec4('FragInfo', 'light_position', Vector4.zero(), at: index);
final color = fromList
? (lightListTexel(bindings, row, 1)
// The intensity and not the colour, for `_pack`'s own reason: the
// same multiply, and only one of them is a number nobody authored.
..w *= fromCell
? (inSlots(bindings, row) ? 0.0 : 1.0)
: lightListScale(bindings, slot))
: bindings.vec4('FragInfo', 'light_color', Vector4.zero(), at: index);
final direction = fromList
? lightListTexel(bindings, row, 2)
: bindings.vec4('FragInfo', 'light_direction', Vector4.zero(), at: index);
Vector4 coneOf() => fromList
? lightListTexel(bindings, row, 3)
: bindings.vec4('FragInfo', 'light_cone', Vector4.zero(), at: index);
// A plugin's light type (codes from 16 up) leaves first: its own hook
// packed the row and its own lighting model reads it — `SampleLight`'s
// branch of the same name.
if (position.w > 15.5) return null;
// **A rectangle leaves before the aim is normalised — `gfx-77n`.** For every
// other kind `direction.xyz` points somewhere and its length means nothing;
// for this one the length *is* half the panel's width, and normalising would
// throw the size away. Transcribed from the branch of the same name in
// `SampleLight`.
if (position.w > 2.5) {
final cone = coneOf();
final halfWidth = Vector3(direction.x, direction.y, direction.z);
final halfHeight = Vector3(cone.x, cone.y, cone.z);
final toCentre = Vector3(position.x, position.y, position.z) - s.world;
final corners = <Vector3>[
toCentre - halfWidth - halfHeight,
toCentre + halfWidth - halfHeight,
toCentre + halfWidth + halfHeight,
toCentre - halfWidth + halfHeight,
];
// One face emits: a point on the other side gets nothing, the test
// `SampleLight` makes before the form factor.
if (toCentre.dot(halfWidth.cross(halfHeight)) >= 0.0) return null;
final formFactor = rectangleFormFactor(corners, s.normal);
if (formFactor <= 0.0) return null;
final area = halfWidth.cross(halfHeight).length * 4.0;
var radiance = area > 1e-9 ? 1.0 / area : 0.0;
// The range window only: a panel twice as far away subtends a quarter of
// the sky, so the inverse square is already inside the form factor.
if (direction.w > 0.0) {
final ratio = toCentre.length / direction.w;
final window = (1.0 - ratio * ratio * ratio * ratio).clamp(0.0, 1.0);
radiance *= window * window;
}
final mirror = s.normal * (2.0 * s.normal.dot(s.view)) - s.view;
final representative = rectangleClosestPoint(
Vector3(position.x, position.y, position.z),
halfWidth,
halfHeight,
s.world,
mirror,
);
final toPoint = representative - s.world;
final pointDistance = toPoint.length;
final l = pointDistance > 1e-6
? (toPoint..scale(1.0 / pointDistance))
: s.normal.clone();
final h = (l + s.view)..normalize();
return (
direction: l,
radiance: Vector3(color.x, color.y, color.z) * (color.w * radiance),
nDotL: formFactor,
nDotH: math.max(s.normal.dot(h), 0.0),
vDotH: math.max(s.view.dot(h), 0.0),
ltc: switch (bindings.textures['ltc_texture']) {
final table? => ltcRectangle(
table,
s.normal,
s.view,
s.roughness,
corners,
),
null => null,
},
corners: corners,
);
}
final aim = Vector3(direction.x, direction.y, direction.z);
final aimLength = aim.length;
if (aimLength > 1e-6) aim.scale(1.0 / aimLength);
Vector3 toLight;
var lightAttenuation = 1.0;
if (position.w < 0.5) {
// Directional: the direction to the light is the reverse of the one it
// points.
toLight = -aim;
} else {
toLight = Vector3(position.x, position.y, position.z) - s.world;
final distance = toLight.length;
if (distance < 1e-6) return null;
toLight.scale(1.0 / distance);
lightAttenuation = attenuation(distance, direction.w);
}
// The spot cone: a smooth ramp between the two cosines, transcribed from
// `SampleLight` in surface.glsl. The Dart side guarantees the denominator is
// non-zero.
//
// Three lines above this there used to be a comment saying spot cones were
// not transcribed, left behind when they were. This file's header promises
// that where it departs from the GLSL it says so, which is only worth
// anything if a departure it names is one it has.
if (position.w > 1.5) {
final cone = fromList
? lightListTexel(bindings, row, 3)
: bindings.vec4('FragInfo', 'light_cone', Vector4.zero(), at: index);
final cosAngle = aim.dot(-toLight);
final ramp = ((cosAngle - cone.y) / (cone.x - cone.y)).clamp(0.0, 1.0);
// Squared, as `surface.glsl`'s and KHR_lights_punctual's falloff is.
lightAttenuation *= ramp * ramp;
}
final half = (toLight + s.view)..normalize();
final nDotL = math.max(s.normal.dot(toLight), 0.0);
if (nDotL <= 0.0) return null;
return (
direction: toLight,
radiance: Vector3(color.x, color.y, color.z) * (color.w * lightAttenuation),
nDotL: nDotL,
nDotH: math.max(s.normal.dot(half), 0.0),
vDotH: math.max(s.view.dot(half), 0.0),
ltc: null,
corners: null,
);
}