run method
Returns linear RGBA for attachment zero. Null discards the fragment.
Implementation
@override
Vector4? run(Float32List v, ShaderBindings b, FragmentContext c) {
final surfaceMap = b.textures['surface_texture'];
if (surfaceMap == null) return Vector4(1.0, 1.0, 1.0, 1.0);
final u = v[0];
final w = v[1];
final surface = surfaceMap.sample(u, w);
// Nothing was drawn here. A zero alpha is the sky, not a surface sitting on
// the near plane, and the sky casts nothing on itself.
if (surface.w <= 0.0) return Vector4(1.0, 1.0, 1.0, 1.0);
final toLight4 = b.vec4('ContactShadowInfo', 'to_light', Vector4.zero());
final toLight = Vector3(toLight4.x, toLight4.y, toLight4.z);
if (toLight.length2 == 0.0) return Vector4(1.0, 1.0, 1.0, 1.0);
toLight.normalize();
final normal = decodeSurfaceNormal(surface.x, surface.y);
// A surface already turned away from the sun is dark by the light term
// itself, and a march from it would find its own far side. One is the honest
// answer: the half of the picture that knows about the normal is the half
// that should darken this.
if (normal.dot(toLight) <= 0.0) return Vector4(1.0, 1.0, 1.0, 1.0);
final params = b.vec4('ContactShadowInfo', 'params', Vector4.zero());
final reach = math.max(params.x, 1e-4);
final steps = (params.y + 0.5).floor().clamp(1, 16);
final thickness = math.max(params.z, 1e-4);
final inverse = b.mat4('ContactShadowInfo', 'inverse_view_projection');
final projection = b.mat4('ContactShadowInfo', 'view_projection');
final eye4 = b.vec4('ContactShadowInfo', 'camera', Vector4.zero());
final eye = Vector3(eye4.x, eye4.y, eye4.z);
final forward4 = b.vec4('ContactShadowInfo', 'forward', Vector4.zero());
final axis = Vector3(forward4.x, forward4.y, forward4.z);
// Lifted along the normal, in metres, for the reason `SsaoShader` lifts its
// own origin: a bias in window depth is a different physical distance at
// every range.
final origin = worldAtDepth(inverse, eye, axis, u, w, surface.w)
..addScaled(normal, params.w);
final stride = reach / steps;
// Jittered start and a tolerance of at least twice a step's depth, as
// `contact_shadow.frag`.
final jitter = pixelNoise(b, c.coord.x, c.coord.y);
double depthOf(Vector3 p) => (p - eye).dot(axis);
final stepDepth = (depthOf(origin + toLight * stride) - depthOf(origin))
.abs();
final tolerance = math.max(thickness, 2.0 * stepDepth);
for (var i = 0; i < steps; i++) {
final along = i + 1.0 - jitter;
final at = origin + toLight * (stride * along);
final Vector4 clip = projection * Vector4(at.x, at.y, at.z, 1.0);
// Behind the eye: the march has left the frame, and dividing by a
// negative w would fold it back into view somewhere it is not.
if (clip.w <= 0.0) break;
final ndcX = clip.x / clip.w;
final ndcY = clip.y / clip.w;
// Off the edge of the buffer. Nothing is known out there, and guessing
// would put a dark rim around every frame.
if (ndcX.abs() > 1.0 || ndcY.abs() > 1.0) break;
final su = ndcX * 0.5 + 0.5;
// v runs the other way from clip-space y; `toFramebufferOrigin` has
// already put the backend's convention into the matrices, so this pair is
// the same arithmetic everywhere.
final sv = 0.5 - ndcY * 0.5;
final there = surfaceMap.sample(su, sv);
if (there.w <= 0.0) continue;
final gap = (at - eye).dot(axis) - there.w;
// In front of the ray, and not so far in front that it is a different
// object seen past the one casting: without the thickness test a wall four
// metres nearer than the floor shadows everything the ray crosses, which
// is the halo the occlusion pass's range check exists to stop.
if (gap > 0.0 && gap < tolerance) {
// Darker the nearer the blocker, which is what makes this a contact
// shadow rather than a stencil: a hit on the first step is a surface
// touching this one, a hit at the far end of the march is most of a
// metre off and barely counts. Without it the pass writes zero or one
// and the march's reach shows up as a hard band on the floor.
final fade = math.max(along - 1.0, 0.0) / steps;
return Vector4(fade, fade, fade, fade);
}
}
return Vector4(1.0, 1.0, 1.0, 1.0);
}