computeCascades method
Builds the shadowCascadeCount shadow cascades that cover
camera's view out to shadowMaxDistance, for a render target of
the given aspectRatio. Returned near-to-far.
Each cascade fits a bounding sphere to its slice of the camera frustum, so the cascade's projection size stays constant as the camera rotates; the projection is then texel-snapped so shadow edges do not shimmer.
Implementation
List<ShadowCascade> computeCascades(
PerspectiveCamera camera,
double aspectRatio,
) {
final count = shadowCascadeCount.clamp(1, 4);
final near = camera.fovNear;
final far = shadowMaxDistance;
// Practical split scheme: a blend of logarithmic and uniform
// spacing, so the near cascades get proportionally more resolution.
final splits = <double>[near];
for (var i = 1; i <= count; i++) {
final ratio = i / count;
final logSplit = near * math.pow(far / near, ratio);
final uniformSplit = near + (far - near) * ratio;
splits.add(
shadowCascadeSplitLambda * logSplit +
(1.0 - shadowCascadeSplitLambda) * uniformSplit,
);
}
// Camera basis and field-of-view tangents.
final forward = (camera.target - camera.position).normalized();
final right = camera.up.cross(forward).normalized();
final up = forward.cross(right).normalized();
final tanV = math.tan(camera.fovRadiansY * 0.5);
final tanH = tanV * aspectRatio;
final lightLength = direction.length;
final lightDir =
lightLength == 0.0
? Vector3(0.0, -1.0, 0.0)
: direction * (1.0 / lightLength);
final cascades = <ShadowCascade>[];
for (var c = 0; c < count; c++) {
// The eight world-space corners of this cascade's frustum slice.
final corners = <Vector3>[];
final center = Vector3.zero();
for (final depth in [splits[c], splits[c + 1]]) {
final planeCenter = camera.position + forward * depth;
for (final sx in const [-1.0, 1.0]) {
for (final sy in const [-1.0, 1.0]) {
final corner =
planeCenter +
right * (sx * depth * tanH) +
up * (sy * depth * tanV);
corners.add(corner);
center.add(corner);
}
}
}
// The slice is symmetric about the view axis, so the corner
// average is the center of their bounding sphere.
center.scale(1.0 / 8.0);
var radius = 0.0;
for (final corner in corners) {
radius = math.max(radius, (corner - center).length);
}
cascades.add(
ShadowCascade(
lightSpaceMatrix: _cascadeLightSpaceMatrix(lightDir, center, radius),
splitDistance: splits[c + 1],
boxSize: radius * 2.0,
),
);
}
return cascades;
}