computeCascades method

List<ShadowCascade> computeCascades(
  1. PerspectiveCamera camera,
  2. double aspectRatio
)

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;
}