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);
if (s == null) return null;
applyCommonMaps(s, v, b, c);
// Roughness sets the band count, so the ORM map matters here too.
applyMetallicRoughnessMap(s, v, b, c);
final lit = accumulateLights(
s,
b,
c,
shadowed: true,
shade: (s, light) {
// Fewer bands as roughness rises, so the slider still does something.
final bands = 5.0 + (2.0 - 5.0) * s.roughness;
var quantised = (light.nDotL * bands).floorToDouble() / bands;
final fraction = fract(light.nDotL * bands);
quantised += smoothstep(0.85, 1.0, fraction) / bands;
// AccumulateLights multiplies by N.L, which is what the banding is meant
// to replace, so divide it back out and keep the ramp.
final ramp = quantised / math.max(light.nDotL, 1e-3);
return s.albedo * ramp;
},
).scaled(s.occlusion);
// The rim belongs to the view, not to any one light: inside the loop it
// would brighten with the number of lamps in the scene.
final specularStrength = b.vec4('FragInfo', 'material', Vector4.zero()).w;
final rim =
math.pow(1.0 - s.nDotV, 3.0).toDouble() * specularStrength * 0.35;
final ambient =
(s.albedo.clone()..multiply(s.ambient + sampleLightmap(v, b, c)))
.scaled(s.occlusion);
final total = lit + ambient + Vector3.all(rim) + s.emissive;
final debug = writeDebugView(c, v, b, s, total);
if (debug != null) return debug;
return writeLit(
c,
v,
b,
color: total,
alpha: s.alpha,
normal: s.normal,
roughness: s.roughness,
);
}