A hard shadow¶
The share of the sun's disc a surface point sees, a hard shadow.
The point sees the sun where its surface faces it (n . l > 0) and nothing stands between them: the nearest shape along the ray from the point toward the sun (blocker, its distance, far where the ray meets nothing) is none. Then 1, else 0: the disc is a point, wholly in view or wholly hidden. A penumbra (soft shadows, lusine.render.sun_share) changes this law, not what calls it. Source: Whitted, An Improved Illumination Model for Shaded Display (CACM 23(6), 1980), shadow rays. Checked against a reference in numpy, which has its own tests.
- Source
- Whitted, An Improved Illumination Model for Shaded Display (CACM 23(6), 1980): shadow rays.
- Proof
- reference, limit.
sun_shadow.trait
namespace lusine.render;
float sun_shadow(vector<float, 3> normal, vector<float, 3> sun_direction, float<m> blocker, float<m> far)
{
return dot(normal, sun_direction) > 0.0 && blocker >= far ? 1.0 : 0.0;
}
The share of the sun's disc a surface point sees: 1 where its surface faces the sun and the nearest shape toward it (blocker) is none (far), else 0. A soft shadow is lusine.render.sun_share's. One of the kernels lusine.render.table draws a table's shapes with.
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/sun_shadow.trait'))
// lusine.render.sun_shadow: the share of the sun's disc a surface point sees, a hard shadow. The point sees the sun
// where its surface faces it (n . l > 0) and nothing stands between them: the nearest shape along the ray from the
// point toward the sun (`blocker`, its distance, `far` where the ray meets nothing) is none. Then 1, else 0: the
// disc is a point, wholly in view or wholly hidden. A penumbra (soft shadows, lusine.render.sun_share) changes this
// law, not what calls it.
// Source: Whitted, An Improved Illumination Model for Shaded Display (CACM 23(6), 1980), shadow rays.
// Checked against a reference in numpy, which has its own tests.
namespace lusine.render;
float sun_shadow(vector<float, 3> normal, vector<float, 3> sun_direction, float<m> blocker, float<m> far)
{
return dot(normal, sun_direction) > 0.0 && blocker >= far ? 1.0 : 0.0;
}
The program¶
Each section runs the kernel over a lattice of items, one a case: the host sets every parameter, and main() binds the kernel over the items.
print(trait_run.law_program('render/sun_shadow.trait', 'sun_shadow'))
from lusine.kernels.render.sun_shadow import sun_shadow as reference
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host vector<float, 3> normal;
host vector<float, 3> sun_direction;
host float<m> blocker;
host float<m> far;
float result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.render.sun_shadow(items.normal, items.sun_direction, items.blocker, items.far); }
Reference: lit, shadowed, facing away¶
Random normals, sun directions and blockers: the kernel's share is the reference's, exactly.
def shadow(normal, sun_direction, blocker, far=1e30):
return trait_run.law('render/sun_shadow.trait', 'sun_shadow', normal=normal, sun_direction=sun_direction,
blocker=blocker, far=far)
v = rng.normal(size=(400, 3)); v /= np.linalg.norm(v, axis=1, keepdims=True)
n = F32(v).astype(float)
v = rng.normal(size=(400, 3)); v /= np.linalg.norm(v, axis=1, keepdims=True)
l = F32(v).astype(float)
blocker = np.where(rng.random(400) < 0.5, 1e30, rng.uniform(0.01, 3.0, 400))
got = shadow(n, l, blocker)
assert np.array_equal(got, reference(n, l, F32(blocker).astype(float), float(F32(1e30))))
print(f'{int(got.sum())} of 400 lit, as the reference says')
108 of 400 lit, as the reference says
Limit: edge on, and a blocker at any distance¶
A surface edge on to the sun sees none of it; a shape a millimetre or a kilometre toward the sun hides it alike.
up = [0.0, 1.0, 0.0]
out = shadow(np.array([up] * 3), np.array([[1.0, 0.0, 0.0], [0.0, 0.6, 0.8], [0.0, 0.6, 0.8]]), np.array([1e30, 1e-3, 1e3]))
assert np.all(out == 0.0)
print('edge on, or blocked near or far: dark')
edge on, or blocked near or far: dark