WebGL Урок 3 — Немного движения

Урок 4 >> << Урок 2

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

Добро пожаловать на мой третий урок по WebGL. На этот раз мы придадим вращение нашим объектам. Урок основан на Урок 4 учебника NeHe по OpenGL.

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

Здесь можно посмотреть онлайн-демонстрацию, если ваш браузер поддерживает WebGL. Здесь можно узнать, что делать, если браузер не поддерживает WebGL.

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

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

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

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

Прежде чем я поясню код, необходимо пролить свет на одну вещь. Анимация 3D-сцены в WebGL устроена очень просто — вы повторяете отрисовку сцены, каждый раз немного изменяя ее. Это может бытьсовершенно очевидно для многих читателей, но для меня это было небольшим сюрпризом, когда я изучал OpenGL, и может оказаться сюрпризом для других, кто приступает к работе с 3D-графикой в WebGL. Причина моего удивления заключается в том, что я думал, что WebGL использует высокоуровневую абстракцию, которая будет понимать команды «скажи 3D-системе, что в точке X есть (например) квадрат при первой отрисовке» и затем для перемещения квадрата «скажи 3D-системе, что квадрат, про который я говорил ранее, переместился в точку Y». Вместо этого оно похоже на «скажи 3D-системе, что в точке X есть квадрат», затем при следующей отрисовке «скажи 3D-системе, что в точке Y есть квадрат», затем «скажи 3D-системе, что в точке Z есть квадрат» и так далее.

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

Так или иначе, это означает, что наша функция drawScene, которая все отрисовывает, теперь должна вызываться постоянно, каждый раз отображая немного измененную сцену для создания анимации. Давайте начнем с низа файла index.html и посмотрим, как это сделано. Для начала посмотрим на функцию webGLStart, которая запускает анимацию при загрузке страницы:


  function webGLStart() {
    var canvas = document.getElementById("lesson03-canvas");
    initGL(canvas);
    initShaders()
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    tick();
  }

Единственное изменение здесь заключается в том, что мы больше не вызываем drawScene в конце функции для отрисовки сцены, а вместо этого вызываем новую функцию tick. Это та самая функция, которая вызывается регулярно. Она меняет состояние сцены (например, треугольник изменил угол поворота с 81 градуса на 82), затем отрисовывает сцену и устанавливает собственный следующий вызов в нужное время. Это следующая функция в файле, поэтому давайте посмотрим на нее:


  function tick() {
    requestAnimFrame(tick);

В первой линии функция устанавливает вызов самой себя для следующей отрисовки. requestAnimFrame — функция из арсенала Google, которую мы подключаем в нашу страницу через тэг <script> в скрипте webgl-utils.js. Это дает нам независимый от браузера способ попросить браузер, чтобы он вызывал нас каждый раз, когда соберется перерисовать сцену WebGL — например, в следующий раз, когда компьютер обновит изображение монитора. Уже сейчас такие функции есть во всех браузерах с поддержкой WebGL, но они называются по-разному (например, в Firefox функция называется mozRequestAnimationFrame, а в Chrome и Safari — webkitRequestAnimationFrame). В будущем у всех должна быть одинаковая функция — requestAnimationFrame. А пока мы будем использовать решение от Google, которое будет работать везде.

Стоит заметить, что вы можете добиться аналогичного эффекта requestAnimationFrame по регулярному вызову drawScene с использованием встроенной JavaScript-функции setInterval. Многие примеры WebGL (включая ранние версии этих уроков) именно так и делали — ровно до тех пор, пока у людей не появилось несколько открытых страниц в разных вкладках. Дело в том, что setInterval регулярно вызывает заданную функцию независимо от того, открыта ли вкладка или нет, а это означает, что работа по отрисовке сцены продолжается даже в закрытой вкладке. Очевидно, что это плохой подход, поэтому и появился requestAnimationFrame, который вызывает функцию только когда вкладка открыта.

Оставшаяся часть функции tick:


    drawScene();
    animate();
  }

То есть после того, как мы установили повторный вызов функции tick при следующей отрисовке браузера, мы просто отобразили текущую сцену и обновили состояние объектов для следующей. Взглянем на функции drawScene и animate.

Переместитесь вниз по файлу index.html примерно на 2/3 и вы найдете там drawScene. Первое, на что стоит обратить внимание, — мы определяем две глобальных переменных перед определением функции:


  var rTri = 0;
  var rSquare = 0;

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

Следующее изменение в drawScene находится во фрагменте кода по отрисовке треугольника. Для контекста я приведу весь код, а новые строки выделю красным:


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

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]);

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

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

    mvPopMatrix();

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

В OpenGL при отрисовке сцены каждый объект отрисовывается в «текущей» позиции с «текущим» поворотом — например, вы говорите «переместись на 20 единиц прямо, повернись на 32 градуса и нарисуй робота», что в свою очередь является сложным набором инструкций «это смести, немного поверни, отрисуй». Это удобно, потому что вы можете поместить код «нарисуй робота» в функцию и затем легко устанавливать положение робота просто изменением параметров перемещения/поворота и вызовом этой функции.

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


    mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]);

