Learning WebGL по-русски. Урок 3.

Итак, перевод третьего урока из серии "Learning WebGL":

Добро пожаловать на мой третий туториал из серии о WebGL. На этот раз мы собираемся заставить объекты двигаться. Этот урок основан на четвёртом туториале NeHe об OpenGL.

Вот так должен выглядеть результат:

Вот тут можно полюбоваться вживую.

Заинтересовавшимся — добро пожаловать под кат!

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

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

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

Прежде чем я начну объяснять код, я разъясню одну вещь. Способ анимировать трёхмерную сцену в WebGL очень прост: вы просто отрисовываете её повторно, каждый раз рисуя немного иначе. Это может быть абсолютно очевидно для многих читателей, но для меня это было сюрпризом, когда я изучал OpenGL и может удивить и других, кто впервые знакомится с трёхмерной графикой посредством WebGL. Причина, по которой я вначале запутался, было то, что я раньше представлял, будто всё более абстрагировано, например "говоришь 3D-системе, что вот тут вначале есть квадрат в точке X, а потом, чтобы его передвинуть, говоришь системе, что он сдвинулся в точку Y". Вместо этого, процесс примерно такой: "Говоришь системе, что есть квадрат в точке X, а в следующий раз, перед отрисовкой, говоришь, что есть квадрат в точке Y, и в другой раз, что в точке 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);

На первой строчке tick просит вызвать себя ещё раз, когда будет необходимо снова перерисовать сцену. requestAnimFrame — это функция из файла webgl-utils.js от Гугла, который мы подключили ранее. Она даёт независимую от браузера возможность запросить браузер вызвать функцию снова в тот момент, когда он хочет перерисовать WebGL-сцену, например, когда обновляется картинка на экране монитора. На данный момент функции для этого есть во всех браузерах, поддерживающих WebGL, но они по-разному называются (например, в Firefox эта функция называется mozRequestAnimationFrame, а в Chrome и Safari — webkitRequestAnimationFrame). В будущем ожидается, что они будут использовать все одно и то же имя — requestAnimationFrame. Но до тех пор мы можем использовать Гугловские утилиты, чтобы быть уверенными, что оно будет работать везде.

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

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

    drawScene();
    animate();
  }

Итак, как только мы сделали, чтоб tick был снова вызван браузером в тот момент, когда он отрисовывает страницу, мы просто отрисовываем её и обновляем наше состояние анимации на следующее. Давайте рассмотрим по очереди функции drawScene и animate.

drawScene находится примерно в последней трети файла index.html. Первое, что следует отметить, это то, что перед объявлением функции, мы объявляем две новых глобальных переменных.

  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? Как вы могли ожидать из названий этих функций, они также относятся к матрице модели-вида. Возвращаясь к моему примеру рисования робота, давайте представим, что ваш код на самом высоком уровне хочет передвинуться к точке A, нарисовать там робота, а потом двинуться куда-нибудь ещё прочь от точки А и нарисовать там чайник. Код, который рисует робота, может принять все типы изменений матрицы модели-вида. Он может начать с тела, потом рисовать ноги, а потом голову и закончить на руках. Проблема в том, что после того, как вы попытались сдвинуться на определённое расстояние, вы отодвинетесь не относительно точки А, а вместо этого относительно того, что вы рисовали в прошлый раз, что может означать, что, если у вашего робота подняты руки, чайник тоже поднимется. Нехорошо.

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

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

перемещает по сцене неперевёрнутую систему координат. (Если всё ещё непонятно, что это всё означает, я рекомендую скопировать код и посмотреть, что происходит, если вы уберёте вызовы push/pop-функций, а затем возвращаете их назад, уверяю, дойдёт куда быстрее)

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

    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();
  }

Тут должно быть всё монятно. У нас есть список, где мы будем держать стек матриц, и описаны функции закладки и извлечения соответственно.

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

    function degToRad(degrees) {

        return degrees * Math.PI / 180;
    }

Ну, вот и всё! Больше изменений нет. Теперь вы знаете, как анимировать простые WebGL-сцены. Если у вас есть какие-либо вопросы, комментарии или уточнения, пожалуйста, оставьте свой комментарий ниже.

В следующий раз (цитируя урок NeHe номер 5) мы "превратим объект в ИСТИННЫЙ 3D-объект, вместо 2D-объектов в 3D-мире".


You can leave a comment with "Facebook":
Не забывайте оставлять комментарии при помощи "ВКонтакте":
Яндекс.Метрика