sampleLight function

LightSample? sampleLight(
  1. ShaderBindings bindings,
  2. int index,
  3. Surface s
)

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,
  );
}