должна быть довольно очевидной. Мы меняем текущее состояние поворота в матрице модель-вид, поворачиваясь на rTri градуса вокруг вертикальной оси (это указано в векторе в третьем параметре). Это означает,что при отрисовке треугольник будет повернут на rTri градуса. Обратите внимание, что mat4.rotate принимает градусы в радианах. Лично для меня градусами оперировать легче, поэтому я написал простую функцию конвертации degToRad, которую здесь и использую.

Теперь разберем, что же это за вызовы mvPushMatrix и mvPopMatrix. Как вы можете догадаться по их именам, они как-то связаны с матрицей модель-вид. Возвращаясь к моему примеру с роботом, предположим, что вам на верхнем уровне в коде необходимо переместиться в точку А, отрисовать робота, потом сместиться от точки А и нарисовать чайник. Код для отрисовки робота может производить всевозможные изменения в матрице модель-вид. Он может начать с тела, потом заняться ногами, подняться к голове и закончить отрисовкой рук. Проблема в том, что когда после этого вы сместитесь, вы сместитесь не относительно точки А, а относительно положения, где была последняя отрисовка. Это означает, что когда робот поднимает руки, чайник начинает левитировать. Не самая лучшая ситуация.

Очевидно, что нам нужно сохранить состояние матрицы модель-вид перед началом отрисовки робота, чтобы впоследствии его восстановить. Этим и занимаются функции mvPushMatrix и mvPopMatrix. mvPushMatrix помещает матрицу в стэк, а mvPopMatrix избавляется от текущей матрицы, берет матрицу из верха стэка и восстанавливает ее. С использованием стэка мы можем иметь любое количество вложенного кода отрисовки, каждый из которых работает с матрицей модель-вид и затем восстанавливает ее. Поэтому после отрисовки нашего повернутого треугольника мы восстанавливаем матрицу модель-вид с помощью mvPopMatrix, поэтому код


    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

… двигается через сцену в неповернутой системе отсчета. (Если по-прежнему неясно, что все это значит, я рекомендую скопировать код и посмотреть, что случится, если вы удалите код по помещению/восстановлению матрицы из стэка. Почти наверняка вы сразу все поймете)

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


    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(rSquare), [1, 0, 0]);

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

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

    mvPopMatrix();
  }

… и это все изменения в коде, касающиеся отрисовки, в функции drawScene.

Очевидно, что для анимации сцены нам еще нужно изменять значения rTri и rSquare с течением времени, чтобы каждый раз сцена выглядела немного по-другому. И, конечно же, это происходит в нашей новой функции animate, которая выглядит следующим образом:


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

      rTri += (90 * elapsed) / 1000.0;
      rSquare += (75 * elapsed) / 1000.0;
    }
    lastTime = timeNow;
  }

В простом случае для анимации сцены нам потребовалось бы поворачивать наши треугольник и квадрат на заданное значение каждый раз при вызове animate (в оригинальном уроке OpenGL, на котором основан этот урок, так и есть), но я выбрал немного более хороший подход, как мне кажется. Значение, на которое мы поворачиваем объекты, зависит от того, как много времени прошло с момента последнего вызова функции. В частности, треугольник вращается на 90 градусов в секунду, а квадрат — на 75 градусов в секунду. Положительный момент в этом случае состоит в том, что каждый видит одинаковую частоту вращения независимо от мощности компьютера. Люди с более слабыми компьютерами (для которых функция из requestAnimFrame будет вызываться реже) просто будут видеть дергающуюся прорисовку. Этот момент не особо важен в демонстрациях вроде этой, но может значить гораздо больше в играх и других подобных случаях.

Что ж, это и весь код, который отрисовывает и анимирует сцену. Давайте посмотрим на вспомогательный код, который нам потребовалось добавить, — mvPushMatrix и mvPopMatrix:


  var mvMatrix = mat4.create();
  var mvMatrixStack = [];
  var pMatrix = mat4.create();

  function mvPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvMatrix, copy);
    mvMatrixStack.push(copy);
  }

  function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
      throw "Invalid popMatrix!";
    }
    mvMatrix = mvMatrixStack.pop();
  }

Здесь не должно быть ничего удивительного. Есть массив для хранения нашего стека матриц и соответствующие функции push и pop.

Осталось объяснить всего одну вещь — функцию degToRad, о которой я говорил ранее. Если у вас остались какие-то знания математики со школы, здесь вас ничем не удивить…


    function degToRad(degrees) {
        return degrees * Math.PI / 180;
    }

И… на этом все! Мы разобрали все изменения в коде. Теперь вы знаете, как анимировать простые сцены WebGL. Если у вас есть какие-либо вопросы, комментарии или поправки, прошу оставлять свои комментарии.

В следующий раз мы сделаем НАСТОЯЩИЕ 3D-объекты, а не 2D-объекты в пространстве 3D. Узнайте, как это делается!

Урок 4 >> << Урок 2

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

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