run method

  1. @override
Vector4? run(
  1. Float32List v,
  2. ShaderBindings bindings,
  3. FragmentContext c
)
override

Returns linear RGBA for attachment zero. Null discards the fragment.

Implementation

@override
Vector4? run(Float32List v, ShaderBindings bindings, FragmentContext c) {
  final scene = bindings.textures['scene_texture'];
  if (scene == null) return Vector4(0.0, 0.0, 0.0, 1.0);
  final params = bindings.vec4(
    'CompositeInfo',
    'params',
    Vector4(1.0, 0.0, 1.0, 0.0),
  );

  final look = bindings.vec4(
    'CompositeInfo',
    'look',
    Vector4(1.0, 1.0, 0.0, 0.0),
  );
  final lookMore = bindings.vec4(
    'CompositeInfo',
    'look_more',
    Vector4(0.0, 0.0, 0.0, 1.0),
  );

  // `P2`: the lens's own bend, before anything is read, so everything laid
  // over the scene bends with it — `Distort` in `composite.frag`. Nought
  // leaves the coordinate the very value it was.
  final lens = bindings.vec4('CompositeInfo', 'lens', Vector4.zero());
  // `P6`: right of a debug view's split the scene holds display values,
  // and only the encode touches them. A return here where the GLSL decides
  // at its end: this rasteriser has no uniform control flow to keep.
  // `A5.22`: lens.y names the sides — one the right, two the left, three
  // both.
  final rightOfWipe = v[0] >= lens.z;
  final showsChannel = switch ((lens.y + 0.5).floor()) {
    1 => rightOfWipe,
    2 => !rightOfWipe,
    >= 3 => true,
    _ => false,
  };
  if (showsChannel) {
    final raw = scene.sample(v[0], v[1]);
    return Vector4(
      toSrgb(raw.x.clamp(0.0, 1.0)),
      toSrgb(raw.y.clamp(0.0, 1.0)),
      toSrgb(raw.z.clamp(0.0, 1.0)),
      raw.w,
    );
  }
  final (u, w) = lens.x != 0.0
      ? _distort(v[0], v[1], lens.x, lookMore.w)
      : (v[0], v[1]);

  // Dispersion at sampling, because that is where a lens does it — see the
  // note in `composite.frag`, which this mirrors operation for operation.
  final sampled = scene.sample(u, w);
  var color = Vector3(sampled.x, sampled.y, sampled.z);
  if (look.w > 0.0) {
    final ox = (u - 0.5) * look.w;
    final oy = (w - 0.5) * look.w;
    color.x = scene.sample(u + ox, w + oy).x;
    color.z = scene.sample(u - ox, w - oy).z;
  }
  // lens.w: a shadow map in the scene slot, shown as its red channel in
  // grey and opaque — `composite.frag` says why the other three cannot be
  // trusted to be the same on every device.
  final shadowMapView = lens.w > 0.5;
  if (shadowMapView) color = Vector3(color.x, color.x, color.x);
  final alpha = shadowMapView ? 1.0 : sampled.w;

  // Occlusion first, then the glow — the order matters and it is the order
  // `composite.frag` uses. Four taps in a 2×2, sized to the artefact rather
  // than tuned against it: the occlusion pass rotates its kernel by the
  // parity of the pixel, and this averages exactly that pattern away.
  //
  // Applied to the scene and **not** to the glow, so a lit crack in a corner
  // keeps glowing. Adding the bloom first and scaling both — which is what
  // the first draft of this function did — dims the one thing in a dark
  // corner that should not dim.
  final ao = bindings.textures['ao_texture'];
  final strength = params.w.clamp(0.0, 1.0);
  // Read once: the occlusion, the contact shadow and the display transform
  // each take a lane of it, and this runs for every pixel of the frame.
  final contactInfo = bindings.vec4(
    'CompositeInfo',
    'contact',
    Vector4.zero(),
  );
  // `R7`: the local exposure, in stops, before anything is added to the
  // scene — `composite.frag` multiplies the scene alone.
  if (contactInfo.w > 0.0) {
    final stops = bindings.textures['local_exposure_texture'];
    if (stops != null) {
      color.scale(
        math.pow(2.0, stops.sample(u, w).x * contactInfo.w).toDouble(),
      );
    }
  }
  var shade = 1.0;
  // `L5`: the light the indirect method left in rgb, added below.
  Vector3? bounced;
  if (ao != null && strength > 0.0) {
    final texel = bindings.vec4('CompositeInfo', 'ao_texel', Vector4.zero());
    final hx = texel.x * 0.5;
    final hy = texel.y * 0.5;
    final t0 = ao.sample(u + hx, w + hy);
    final t1 = ao.sample(u - hx, w + hy);
    final t2 = ao.sample(u + hx, w - hy);
    final t3 = ao.sample(u - hx, w - hy);
    // The share left open is in a; the occlusion methods write it into
    // every channel.
    final occlusion = 0.25 * (t0.w + t1.w + t2.w + t3.w);
    shade = 1.0 + (occlusion - 1.0) * strength;
    final indirect = contactInfo.z;
    if (indirect != 0.0) {
      final k = 0.25 * indirect * strength;
      bounced = Vector3(
        (t0.x + t1.x + t2.x + t3.x) * k,
        (t0.y + t1.y + t2.y + t3.y) * k,
        (t0.z + t1.z + t2.z + t3.z) * k,
      );
    }
  }

  // `gfx-76n`, into the same multiplier and with a strength of its own: the
  // occlusion says how enclosed a point is, this says whether the sun reaches
  // it, and a scene wants them at different amounts. One tap rather than the
  // 2×2 above, because the march runs at the frame's own resolution and there
  // is no rotated kernel to average away — see `composite.frag`, which this
  // mirrors operation for operation.
  final contactMap = bindings.textures['contact_shadow_texture'];
  final contactStrength = contactInfo.x.clamp(0.0, 1.0);
  if (contactMap != null && contactStrength > 0.0) {
    final contact = contactMap.sample(u, w).x;
    shade *= 1.0 + (contact - 1.0) * contactStrength;
  }

  // Skipped at exactly one, which is what both settings off comes to: a
  // multiply by one is exact, so this is a shortcut rather than a difference,
  // and it keeps the frames 96 goldens hold untouched by arithmetic
  // they never used to go through.
  if (shade != 1.0) color.scale(shade);

  // Additive, and unconditional: the engine binds a black texture when bloom
  // is off rather than leaving the sampler unbound, so there is no branch to
  // make here either.
  final bloom = bindings.textures['bloom_texture'];
  if (bloom != null) {
    final b = bloom.sample(u, w);
    color += Vector3(b.x, b.y, b.z) * params.y;
  }
  if (bounced != null) color += bounced;

  color.scale(math.max(params.x, 0.0));
  final curve = (params.z + 0.5).floor();
  final display = bindings.textures['display_texture'];
  color = curve == 6 && display != null
      ? _sampleDisplay(display, color, math.max(contactInfo.y, 2.0))
      : tonemapBy(color, curve);

  // Grading after the tone map, then the barrel, then the film. The order is
  // the one a camera imposes and it is the order `composite.frag` uses; the
  // two are compared by thirty golden images and have to agree.
  // A power about mid grey (0.18 in linear light), as `composite.frag`.
  if (look.x != 1.0) {
    double pivot(double x) =>
        0.18 * math.pow(math.max(x, 0.0) / 0.18, look.x).toDouble();
    color = Vector3(pivot(color.x), pivot(color.y), pivot(color.z));
  }
  final luma = 0.2126 * color.x + 0.7152 * color.y + 0.0722 * color.z;
  color = Vector3(
    luma + (color.x - luma) * look.y,
    luma + (color.y - luma) * look.y,
    luma + (color.z - luma) * look.y,
  );
  color.x *= 1.0 + look.z * 0.1;
  color.z *= 1.0 - look.z * 0.1;

  // `gfx-27n`: lift, then gamma, then gain — the order `composite.frag`
  // applies them and the order a grading panel names them.
  final lift = bindings.vec4('CompositeInfo', 'lift', Vector4.zero());
  final gammaCurve = bindings.vec4(
    'CompositeInfo',
    'gamma',
    Vector4(1.0, 1.0, 1.0, 0.0),
  );
  final gain = bindings.vec4(
    'CompositeInfo',
    'gain',
    Vector4(1.0, 1.0, 1.0, 0.0),
  );
  // `c * (1 - lift) + lift`: black rises, white stays.
  color = Vector3(
    math.max(color.x * (1.0 - lift.x) + lift.x, 0.0),
    math.max(color.y * (1.0 - lift.y) + lift.y, 0.0),
    math.max(color.z * (1.0 - lift.z) + lift.z, 0.0),
  );
  if (gammaCurve.x != 1.0 || gammaCurve.y != 1.0 || gammaCurve.z != 1.0) {
    color = Vector3(
      math.pow(color.x, 1.0 / gammaCurve.x).toDouble(),
      math.pow(color.y, 1.0 / gammaCurve.y).toDouble(),
      math.pow(color.z, 1.0 / gammaCurve.z).toDouble(),
    );
  }
  color = Vector3(color.x * gain.x, color.y * gain.y, color.z * gain.z);

  // White balance and tint, which are the correction rather than the look
  // `look.z` above is — see `LookSettings.whiteBalance`.
  final encode = bindings.vec4(
    'CompositeInfo',
    'output_encode',
    Vector4.zero(),
  );
  if (encode.y != 0.0 || encode.z != 0.0) {
    color = Vector3(
      color.x * (1.0 + encode.y * 0.20) - encode.z * 0.075,
      color.y * (1.0 + encode.z * 0.15),
      color.z * (1.0 - encode.y * 0.20) - encode.z * 0.075,
    );
  }

  // The colour table, after the grade and before the barrel — the order
  // `composite.frag` uses and the order a grading suite does.
  final aoTexel = bindings.vec4('CompositeInfo', 'ao_texel', Vector4.zero());
  final lut = bindings.textures['lut_texture'];
  if (lut != null && aoTexel.z > 0.0) {
    // Indexed and answered in sRGB, the space a `.cube` is written in.
    final encodedIn = Vector3(
      toSrgb(color.x.clamp(0.0, 1.0)),
      toSrgb(color.y.clamp(0.0, 1.0)),
      toSrgb(color.z.clamp(0.0, 1.0)),
    );
    final gradedEncoded = _sampleLut(
      lut,
      encodedIn,
      math.max(aoTexel.w, 2.0),
    );
    final graded = Vector3(
      toLinear(gradedEncoded.x),
      toLinear(gradedEncoded.y),
      toLinear(gradedEncoded.z),
    );
    final amount = aoTexel.z.clamp(0.0, 1.0);
    color = Vector3(
      color.x + (graded.x - color.x) * amount,
      color.y + (graded.y - color.y) * amount,
      color.z + (graded.z - color.z) * amount,
    );
  }

  if (lookMore.x > 0.0) {
    final aspect = math.max(lookMore.w, 1e-4);
    final fx = (v[0] - 0.5) * (1.0 + (aspect - 1.0) * lookMore.y);
    final fy = v[1] - 0.5;
    final radius = (math.sqrt(fx * fx + fy * fy) * 1.41421356).clamp(
      0.0,
      1.0,
    );
    color.scale(1.0 - lookMore.x * radius);
  }

  // `gfx-24n`, and the grain below it: both are applied after the encode,
  // because banding is an artefact of the 8-bit target and one output step
  // is a fixed distance in display space and a wildly varying one in linear
  // space.
  final outputEncode = bindings.vec4(
    'CompositeInfo',
    'output_encode',
    Vector4.zero(),
  );
  var encoded = Vector3(
    toSrgb(math.max(color.x, 0.0)),
    toSrgb(math.max(color.y, 0.0)),
    toSrgb(math.max(color.z, 0.0)),
  );

  if (lookMore.z > 0.0) {
    // **Moved out of linear light, where its own comment was wrong.** The
    // noise is symmetric either way, but in linear the clamp took the
    // negative half and the encode stretched the rest, so black rose. See
    // `composite.frag`, which carries the measurement.
    //
    // **Not bit-identical to the GPU's, and it cannot be.** The hash is a
    // sine of a large product, so single and double precision diverge in
    // the fraction this keeps. The two golden sets are independent for
    // exactly this class of difference; what has to match is the shape of
    // the noise, not the bits.
    final amount = (_hash(c.coord.x, c.coord.y) - 0.5) * lookMore.z;
    encoded = Vector3(
      encoded.x + amount,
      encoded.y + amount,
      encoded.z + amount,
    );
  }
  if (outputEncode.x > 0.0) {
    // **Bit-identical to the GPU's, unlike the grain above.** The Bayer cell
    // is an integer table and a divide, so there is no precision to lose —
    // which is the argument for an ordered matrix over a hash said in
    // arithmetic rather than in taste.
    // Centred: the cells' mean is -1/32, and the thirty-second undoes it.
    final offset =
        (_bayerCell(c.coord.x, c.coord.y) + 0.03125) * outputEncode.x;
    encoded = Vector3(
      encoded.x + offset,
      encoded.y + offset,
      encoded.z + offset,
    );
  }

  return Vector4(encoded.x, encoded.y, encoded.z, alpha);
}