WebGL Урок 12 — Точечное освещение

Урок 13 >> << Урок 11

Материал в оригинале можно найти здесь

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

Вот как выглядит результат урока в браузере с поддержкой WebGL:

Здесь можно посмотреть онлайн-демонстрацию, если ваш браузер поддерживает WebGL. Здесь можно узнать, что делать, если браузер не поддерживает WebGL. Вы увидите вращающиеся сферу и куб. Возможно, что оба этих объекта некоторое время будут белыми, пока текстуры не загрузятся, но после загрузки вы поймете, что сфера — это на самом деле Луна, а куб (масштаб не соблюден) — это деревянный ящик. Оба освещаются точечным источником света, расположенным между ними. Если вы хотите изменить положение света, его цвет и прочее, то для этого под элементом canvas есть уже привычные вам поля ввода.

Теперь о том, как это все работает…

Уже обычное предупреждение: эти уроки ориентированы на людей с некоторым знанием программирования, но без опыта работы с 3D-графикой. С хорошим пониманием того, что происходит в коде, вы быстро начнете писать собственные 3D веб-страницы. Если вы не прочитали предыдущие уроки, возможно, вам следует сделать это перед чтением урока, где я буду объяснять лишь новые вещи. Урок основан на одиннадцатом уроке, поэтому вы должны хорошо понимать происходившие там вещи (и прошу оставлять комментарии, если вдруг что-то осталось неясным!).

Как и прежде здесь могут быть ошибки. Если встретите что-то некорректное, дайте мне знать и я постараюсь поскорей их исправить.

Вы можете посмотреть код этого примера двумя способами: посмотреть исходный код страницы с демонстрацией или, если вы используете GitHub, вы можете копировать урок (и другие уроки) из репозитория.

Начнем с описание того, что именно мы хотим сделать с точечным освещением. Разница между им и направленным освещением заключается в том, что свет идет из точки внутри сцены. Это означает, что угол, под которым падает свет, отличается для каждой точки сцены. Поэтому очевидным подходом будет вычисление направления источника света в каждой вершине и затем проведение тех же самых расчетов, которые были в случае с направленным освещением. Этим мы и займемся!

Возможно, вы подумаете, что лучше бы было рассчитывать направление света не для каждой вершины, а для каждой точки между вершинами — то есть для каждого фрагмента. И вы будете совершенно правы в этом. Подобная модель более трудоемка для видеокарты, но и выглядит гораздо лучше. А поэтому мы рассмотрим эту тему в следующем уроке :).

Итак, мы определились с планом на этот урок. Взглянув снова на онлайн-демонстрацию, мы обнаружим, что в точке, откуда исходит свет, нет никакого объекта сцены. Если вам нужен светящийся объект (например, солнце при моделировании солнечной системы), тогда вам нужно определить источник света и объект отдельно. Создание объекта к настоящему времени является уже довольно простой вещью после всех предыдущих уроков, поэтому здесь я только объясню, как работает точечный источник света. Как вы могли бы ожидать из описания выше, это очень просто. Самой сложной частью по сравнению с одиннадцатым уроков является отрисовка куба и моделирование вращения куба и сферы…

И снова начнем с низа страницы HTML и будем затем продвигаться вверх, рассматривая отличия от одиннадцатого урока. Первые изменения касаются тела HTML, где вместо полей, задающих направление света, теперь находятся поля, задающие местоположение источника. Эти изменения довольно простые и не стоят заострения внимания, двинемся дальше к webGLStart. Здесь мы увидим, что просто исчез код по управлению мышью и функция initTexture теперь называется initTextures, так как мы будем грузить две текстуры. Не особо захватывающе…

Следующая функция по пути вверх — tick — имеет новый вызов к функции animate, чтобы наша сцена изменялась со временем:


  function tick() {
    requestAnimFrame(tick);
    drawScene();
    animate();
  }

Далее идет сама функция animate, которая просто изменяет две глобальные переменные, которые описывают, как далеко Луна и куб ушли по своим орбитам, учитывая, что они вращаются со скоростью 50°/секунду:


  var lastTime = 0;
  function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
      var elapsed = timeNow - lastTime;

      moonAngle += 0.05 * elapsed;
      cubeAngle += 0.05 * elapsed;
    }
    lastTime = timeNow;
  }

