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