describeDiffuseSphericalHarmonics function Lighting and environment

ShDiffuseSummary describeDiffuseSphericalHarmonics(
  1. List<Vector3> sh, {
  2. int directionCount = 512,
})

Evaluates sh over a Fibonacci-distributed sphere of directions and summarizes the field, for checking coefficients baked outside the engine.

The engine's contract is that coefficients are irradiance-domain, with the Lambertian A_l band factors and the 1/pi BRDF term already folded in at projection time. Under that contract ShDiffuseSummary.mean equals the source environment's average linear radiance. Two self-checks follow:

  • Feed a constant environment of radiance L (coefficient 0 set to L / kShBand0Basis, the rest zero, which is what EnvironmentMap.constantDiffuse builds). mean must come back L and ShDiffuseSummary.constantDeviation must be ~0.
  • A chain that left out the 1/pi reads back pi times too bright; one that folded it twice reads back pi times too dim. Compare mean against the average radiance of the environment that was baked.

directionCount trades accuracy for speed; the default is plenty for a smooth L2 field. Cheap enough for a debug assertion, not for a hot loop.

Implementation

ShDiffuseSummary describeDiffuseSphericalHarmonics(
  List<Vector3> sh, {
  int directionCount = 512,
}) {
  if (directionCount < 1) {
    throw ArgumentError.value(directionCount, 'directionCount', 'Must be >= 1');
  }
  final total = Vector3.zero();
  var minimum = Vector3.all(double.infinity);
  var maximum = Vector3.all(double.negativeInfinity);
  var negative = 0;
  // Golden-angle spiral: near-uniform sphere coverage without a quadrature
  // grid's pole clustering.
  const goldenAngle = 2.399963229728653;
  for (var i = 0; i < directionCount; i++) {
    final y = 1.0 - 2.0 * (i + 0.5) / directionCount;
    final radius = math.sqrt(math.max(0.0, 1.0 - y * y));
    final theta = goldenAngle * i;
    final direction = Vector3(
      radius * math.cos(theta),
      y,
      radius * math.sin(theta),
    );
    final value = evaluateDiffuseSphericalHarmonics(sh, direction);
    total.add(value);
    minimum = Vector3(
      math.min(minimum.x, value.x),
      math.min(minimum.y, value.y),
      math.min(minimum.z, value.z),
    );
    maximum = Vector3(
      math.max(maximum.x, value.x),
      math.max(maximum.y, value.y),
      math.max(maximum.z, value.z),
    );
    if (value.x < 0.0 || value.y < 0.0 || value.z < 0.0) negative++;
  }
  return ShDiffuseSummary(
    mean: total / directionCount.toDouble(),
    minimum: minimum,
    maximum: maximum,
    negativeFraction: negative / directionCount,
  );
}