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.
worldDirection is the light's world-space travel direction. When
omitted it falls back to direction (the light's own field), which
is correct for a light placed without a node transform.
Implementation
List<ShadowCascade> computeCascades(
Camera camera,
double aspectRatio, [
Vector3? worldDirection,
]) {
// Cascades fit the camera frustum, which is perspective-specific.
final perspective = camera.projection as PerspectiveProjection;
final count = shadowCascadeCount.clamp(1, 4);
final near = perspective.near;
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 direction and field-of-view tangents.
final forward = camera.forward;
final tanV = math.tan(perspective.fovRadiansY * 0.5);
final tanH = tanV * aspectRatio;
final tanRadius2 = tanH * tanH + tanV * tanV;
final effectiveDirection = worldDirection ?? direction;
final lightLength = effectiveDirection.length;
final lightDir = lightLength == 0.0
? Vector3(0.0, -1.0, 0.0)
: effectiveDirection * (1.0 / lightLength);
final cascades = <ShadowCascade>[];
for (var c = 0; c < count; c++) {
// The smallest stable sphere enclosing both rectangular end planes has
// its center on the view axis. Equalize the near/far corner distances,
// unless that point lies beyond the far plane, where the far rectangle's
// own circumcircle is the minimum. This keeps the rotation-invariant
// cascade fit while wasting less shadow-map area than a midpoint sphere.
final sliceNear = splits[c];
final sliceFar = splits[c + 1];
final centerDepth = math.min(
sliceFar,
(sliceNear + sliceFar) * (1.0 + tanRadius2) * 0.5,
);
final position = camera.position;
final center = Vector3(
position.x + forward.x * centerDepth,
position.y + forward.y * centerDepth,
position.z + forward.z * centerDepth,
);
final nearRadius2 =
(centerDepth - sliceNear) * (centerDepth - sliceNear) +
sliceNear * sliceNear * tanRadius2;
final farRadius2 =
(sliceFar - centerDepth) * (sliceFar - centerDepth) +
sliceFar * sliceFar * tanRadius2;
final radius = math.sqrt(math.max(nearRadius2, farRadius2));
cascades.add(
ShadowCascade(
lightSpaceMatrix: _cascadeLightSpaceMatrix(lightDir, center, radius),
splitDistance: splits[c + 1],
boxSize: radius * 2.0,
center: center,
radius: radius,
),
);
}
return cascades;
}