run method

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

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

Implementation

@override
Vector4? run(Float32List v, ShaderBindings b, FragmentContext c) {
  final sceneMap = b.textures['scene_texture'];
  final surfaceMap = b.textures['surface_texture'];
  if (sceneMap == null || surfaceMap == null) {
    return Vector4(0.0, 0.0, 0.0, 1.0);
  }
  final u = v[0];
  final w = v[1];

  final surface = surfaceMap.sample(u, w);
  final sampled = sceneMap.sample(u, w);
  final scene = Vector3(sampled.x, sampled.y, sampled.z);

  final screen = b.vec4('ReflectionInfo', 'screen', Vector4.zero());
  final debugOnly = screen.w > 0.5;
  final background = debugOnly ? Vector3.zero() : scene;

  Vector4 done(Vector3 rgb) => Vector4(rgb.x, rgb.y, rgb.z, 1.0);

  if (surface.w <= 0.0) return done(background);

  final normal = decodeSurfaceNormal(surface.x, surface.y);
  final roughness = surface.z;
  final polish = 1.0 - smoothstep(0.05, 0.25, roughness);
  if (polish <= 0.0) return done(background);

  final inverse = b.mat4('ReflectionInfo', 'inverse_view_projection');
  final camera = b.vec4('ReflectionInfo', 'camera', Vector4.zero());
  final eye = Vector3(camera.x, camera.y, camera.z);
  final forward = b.vec4('ReflectionInfo', 'forward', Vector4.zero());
  final axis = Vector3(forward.x, forward.y, forward.z);

  // The two halves of the origin convention, kept next to each other so they
  // cannot drift apart: v runs the other way from clip-space y, and the
  // matrices carry whichever backend this is.
  double vFromNdc(double ndcY) => 0.5 - ndcY * 0.5;

  /// `WorldAt`: the pixel's ray crossed with the plane the stored depth
  /// names, because the buffer holds metres along the view axis rather than a
  /// window depth. Shared with the occlusion pass; see [worldAtDepth].
  Vector3 worldFrom(double uu, double vv, double depth) =>
      worldAtDepth(inverse, eye, axis, uu, vv, depth);

  final position = worldFrom(u, w, surface.w);

  // Back along the ray this pixel looks down, not towards the camera
  // position: the two agree under a perspective camera and do not under an
  // orthographic one, whose rays are parallel. See `PixelRay` in the GLSL.
  final toEye = -pixelRay(inverse, u, w).along;
  final facing = normal.dot(toEye);
  if (facing <= 0.05) return done(background);

  // reflect(-toEye, normal)
  final incident = -toEye;
  final ray = incident - normal * (2.0 * incident.dot(normal));

  final params = b.vec4('ReflectionInfo', 'params', Vector4.zero());
  final steps = params.x.toInt();
  final stride = params.y;
  final thickness = params.z;
  final intensity = params.w;

  final viewProjection = b.mat4('ReflectionInfo', 'view_projection');

  /// `UvOf`: where [at] lands in the textures, or null behind the camera.
  ({double u, double v})? uvOf(Vector3 at) {
    final Vector4 clip = viewProjection * Vector4(at.x, at.y, at.z, 1.0);
    if (clip.w <= 0.0) return null;
    return (u: clip.x / clip.w * 0.5 + 0.5, v: vFromNdc(clip.y / clip.w));
  }

  double depthOf(Vector3 at) => (at - eye).dot(axis);

  // Jittered start and reach, as the GLSL: half a stride at least, plus a
  // Bayer cell of one.
  final jitter = 0.5 + pixelNoise(b, c.coord.x, c.coord.y);
  var travelled = stride * jitter;
  final march = position + normal * 0.01 + ray * travelled;
  final reach = stride * (steps + 0.5);
  var hitColour = Vector3.zero();
  var hit = 0.0;

  // Sixty-four is the shader's own ceiling, and it is a real one: GLSL needs
  // a constant bound. Kept so the two loops end in the same place.
  for (var i = 0; i < 64; i++) {
    if (i >= steps) break;
    final at = uvOf(march);
    if (at == null) break;
    final su = at.u;
    final sv = at.v;
    if (su < 0.0 || su > 1.0 || sv < 0.0 || sv > 1.0) break;

    final seenSurface = surfaceMap.sample(su, sv);
    final sceneDepth = seenSurface.w;
    final marchDepth = depthOf(march);
    // Behind what was drawn here, then how far behind. The second in the
    // world rather than in depths: the two points sit on one ray, and along a
    // ray running away from the camera a depth difference is shorter than the
    // gap it stands for. Thickness is a size in the world.
    if (sceneDepth > 0.0 && marchDepth > sceneDepth) {
      final seen = worldFrom(su, sv, sceneDepth);
      final behind = march.distanceTo(seen);
      final seenNormal = decodeSurfaceNormal(seenSurface.x, seenSurface.y);
      if (behind < thickness && seenNormal.dot(ray) < 0.0) {
        // Five halvings of the last stride, as the GLSL, and no further back
        // than the march has come: the first step travels only the jittered
        // fraction, and a full stride back from it can sit under the floor.
        var lo = march - ray * math.min(stride, travelled);
        var hi = march.clone();
        for (var j = 0; j < 5; j++) {
          final mid = (lo + hi)..scale(0.5);
          final m = uvOf(mid);
          final d = m == null ? 0.0 : surfaceMap.sample(m.u, m.v).w;
          if (d > 0.0 && depthOf(mid) > d) {
            hi = mid;
          } else {
            lo = mid;
          }
        }
        final landed = uvOf(hi) ?? (u: su, v: sv);
        final tex = sceneMap.sample(landed.u, landed.v);
        hitColour = Vector3(tex.x, tex.y, tex.z);
        final border =
            1.0 -
            math.max(
              (landed.u * 2.0 - 1.0).abs(),
              (landed.v * 2.0 - 1.0).abs(),
            );
        final along = (1.0 - travelled / reach).clamp(0.0, 1.0);
        hit = smoothstep(0.0, 0.15, border) * along * along;
        break;
      }
    }
    march.add(ray * stride);
    travelled += stride;
  }

  // Schlick, F0 = [dielectricF0], as the GLSL.
  final fresnel =
      dielectricF0 +
      (1.0 - dielectricF0) * math.pow(1.0 - facing, 5.0).toDouble();
  final reflection = hitColour * (hit * intensity * polish * fresnel);
  // The share of the hit that is used, intensity included, as the GLSL.
  final confidence = hit * intensity * polish;
  // A hit takes the place of the environment's reflection the lit pass
  // already added, read along the same ray at the same roughness, in the
  // share the hit is trusted. See the end of the GLSL's `main`.
  final environment = b.vec4('ReflectionInfo', 'environment', Vector4.zero());
  final levels = environment.x;
  final cube = b.textures['environment_texture'];
  final Vector3 composed;
  if (levels > 0.0 && cube != null) {
    final texel = cube.sampleCube(ray.x, ray.y, ray.z, roughness * levels);
    final replaced =
        Vector3(texel.x, texel.y, texel.z) *
        (environment.y * confidence * fresnel);
    final sum = scene + reflection - replaced;
    composed = Vector3(
      math.max(sum.x, 0.0),
      math.max(sum.y, 0.0),
      math.max(sum.z, 0.0),
    );
  } else {
    composed = scene + reflection;
  }
  return done(debugOnly ? reflection : composed);
}