computeCascades method

List<ShadowCascade> computeCascades(
  1. Camera camera,
  2. double aspectRatio, [
  3. Vector3? worldDirection
])

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. A
  // pinned first bound takes the first split and the same scheme spreads the
  // rest from there; a single cascade has no rest, so it keeps far.
  // A pin at or past shadowMaxDistance leaves no range for the remaining
  // cascades (every later split collapses onto far), so it falls back to
  // the automatic scheme; the lower clamp stays strictly above the near
  // plane so cascade 0 keeps thickness.
  final bound = firstCascadeFarBound;
  final pinned = bound != null && count > 1 && bound < far && far > near
      ? math.max(bound, near + (far - near) * 1e-3)
      : null;
  final splits = <double>[near];
  if (pinned != null) splits.add(pinned);
  final splitNear = pinned ?? near;
  final splitCount = pinned != null ? count - 1 : count;
  for (var i = 1; i <= splitCount; i++) {
    final ratio = i / splitCount;
    final logSplit = splitNear * math.pow(far / splitNear, ratio);
    final uniformSplit = splitNear + (far - splitNear) * 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 overlap = cascadeOverlap.clamp(0.0, 1.0);

  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];
    // Overlap fits a cascade past its split so it and its successor both
    // cover the band the shader cross-fades over. The last cascade has no
    // successor, so it keeps its bound.
    final sliceFar = overlap > 0.0 && c < count - 1
        ? splits[c + 1] + (splits[c + 1] - sliceNear) * overlap
        : 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;
}