describeDiffuseSphericalHarmonics function Lighting and environment
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 toL / kShBand0Basis, the rest zero, which is whatEnvironmentMap.constantDiffusebuilds).meanmust come backLand ShDiffuseSummary.constantDeviation must be ~0. - A chain that left out the
1/pireads backpitimes too bright; one that folded it twice reads backpitimes too dim. Comparemeanagainst 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,
);
}