Следующая функция — drawScene, которая имеет несколько интересных изменений. Она начинается с привычного кода очистки canvas, установки перспективы. Также идет аналогичная с одиннадцатым уроком проверка установки галочки освещения и передача цвета фонового освещения в видеокарту:


  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

    var lighting = document.getElementById("lighting").checked;
    gl.uniform1i(shaderProgram.useLightingUniform, lighting);
    if (lighting) {
      gl.uniform3f(
        shaderProgram.ambientColorUniform,
        parseFloat(document.getElementById("ambientR").value),
        parseFloat(document.getElementById("ambientG").value),
        parseFloat(document.getElementById("ambientB").value)
      );

Теперь мы заносим координаты положения точечного освещения в видеокарту через uniform-переменную. Код похож на тот, который мы использовали в предыдущем уроке для передачи направления освещения. Разница в том, что теперь из кода мы кое-что убрали. При отправлении направления освещения в видеокарту нам нужно было преобразовать ее в единичный вектор (то есть масштабировать вектор до длины, равной единице), а затем изменить его направление на противоположное. Больше ничего подобного нам не требуется: мы просто передаем координаты источника:


      gl.uniform3f(
        shaderProgram.pointLightingLocationUniform,
        parseFloat(document.getElementById("lightPositionX").value),
        parseFloat(document.getElementById("lightPositionY").value),
        parseFloat(document.getElementById("lightPositionZ").value)
      );

Далее передадим цвета точечного освещения и на этом работа с кодом по освещению в функции drawScene окончена.


      gl.uniform3f(
        shaderProgram.pointLightingColorUniform,
        parseFloat(document.getElementById("pointR").value),
        parseFloat(document.getElementById("pointG").value),
        parseFloat(document.getElementById("pointB").value)
      );
    }

Теперь сама отрисовка сферы и куба в соответствующих координатах:


    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [0, 0, -20]);

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(moonAngle), [0, 1, 0]);
    mat4.translate(mvMatrix, [5, 0, 0]);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, moonTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, moonVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, moonVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexNormalBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, moonVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, moonVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, moonVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
    mvPopMatrix();

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(cubeAngle), [0, 1, 0]);
    mat4.translate(mvMatrix, [5, 0, 0]);
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexNormalBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, cubeVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, crateTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
    mvPopMatrix();
  }

Это была drawScene. Двигаясь дальше, мы обнаруживаем initBuffers, в котором появился наш стандартный код по созданию буферов для куба, аналогичный коду для сферы, а еще дальше находится функция initTextures, которая теперь загружает две текстуры вместо одной.

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


  attribute vec3 aVertexPosition;
  attribute vec3 aVertexNormal;
  attribute vec2 aTextureCoord;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
  uniform mat3 uNMatrix;

  uniform vec3 uAmbientColor;

  uniform vec3 uPointLightingLocation;
  uniform vec3 uPointLightingColor;

Итак, у нас есть uniform-переменные для местоположения и цвета освещения, которые заменили направление освещения и цвет. Далее:


  uniform bool uUseLighting;

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

  void main(void) {
    vec4 mvPosition = uMVMatrix * vec4(aVertexPosition, 1.0);
    gl_Position = uPMatrix * mvPosition;

Здесь мы разделили наш старый код на две части. Во всех предыдущих вершинных шейдерах мы применяли матрицу модель-вид и проекционную матрицу за один проход:


    // код из одиннадцатого урока
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

Теперь же мы храним промежуточное значение — положение вершины после применения матрицы модель-вид, но до корректировки ее для перспективы. Вот, как это выглядит:


    vTextureCoord = aTextureCoord;

    if (!uUseLighting) {
      vLightWeighting = vec3(1.0, 1.0, 1.0);
    } else {
      vec3 lightDirection = normalize(uPointLightingLocation - mvPosition.xyz);

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


      vec3 transformedNormal = uNMatrix * aVertexNormal;
      float directionalLightWeighting = max(dot(transformedNormal, lightDirection), 0.0);
      vLightWeighting = uAmbientColor + uPointLightingColor * directionalLightWeighting;

Готово! Вот вы и знаете, как написать шейдеры для создания точечного источника освещения.

На этом все. В следующий раз мы снова рассмотрим освещение и повысим реалистичность сцены за счет попиксельного освещения вместо используемого вершинного освещения.

Урок 13 >> << Урок 11

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *