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

Решил не тянуть резину и перевести следующий урок.

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

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

А тут можно посмотреть "живую" версию.

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

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

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

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

      rPyramid += (90 * elapsed) / 1000.0;
      rCube -= (75 * elapsed) / 1000.0;

Для той функции это всё. Давайте теперь перейдём к drawScene. Выше объявления фунции у нас объявления новых переменных:

  var rPyramid = 0;
  var rCube = 0;

Дальше идёт заголовок функции, за которым следует наш установочный код и код для перемещения в позицию рисования пирамиды. Как только это всё проделано, мы поворачиваем её вокруг оси Y, точно так же, как мы это делали с треугольником в предыдущем уроке:

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

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

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

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

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

Ну, это было просто. Давайте взглянем на код для куба. Первый шаг — крутить его. На этот раз, вместо просто поворота вокруг оси X, мы будем крутить его вокруг той оси (с перспективы наблюдателя), которая направлена вверх, вправо и вперёд от вас:

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

Дальше, мы рисуем куб. Тут всё чуток посложнее. Есть три способа, которыми мы можем отрисовать куб:

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

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

Финальный вариант — описать куб как шесть квадратов, каждый сделанный из двух треугольников, но таким образом отправленный в WebGL, чтобы его отрисовало за один присест. Это похоже на то, как мы делали ленту треугольников, но так как мы описываем треугольники полность каждый раз вместо того, чтобы описать каждый треугольник, добавляя вершину к предыдущему, проще описать цвета для каждой стороны. Кроме того, это даст мне возможность представить вам новую функцию, drawElements — так что именно так мы и поступим :-)

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

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);

    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

Следующий шаг — отрисовать треугольники. Тут есть некоторая проблема. Давайте для примера возьмём переднюю грань. У нас есть для неё четыре позиции вершин, и с каждой из них связан цвет. Однако, грань нужно нарисовать двумя треугольниками и, так как мы используем простые треугольники, которые нуждаются в индивидуальном указании вершин, в отличие от лент треугольников, которые могут иметь общие вершины, нам потребуется указать в целом шесть вершин. Но в нашем буферном массиве их всего четыре.

То, что мы сейчас собираемся сделать, выглядит примерно так: "нарисуй треугольник, состоящий из первых трёх вершин в буферном массиве, а затем нарисуй другой, сделанный из первой, третьей и четвёртой". Это отрисует нашу переднюю грань. Рисование остальной части куба будет  аналогичным. Этим и займёмся.

Для этого мы используем кое-что под названием массив буфера элементов и новую функцию, drawElements. Так же как и буферные массивы, которые мы использовали ранее, массив буфера элементов будет заполнен соответствующими значениями в initBuffers, и будет содержать список вершин, ссылаясь, начиная с нуля, на массивы, которые мы использовали для цветов и позиций. Сейчас мы это рассмотрим.

Чтобы это использовать, мы сделаем массив буфера элементов нашего куба текущим (WebGL хранит различные текущие буферные массивы и массивы буфера элементов, так что нам нужно указать, какой мы намерены привязывать при вызове gl.bindBuffers), затем мы делаем наш обычный код для отправки наших матриц модели-вида и проекции на видеокарту, а затем вызываем drawElements для отрисовки треугольников:

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);

    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

Для drawScene на этом всё. Оставшийся код находится в initBuffers и довольно очевиден. Мы описываем буфера с новыми именами, чтобы отразить новые типы объектов, с которыми мы работаем, и добавляем новый для массива буфера элементов куба.

  var pyramidVertexPositionBuffer;
  var pyramidVertexColorBuffer;
  var cubeVertexPositionBuffer;
  var cubeVertexColorBuffer;
  var cubeVertexIndexBuffer;

