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