WebGL Урок 4 — Настоящие 3D-объекты

Урок 5 >> << Урок 3

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

Добро пожаловать на мой четвертый урок по WebGL. На этот раз мы собираемся отобразить 3D-объекты. Урок основан на Урок 5 учебника NeHe по OpenGL.

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

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

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

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

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

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

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

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

Для animate это и все изменения. Двинемся дальше к функции 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]);

Теперь будем рисовать куб. Это немного более сложно. Есть три способа нарисовать куб:

  1. Использовать одну серию треугольников (TRIANGLE_STRIP). Если бы весь куб был одного цвета, это было бы достаточно легко — мы могли бы использовать координаты вершин, которые мы использовали до этого момента, для отрисовки лицевой плоскости, затем добавили бы две точки для добавления другой плоскости, и еще две точки для другой плоскости, и так далее. Это было бы очень эффективно. К сожалению, нам нужен отдельный цвет для каждой плоскости. Из-за того, что каждая вершина задает угол куба, а угол является общим для трех плоскостей, нам нужно указывать каждую вершину три раза, и это будет так запутанно, что я не буду даже пытаться объяснить это…
  2. Мы могли бы схитрить и отрисовать наш куб через шесть отдельных квадратов, один на каждую плоскость, с отдельным набором координат вершин и цветов. Первая версия этого урока (до 30 октября 2009) так и делала и справлялась довольно неплохо. Однако, это не было хорошим подходом. Отрисовка нового объекта сцены в WebGL — довольно трудозатратная операция, поэтому следует свести к минимуму количество обращений к функции drawArrays.
  3. Последним вариантом будет описание куба в виде четырех квадратов, состоящих из двух треугольников каждый, но отправлять их на отрисовку за один проход. Это похоже на серию треугольников, но из-за того, что мы описываем каждый треугольник отдельно, а не добавляем просто одну вершину к предыдущему треугольнику для получения следующего, нам проще определить отдельные цвета для сторон. Преимуществом будет также то, что реализация самого изящного подхода предоставит мне возможность познакомить вас с новой функцией 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.bindBuffer), затем выполняем уже привычный код для передачи матрицы модель-вид и проекционной матрицы на видеокарту и затем вызываем 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;

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

Теперь вы знаете, как создавать сцены WebGL с 3D-объектами, и вы знаете, как повторно использовать вершины в массиве буфера с помощью массива элементов буфера и функции drawElements. Если у вас есть вопросы, комментарии или поправки, прошу оставлять комментарии.

В следующий раз мы разберем наложение текстур.

Урок 5 >> << Урок 3

3 thoughts on “WebGL Урок 4 — Настоящие 3D-объекты

  • 25.01.2017 at 16:20
    Permalink

    В функции animate постоянно выполняется инкремент переменной rCube, которое содержит значение угла в градусах. полный оборот по оси совершается при 360. И хотя код будет правильно обрабатывать значения превышающие 360, со временем будет оперировать большими числами. Посему у меня возник вопрос: насколько сильно это отразится на производительности? Стоит ли дописывать обнуление значений подобных переменных?

    Ответить
    • 25.01.2017 at 21:54
      Permalink

      Правильное замечание, бесконтрольный рост значения поворота может привести к переполнению — особенно в играх, где люди могут проводить продолжительное время. Поэтому в реальных проектах сбрасывайте значение поворота примерно следующим образом:
      rPyramid = rPyramid % 360

      Ответить

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

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