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