Строим 3D проекты – Шаг 9

До настоящего момента мы работали с объектами в равномерном окружающем свете. Теперь рассмотрим направленные источники света и отражение света от объектов при этом. Направленный свет делает объект более реалистичным и трехмерным.
Виды источников света и отражений:
Окружающий свет (ОС). Равномерный.
|
Отражение окружающего света (ОО). От ОС. Матовое. |
Направленный свет (НС). От очень далеких источников, например, от солнца или луны.
|
Рассеянное отражение (РО) и зеркальное отражение (ЗО).
|
Точечный свет (ТС). От близкого источника, например, от лампочки. |
|
Точечный свет направленный (ТСН). Сформированный светильником. |
|
Сумма трех видов отражений |
|
По умолчанию мы работали с ОО в ОС. Теперь сделаем для начала РО в НС. Должен получиться примерно такой результат:

Отражение в окружающем свете
Настроим сначала по-другому ОО в ОС. Подключим буфер для цвета, как в шаге 5, чтобы цвет при отключенной текстуре передавался в шейдеры из программы JS таким образом:
<script id="shader-vs-09pr" type="x-shader/x-vertex"> varying highp vec4 vColor09pr; … vColor09pr = vec4(aVertexColor09pr, 1.0); </script> <script id="shader-fs-09pr" type="x-shader/x-fragment"> varying highp vec4 vColor09pr; … //Было так: gl_FragColor = vec4(1.0, 0.1, 0.1, 1.0); //Red. gl_FragColor = vec4(vColor09pr.xyz, vColor09pr.a); </script>
Включаем переменные для буфера и отправки буфера в шейдер:
//Colors. var trianglesColorBuffer09pr = null; var vertexColorAttribute09pr = null;
Задаем цвета для вершин. Все цвета делаем одинаковые, чтобы потом лучше были видны эффекты от освещения:
function setupBuffers09pr()
{
var triangleVerticeColors = [];
…
for(var i=0; i<VertexIndices.length; ++i) //0 to 60
{
…
//Color of vertice. -----------------------------
triangleVerticeColors.push (0.8); //R
triangleVerticeColors.push (0.1); //G
triangleVerticeColors.push (0.1); //B
}
…
//Color.
trianglesColorBuffer09pr = gl09pr.createBuffer();
gl09pr.bindBuffer (gl09pr.ARRAY_BUFFER, trianglesColorBuffer09pr);
gl09pr.bufferData (gl09pr.ARRAY_BUFFER, new Float32Array (triangleVerticeColors), gl09pr.STATIC_DRAW );
}
Отправляем цвета в шейдеры:
function MoveStaticDataInShader09pr()
{
//Move data into shader.
…
//Color
vertexColorAttribute09pr = gl09pr.getAttribLocation (glProgram09pr, "aVertexColor09pr");
gl09pr.enableVertexAttribArray (vertexColorAttribute09pr);
gl09pr.bindBuffer (gl09pr.ARRAY_BUFFER, trianglesColorBuffer09pr);
gl09pr.vertexAttribPointer (vertexColorAttribute09pr, 3, gl09pr.FLOAT, false, 0, 0);
}
Смотрим результат, он такой же, как и был раньше:

Теперь переходим к РО в НС
Рассеянное отражение (РО) и зеркальное отражение (ЗО) зависят от угла падения света, поэтому необходимо определить лицевую сторону каждой поверхности. Это делается с помощью вычисления нормального вектора N к поверхности (нормальный — перпендикулярный к поверхности):

Основное выражение освещённости записывается как скалярное произведение N • L, где N — это нормаль к освещаемой поверхности, а L — это нормализованный вектор направления света. В результате поверхность выглядит яркой, когда на неё прямо падает свет, и тёмной, когда этого не происходит.
Нам понадобится преобразование длины вектора в равную единице – нормализация вектора (мы просто делим каждый компонент вектора на длину вектора):

