glTF Spot Light Angular Falloff Explained
In glTF 2.0, spot light angular falloff is governed by the
KHR_lights_punctual extension, which defines how light
intensity transitions from full brightness to darkness between the cone
boundaries. This article explains the exact mathematical formulation
used by glTF renderers to interpolate intensity between
innerConeAngle and outerConeAngle,
demonstrating how cosine-space clamping and quadratic smoothing produce
seamless penumbra transitions.
Cone Angle Properties
The glTF KHR_lights_punctual extension defines two
properties to describe the conical boundary of a spot light:
innerConeAngle: The half-angle (in radians) from the light's direction vector within which the light shines at maximum intensity. The default value is0.0.outerConeAngle: The half-angle (in radians) from the light's direction vector at which the light intensity reaches zero. The default value isMath.PI / 4.0(45 degrees).
According to the specification, innerConeAngle must be
less than or equal to outerConeAngle.
Evaluating Alignment in Cosine Space
To maximize runtime performance and avoid costly inverse trigonometric functions in fragment shaders, glTF evaluates angular falloff using the dot product (cosine space) rather than computing angles in radians:
- Let \(\vec{D}\) be the normalized spot light direction vector.
- Let \(\vec{L}\) be the normalized vector pointing from the light position toward the illuminated surface fragment.
- Compute the cosine of the angle between them:
\[C_d = \vec{D} \cdot (-\vec{L})\]
If the surface point lies directly along the light axis, \(C_d = 1.0\). As the point moves toward the outer edges of the cone, \(C_d\) decreases.
Scale and Offset Formula
The transition gradient is constructed using a scale and offset approach. These coefficients can be precomputed on the CPU per frame or set as shader uniforms:
\[\text{lightAngleScale} = \frac{1.0}{\max(0.001, \cos(\theta_{\text{inner}}) - \cos(\theta_{\text{outer}}))}\]
\[\text{lightAngleOffset} = -\cos(\theta_{\text{outer}}) \times \text{lightAngleScale}\]
The max(0.001, ...) guard prevents division by zero in
instances where the inner cone angle is configured to match the outer
cone angle.
Linear Clamping and Quadratic Falloff
The raw dot product \(C_d\) is transformed linearly into a normalized \([0.0, 1.0]\) range and clamped:
\[\text{angularAttenuation} = \text{clamp}(C_d \times \text{lightAngleScale} + \text{lightAngleOffset}, 0.0, 1.0)\]
- If \(C_d \ge
\cos(\theta_{\text{inner}})\), the formula yields values \(\ge 1.0\), which are clamped to
1.0. - If \(C_d \le
\cos(\theta_{\text{outer}})\), the formula yields values \(\le 0.0\), which are clamped to
0.0. - Between the two boundaries, the value increases linearly from
0.0at the outer cone to1.0at the inner cone.
To prevent harsh, linear transitions at the edge of the penumbra, glTF applies a quadratic curve to smooth the falloff:
\[\text{angularAttenuation} = \text{angularAttenuation} \times \text{angularAttenuation}\]
Squaring the attenuation creates an ease-in transition as the light
fades to zero at the outer perimeter, resulting in a soft, visually
realistic penumbra without requiring expensive cubic Hermite
(smoothstep) functions.