пятница, 21 января 2011 г.

WebGL Урок 4. Несколько реальнх 3D объектов.



<< Урок 3

Урок 5 >>


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


Нажмите здесь, если ваш браузер поддерживает WebGL и вы сможете увидеть этот урок "вживую"
Здесь можно почитать как получить браузер с поддержкой WebGL
Более подробно о том, как это работает, ниже

Примечание: этот урок рассчитан на людей с достаточным количеством знаний в области программирования, но не имеющих реального опыта в программировании 3D графики. Цель курса состоит в том, чтобы вы как можно быстрее начали создавать собственные 3D страницы и имели хорошее представление о том, что происходит в коде. Если вы ещё не ознакомились с предыдущими уроками, то советую вам сделать это до прочтения этого урока.
В тексте могут встречатья ошибки и неточности. Если вы заметите, что что-то неверно, дайте мне знать об этом в комментариях и мы исправим это как можно скорее.
Есть 2 способа получения исходного кода для этого примера: просмотр кода в браузере, если он поддерживает WebGL и вы просматриваете урок "вживую" в своём браузере, или вы можете скопировать его и другие уроки с GitHub.
В любом случае, после того, как получите код, загрузите его в ваш любимый редактор кода и просмотрите.

Разница между кодом этого урока и предыдущего сконцентрирована в функциях animate, initBuffers и drawScene. Если вы промотаете вниз до функции animate, то сперва увидете совсем незначительное изменение : переменные, которые хранит текущее состояние вращения 2х объектов сцены переиенованы; теперь они называются 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]);

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

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

Следующим шагом является рисование треугольников. Тут есть некоторая проблема. Давайте рассмотрим переднюю грань; для неё существует 4 позиции, и каждая имеет свой цвет. Однако, необходимо чтобы она была отрисована при помощи примитивных треугольников, а так как мы используем примитивные треугольники, для каждого из которых необходимо указать вершины индивидуально, в отличие от полосок треугольников( triangle strips), которые используют общие вершины, нам необходимо указать все 6 вершин. Но в нашем массиве находятся только 4.

Мы хотим сделать примерно следующее “ нарисовать треугольник, сделанный на основе первых 3х вершин буфера, затем нарисовать другой на основе первой, третьей и четвёртой. Таким оброазом, мы получим переднюю грань; рисование остальной части куба аналогично. В точности так мы и делаем.

Для этого мы используем так называемый буфер элементов и вызываем новую функцию 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 = [
        // Передняя грань
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
        // Правая грань
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
        // Задняя грань
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,
        // Левая грань
         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 = [
        // Передняя грань
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Правая грань
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        // Задняя грань
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Левая грань
        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 = [
      // Передняя грань
      -1.0, -1.0,  1.0,
       1.0, -1.0,  1.0,
       1.0,  1.0,  1.0,
      -1.0,  1.0,  1.0,
 
      // Задняя грань
      -1.0, -1.0, -1.0,
      -1.0,  1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0, -1.0, -1.0,
 
      // Верхняя грань
      -1.0,  1.0, -1.0,
      -1.0,  1.0,  1.0,
       1.0,  1.0,  1.0,
       1.0,  1.0, -1.0,
 
      // Нижняя грань
      -1.0, -1.0, -1.0,
       1.0, -1.0, -1.0,
       1.0, -1.0,  1.0,
      -1.0, -1.0,  1.0,
 
      // Правая грань
       1.0, -1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0,  1.0,  1.0,
       1.0, -1.0,  1.0,
 
      // Левая грань
      -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;

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

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 сцены с использованием 3D объектов, и вы знаете, что при помощи буфера элементов и drawElements можно повторно использовать вершины, которые вы задали в буфере. Если у вас появились какие-либо вопросы, комментарии или уточнения, пожалуйста, оставьте их ниже.

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

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

Благодарности: Как всегда я глубоко благодарен NeHe за скрипт для этого урока в Урок по OpenGL. Chris Marrin's WebKit spinning box вдохновило меня на адаптирование этого урока для введения в массивы элементов.

Комментариев нет:

Отправить комментарий