Первая версия нашей программы вычисляет интенсивность освещения в вершинном шейдере (на каждую вершину). Затем пикселный шейдер использует интерполированные в вершинном шейдере данные для вычисления оттенка пиксела. Поэтому в вершинном шейдере должна быть обьявлена varying-переменная для хранения интенсивности. Пикселный шейдер должен будет обьявить такую же переменную, с тем же типом (varying), чтобы получить данные интенсивности от вершинного шейдера.

Направление света в вершинном шейдере может быть обьявлено как локальная переменная или как константа, однако тип переменной поставим uniform, чтобы получать данные из приложения OpenGl.

uniform vec3 lightDir;

Вершинный шейдер имеет доступ к нормалям, установленным OpenGL, через attribute-переменную gl_Normal. Это нормаль, объявленная в OpenGL с помощью функции glNormal().

Поскольку оба направления, и нормали, и луча света, объявлены в одном пространстве, вершинный шейдер может сразу приступить к вычислению косинуса между направлением света (lightDir), и нормалью. Косинус может быть рассчитан с помощью следующей функции:

cos(lightDir,normal) = lightDir . normal / ( |lightDir| * |normal| )

Где «.» — формула dot product, описание которой вы можете найти в [a=https://masandilov.ru/opengl/:a]уроках OpenGL.
Фурмула будет проще, если оба направления — и нормаль, и направление освещения, нормализованы, т.е:

| normal | = 1
| lightDir | = 1

Если они гарантированно нормализованы, формула косинуса также может быть упрощена:

cos(lightDir,normal) = lightDir . normal

Так как переменная lightDir обьявлена в приложении OpenGL, мы можем предположить, что она передается в шейдер уже нормализованной. Нормализация нормалей дял каждой вершины в шейдере займёт уйму времени и ресурсов, так что лучше бы вам заиметь привычку «нормализовывать» нормали перед передачей в шейдер.

А косинус, который мы назовём «intensity», может быть вычислен с помощью функции dot(), предоставляемой OpenGL:

intensity = dot(lightDir, gl_Normal);

И единственная вещь, которую остаётся сделать в вершинном шейдере — это трансформировать вершины.

Полный код шейдера, файл toon.vert:

    uniform vec3 lightDir;

varying float intensity;

void main()
{
vec3 ld;

intensity = dot(lightDir,gl_Normal);

gl_Position = ftransform();
}

 

Теперь фрагментный шейдер. Всё, что остаётся сделать, это задать цвет пиксела, основываясь на интенсивности. Интенсивность должна быть передана пикселному шейдеру, так как именно он отвечает за установку цвета пиксела. Как говорилось выше, интенсивность обьявляется как varying-переменная в обоих шейдерах, и должна быть записана в вершинном шейдере, чтобы пикселный шейдер её считал.

Цвет может быть вычислен пикселным шейдером следующим образом:

vec4 color;

if (intensity > 0.95)
color = vec4(1.0,0.5,0.5,1.0);
else if (intensity > 0.5)
color = vec4(0.6,0.3,0.3,1.0);
else if (intensity > 0.25)
color = vec4(0.4,0.2,0.2,1.0);
else
color = vec4(0.2,0.1,0.1,1.0);

Как следует из этого кода, самый яркий цвет устанавливается при интенсивности света > 0.95, а самый темный — при < 0.25. Всё, что остаётся сделать в пикселном шейдере — это установить gl_FragColor в значение, равное color.

Код пикселного шейдера, файл toon.frag:

    varying float intensity;

void main()
{
vec4 color;

if (intensity > 0.95)
color = vec4(1.0,0.5,0.5,1.0);
else if (intensity > 0.5)
color = vec4(0.6,0.3,0.3,1.0);
else if (intensity > 0.25)
color = vec4(0.4,0.2,0.2,1.0);
else
color = vec4(0.2,0.1,0.1,1.0);

gl_FragColor = color;
}

 

Следующее изображение показывает конечный результат. Не слишком красиво, верно?
Главная проблема состоит в том, что мы интерполируем интенсивность. Это не то же самое, что вычислять интенсивность из нормали полигона. В следующем уроке рассмотрим способ покрасивее!