shadowFactor function
double
shadowFactor(
- Surface s,
- ShaderBindings b,
- int lightIndex,
- double lightNDotL, [
- FragmentContext? c,
Implementation
double shadowFactor(
Surface s,
ShaderBindings b,
int lightIndex,
double lightNDotL, [
FragmentContext? c,
]) {
shadowTransmittance.setValues(1.0, 1.0, 1.0);
final params = b.vec4('FragInfo', 'shadow_params', Vector4.zero());
final strength = params.w;
if (strength <= 0.0) return 1.0;
final frame = b.vec4('FragInfo', 'frame_params', Vector4.zero());
if (lightIndex != (frame.z + 0.5).floor()) return 1.0;
final map = b.textures['shadow_texture'];
if (map == null) return 1.0;
// Normal offset: move the sample along the normal before projecting. It
// fixes acne a depth bias cannot, because that error is proportional to the
// surface's slope relative to the light rather than to depth. A flat
// distance plus a texel and a half of the cascade times the sine of the
// slope along each axis of the map, measured per cascade below, for the
// reason `shadow.glsl` gives.
// Which cascade covers this fragment. The mirror of shadow.glsl, and it has
// to stay one: the parity suite compares this backend's picture against
// Impeller's, so a difference here reads as a *rendering* bug in whichever
// one somebody happens to be looking at.
final cascades = b.vec4('FragInfo', 'shadow_cascades', Vector4(0, 0, 1, 0));
final count = (cascades.z + 0.5).floor().clamp(1, 3);
final camera = b.vec4('FragInfo', 'camera_position', Vector4.zero());
// `P7`: by depth along the axis through an orthographic lens.
/// How far the fragment is from the eye, in metres.
final double viewDistance;
if (orthographic(b)) {
final eye = b.vec4('FogInfo', 'eye', Vector4.zero());
final forward = b.vec4('FogInfo', 'forward', Vector4.zero());
viewDistance =
(s.world.x - eye.x) * forward.x +
(s.world.y - eye.y) * forward.y +
(s.world.z - eye.z) * forward.z;
} else {
viewDistance = (s.world - Vector3(camera.x, camera.y, camera.z)).length;
}
var cascade = 0;
if (count > 1 && viewDistance > cascades.x) cascade = 1;
if (count > 2 && viewDistance > cascades.y) cascade = 2;
var u = 0.0;
var vv = 0.0;
var cascadeIndex = 0;
Vector3? projected;
// `S3`: metres per texel across, and per unit of stored depth along the
// light, of the cascade the fragment lands in.
var cascadeTexel = 1.0;
var cascadeDepth = 1.0;
// The fragment's own plane in stored depth per texel across the map, as
// `shadow.glsl` explains: what a PCSS tap past the normal offset finds.
var receiverSlope = 0.0;
for (var attempt = 0; attempt < 3; attempt++) {
final which = cascade + attempt;
if (which >= count) break;
final matrix = b.mat4(
'FragInfo',
which == 0
? 'shadow_matrix'
: (which == 1 ? 'shadow_matrix_far' : 'shadow_matrix_farthest'),
);
// One texel of this cascade in metres: the first row of an orthographic
// projection is 2 / width. The rows are the map's axes in the world, and
// the normal's share along each is the sine of the slope that way.
final axisX = Vector3(
matrix.entry(0, 0),
matrix.entry(0, 1),
matrix.entry(0, 2),
);
final axisY = Vector3(
matrix.entry(1, 0),
matrix.entry(1, 1),
matrix.entry(1, 2),
);
final rowX = math.max(axisX.length, 1e-6);
final rowY = math.max(axisY.length, 1e-6);
final tileTexel = cascades.w > 0.0 ? cascades.w : params.x;
final texelMetres = 2.0 * tileTexel / rowX;
final reach =
1.5 *
2.0 *
tileTexel *
(s.normal.dot(axisX).abs() / (rowX * rowX) +
s.normal.dot(axisY).abs() / (rowY * rowY));
// The flat part held to a texel of this cascade, as the GLSL explains.
final flatOffset = math.min(params.z, texelMetres);
final origin = s.world + s.normal * (flatOffset + reach);
final Vector4 lightSpace =
matrix * Vector4(origin.x, origin.y, origin.z, 1.0);
if (lightSpace.w <= 0.0) continue;
final candidate = Vector3(lightSpace.x, lightSpace.y, lightSpace.z)
..scale(1.0 / lightSpace.w);
final tileU = candidate.x * 0.5 + 0.5;
final tileV = 0.5 - candidate.y * 0.5;
if (tileU < 0.0 || tileU > 1.0 || tileV < 0.0 || tileV > 1.0) continue;
// Past the far plane is behind every caster: the last cascade clamps
// rather than calling the point lit — see `shadow.glsl`.
if (candidate.z > 1.0) {
if (which < count - 1) continue;
candidate.z = 1.0;
}
u = (tileU + which) / count;
vv = tileV;
cascadeIndex = which;
projected = candidate;
cascadeTexel = texelMetres;
cascadeDepth =
1.0 /
math.max(
Vector3(
matrix.entry(2, 0),
matrix.entry(2, 1),
matrix.entry(2, 2),
).length,
1e-6,
);
final cosSlope =
(s.normal
.dot(
Vector3(
matrix.entry(2, 0),
matrix.entry(2, 1),
matrix.entry(2, 2),
),
)
.abs() *
cascadeDepth)
.clamp(0.1, 1.0);
receiverSlope =
texelMetres *
math.sqrt(math.max(1.0 - cosSlope * cosSlope, 0.0)) /
cosSlope /
cascadeDepth;
break;
}
if (projected == null) return 1.0;
// Each cascade's own bias — `shadow_bias` in `surface.glsl`.
final biases = b.vec4('FragInfo', 'shadow_bias', Vector4.zero());
// `A2.8`: how the map keeps its depth, and with half floats turned round
// the floor at this fragment's own depth — `lib/shadow_storage.glsl`.
final storage = params.y;
final bias = storage > 0.5 && storage < 1.5
? math.max(biases[cascadeIndex], shadowStoredStep(projected.z))
: biases[cascadeIndex];
// Horizontally a texel of the atlas, vertically a texel of a tile.
final texelU = params.x;
final texelV = cascades.w > 0.0 ? cascades.w : params.x;
// Every tap held inside its own cascade's tile — see `shadow.glsl`.
final loU = cascadeIndex / count + 0.5 * texelU;
final hiU = (cascadeIndex + 1) / count - 0.5 * texelU;
final loV = 0.5 * texelV;
final hiV = 1.0 - 0.5 * texelV;
double tap(double du, double dv) => shadowStored(
map.sample((u + du).clamp(loU, hiU), (vv + dv).clamp(loV, hiV)).x,
storage,
);
// What the see-through casters let through, before the filter, whose soft
// path leaves early — `ShadowSettings.translucentCasters`; `shadow.glsl`
// has the layout.
if (biases.w > 0.5) {
final stored = map.sample(u.clamp(loU, hiU), vv.clamp(loV, hiV));
final k = strength.clamp(0.0, 1.0);
double through(double value) => 1.0 + (value.clamp(0.0, 4.0) - 1.0) * k;
shadowTransmittance.setValues(
through(1.0 - stored.y),
through(1.0 - stored.z),
through(stored.w),
);
}
// `gfx-15n`: the directional light's apparent size, riding in
// `ambient_ground.w` for the reason `surface.glsl` gives. Zero is the 3×3
// kernel this has always had.
// Below zero is the `evsm` filter (`S2`): the texture is the moments
// atlas, one filtered tap is the kernel, and how far under minus one the
// value sits is the light-bleeding cut.
final softness = b.vec4('FragInfo', 'ambient_ground', Vector4.zero()).w;
var lit = 0.0;
if (softness < 0.0) {
final moments = map.sample(u.clamp(loU, hiU), vv.clamp(loV, hiV));
lit = evsmVisibility(
moments,
projected.z - bias,
(-softness - 1.0).clamp(0.0, 0.95),
);
} else if (softness <= 0.0) {
// PCF 3x3: four samples band visibly at this map size and nine is the
// smallest kernel that reads as a soft edge rather than as stair steps.
for (var y = -1; y <= 1; y++) {
for (var x = -1; x <= 1; x++) {
final occluder = tap(x * texelU, y * texelV);
lit += projected.z - bias > occluder ? 0.0 : 1.0;
}
}
lit /= 9.0;
} else {
// `S3`: sixteen taps each way on a Vogel disc turned per pixel, and the
// penumbra in metres — mirroring `shadow.glsl` step for step. The tangent
// of the angular radius, not twice it: the disc's radius is half the
// penumbra it sweeps.
final spread = math.tan(math.min(softness, 0.5));
final turn = shadowNoise(b, c) * 6.2831853;
final searchRadius = (spread * projected.z * cascadeDepth / cascadeTexel)
.clamp(1.0, 16.0);
var blockerSum = 0.0;
var blockerCount = 0.0;
// Each tap allows for the surface's own slope past the normal offset's
// reach, as `shadow.glsl` explains.
for (var i = 0; i < 16; i++) {
final (dx, dy) = vogelDisc(i, 16, turn);
final reachTexels = math.sqrt(dx * dx + dy * dy) * searchRadius;
final tapBias = bias + math.max(reachTexels - 1.5, 0.0) * receiverSlope;
final occluder = tap(
dx * texelU * searchRadius,
dy * texelV * searchRadius,
);
if (projected.z - tapBias > occluder) {
blockerSum += occluder;
blockerCount += 1.0;
}
}
if (blockerCount <= 0.0) return 1.0;
final averaged = projected.z - blockerSum / blockerCount;
final gap = (averaged > 0.0 ? averaged : 0.0) * cascadeDepth;
final radius = (spread * gap / cascadeTexel).clamp(1.0, 16.0);
for (var i = 0; i < 16; i++) {
final (dx, dy) = vogelDisc(i, 16, turn + 1.0);
final reachTexels = math.sqrt(dx * dx + dy * dy) * radius;
final tapBias = bias + math.max(reachTexels - 1.5, 0.0) * receiverSlope;
final occluder = tap(dx * texelU * radius, dy * texelV * radius);
lit += projected.z - tapBias > occluder ? 0.0 : 1.0;
}
lit /= 16.0;
}
return 1.0 + (lit - 1.0) * strength.clamp(0.0, 1.0);
}