Эта статья является продолжением WebGL 3D Камеры. Если вы не читали это, я предлагаю начать там.
Есть много способов реализовать освещение. Возможно, самый простой - это направленное освещение.
Направленное освещение предполагает, что свет идет равномерно с одного направления. Солнце в ясный день часто считается направленным светом. Оно так далеко, что его лучи можно считать падающими на поверхность объекта все параллельно.
Вычисление направленного освещения на самом деле довольно просто. Если мы знаем, в каком направлении движется свет, и мы знаем, в каком направлении обращена поверхность объекта, мы можем взять скалярное произведение 2 направлений, и оно даст нам косинус угла между 2 направлениями.
Вот пример
Перетащите точки вокруг, если вы получите их точно противоположными друг другу, вы увидите, что скалярное произведение равно -1. Если они находятся в точно том же месте, скалярное произведение равно 1.
Как это полезно? Ну, если мы знаем, в каком направлении обращена поверхность нашего 3d объекта, и мы знаем направление, в котором светит свет, то мы можем просто взять скалярное произведение их, и оно даст нам число 1, если свет направлен прямо на поверхность, и -1, если они направлены прямо противоположно.
Мы можем умножить наш цвет на это значение скалярного произведения, и бум! Свет!
Одна проблема, как мы знаем, в каком направлении обращены поверхности нашего 3d объекта.
Я не имею представления, почему они называются нормалями, но по крайней мере в 3D графике нормаль
Вот некоторые нормали для куба и сферы.
Линии, торчащие из объектов, представляют нормали для каждой вершины.
Обратите внимание, что у куба есть 3 нормали в каждом углу. Это потому, что вам нужно 3 разные нормали, чтобы представить способ, которым каждая грань куба, эм, … обращена.
Здесь нормали также окрашены в зависимости от их направления с положительным x, будучи красным, вверх, будучи зеленым, и положительным z, будучи синим.
Итак, давайте добавим нормали к нашему F
из наших предыдущих примеров,
чтобы мы могли его осветить. Поскольку F
очень блочный, и его грани выровнены
по оси x, y или z, это будет довольно легко. Вещи, которые обращены вперед,
имеют нормаль 0, 0, 1
. Вещи, которые обращены назад, имеют 0, 0, -1
. Обращенные
влево имеют -1, 0, 0
, обращенные вправо имеют 1, 0, 0
. Вверх - это 0, 1, 0
, а вниз - 0, -1, 0
.
function setNormals(gl) {
var normals = new Float32Array([
// левая колонка спереди
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
// верхняя перекладина спереди
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
// средняя перекладина спереди
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
// левая колонка сзади
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
// верхняя перекладина сзади
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
// средняя перекладина сзади
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
// верх
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
// правая сторона верхней перекладины
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// под верхней перекладиной
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
// между верхней перекладиной и средней
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// верх средней перекладины
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
// правая сторона средней перекладины
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// низ средней перекладины
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
// правая сторона низа
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// низ
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
// левая сторона
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
]);
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
}
и настроим их. Пока мы этим занимаемся, давайте уберем цвета вершин,
чтобы было легче увидеть освещение.
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
-var colorLocation = gl.getAttribLocation(program, "a_color");
+var normalLocation = gl.getAttribLocation(program, "a_normal");
...
-// Create a buffer for colors.
-var buffer = gl.createBuffer();
-gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
-gl.enableVertexAttribArray(colorLocation);
-
-// We'll supply RGB as bytes.
-gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
-
-// Set Colors.
-setColors(gl);
// Create a buffer for normals.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(normalLocation);
gl.vertexAttribPointer(normalLocation, 3, gl.FLOAT, false, 0, 0);
// Set normals.
setNormals(gl);
Теперь нам нужно заставить наши шейдеры использовать их
Сначала вершинный шейдер, мы просто передаем нормали в
фрагментный шейдер
#version 300 es
// атрибут - это вход (in) в вершинный шейдер. // Он будет получать данные из буфера in vec4 a_position; -in vec4 a_color; +in vec3 a_normal;
// Матрица для преобразования позиций uniform mat4 u_matrix;
-// varying для передачи цвета в фрагментный шейдер -out vec4 v_color;
+// varying для передачи нормали в фрагментный шейдер +out vec3 v_normal;
// все шейдеры имеют основную функцию void main() { // Умножаем позицию на матрицу. gl_Position = u_matrix * a_position;
И фрагментный шейдер, мы будем делать математику, используя скалярное произведение
направления света и нормали
#version 300 es
precision highp float;
-// изменяемый цвет, переданный из вершинного шейдера -in vec4 v_color;
+// Переданный и изменяемый из вершинного шейдера. +in vec3 v_normal; + +uniform vec3 u_reverseLightDirection; +uniform vec4 u_color;
// нам нужно объявить выход для фрагментного шейдера out vec4 outColor;
void main() {
Затем нам нужно найти местоположения `u_color` и `u_reverseLightDirection`.
// lookup uniforms var matrixLocation = gl.getUniformLocation(program, “u_matrix”);
gl.getUniformLocation(program, "u_reverseLightDirection");
и нам нужно их установить
// Set the matrix. gl.uniformMatrix4fv(matrixLocation, false, matrix);
`normalize`, который мы разбирали раньше, сделает любые значения, которые мы туда положим,
единичным вектором. Конкретные значения в примере:
`x = 0.5`, что положительный `x` означает, что свет справа, указывая влево.
`y = 0.7`, что положительный `y` означает, что свет сверху, указывая вниз.
`z = 1`, что положительный `z` означает, что свет спереди, указывая в сцену.
относительные значения означают, что направление в основном указывает в сцену
и указывает больше вниз, чем вправо.
И вот это
Если вы повернете F, вы можете заметить что-то. F вращается,
но освещение не меняется. Когда F вращается, мы хотим, чтобы та часть,
которая обращена к направлению света, была самой яркой.
Чтобы исправить это, нам нужно переориентировать нормали, когда объект переориентируется.
Как мы делали для позиций, мы можем умножить нормали на какую-то матрицу. Самая очевидная
матрица была бы `world` матрица. Как есть сейчас, мы передаем только
1 матрицу, называемую `u_matrix`. Давайте изменим это, чтобы передавать 2 матрицы. Одну, называемую
`u_world`, которая будет мировой матрицей. Другую, называемую `u_worldViewProjection`,
которая будет тем, что мы сейчас передаем как `u_matrix`
#version 300 es
// атрибут - это вход (in) в вершинный шейдер. // Он будет получать данные из буфера in vec4 a_position; in vec3 a_normal;
*uniform mat4 u_worldViewProjection; +uniform mat4 u_world;
out vec3 v_normal;
void main() { // Умножаем позицию на матрицу.
gl_Position = u_worldViewProjection * a_position;
// ориентируем нормали и передаем в фрагментный шейдер
v_normal = mat3(u_world) * a_normal; }
Обратите внимание, что мы умножаем `a_normal` на `mat3(u_world)`. Это
потому, что нормали - это направление, поэтому нас не волнует трансляция.
Ориентационная часть матрицы находится только в верхней области 3x3
матрицы.
Теперь нам нужно найти эти uniform переменные
// lookup uniforms
gl.getUniformLocation(program, "u_matrix");
И нам нужно изменить код, который их обновляет
*var worldMatrix = m4.yRotation(fRotationRadians); *var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);
*// Set the matrices *gl.uniformMatrix4fv(
и вот это
Поверните F и обратите внимание, какая бы сторона ни была обращена к направлению света, она освещается.
Есть одна проблема, которую я не знаю, как показать напрямую, поэтому я
покажу это в диаграмме. Мы умножаем `normal` на
матрицу `u_world`, чтобы переориентировать нормали.
Что происходит, если мы масштабируем мировую матрицу?
Оказывается, мы получаем неправильные нормали.
нажмите, чтобы переключить нормали
Я никогда не беспокоился понять
решение, но оказывается, вы можете получить обратную матрицу мира,
транспонировать ее, что означает поменять местами столбцы и строки, и использовать это вместо
и вы получите правильный ответ.
В диаграмме выше <span style="color: #F0F;">фиолетовая</span> сфера
не масштабирована. <span style="color: #F00;">Красная</span> сфера слева
масштабирована, и нормали умножаются на мировую матрицу. Вы
можете видеть, что что-то не так. <span style="color: #00F;">Синяя</span>
сфера справа использует матрицу обратной транспонированной мира.
Нажмите на диаграмму, чтобы переключаться между разными представлениями. Вы должны
заметить, когда масштаб экстремальный, очень легко увидеть, что нормали
слева (world) **не** остаются перпендикулярными к поверхности сферы,
тогда как те, что справа (worldInverseTranspose), остаются перпендикулярными
к сфере. Последний режим делает их все затененными красным. Вы должны увидеть, что освещение
на 2 внешних сферах очень разное в зависимости от того, какая матрица используется.
Трудно сказать, какая правильная, поэтому это тонкая проблема, но
основанная на других визуализациях, ясно, что использование worldInverseTranspose
правильно.
Чтобы реализовать это в нашем примере, давайте изменим код так. Сначала мы обновим
шейдер. Технически мы могли бы просто обновить значение `u_world`,
но лучше, если мы переименуем вещи, чтобы они назывались тем, чем они на самом деле являются,
иначе это будет запутанно.
#version 300 es
// атрибут - это вход (in) в вершинный шейдер. // Он будет получать данные из буфера in vec4 a_position; in vec3 a_normal;
uniform mat4 u_worldViewProjection; -uniform mat4 u_world +uniform mat4 u_worldInverseTranspose;
// varyings для передачи нормали и цвета в фрагментный шейдер out vec4 v_color; out vec3 v_normal;
// все шейдеры имеют основную функцию void main() { // Умножаем позицию на матрицу. gl_Position = u_worldViewProjection * a_position;
// ориентируем нормали и передаем в фрагментный шейдер
Затем нам нужно найти это
gl.getUniformLocation(program, "u_worldInverseTranspose");
И нам нужно вычислить и установить это
var worldMatrix = m4.yRotation(fRotationRadians); var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix); +var worldInverseMatrix = m4.inverse(worldMatrix); +var worldInverseTransposeMatrix = m4.transpose(worldInverseMatrix);
// Set the matrices gl.uniformMatrix4fv( worldViewProjectionLocation, false, worldViewProjectionMatrix); -gl.uniformMatrix4fv(worldLocation, false, worldMatrix); +gl.uniformMatrix4fv(
и вот код для транспонирования матрицы
var m4 = { transpose: function(m) { return [ m[0], m[4], m[8], m[12], m[1], m[5], m[9], m[13], m[2], m[6], m[10], m[14], m[3], m[7], m[11], m[15], ]; }, …
Поскольку эффект тонкий и поскольку мы ничего не масштабируем,
нет заметной разницы, но по крайней мере теперь мы готовы.
Я надеюсь, что этот первый шаг в освещение был ясен. Далее [точечное освещение](webgl-3d-lighting-point.html).
<div class="webgl_bottombar">
<h3>Альтернативы mat3(u_worldInverseTranspose) * a_normal</h3>
<p>В нашем шейдере выше есть строка, как эта</p>
<pre class="prettyprint">
v_normal = mat3(u_worldInverseTranspose) * a_normal;
</pre>
<p>Мы могли бы сделать это</p>
<pre class="prettyprint">
v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
</pre>
<p>Потому что мы установили <code>w</code> в 0 перед умножением, это
привело бы к умножению трансляции из матрицы на 0, эффективно удаляя ее. Я думаю, что это
более распространенный способ сделать это. Способ mat3 выглядел чище для меня, но
я часто делал это и так тоже.</p>
<p>Еще одно решение было бы сделать <code>u_worldInverseTranspose</code> <code>mat3</code>.
Есть 2 причины не делать этого. Одна в том, что у нас могут быть
другие потребности для полного <code>u_worldInverseTranspose</code>, поэтому передача всего
<code>mat4</code> означает, что мы можем использовать это для этих других потребностей.
Другая в том, что все наши матричные функции в JavaScript
делают матрицы 4x4. Создание целого другого набора для матриц 3x3
или даже преобразование из 4x4 в 3x3 - это работа, которую мы бы предпочли
не делать, если бы не было более убедительной причины.</p>
</div>