Phong model: why no multiplication by N dot L in specular term? Classical Phong reflection model, as described on wikipedia, computes diffuse and specular light as follows:
vec3 diffuse = kd * dot(n, l) * i;
vec3 specular = ks * pow(dot(r, v), a) * i;

Where n, l, r, v are the normal, light, reflection and view vectors, kd, ks are surface diffuse and specular color, a is gloss and i is incoming light intensity.
I understand that the diffuse term is such as it is because 1) diffuse reflection is not dependent on viewer direction and 2) surfaces angled away from light recieve less light to scatter, thus the dot(n, l) term.
However, shouldn't the specular term also be multiplied by dot(n, l) for the same reason? Is my assumtion that dot(n, l) computes incoming light wrong?
I know that the Phong model is mostly a hack, but this seems like a simple and performant modification and I haven't found this issue discussed anywhere. I've also looked into the source code of the game Tesseract, which does seem to multiply the specular term by dot(n, l).
The $N \cdot L$ term (I've seen &quot;geometry term&quot; thrown around; not quite accurate, but good enough for here) is part of the rendering equation.  What you're looking at here is an estimate of the inside of the rendering equation—that is, the incoming radiance i times the geometry term dot(n,l) times the BRDF.
The simple answer for why the Phong BRDF doesn't include the geometry term is &quot;Well, it probably should, but the Phong model predates the invention of the rendering equation by more than a decade.&quot;.

Phong, B.T.: Illumination for computer generated pictures.  Communications of the ACM 18(6), 311–317 (1975).
Kajiya, J.T.: The rendering equation.  In: Proceedings of the 13th Annual Conference on Computer Graphics and Interactive Techniques, SIGGRAPH ’86, pp. 143–150.  ACM, New York, NY, USA (1986).

This deficiency was fixed in what is now called the Modified Phong BRDF:

Lewis, R.R.: Making Shaders More Physically Plausible.  Computer Graphics Forum 13(2), 109–120 (1994)

At the end of the day, whether the term is present or not isn't an issue of correctness per-se.  Not having the geometry term when you might expect it just means that the BRDF corresponds to a different surface reflection that doesn't foreshorten much at glancing angles.  Indeed, many other BRDFs have terms that look like reciprocal geometry terms, so that they cancel with the actual geometry term in the rendering equation.
In code, the BRDFs look like:
vec3 f_phong    = rho_s/(TWO_PI/(a+1.0)) * pow(max(0.0,dot(r,v)),a) / dot(n,l);
vec3 f_modphong = rho_s/(TWO_PI/(a+2.0)) * pow(max(0.0,dot(r,v)),a);

And so when multiplied by the light and geometry term in the rendering equation (i.e., i * f * dot(n,l)), you get:
vec3 spec_phong    = i * rho_s/(TWO_PI/(a+1.0)) * pow(max(0.0,dot(r,v)),a);
vec3 spec_modphong = i * rho_s/(TWO_PI/(a+2.0)) * pow(max(0.0,dot(r,v)),a) * dot(n,l);


The obvious should be noted, however: while the Phong BRDF is simple to understand and implement, it is of limited accuracy on real-world materials.  It's not terrible, the way many people think it is, but it does lack important features that can be important on many materials.  It is not the state-of-the-art, and depending on your needs you may like to look elsewhere (GGX microfacet, etc.) for more modern BRDFs.