Simplified side-view cross-section: the lamp is treated as a segment of many independent point-source samples spanning its real size, not a single point. For every sample point on the lamp, a ray to a point on the screen is geometrically tested against the object's silhouette at the object's plane — the fraction of the ~40 lamp samples that are blocked sets that screen point's darkness, sampled independently every frame (no reuse of the 3D engine's math).
umbra half-width u = r_o − (r_L − r_o)·d2/d1
penumbra half-width p = r_o + (r_L + r_o)·d2/d1
Both follow directly from similar triangles between the lamp, object and screen — the same construction used for real eclipse umbra/penumbra cones. When u drops to zero or below, no ray position is blocked from every lamp sample: the "shadow" is soft-edged everywhere (no sharp umbra core).
Drag the silhouette to move it between lamp and screen (or up/down); drag empty canvas to pan, scroll to zoom.