На такой вектор удобно умножать – он не искажает сомножитель.
Сначала создадим шейдеры. Начнем с финального действия программы, то есть с пиксельного шейдера.
<script id="shader-fs-09pr" type="x-shader/x-fragment">
varying highp vec3 vLight09pr;
…
else
{
//gl_FragColor = vec4(0.8, 0.1, 0.1, 1.0); //RED
//gl_FragColor = vec4(vColor09pr.xyz, vColor09pr.a); //No light.
gl_FragColor = vec4(vColor09pr.xyz * vLight09pr, vColor09pr.a);
}
}
</script>
Мы умножили вектор цвета на вектор света. Вектор света мы вычисляем в вершинном шейдере:
<script id="shader-vs-09pr" type="x-shader/x-vertex">
…
//Light.
attribute vec3 aVertexNormal09pr;
uniform mat3 uConvertNormalMatrix09pr;
varying highp vec3 vLight09pr;
void main(void)
{
…
//Light.
//Ambient.
vec3 ambientLight = vec3(0.5, 0.5, 0.5);
//Diffuse.
vec3 diffuseLightColor = vec3(0.5, 0.5, 0.5);
//1st step - vector L, length = 1.
vec3 directionalLightPosition = normalize( vec3(10.0, 10.0, 5.0) );
//2nd step - vector N.
vec3 transformedNormal = uConvertNormalMatrix09pr * aVertexNormal09pr;
//3rd step.
float diffuseLightAmount = max( dot( transformedNormal, directionalLightPosition), 0.0);
//Out.
//1 variant - Ambient.
//vLight09pr = ambientLight;
//2 variant - Diffuse.
//vLight09pr = diffuseLightAmount * diffuseLightColor;
//3 variant - Ambient + diffuse.
vLight09pr = ambientLight + (diffuseLightAmount * diffuseLightColor);
}
</script>
Определение степени рассеяния света требует непростого комментария:

Теперь нам осталось в программе JS задать входные данные вершинного шейдера – матрицы нормалей aVertexNormal09pr и матрицы преобразования этих нормалей в проекцию МВП uConvertNormalMatrix09pr.
Добавляем переменные для хранения буфера нормальных векторов, его передачи в шейдер и матрицы их преобразования в проекцию МВП.
//Light. var trianglesNormalBuffer09pr = null; var vertexNormalAttribute09pr = null; var ConvertNormalMatrix09pr = mat3.create();
В setupWebGL мы делаем инверсную матрицу ConvertNormalMatrix09pr (при умножении на которую исходная матрица даёт в результате единичную матрицу) из матрицы модельных координат mvMatrix09pr:
mat4.toInverseMat3 (mvMatrix09pr, ConvertNormalMatrix09pr); mat3.transpose (ConvertNormalMatrix09pr);
Матрица ConvertNormalMatrix09pr должна быть 3×3. Поэтому мы берем верхнюю часть матрицы mvMatrix09pr и вычисляем транспонированную матрицу (замена строк на столбцы).
Включаем поиск униформы для матрицы преобразования:
function getMatrixUniforms()
{
…
glProgram09pr.normalMatrixUniform = gl09pr.getUniformLocation (glProgram09pr, "uConvertNormalMatrix09pr");
}
и готовимся передавать в униформу вычисленную матрицу преобразования ConvertNormalMatrix09pr:
function setMatrixUniforms()
{
…
gl09pr.uniformMatrix3fv (glProgram09pr.normalMatrixUniform, false, ConvertNormalMatrix09pr);
}
Теперь к вычислению нормалей. Мы используем две грани каждого треугольника в качестве пересекающихся векторов:

Сначала вычисляем координаты двух граней, и для определения перпендикулярного к ним вектора-нормали вычисляем векторное произведение этих граней.
Вставляем в setupBuffers09pr () вычисление нормалей для каждого треугольника:
var triangleNormals = [];
//20 triangles - normal will be the same for each vertex of triangle
for(var i=0; i<VertexIndices.length; i+=3)
{
var a = VertexIndices[i]; //1st triangle's point.
var b = VertexIndices[i + 1]; //2nd triangle's point.
var c = VertexIndices[i + 2]; //3rd triangle's point.
//1st side (a-b) of triangle.
var v1 = [
triangleVerticesOriginal[ a*3] - triangleVerticesOriginal[ b*3],
triangleVerticesOriginal[ a*3 + 1] - triangleVerticesOriginal[ b*3 + 1],
triangleVerticesOriginal[ a*3 + 2] - triangleVerticesOriginal[ b*3 + 2],
];
//2nd side (a-c) of triangle.
var v2 = [
triangleVerticesOriginal[ a*3] - triangleVerticesOriginal[ c*3],
triangleVerticesOriginal[ a*3 + 1] - triangleVerticesOriginal[ c*3 + 1],
triangleVerticesOriginal[ a*3 + 2] - triangleVerticesOriginal[ c*3 + 2],
];
//Count normal - the cross-product of two sides.
//[w.x = u.Y * v.Z - u.Z * v.Y;
// w.y = u.Z * v.X - u.X * v.Z;
// w.z = u.X * v.Y - u.Y * v.X;]
var cross = [
v1[1]*v2[2] - v1[2]*v2[1],
v1[2]*v2[0] - v1[0]*v2[2],
v1[0]*v2[1] - v1[1]*v2[0],
];
//Push in array same value for each of the three vertices.
triangleNormals.push.apply (triangleNormals, cross);
triangleNormals.push.apply (triangleNormals, cross);
triangleNormals.push.apply (triangleNormals, cross);
}
trianglesNormalsBuffer09pr = gl09pr.createBuffer();
gl09pr.bindBuffer (gl09pr.ARRAY_BUFFER, trianglesNormalsBuffer09pr);
gl09pr.bufferData (gl09pr.ARRAY_BUFFER, new Float32Array (triangleNormals), gl09pr.STATIC_DRAW);
Передаем буфер нормалей в шейдер:
function MoveStaticDataInShader09pr()
{
…
//Light.
vertexNormalAttribute09pr = gl09pr.getAttribLocation (glProgram09pr, "aVertexNormal09pr");
gl09pr.enableVertexAttribArray (vertexNormalAttribute09pr);
gl09pr.bindBuffer (gl09pr.ARRAY_BUFFER, trianglesNormalBuffer09pr);
gl09pr.vertexAttribPointer (vertexNormalAttribute09pr, 3, gl09pr.FLOAT, false, 0, 0);
}
В результате, пока что видим отдельные треугольники фигуры:

