Основная работа по созданию освещения объекта выполняется в шейдерах в GPU, в стандартном коде javascript от нас требуется только установить ряд объектов и передать их в шейдеры. Это следующие объекты:
Матрицу нормалей
Нормали вершины
Направления света и световые точки
Цвета освещения
Сразу перейдем к шейдерам, чтобы рассмотреть принцип создания освещенной трехмерной сцены. А затем отдельно рассмотрим код javascript.
Итак, оба шейдера буду выглядеть так:
<script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; attribute vec3 aVertexNormal; attribute vec2 aVertexTextureCoords; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat3 uNMatrix; uniform vec3 uLightPosition; uniform vec3 uAmbientLightColor; uniform vec3 uDiffuseLightColor; uniform vec3 uSpecularLightColor; varying vec2 vTextureCoords; varying vec3 vLightWeighting; const float shininess = 16.0; void main() { // установка позиции наблюдателя сцены vec4 vertexPositionEye4 = uMVMatrix * vec4(aVertexPosition, 1.0); vec3 vertexPositionEye3 = vertexPositionEye4.xyz / vertexPositionEye4.w; // получаем вектор направления света vec3 lightDirection = normalize(uLightPosition - vertexPositionEye3); // получаем нормаль vec3 normal = normalize(uNMatrix * aVertexNormal); // получаем скалярное произведение векторов нормали и направления света float diffuseLightDot = max(dot(normal, lightDirection), 0.0); // получаем вектор отраженного луча и нормализуем его vec3 reflectionVector = normalize(reflect(-lightDirection, normal)); // установка вектора камеры vec3 viewVectorEye = -normalize(vertexPositionEye3); float specularLightDot = max(dot(reflectionVector, viewVectorEye), 0.0); float specularLightParam = pow(specularLightDot, shininess); // отраженный свет равен сумме фонового, диффузного и зеркального отражений света vLightWeighting = uAmbientLightColor + uDiffuseLightColor * diffuseLightDot + uSpecularLightColor * specularLightParam; // Finally transform the geometry gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoords = aVertexTextureCoords; } </script> <script id="shader-fs" type="x-shader/x-fragment"> precision mediump float; varying vec2 vTextureCoords; varying vec3 vLightWeighting; uniform sampler2D uSampler; void main() { vec4 texelColor = texture2D(uSampler, vTextureCoords); gl_FragColor = vec4(vLightWeighting.rgb * texelColor.rgb, texelColor.a); } </script>
Все переменные типа attribute
и uniform
мы затем передадим из основной программы в шейдер. Эти переменные и задают параметры
освещения.
В модели отражения Фонга отраженный свет представлен как сумма фонового, диффузного и зеркального отражений. В шейдерах за хранение параметров
отраженного света отвечает переменная varying vec3 vLightWeighting
. В вершинном шейдере мы находим эту сумму и затем передаем переменную
во фрагментный шейдер для окончательной установки цвета фрагмента.
Все цвета для нахождения параметров отраженного света передаются через переменные uniform vec3 uAmbientLightColor
,
uniform vec3 uDiffuseLightColor
и uniform vec3 uSpecularLightColor
.
Также передаем в шейдер положение источника света через переменную uniform vec3 uLightPosition
.
Первым делом нам надо модифицировать координаты вершины и ее нормаль, чтобы правильно провести вычисления по нахождению отражения света, поскольку на входе в шейдер и вершина, и нормаль пока не учитывают преобразования с матрицами - вращения, перемещения и т.д.:
// установка позиции наблюдателя сцены vec4 vertexPositionEye4 = uMVMatrix * vec4(aVertexPosition, 1.0); vec3 vertexPositionEye3 = vertexPositionEye4.xyz / vertexPositionEye4.w; // получаем вектор направления света vec3 lightDirection = normalize(uLightPosition - vertexPositionEye3); // получаем нормаль vec3 normal = normalize(uNMatrix * aVertexNormal);
Вершину преобразуем с помощью матрицы, для установки нормалей применяем матрицу нормалей, которую также устанавливаем в коде javascript. А также вычисляем вектор от вершины до источника света.
Фоновое отражение света (ambient light) представляет собой отражение при естественном освещении. Его можно выразить формулой I = Ka*Ia
, где Ka
- цветовые параметры материала в
виде значений RGB, а Ia
- это цвет фонового света также в виде значений RGB.
При диффузном отражении света (diffuse light) лучи отражаются под несколькими углами, а не под одним, как при зеркальном отражении. Диффузное отражение характерно прежде всего для неровных шершавых поверхностей.
Диффузное отражение света высчитывается по формуле I = Kd *Id *max(cos θ, 0)
. Здесь кроме диффузного материала Kd
и цвета освещения
Id
присутствует дополнительный параметр. Этот параметр учитывает направление направленного на поверхность луча. А угол θ
как раз представляет собой угол между нормалью поверхности и вектором направления луча света. Наличие функции максимума, которая выбирает максимальное
число из косинуса угла и нуля позволяет отсечь отрицательные значения и свести их к нулю.
Схематично диффузное отражение можно показать так:
Но что такое cos θ
? Это скалярное произведение векторов N (вектор нормали) и L (вектор, представляющий луч света). И, таким образом,
зная эти вектора, мы можем рассчитать диффузное отражение (для определения скалярного произведения в языке шейдеров есть специальная функция dot
):
// получаем скалярное произведение векторов нормали и направления света float diffuseLightDot = max(dot(normal, lightDirection), 0.0);
Затем получаем зеркальное отражение света (specular light). Подобное отражение характеризуется тем, что падающий луч света отражается под одним углом.
Зеркальное отражение рассчитывается по формуле I = Ks * Is max(cos θ, 0)a
. В данном случае
Ks
- материал, а Is
- цвет зеркального отражения. Угол θ
здесь представляет угол между
вектором, направленным от точки к наблюдателю, и вектором отражаемого луча. Степень a
указывает на блеск материала.
Схематично формулу можно представить себе так:
И опять же cos θ
в данном случае это скалярное произведение векторов R (вектор отраженного луча) и V (вектор, направленный к наблюдателю). А,
получив на предыдущем шаге векторы нормали и направления падающего луча, мы можем получить вектор луча отражения R и вычислить значение зеркального
отражения: R=2(L * N)*N-L
. Однако язык шейдеров имеет встроенную функцию reflect
, которая позволяет найти вектор отраженного
луча по нормали и направлению падающего света. Собственно эта функция и применяется.
Но поскольку при зеркальном отражении вектор луча падающего света направлен в противоположную сторону в отличие от вектора, который применяется
при подсчете диффузного отражения света, поэтому мы используем знак минуса: vec3 reflectionVector = normalize(reflect(-lightDirection, normal));
А затем применяем скалярное произведение и возводим в степень shininess. Параметр shininess задается произвольно: чем он выше, тем более блестящим будет казаться отблекс.
vec3 viewVectorEye = -normalize(vertexPositionEye3); float specularLightDot = max(dot(reflectionVector, viewVectorEye), 0.0); float specularLightParam = pow(specularLightDot, shininess);
И в конце мы собираем все световые значения и получаем общее значение отраженного света, которое затем передаем во фрагментный шейдер.
А там применяем полученные значения к цветам текстуры: gl_FragColor = vec4(vLightWeighting.rgb * texelColor.rgb, texelColor.a);
.
И в итоге получаем имитацию освещения объекта.
В целях упрощения примера здесь не используются материалы, но далее мы их затронем, особой сложности они представляют, просто добавляются три дополнительные переменные. А в данном случае передаваемые цветовые значения отражений света уже можно представлять как произведение цвета освещения и цвета материала. Поэтому большой ошибки от неиспользования материалов здесь не будет.
Модель Фонга - не единственная модель отражения света, которая есть и которую можно использовать в WebGL. Существуют различные техники создания освещения. Однако она довольно популярная и позволяет учесть различные аспекты при освещении. Ну а теперь перейдем к основной программе на javascript.