Модель освещения Фонга
Простой и наглядный способ «осветить» поверхность — сложить три вклада: фоновый, рассеянный и блик.
Модель Фонга приближает освещённость точки суммой трёх компонент: ambient (фон), diffuse (рассеянный свет) и specular (зеркальный блик).
Зачем это знать
Освещение Фонга — первая модель, которую реализуют во фрагментном шейдере. Она дёшева, интуитивна и до сих пор используется в стилизованной графике. Поняв её три слагаемых, вы поймёте, откуда на 3D-объекте берётся объём, тень на неосвещённой стороне и яркий блик.
Три составляющие
| Компонента | Смысл | Зависит от |
| Ambient | фоновая засветка, чтобы тень не была чёрной | константа |
| Diffuse | матовый свет, ярче там, где свет падает прямо | угол N и L |
| Specular | яркий блик от гладкой поверхности | угол отражения и взгляда |
Векторы освещения в точке поверхности:
L (на свет) N (нормаль) V (на камеру)
\ | /
\ | /
\ | /
====поверхность====
Диффузная компонента: косинус угла
Сердце модели — диффузный свет: diffuse = max(dot(N, L), 0). Чем прямее свет падает на поверхность (N и L сонаправлены), тем ярче. Свет вскользь почти не освещает, свет сзади — не освещает вовсе (отсюда max с нулём).
import math
def normalize(v):
L = math.sqrt(sum(c*c for c in v))
return tuple(c / L for c in v)
def dot(a, b):
return sum(x*y for x, y in zip(a, b))
N = normalize((0, 1, 0)) # нормаль вверх
for light in [(0,1,0), (1,1,0), (1,0,0), (0,-1,0)]:
Ln = normalize(light)
diffuse = max(dot(N, Ln), 0.0)
print(f"свет {light}: diffuse = {diffuse:.3f}")
Вывод:
свет (0, 1, 0): diffuse = 1.000 свет (1, 1, 0): diffuse = 0.707 свет (1, 0, 0): diffuse = 0.000 свет (0, -1, 0): diffuse = 0.000
Свет прямо сверху (совпал с нормалью) даёт максимум 1.0; под 45° — 0.707; сбоку и снизу — 0. Так возникает мягкая растушёвка от света к тени.
Сборка цвета
Итоговая яркость — сумма трёх компонент, домноженных на цвет материала и света. Спекуляр добавляет резкий блик, зависящий от того, как отражённый луч совпадает с направлением на камеру (V), возведённый в степень «глянцевости».
// Фрагментный шейдер: упрощённый Фонг
precision mediump float;
varying vec3 vNormal;
uniform vec3 uLightDir; // направление на свет
uniform vec3 uViewDir; // направление на камеру
void main() {
vec3 N = normalize(vNormal);
vec3 L = normalize(uLightDir);
float ambient = 0.1;
float diffuse = max(dot(N, L), 0.0);
vec3 R = reflect(-L, N);
float spec = pow(max(dot(R, normalize(uViewDir)), 0.0), 32.0);
float light = ambient + diffuse + spec;
gl_FragColor = vec4(vec3(light), 1.0);
}
Как работает под капотом
Нормали интерполируются по треугольнику (Phong shading) — поэтому освещение плавное, а не плоское по граням. Важно нормализовать N во фрагментном шейдере: после интерполяции длина нормали уже не равна 1. Спекуляр-степень (shininess) управляет размером блика: больше степень — меньше и резче блик.
Частые ошибки
- Не нормализовать интерполированную нормаль — свет станет неровным.
- Забыть max(...,0) для диффуза — появится «отрицательный свет» на обратной стороне.
- Считать освещение в разных пространствах для N, L, V — векторы должны быть в одном.
Итоги
- Фонг = ambient + diffuse + specular.
- Diffuse = max(dot(N, L), 0): ярче, где свет падает прямее.
- Specular зависит от отражённого луча и направления на камеру, степень задаёт резкость блика.
- Нормали нужно нормализовать во фрагментном шейдере после интерполяции.