A point light, such as a reading lamp, sits at a position and shines in all directions, so the direction to it differs at every point: L = normalize(light position - surface position). Its strength also falls with the distance d. Real light follows the inverse-square law, 1 / d^2; old fixed-function OpenGL instead offered 1 / (constant + linear x d + quadratic x d^2), easier to tune but not physical. glTF's KHR_lights_punctual recommends inverse square multiplied by a window that reaches zero at the light's range, so distant objects can skip the light: clamp(1 - (d / range)^4, 0, 1) / d^2. The listing evaluates all three with Scalars, Vectors and Matrices's show():
for (const d of ['0.5', '1.0', '2.0', '3.0', '4.0']) { // distances from the light
show(`float d = ${d}; result = vec4(d, 1.0 / (1.0 + 0.35 * d + 0.44 * d * d),
1.0 / (d * d), clamp(1.0 - pow(d / 4.0, 4.0), 0.0, 1.0) / (d * d));`);
}0.5, 0.77821, 4, 3.999023 1, 0.558659, 1, 0.996094 2, 0.289017, 0.25, 0.234375 3, 0.166389, 0.111111, 0.075955 4, 0.105932, 0.0625, 0
The columns are d, the classic curve (constants 1, 0.35, 0.44), inverse square, and glTF's version with a range of 4, which tracks inverse square until it bends to exactly 0 at the range. Inverse square exceeds 1 near the light, so scale light colors as intensities.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.key { max-width: 600px; margin-top: 4px; font-family: monospace; }
.key span { display: inline-block; width: 10px; height: 10px; margin: 0 4px 0 10px; vertical-align: -1px; }
</style>
<canvas id="c" width="600" height="240"></canvas>
<div class="key">
<span style="background:#1f5f8b"></span>1 / (1 + 0.35d + 0.44d²)
<span style="background:#b5452f"></span>1 / d²
<span style="background:#3f7d3a"></span>clamp(1 - (d/4)⁴, 0, 1) / d²
</div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
void main() { gl_Position = vec4(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0, 0.0, 1.0); }`],
[gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
uniform float uTime;
out vec4 outColor;
float classic(float d) { return 1.0 / (1.0 + 0.35 * d + 0.44 * d * d); } // old fixed-function style
float inverseSquare(float d) { return 1.0 / (d * d); } // physical
float windowed(float d) { return clamp(1.0 - pow(d / 4.0, 4.0), 0.0, 1.0) / (d * d); } // glTF, range 4
float curve(float y, float f) { return 1.0 - smoothstep(0.0, 1.8, abs(y - f) / fwidth(y - f)); }
void main() {
vec2 px = gl_FragCoord.xy;
vec3 color = vec3(0.97, 0.96, 0.93);
if (px.x < 290.0) { // the graph: d from 0 to 5, value 0 to 1.5
float d = (px.x - 10.0) / 270.0 * 5.0, y = (px.y - 15.0) / 210.0 * 1.5;
if (abs(fract(d) - 0.0) < 0.012 || abs(y - 1.0) < 0.004) color *= 0.9; // grid: whole d, value 1
if (px.x > 10.0) {
color = mix(color, vec3(0.12, 0.37, 0.55), curve(y, classic(d)));
color = mix(color, vec3(0.71, 0.27, 0.18), curve(y, inverseSquare(d)));
color = mix(color, vec3(0.25, 0.49, 0.23), curve(y, windowed(d)));
}
} else if (px.x > 300.0) { // three floor strips lit from a lamp above
int strip = int(px.y / 80.0);
vec3 floorPoint = vec3((px.x - 450.0) / 40.0, 0.0, 0.0);
vec3 lamp = vec3(sin(uTime) * 1.5, 0.6, 0.0);
vec3 toLight = lamp - floorPoint;
float d = length(toLight), cosine = toLight.y / d;
float fade = strip == 2 ? classic(d) : strip == 1 ? inverseSquare(d) : windowed(d);
vec3 tints[3] = vec3[](vec3(0.25, 0.49, 0.23), vec3(0.71, 0.27, 0.18), vec3(0.12, 0.37, 0.55));
color = vec3(0.12) + vec3(0.95, 0.88, 0.75) * cosine * fade * 0.6;
if (mod(px.y, 80.0) < 3.0) color = tints[strip]; // a coloured edge per strip
}
outColor = vec4(color, 1.0);
}`]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const uTime = gl.getUniformLocation(program, 'uTime');
function frame(ms) {
gl.uniform1f(uTime, ms * 0.001);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>