оглавление

WebGLFundamentals.org

Fix, Fork, Contribute

WebGL 3D - Точечное освещение

Эта статья продолжает статью о направленном освещении. Если вы её ещё не читали, рекомендую ознакомиться сначала с ней.

В последней статье мы рассмотрели направленное освещение, где свет падает повсеместно с одним направлением. Мы установили это направление перед рендерингом.

А что если вместо задания направления света мы выберем точку источника света в 3D-пространстве и вычислим направление освещения в каждой точке поверхности модели в нашем шейдере? Это и даст нам точечное освещение.

Если вы повернёте поверхность, то увидите, что каждая точка поверхности имеет разный вектор поверхность-источник света (surfaceToLight). Значение скалярного произведения нормали поверхности и каждого вектора от поверхности до источника света будет отличаться для каждой точки поверхности.

Итак, приступим к реализации.

Для начала нам понадобится положение источника света.

  • uniform vec3 u_lightWorldPosition;

Ещё нам нужно вычислить мировые координаты поверхности. Для этого мы можем умножить наши координаты на мировую матрицу

  • uniform mat4 u_world;
  •  
  • ...
  •  
  • // вычисляем мировые координаты поверхности
  • vec3 surfaceWorldPosition = (u_world * a_position).xyz;

Далее вычислим вектор от поверхности к источнику света по аналогии с направленным освещением, но на этот раз мы будем вычислять вектор для каждой точки поверхности.

  • v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;

Всё вместе выглядит следующим образом

  • attribute vec4 a_position;
  • attribute vec3 a_normal;
  •  
  • uniform vec3 u_lightWorldPosition;
  •  
  • uniform mat4 u_world;
  • uniform mat4 u_worldViewProjection;
  • uniform mat4 u_worldInverseTranspose;
  •  
  • varying vec3 v_normal;
  •  
  • varying vec3 v_surfaceToLight;
  •  
  • void main() {
  • // Умножаем координаты на матрицу
  • gl_Position = u_worldViewProjection * a_position;
  •  
  • // Направляем нормали и передаём во фрагментный шейдер
  • v_normal = mat3(u_worldInverseTranspose) * a_normal;
  •  
  • // Вычисляем мировые координаты поверхности
  • vec3 surfaceWorldPosition = (u_world * a_position).xyz;
  • // вычисляем вектор от поверхности к источнику света
  • // и передаём его во фрагментный шейдер
  • v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
  • }

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

  • precision mediump float;
  •  
  • // Передаётся из вершинного шейдера
  • varying vec3 v_normal;
  • varying vec3 v_surfaceToLight;
  •  
  • uniform vec3 u_reverseLightDirection;
  • uniform vec4 u_color;
  •  
  • void main() {
  • // v_normal - это varying-переменная, которая интерполируется
  • // и поэтому не будет единичным вектором. Нормализуем
  • // переменную и получаем единичный вектор.
  • vec3 normal = normalize(v_normal);
  •  
  • vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
  •  
  • float light = dot(normal, u_reverseLightDirection);
  • float light = dot(normal, surfaceToLightDirection);
  •  
  • gl_FragColor = u_color;
  •  
  • // Умножаем только значение цвета (без прозрачности)
  • // на значение света
  • gl_FragColor.rgb *= light;
  • }

Далее получаем ссылки на u_world и u_lightWorldPosition

  • var reverseLightDirectionLocation =
  • gl.getUniformLocation(program, "u_reverseLightDirection");
  • var lightWorldPositionLocation =
  • gl.getUniformLocation(program, "u_lightWorldPosition");
  • var worldLocation =
  • gl.getUniformLocation(program, "u_world");

