shadowFactor function

double shadowFactor(
  1. Surface s,
  2. ShaderBindings b,
  3. int lightIndex,
  4. double lightNDotL, [
  5. FragmentContext? c,
])

Implementation

double shadowFactor(
  Surface s,
  ShaderBindings b,
  int lightIndex,
  double lightNDotL, [
  FragmentContext? c,
]) {
  shadowTransmittance.setValues(1.0, 1.0, 1.0);
  final params = b.vec4('FragInfo', 'shadow_params', Vector4.zero());
  final strength = params.w;
  if (strength <= 0.0) return 1.0;
  final frame = b.vec4('FragInfo', 'frame_params', Vector4.zero());
  if (lightIndex != (frame.z + 0.5).floor()) return 1.0;

  final map = b.textures['shadow_texture'];
  if (map == null) return 1.0;

  // Normal offset: move the sample along the normal before projecting. It
  // fixes acne a depth bias cannot, because that error is proportional to the
  // surface's slope relative to the light rather than to depth. A flat
  // distance plus a texel and a half of the cascade times the sine of the
  // slope along each axis of the map, measured per cascade below, for the
  // reason `shadow.glsl` gives.

  // Which cascade covers this fragment. The mirror of shadow.glsl, and it has
  // to stay one: the parity suite compares this backend's picture against
  // Impeller's, so a difference here reads as a *rendering* bug in whichever
  // one somebody happens to be looking at.
  final cascades = b.vec4('FragInfo', 'shadow_cascades', Vector4(0, 0, 1, 0));
  final count = (cascades.z + 0.5).floor().clamp(1, 3);
  final camera = b.vec4('FragInfo', 'camera_position', Vector4.zero());
  // `P7`: by depth along the axis through an orthographic lens.
  /// How far the fragment is from the eye, in metres.
  final double viewDistance;
  if (orthographic(b)) {
    final eye = b.vec4('FogInfo', 'eye', Vector4.zero());
    final forward = b.vec4('FogInfo', 'forward', Vector4.zero());
    viewDistance =
        (s.world.x - eye.x) * forward.x +
        (s.world.y - eye.y) * forward.y +
        (s.world.z - eye.z) * forward.z;
  } else {
    viewDistance = (s.world - Vector3(camera.x, camera.y, camera.z)).length;
  }
  var cascade = 0;
  if (count > 1 && viewDistance > cascades.x) cascade = 1;
  if (count > 2 && viewDistance > cascades.y) cascade = 2;

  var u = 0.0;
  var vv = 0.0;
  var cascadeIndex = 0;
  Vector3? projected;
  // `S3`: metres per texel across, and per unit of stored depth along the
  // light, of the cascade the fragment lands in.
  var cascadeTexel = 1.0;
  var cascadeDepth = 1.0;
  // The fragment's own plane in stored depth per texel across the map, as
  // `shadow.glsl` explains: what a PCSS tap past the normal offset finds.
  var receiverSlope = 0.0;
  for (var attempt = 0; attempt < 3; attempt++) {
    final which = cascade + attempt;
    if (which >= count) break;

    final matrix = b.mat4(
      'FragInfo',
      which == 0
          ? 'shadow_matrix'
          : (which == 1 ? 'shadow_matrix_far' : 'shadow_matrix_farthest'),
    );
    // One texel of this cascade in metres: the first row of an orthographic
    // projection is 2 / width. The rows are the map's axes in the world, and
    // the normal's share along each is the sine of the slope that way.
    final axisX = Vector3(
      matrix.entry(0, 0),
      matrix.entry(0, 1),
      matrix.entry(0, 2),
    );
    final axisY = Vector3(
      matrix.entry(1, 0),
      matrix.entry(1, 1),
      matrix.entry(1, 2),
    );
    final rowX = math.max(axisX.length, 1e-6);
    final rowY = math.max(axisY.length, 1e-6);
    final tileTexel = cascades.w > 0.0 ? cascades.w : params.x;
    final texelMetres = 2.0 * tileTexel / rowX;
    final reach =
        1.5 *
        2.0 *
        tileTexel *
        (s.normal.dot(axisX).abs() / (rowX * rowX) +
            s.normal.dot(axisY).abs() / (rowY * rowY));
    // The flat part held to a texel of this cascade, as the GLSL explains.
    final flatOffset = math.min(params.z, texelMetres);
    final origin = s.world + s.normal * (flatOffset + reach);
    final Vector4 lightSpace =
        matrix * Vector4(origin.x, origin.y, origin.z, 1.0);
    if (lightSpace.w <= 0.0) continue;
    final candidate = Vector3(lightSpace.x, lightSpace.y, lightSpace.z)
      ..scale(1.0 / lightSpace.w);

    final tileU = candidate.x * 0.5 + 0.5;
    final tileV = 0.5 - candidate.y * 0.5;
    if (tileU < 0.0 || tileU > 1.0 || tileV < 0.0 || tileV > 1.0) continue;
    // Past the far plane is behind every caster: the last cascade clamps
    // rather than calling the point lit — see `shadow.glsl`.
    if (candidate.z > 1.0) {
      if (which < count - 1) continue;
      candidate.z = 1.0;
    }

    u = (tileU + which) / count;
    vv = tileV;
    cascadeIndex = which;
    projected = candidate;
    cascadeTexel = texelMetres;
    cascadeDepth =
        1.0 /
        math.max(
          Vector3(
            matrix.entry(2, 0),
            matrix.entry(2, 1),
            matrix.entry(2, 2),
          ).length,
          1e-6,
        );
    final cosSlope =
        (s.normal
                    .dot(
                      Vector3(
                        matrix.entry(2, 0),
                        matrix.entry(2, 1),
                        matrix.entry(2, 2),
                      ),
                    )
                    .abs() *
                cascadeDepth)
            .clamp(0.1, 1.0);
    receiverSlope =
        texelMetres *
        math.sqrt(math.max(1.0 - cosSlope * cosSlope, 0.0)) /
        cosSlope /
        cascadeDepth;
    break;
  }
  if (projected == null) return 1.0;

  // Each cascade's own bias — `shadow_bias` in `surface.glsl`.
  final biases = b.vec4('FragInfo', 'shadow_bias', Vector4.zero());
  // `A2.8`: how the map keeps its depth, and with half floats turned round
  // the floor at this fragment's own depth — `lib/shadow_storage.glsl`.
  final storage = params.y;
  final bias = storage > 0.5 && storage < 1.5
      ? math.max(biases[cascadeIndex], shadowStoredStep(projected.z))
      : biases[cascadeIndex];
  // Horizontally a texel of the atlas, vertically a texel of a tile.
  final texelU = params.x;
  final texelV = cascades.w > 0.0 ? cascades.w : params.x;
  // Every tap held inside its own cascade's tile — see `shadow.glsl`.
  final loU = cascadeIndex / count + 0.5 * texelU;
  final hiU = (cascadeIndex + 1) / count - 0.5 * texelU;
  final loV = 0.5 * texelV;
  final hiV = 1.0 - 0.5 * texelV;
  double tap(double du, double dv) => shadowStored(
    map.sample((u + du).clamp(loU, hiU), (vv + dv).clamp(loV, hiV)).x,
    storage,
  );

  // What the see-through casters let through, before the filter, whose soft
  // path leaves early — `ShadowSettings.translucentCasters`; `shadow.glsl`
  // has the layout.
  if (biases.w > 0.5) {
    final stored = map.sample(u.clamp(loU, hiU), vv.clamp(loV, hiV));
    final k = strength.clamp(0.0, 1.0);
    double through(double value) => 1.0 + (value.clamp(0.0, 4.0) - 1.0) * k;
    shadowTransmittance.setValues(
      through(1.0 - stored.y),
      through(1.0 - stored.z),
      through(stored.w),
    );
  }

  // `gfx-15n`: the directional light's apparent size, riding in
  // `ambient_ground.w` for the reason `surface.glsl` gives. Zero is the 3×3
  // kernel this has always had.
  // Below zero is the `evsm` filter (`S2`): the texture is the moments
  // atlas, one filtered tap is the kernel, and how far under minus one the
  // value sits is the light-bleeding cut.
  final softness = b.vec4('FragInfo', 'ambient_ground', Vector4.zero()).w;

  var lit = 0.0;
  if (softness < 0.0) {
    final moments = map.sample(u.clamp(loU, hiU), vv.clamp(loV, hiV));
    lit = evsmVisibility(
      moments,
      projected.z - bias,
      (-softness - 1.0).clamp(0.0, 0.95),
    );
  } else if (softness <= 0.0) {
    // PCF 3x3: four samples band visibly at this map size and nine is the
    // smallest kernel that reads as a soft edge rather than as stair steps.
    for (var y = -1; y <= 1; y++) {
      for (var x = -1; x <= 1; x++) {
        final occluder = tap(x * texelU, y * texelV);
        lit += projected.z - bias > occluder ? 0.0 : 1.0;
      }
    }
    lit /= 9.0;
  } else {
    // `S3`: sixteen taps each way on a Vogel disc turned per pixel, and the
    // penumbra in metres — mirroring `shadow.glsl` step for step. The tangent
    // of the angular radius, not twice it: the disc's radius is half the
    // penumbra it sweeps.
    final spread = math.tan(math.min(softness, 0.5));
    final turn = shadowNoise(b, c) * 6.2831853;
    final searchRadius = (spread * projected.z * cascadeDepth / cascadeTexel)
        .clamp(1.0, 16.0);
    var blockerSum = 0.0;
    var blockerCount = 0.0;
    // Each tap allows for the surface's own slope past the normal offset's
    // reach, as `shadow.glsl` explains.
    for (var i = 0; i < 16; i++) {
      final (dx, dy) = vogelDisc(i, 16, turn);
      final reachTexels = math.sqrt(dx * dx + dy * dy) * searchRadius;
      final tapBias = bias + math.max(reachTexels - 1.5, 0.0) * receiverSlope;
      final occluder = tap(
        dx * texelU * searchRadius,
        dy * texelV * searchRadius,
      );
      if (projected.z - tapBias > occluder) {
        blockerSum += occluder;
        blockerCount += 1.0;
      }
    }
    if (blockerCount <= 0.0) return 1.0;

    final averaged = projected.z - blockerSum / blockerCount;
    final gap = (averaged > 0.0 ? averaged : 0.0) * cascadeDepth;
    final radius = (spread * gap / cascadeTexel).clamp(1.0, 16.0);
    for (var i = 0; i < 16; i++) {
      final (dx, dy) = vogelDisc(i, 16, turn + 1.0);
      final reachTexels = math.sqrt(dx * dx + dy * dy) * radius;
      final tapBias = bias + math.max(reachTexels - 1.5, 0.0) * receiverSlope;
      final occluder = tap(dx * texelU * radius, dy * texelV * radius);
      lit += projected.z - tapBias > occluder ? 0.0 : 1.0;
    }
    lit /= 16.0;
  }
  return 1.0 + (lit - 1.0) * strength.clamp(0.0, 1.0);
}