Модель освещения Фонга

Простой и наглядный способ «осветить» поверхность — сложить три вклада: фоновый, рассеянный и блик.

Модель Фонга приближает освещённость точки суммой трёх компонент: 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 зависит от отражённого луча и направления на камеру, степень задаёт резкость блика.
  • Нормали нужно нормализовать во фрагментном шейдере после интерполяции.
Проверьте себя
1. Из каких трёх компонент состоит модель освещения Фонга?
Ared, green, blue
Bambient, diffuse, specular
Cnear, far, fov
Dx, y, z
2. Как считается диффузная компонента?
Adot(N, L) без ограничений
Bmax(dot(N, L), 0) — косинус угла между нормалью и направлением на свет
Ccross(N, L)
Dдлина N
3. Почему интерполированную нормаль нужно нормализовать во фрагментном шейдере?
AДля красоты кода
BПосле интерполяции её длина уже не равна 1, и освещение исказится
CGLSL этого требует синтаксически
DЧтобы ускорить рендер