Проблема в разном направлении обхода вершин треугольников: одни – по часовой стрелке, другие – против. Чтобы нормали были направлены в одном направлении, необходимо, чтобы обход вершин был везде одинаковым, например, против часовой стрелки. Поменяем порядок части треугольников:

VertexIndices = [ //front face 0,1,3, //tr1 1,4,3, //tr2 1,2,4, //tr3 3,4,5, //tr4 //rear face 6,7,9, //tr5 7,10,9, //tr6 7,8,10, //tr7 9,10,11, //tr8 //left side 0,3,6, //tr9 3,9,6, //tr10 3,5,9, //tr11 5,11,9, //tr12 //right side 2,8,4, //tr13 4,8,10, //tr14 4,10,5, //tr15 5,10,11, //tr16 //bottom side 0,6,1, //tr17 1,6,7, //tr18 1,7,2, //tr19 2,7,8, //tr20 ];
Теперь результат ожидаемый:

Попробуем не делать нормализацию вектора направления света, тогда значения влияют на близость источника:
//vec3 directionalLightPosition = normalize( vec3(10.0, 10.0, 5.0) ); vec3 directionalLightPosition = vec3(10.0, 10.0, 5.0);

Точечный свет (ТС)
Чтобы преобразовать НС в ТС нужно подсчитать направления от ТС к каждой точке объекта. В вершинном шейдере добавим такой расчет:
//Point. vec3 pointLightPosition = vec3(1.0,2.0,-1.0); //1st vec3 pointLightDirection = normalize( vec3(pointLightPosition.xyz - aVertexPosition09pr.xyz)); //2nd vec3 L = vec3(uPMatrix09pr * uMVMatrix09pr * vec4(pointLightDirection, 1.0)); //3rd vec3 N = uConvertNormalMatrix09pr * aVertexNormal09pr; //4th float diffuseLightAmountPoint = max( dot(normalize(N), normalize(L)), 0.0); … //4 variant - Ambient + Point. vLight09pr = ambientLight + (diffuseLightAmountPoint * aVertexColor09pr);
В результате более плавный градиентный свет на объекте:

Свет и текстура
Наконец, попробуем добавить к текстуре наше новое освещение с РО от ТС. В вершинном шейдере сделаем однотонный свет vec3 (.8,.8,.8) вместо красного:
//5 variant - Ambient + Point + gray light for Texture. vLight09pr = ambientLight + (diffuseLightAmountPoint * vec3(.8,.8,.8));
А в пиксельном шейдере смешиваем цвет текстур и цвет освещения:
if(uDoTexturing09pr == 1)
{
…
highp vec4 textureColor = mix(tileColor, logoColor, logoColor.a);
gl_FragColor = vec4(textureColor.xyz * vLight09pr, textureColor.a);
}

Наша модель освещения далека от совершенства и реалистичности. Можно еще добавить зеркальное отражение (ЗО), световую модель (например Phong), интерполяцию значений нормалей, прозрачность, туман, тени… Но это уже “красивости”, которые можно рассмотреть в свободное время. А так, можно уже с нашими знаниями воплощать в жизнь простые проекты в мире 3D!