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 s = readSurface(v, b, c, transformed: layered);
  if (s == null) return null;
  final layers = layered ? _Layers.read(s, v, b, c) : null;
  applyCommonMaps(s, v, b, c, transformed: layered);
  applyMetallicRoughnessMap(s, v, b, c, transformed: layered);
  layers?.readOnMaps(s, b, c);
  final specularStrength = b.vec4('FragInfo', 'material', Vector4.zero()).w;
  // `EnergyCompensation()` — `L1`.
  final compensate =
      b.vec4('FragInfo', 'target_origin', Vector4.zero()).z > 0.5;
  // `EonDiffuse()` — `L8`. Read on its own, with a zero default: the
  // surface reads this vector with a white one for the colour.
  final eon = b.vec4('FragInfo', 'ambient_sky', Vector4.zero()).w > 0.5;
  // Plain metal-rough reflects four per cent head-on and all of it at
  // grazing; the layered stage takes both from its layers. Doubles, not a
  // `Vector3`: its float32 lanes would round the plain stage's
  // [dielectricF0].
  final (d0x, d0y, d0z) = switch (layers?.f0Dielectric) {
    final Vector3 f0 => (f0.x, f0.y, f0.z),
    null => (dielectricF0, dielectricF0, dielectricF0),
  };

  final lit = accumulateLights(
    s,
    b,
    c,
    shadowed: true,
    shade: (s, light) {
      // Perceptual roughness squared is the GGX alpha; this is what makes the
      // roughness slider feel linear. Held at the lobe's own floor, which
      // sits above the surface's.
      final lobe = math.max(s.roughness, minGgxRoughness);
      final alpha = lobe * lobe;

      // Dielectrics reflect about four percent head-on; metals tint the
      // reflection with their albedo and have no diffuse response.
      final f0 = Vector3(
        d0x + (s.albedo.x - d0x) * s.metallic,
        d0y + (s.albedo.y - d0y) * s.metallic,
        d0z + (s.albedo.z - d0z) * s.metallic,
      );
      final f90 = layers == null
          ? 1.0
          : layers.f90 + (1.0 - layers.f90) * s.metallic;
      final diffuseColour = s.albedo * (1.0 - s.metallic);

      final (d, vis) = switch (layers) {
        // `M2`: the lobe stretched along the tangent.
        final layers? when layers.anisotropy > 0.0 => () {
          final h = (light.direction + s.view)..normalize();
          final at =
              alpha + (1.0 - alpha) * layers.anisotropy * layers.anisotropy;
          final ab = math.max(alpha, 1e-3);
          final t = layers.anisotropyT;
          final bt = layers.anisotropyB;
          return (
            _dGgxAnisotropic(light.nDotH, t.dot(h), bt.dot(h), at, ab),
            _vGgxAnisotropic(
              light.nDotL,
              s.nDotV,
              bt.dot(s.view),
              t.dot(s.view),
              t.dot(light.direction),
              bt.dot(light.direction),
              at,
              ab,
            ),
          );
        }(),
        _ => (
          _dGgx(light.nDotH, alpha),
          _vSmith(s.nDotV, light.nDotL, alpha),
        ),
      };
      final plainF = _fSchlick(f0, light.vDotH, f90);
      // `M3`: the thin film's colours in place of the plain Fresnel.
      final f = layers == null ? plainF : layers.withFilm(plainF);

      final ltc = light.ltc;
      final specular = ltc == null
          ? f * (d * vis * specularStrength)
          // `L7`: integrated over the rectangle already, with the fit's own
          // Fresnel, and over `nDotL` for `pbr.frag`'s reason.
          : Vector3(
                  f0.x * ltc.y + (f90 - f0.x) * ltc.z,
                  f0.y * ltc.y + (f90 - f0.y) * ltc.z,
                  f0.z * ltc.y + (f90 - f0.z) * ltc.z,
                ) *
                (ltc.x * specularStrength / math.max(light.nDotL, 1e-6));
      if (compensate) specular.multiply(_multiscatterScale(f0, s, b));
      // Energy left over after reflection is what scatters diffusely.
      final diffuse = eon
          // `L8`: on the direction to the light, as `pbr.glsl` has it.
          ? (eonLobe(
              diffuseColour,
              s.roughness,
              s.normal.dot(light.direction).clamp(1e-4, 1.0),
              s.nDotV,
              light.direction.dot(s.view),
            )..multiply(Vector3(1.0 - f.x, 1.0 - f.y, 1.0 - f.z)))
          : Vector3(
              diffuseColour.x * (1.0 - f.x) / _pi,
              diffuseColour.y * (1.0 - f.y) / _pi,
              diffuseColour.z * (1.0 - f.z) / _pi,
            );
      // `M3`: what passes through is not scattered back.
      if (layers != null) diffuse.scale(1.0 - layers.transmission);
      // The pi puts the result back on the scale the tone mapper and the
      // exposure default were calibrated against.
      final base = diffuse + specular;
      if (layers == null) return base..scale(_pi);
      // Under the sheen, what its albedo leaves; under the coat, what its
      // Fresnel lets through; on top, the coat's lobe.
      final sheen = layers.sheen * sheenLobe(layers.sheenRoughness, s, light);
      final coat =
          layers.coat *
          _coatLobe(
            layers,
            s,
            light,
            specularStrength,
            b.textures['ltc_texture'],
          );
      return (base
          ..scale(layers.sheenScale)
          ..add(sheen)
          ..scale(layers.coatThrough))
        ..add(Vector3.all(coat))
        ..scale(_pi);
    },
  ).scaled(s.occlusion);

  // Occlusion darkens indirect light, and is applied to the direct term too.
  // Not physical; with no IBL the flat ambient is far too weak for an
  // occlusion map to be visible otherwise.
  final metallic = s.metallic.clamp(0.0, 1.0);
  // `L8`: light from everywhere alike meets the EON lobe's albedo at this
  // view rather than the colour itself.
  final diffuseColour = eon
      ? eonAlbedo(s.albedo * (1.0 - metallic), s.roughness, s.nDotV)
      : s.albedo * (1.0 - metallic);
  var ambient = (diffuseColour.clone()..multiply(s.ambient)).scaled(
    s.occlusion,
  );
  // `SceneColourBound()` — `M3`: the transparent pass lends a copy of the
  // scene, and the transmitted share is read from it instead.
  final sceneBound =
      layers != null &&
      b.vec4('LayerInfo', 'scene_colour', Vector4.zero()).x > 0.0;
  // `g_pane`: a blended, transmitting surface with no volume lets the
  // target through the blend rather than reading what is behind it.
  final pane =
      layers != null &&
      premultiplies(b) &&
      layers.transmission > 0.0 &&
      layers.thickness <= 0.0;
  if (layers != null) {
    // `M3`: without an environment, the flat ambient passes through too —
    // unless the scene behind is there to be read.
    final (tr, tg, tb) = sceneBound || pane
        ? (0.0, 0.0, 0.0)
        : layers.transmittance;
    final t = layers.transmission;
    ambient.multiply(
      Vector3(
        1.0 + (tr - 1.0) * t,
        1.0 + (tg - 1.0) * t,
        1.0 + (tb - 1.0) * t,
      ),
    );
  }
  var coatAmbient = Vector3.zero();
  var sheenIncoming = s.ambient.clone();

  final levels = b.vec4('FragInfo', 'frame_params', Vector4.zero()).w;
  final environment = b.textures['environment_texture'];
  if (levels > 0.0 && environment != null) {
    // The term that made metal black: a metal has no diffuse response, so
    // with nothing to reflect it was lit by direct light alone.
    final dielectric = Vector3(d0x, d0y, d0z);
    final f0 = dielectric + (s.albedo - dielectric) * metallic;
    final f90 = layers == null
        ? 1.0
        : layers.f90 + (1.0 - layers.f90) * metallic;
    // `M2`: an anisotropic surface reflects along a normal bent towards the
    // stretch.
    final bent = switch (layers) {
      final layers? when layers.anisotropy > 0.0 => () {
        final across = layers.anisotropyT.cross(s.view);
        final anisoN = across.cross(layers.anisotropyT);
        final bend = 1.0 - layers.anisotropy * (1.0 - s.roughness);
        final bend4 = bend * bend * bend * bend;
        return (anisoN + (s.normal - anisoN) * bend4)..normalize();
      }(),
      _ => s.normal,
    };
    // reflect(-v, n) = 2(n·v)n - v, with v already the direction to the eye.
    final nDotV = bent.dot(s.view);
    final reflected = bent * (2.0 * nDotV) - s.view;

    final irradiance = environment.sampleCube(
      s.normal.x,
      s.normal.y,
      s.normal.z,
      levels,
    );
    final prefiltered = environment.sampleCube(
      reflected.x,
      reflected.y,
      reflected.z,
      s.roughness * levels,
    );
    // The surface's own `n·v`, clamped to 1e-4 as `pbr.frag` passes it —
    // not this raw one clamped to zero. They part only at grazing angles,
    // where the split-sum term is steepest, and a multiscatter term built
    // on top of it would widen the gap it was handed.
    final ab = _envBrdf(b, s.roughness, s.nDotV);

    final strength = b.vec4('FragInfo', 'material', Vector4.zero()).z;
    final diffusePart = diffuseColour.clone()
      ..multiply(Vector3(irradiance.x, irradiance.y, irradiance.z));
    // The single-scatter albedo, thin film included, read by both the
    // specular term and the multiscatter term — as `pbr.frag` builds it.
    final single =
        (layers == null ? f0 : layers.withFilm(f0)) * ab.x +
        Vector3.all(f90 * ab.y);
    final specularPart = Vector3(prefiltered.x, prefiltered.y, prefiltered.z)
      ..multiply(single);
    if (compensate) {
      // Fdez-Agüera, as `pbr.frag` adds it — `L1`.
      final missed = 1.0 - (ab.x + ab.y);
      final average = f0 + (Vector3.all(1.0) - f0) / 21.0;
      final multiple = Vector3(
        single.x * average.x / (1.0 - missed * average.x),
        single.y * average.y / (1.0 - missed * average.y),
        single.z * average.z / (1.0 - missed * average.z),
      );
      specularPart.add(
        multiple
          ..multiply(Vector3(irradiance.x, irradiance.y, irradiance.z))
          ..scale(missed),
      );
    }
    ambient = ((diffusePart + specularPart) * strength).scaled(s.occlusion);
    if (layers != null) {
      // `M3`: the transmitted share of the diffuse light is the
      // environment behind, less what the dielectric reflects and the
      // medium takes.
      final reflects =
          layers.withFilm(layers.f0Dielectric) * ab.x +
          Vector3.all(layers.f90 * ab.y);
      final (tr, tg, tb) = layers.transmittance;
      final passed = sceneBound || pane
          ? Vector3.zero()
          : (layers.transmitted(s, environment, levels)..multiply(
              Vector3(
                tr * (1.0 - math.min(reflects.x, 1.0)),
                tg * (1.0 - math.min(reflects.y, 1.0)),
                tb * (1.0 - math.min(reflects.z, 1.0)),
              ),
            ));
      ambient.add(
        (diffuseColour.clone()..multiply(
              passed - Vector3(irradiance.x, irradiance.y, irradiance.z),
            ))
            .scaled(layers.transmission * strength * s.occlusion),
      );
      // The coat's own reflection of the environment, on its normal.
      final n = layers.coatNormal;
      final coatReflected = n * (2.0 * n.dot(s.view)) - s.view;
      final coatPrefiltered = environment.sampleCube(
        coatReflected.x,
        coatReflected.y,
        coatReflected.z,
        layers.coatRoughness * levels,
      );
      final coatAb = _envBrdf(b, layers.coatRoughness, layers.coatNDotV);
      coatAmbient =
          Vector3(coatPrefiltered.x, coatPrefiltered.y, coatPrefiltered.z)
            ..scale(
              (dielectricF0 * coatAb.x + coatAb.y) *
                  layers.coat *
                  strength *
                  s.occlusion,
            );
      final incoming = environment.sampleCube(
        s.normal.x,
        s.normal.y,
        s.normal.z,
        layers.sheenRoughness * levels,
      );
      sheenIncoming = Vector3(incoming.x, incoming.y, incoming.z)
        ..scale(strength);
    }
  }
  var passThrough = 0.0;
  if (sceneBound || pane) {
    // `M3`: the scene behind, less what the dielectric reflects and the
    // medium takes, tinted by the base colour — light already, so neither
    // the ambient strength nor the occlusion scales it. A pane hands the
    // mean of that share to the blend instead.
    final ab = _envBrdf(b, s.roughness, s.nDotV);
    final reflects =
        layers.withFilm(layers.f0Dielectric) * ab.x +
        Vector3.all(layers.f90 * ab.y);
    final (tr, tg, tb) = layers.transmittance;
    final passes =
        (diffuseColour.clone()..multiply(
              Vector3(
                tr * (1.0 - math.min(reflects.x, 1.0)),
                tg * (1.0 - math.min(reflects.y, 1.0)),
                tb * (1.0 - math.min(reflects.z, 1.0)),
              ),
            ))
            .scaled(layers.transmission);
    if (pane) {
      passThrough = ((passes.x + passes.y + passes.z) / 3.0).clamp(0.0, 1.0);
    } else {
      ambient.add(layers.sceneBehind(s, v, b)..multiply(passes));
    }
  }
  // The baked bounce light, diffuse only, as `pbr.frag` adds it.
  ambient += (diffuseColour.clone()..multiply(sampleLightmap(v, b, c)))
      .scaled(s.occlusion);

  // Under a coat, the ambient and the emission come out through it.
  final through = layers?.coatThrough ?? 1.0;
  final total = switch (layers) {
    null => lit + ambient + s.emissive,
    final layers =>
      lit +
          (ambient.scaled(layers.sheenScale) +
                  (layers.sheen.clone()
                    ..multiply(sheenIncoming)
                    ..scale(layers.sheenAlbedoAtView * s.occlusion)) +
                  s.emissive)
              .scaled(through) +
          coatAmbient,
  };
  final debug = writeDebugView(c, v, b, s, total, transformed: layered);
  if (debug != null) return debug;
  return writeLit(
    c,
    v,
    b,
    color: total,
    alpha: s.alpha,
    normal: s.normal,
    roughness: s.roughness,
    passThrough: passThrough,
  );
}