и задаём им значения

  • // Задаём значения матрицам
  • gl.uniformMatrix4fv(
  • worldLocation, false,
  • worldMatrix);
  • gl.uniformMatrix4fv(
  • worldViewProjectionLocation, false,
  • worldViewProjectionMatrix);
  •  
  • ...
  •  
  • // Задаём направление освещения
  • gl.uniform3fv(reverseLightDirectionLocation, m4.normalize([0.5, 0.7, 1]));
  • // Задаём положение освещения
  • gl.uniform3fv(lightWorldPositionLocation, [20, 30, 50]);

И получаем результат:

Теперь мы можем добавить нечто под названием блики.

Если вы посмотрите на глянцевый объект в реальном мире, то можно заметить, что когда свет отражается вам прямо в глаза, то эффект почти как от зеркала.

Мы можем имитировать этот эффект, достаточно рассчитать, отражается ли свет прямо в наблюдателя. Здесь нам снова поможет скалярное произведение.

Что же нам нужно проверить? Подумаем над этим. Угол падения и отражения света от поверхности равны, поэтому когда направление от поверхности к источнику света в точности равно направлению от поверхности до глаза, то перед нами идеальный угол для отражения света.

Если нам известно направление от поверхности нашей модели к свету (а нам это известно, мы как раз этим и занимались), и если нам известно направление поверхности к глазу/наблюдателю/камере (что мы можем вычислить), то мы можем сложить эти два вектора, нормализовать эту сумму и получить halfVector - то есть вектор, который находится на полпути между ними. Если этот половинный вектор и нормаль поверхности совпадают, то перед нами угол идеального отражения света. Но как можно узнать, что они совпадают? Нужно воспользоваться скалярным произведением, как и прежде. 1 = они совпадают, одинаковое направление, 0 = они перпендикулярны, -1 = они направлены в разные стороны.

Итак, сначала передадим положение глаза/наблюдателя/камеры, вычислим вектор от поверхности к наблюдателю и передадим его во фрагментный шейдер.

  • attribute vec4 a_position;
  • attribute vec3 a_normal;
  •  
  • uniform vec3 u_lightWorldPosition;
  • uniform vec3 u_viewWorldPosition;
  •  
  • uniform mat4 u_world;
  • uniform mat4 u_worldViewProjection;
  • uniform mat4 u_worldInverseTranspose;
  •  
  • varying vec3 v_normal;
  •  
  • varying vec3 v_surfaceToLight;
  • varying vec3 v_surfaceToView;
  •  
  • void main() {
  • // Умножаем координаты на матрицу
  • gl_Position = u_worldViewProjection * a_position;
  •  
  • // Направляем нормали и передаём во фрагментный шейдер
  • v_normal = mat3(u_worldInverseTranspose) * a_normal;
  •  
  • // Вычисляем мировые координаты поверхности
  • vec3 surfaceWorldPosition = (u_world * a_position).xyz;
  •  
  • // вычисляем вектор от поверхности к источнику света
  • // и передаём его во фрагментный шейдер
  • v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
  •  
  • // вычисляем вектор от поверхности к наблюдателю
  • // и передаём его во фрагментный шейдер
  • v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
  • }

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

  • // Получаем из вершинного шейдера
  • varying vec3 v_normal;
  • varying vec3 v_surfaceToLight;
  • varying vec3 v_surfaceToView;
  •  
  • uniform vec4 u_color;
  •  
  • void main() {
  • // v_normal - это varying-переменная, которая интерполируется
  • // и поэтому не будет единичным вектором. Нормализуем
  • // переменную и получаем единичный вектор.
  • vec3 normal = normalize(v_normal);
  •  
  • vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
  • vec3 surfaceToViewDirection = normalize(v_surfaceToView);
  • vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
  •  
  • float light = dot(normal, surfaceToLightDirection);
  • float specular = dot(normal, halfVector);
  •  
  • gl_FragColor = u_color;
  •  
  • // Умножаем только значение цвета (без прозрачности)
  • // на значение света
  • gl_FragColor.rgb *= light;
  •  
  • // Просто добавляем блики
  • gl_FragColor.rgb += specular;
  • }