Мы заносим значения в буфер позиций вершин пирамиды для всех граней, с соответствующим изменением numItems:

    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices = [
        // Front face
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
        // Right face
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
        // Back face
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,
        // Left face
         0.0,  1.0,  0.0,
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    pyramidVertexPositionBuffer.itemSize = 3;
    pyramidVertexPositionBuffer.numItems = 12;

...также как и для буфера цветов пирамиды:

    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors = [
        // Front face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Right face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        // Back face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Left face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 12;


…и для буфера позиций вершин куба:

    cubeVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    vertices = [
      // Front face
      -1.0, -1.0,  1.0,
       1.0, -1.0,  1.0,
       1.0,  1.0,  1.0,
      -1.0,  1.0,  1.0,

      // Back face
      -1.0, -1.0, -1.0,
      -1.0,  1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0, -1.0, -1.0,

      // Top face
      -1.0,  1.0, -1.0,
      -1.0,  1.0,  1.0,
       1.0,  1.0,  1.0,
       1.0,  1.0, -1.0,

      // Bottom face
      -1.0, -1.0, -1.0,
       1.0, -1.0, -1.0,
       1.0, -1.0,  1.0,
      -1.0, -1.0,  1.0,

      // Right face
       1.0, -1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0,  1.0,  1.0,
       1.0, -1.0,  1.0,

      // Left face
      -1.0, -1.0, -1.0,
      -1.0, -1.0,  1.0,
      -1.0,  1.0,  1.0,
      -1.0,  1.0, -1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    cubeVertexPositionBuffer.itemSize = 3;
    cubeVertexPositionBuffer.numItems = 24;

Буфер цветов немного более сложен, так как мы используем цикл для создания списка цветов вершин, чтобы не надо было указывать каждый цвет четыре раза, по одному для каждой вершины:

    cubeVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
    colors = [
      [1.0, 0.0, 0.0, 1.0],     // Front face
      [1.0, 1.0, 0.0, 1.0],     // Back face
      [0.0, 1.0, 0.0, 1.0],     // Top face
      [1.0, 0.5, 0.5, 1.0],     // Bottom face
      [1.0, 0.0, 1.0, 1.0],     // Right face
      [0.0, 0.0, 1.0, 1.0],     // Left face
    ];
    var unpackedColors = [];
    for (var i in colors) {
      var color = colors[i];
      for (var j=0; j < 4; j++) {
        unpackedColors = unpackedColors.concat(color);
      }
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
    cubeVertexColorBuffer.itemSize = 4;
    cubeVertexColorBuffer.numItems = 24;

Наконец, мы описываем массив буфера элементов (заметьте снова отличие в первом параметре gl.bindBuffer и gl.bufferData):

    cubeVertexIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    var cubeVertexIndices = [
      0, 1, 2,      0, 2, 3,    // Front face
      4, 5, 6,      4, 6, 7,    // Back face
      8, 9, 10,     8, 10, 11,  // Top face
      12, 13, 14,   12, 14, 15, // Bottom face
      16, 17, 18,   16, 18, 19, // Right face
      20, 21, 22,   20, 22, 23  // Left face
    ]
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
    cubeVertexIndexBuffer.itemSize = 1;
    cubeVertexIndexBuffer.numItems = 36;

 

Помните, что каждое число в этом буфере является ссылкой на буфера позиций вершин и цветов. Так что первая строка, в сочетании с инструкцией рисовать треугольники в drawScene, означает, что мы получим треугольник на вершинах 0, 1 и 2, а затем другой на 0, 2 и 3. Так как оба треугольника одного и того же цвета и смежны, результатом будет квадрат на вершинах 0, 1, 2 и 3. Повторите для всех граней куба, и готово!

Теперь вы знаете, как заставить WebGL-сцены иметь трёхмерные объекты, и знаете, как использовать по нескольку раз вершины, описанные в буферном массиве, используя массив буфера элементов и drawElements. Если у вас есть какие-либо вопросы, комментарии или исправления, пожалуйста, оставьте их в комментариях ниже.

В другой раз мы займёмся текстурами.


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