The sky's light on a surface¶
The sky's light on a surface, the radiance of a uniform sky that gives a level surface the irradiance lusine.render.sky's gradient gives it: the gradient's cosine-weighted mean over the upper hemisphere, horizon + 2/3 (zenith - horizon) (the integral of (h + (z - h) cos t) cos t over the hemisphere, over pi).
The sun's disc is not in it: the sun lights a surface directly (lusine.render.pbr's sun term). A zenith equal to the horizon gives that radiance exactly, the uniform sky's ambient. Source: the cosine-weighted hemispherical mean (Pharr, Jakob and Humphreys, Physically Based Rendering, 4th ed., 4.2.2, the irradiance of a radiance distribution). Checked against a reference in numpy, which has its own tests.
- Source
- Pharr, Jakob and Humphreys, Physically Based Rendering, 4th ed., 4.2.2 (the irradiance of a radiance distribution).
- Proof
- reference, limit.
sky_ambient.trait
namespace lusine.render;
vector<float<W/m2/sr>, 3> sky_ambient(vector<float<W/m2/sr>, 3> zenith, vector<float<W/m2/sr>, 3> horizon)
{
return horizon + (zenith - horizon) * (2.0 / 3.0);
}
The radiance of the uniform sky that lights a level surface as lusine.render.sky's gradient does, its cosine-weighted mean, horizon + 2/3 (zenith - horizon). lusine.render.table lights every ball and the ground with it, through lusine.render.pbr's ambient.
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import trait_run
F32 = np.float32
U = float(np.finfo(F32).eps) / 2 # binary32's unit roundoff, 2^-24
rng = np.random.default_rng(2026)
The kernel¶
The law as the library states it.
print(trait_run.source('render/sky_ambient.trait'))
// lusine.render.sky_ambient: the sky's light on a surface, the radiance of a uniform sky that gives a level surface the
// irradiance lusine.render.sky's gradient gives it: the gradient's cosine-weighted mean over the upper hemisphere,
// horizon + 2/3 (zenith - horizon) (the integral of (h + (z - h) cos t) cos t over the hemisphere, over pi). The sun's
// disc is not in it: the sun lights a surface directly (lusine.render.pbr's sun term). A zenith equal to the horizon
// gives that radiance exactly, the uniform sky's ambient.
// Source: the cosine-weighted hemispherical mean (Pharr, Jakob and Humphreys, Physically Based Rendering, 4th ed.,
// 4.2.2, the irradiance of a radiance distribution).
// Checked against a reference in numpy, which has its own tests.
namespace lusine.render;
vector<float<W/m2/sr>, 3> sky_ambient(vector<float<W/m2/sr>, 3> zenith, vector<float<W/m2/sr>, 3> horizon)
{
return horizon + (zenith - horizon) * (2.0 / 3.0);
}
The program¶
print(trait_run.law_program('render/sky_ambient.trait', 'sky_ambient'))
from lusine.kernels.render.sky_ambient import sky_ambient as reference
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host vector<float<W/m2/sr>, 3> zenith;
host vector<float<W/m2/sr>, 3> horizon;
vector<float<W/m2/sr>, 3> result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.render.sky_ambient(items.zenith, items.horizon); }
Reference: skies of every tint¶
Zeniths and horizons drawn at random: each within a few of binary32's roundings of the reference.
def sky_ambient(zenith, horizon):
return trait_run.law('render/sky_ambient.trait', 'sky_ambient', zenith=zenith, horizon=horizon)
Z = F32(rng.random(size=(200, 3))).astype(float)
H = F32(rng.random(size=(200, 3))).astype(float)
got = sky_ambient(Z, H)
want = reference(Z, H)
err = np.abs(got - want) / np.maximum(np.abs(want), 1e-6)
assert err.max() < 8 * U, err.max()
print(f'within {err.max() / U:.1f} roundings of the reference')
within 3.3 roundings of the reference
Limit: a uniform sky¶
A zenith equal to the horizon lights as that radiance, bit for bit: the table view's ambient before the gradient.
assert np.array_equal(sky_ambient(H, H), H)
print('uniform, bit for bit')
uniform, bit for bit
Reference: the hemisphere's integral¶
The level surface's irradiance under the gradient, integrated over the hemisphere by the polar angle, is pi times the ambient.
z, h = F32([0.15, 0.25, 0.55]).astype(float), F32([0.5, 0.55, 0.6]).astype(float)
n = 2000
t = (np.arange(n) + 0.5) / n * (np.pi / 2)
w = np.cos(t) * np.sin(t) * (np.pi / 2 / n) * 2 * np.pi
irradiance = ((h + (z - h) * np.cos(t)[:, None]) * w[:, None]).sum(axis=0)
assert np.allclose(irradiance, np.pi * sky_ambient(z[None, :], h[None, :])[0], rtol=1e-5)
print('pi A =', irradiance)
pi A = [0.83775816 1.09955757 1.78023607]