Наконец, получаем ссылку на u_viewWorldPosition и задаём ей значение.

  • var lightWorldPositionLocation =
  • gl.getUniformLocation(program, "u_lightWorldPosition");
  • var viewWorldPositionLocation =
  • gl.getUniformLocation(program, "u_viewWorldPosition");
  •  
  • ...
  •  
  • // Матрица камеры
  • var camera = [100, 150, 200];
  • var target = [0, 35, 0];
  • var up = [0, 1, 0];
  • var cameraMatrix = makeLookAt(camera, target, up);
  •  
  • // Положение камеры/наблюдателя
  • gl.uniform3fv(viewWorldPositionLocation, camera);

И вот наш результат:

ТЫСЯЧА ЧЕРТЕЙ, КАК ЖЕ ЯРКО!

Мы можем укротить яркость, возведя результат скалярного произведения в степень. Это сузит блики и приведёт график от линейного к экспоненциальному.

Чем ближе красная линия к верхнему краю графика, тем ярче будут блики. С увеличением степени уменьшается диапазон ярких бликов, прижимаясь к правому краю.

Назовём эту степень shininess и добавим её в наш шейдер.

  • uniform vec4 u_color;
  • uniform float u_shininess;
  •  
  • ...
  •  
  • float specular = dot(normal, halfVector);
  • float specular = 0.0;
  • if (light > 0.0) {
  • specular = pow(dot(normal, halfVector), u_shininess);
  • }

Скалярное произведение может принимать отрицательные значения. Возведение в степень негативного значения не определено в WebGL, что не очень-то хорошо. Поэтому в случае отрицательного значения мы просто задаём ему значение 0.0.

Само собой, нам нужно получить ссылку на переменную и задать ей значение

  • var shininessLocation = gl.getUniformLocation(program, "u_shininess");
  •  
  • ...
  •  
  • // установка степени яркости
  • gl.uniform1f(shininessLocation, shininess);

И вот результат

Последнее, о чём бы я хотел поговорить в этой статье, - это цвета света.

До этого момента мы умножали light на цвет буквы F. Но мы также могли бы задать цвет самого источника света, если бы нам было нужно цветное освещение.

  • uniform vec4 u_color;
  • uniform float u_shininess;
  • uniform vec3 u_lightColor;
  • uniform vec3 u_specularColor;
  •  
  • ...
  •  
  • // Умножаем только значение цвета (без прозрачности)
  • // на значение света
  • gl_FragColor.rgb *= light * u_lightColor;
  •  
  • // Просто добавляем блики
  • gl_FragColor.rgb += specular * u_specularColor;
  • }

и конечно же

  • var lightColorLocation =
  • gl.getUniformLocation(program, "u_lightColor");
  • var specularColorLocation =
  • gl.getUniformLocation(program, "u_specularColor");

и

  • // задаём цвет освещения
  • gl.uniform3fv(lightColorLocation, m4.normalize([1, 0.6, 0.6])); // красный свет
  • // заадаём цвет бликов
  • gl.uniform3fv(specularColorLocation, m4.normalize([1, 0.6, 0.6])); // красный свет

Далее в программе - прожектор.

Почему pow(negative, power) не определено?

Что означает следующее?

pow(5, 2)

Это можно представить как

5 * 5 = 25

А что насчёт?

pow(5, 3)

Аналогично, можно выразить как

5 * 5 * 5 = 125

Хорошо, тогда

pow(-5, 2)

можно выразить как

-5 * -5 = 25

а

pow(-5, 3)

примет вид

-5 * -5 * -5 = -125

Насколько вам известно, умножение отрицательного числа на отрицательное даёт положетильное. Умножение ещё раз на отрицательное даст отрицательное.

Но в таком случае что будет означать следующее выражение?

pow(-5, 2.5)

Как понять, будет ли результат положительным или отрицательным? Это относится к области мнимых чисел.

Нашли ошибку? Создайте задачу на github.