среда, 11 января 2012 г.

WebGL Урок 6. Ввод с клавиатуры и текстурные фильтры

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

WebGL урок 5. Введение в текстуры.


<< Урок 4

Урок 6 >>




Добро пожаловать в мой пятый урок из серии WebGL, основанный на уроке 6 из курса OpenGL от NeHe. Теперь перейдём к добавлению текстуры к 3D объекту, мы обтянем объект изображением, которое загрузим из указанного файла.
Это действительно рабочий способ для добавления деталей вашей сцене без усложения отрисовываемых объектов.
Представьте себе каменную стену в игре типа лабиринт. Вы, вероятно, не захотите моделировать каждый блок стены как отдельный объект. Поэтому вместо создания образа кладки и покрытия стен с ним всю стену теперь можно создать как один целый объект.
Здесь можно посмотреть, как выглядит этот урок в браузере с поддержкой WebGL:


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


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

В тексте могут встречатья ошибки и неточности. Если вы заметите, что что-то неверно, дайте мне знать об этом в комментариях и мы исправим это как можно скорее.
Есть 2 способа получения исходного кода для этого примера: просмотр кода в браузере, если он поддерживает WebGL и вы просматриваете урок "вживую" в своём браузере, или вы можете скопировать его и другие уроки с GitHub.
В любом случае, после того, как получите код, загрузите его в ваш любимый текстовый редактор и просмотрите.
Ключом к пониманию работы с текстурами является то, что текстуры представляют собой настройки цвета для каждой точки 3D объекта.
Как вы помните из урока 2,цвета указываются при помощи фрагментных шейдеров.
Всё что нам нужно сделать - это загрузить изображение и передать его фрагментому шейдеру.
Фрагментному шейдеру также необходимо знать, какой бит изображения использовать для того фрагмента, с к отрым он работает, поэтому мы также должны предоставить ему эту информацию.
Двайте начнём с просмотра кода, который загружает текстуру. Мы вызываем его прямо в начале выполнения JavaScript кода нашей страницы, в функции webGLStart внизу страницы (новый код выделен красным цветом):
function webGLStart() {
    var canvas = document.getElementById("lesson05-canvas");
    initGL(canvas);
    initShaders();
    initTexture(); 
 
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
Давайте посмотрим на initTexture — она находится если промотать от начала вниз на треть файла, и этот код весь новый:
var neheTexture;
  function initTexture() {
    neheTexture = gl.createTexture();
    neheTexture.image = new Image();
    neheTexture.image.onload = function() {
      handleLoadedTexture(neheTexture)
    }
 
    neheTexture.image.src = "nehe.gif";
  }
Итак, мы создаём глобальную переменную для хранения текстуры. В реальном примере вы будете иметь множество текстур и не будете использовать глобальные переменные,но мы сейчас нарочно всё упрощаем. Мы используем функцию gl.createTexture для создания ссылки на текстуру и помещения её в глобальную область видимости, затем мы создаём JavaScript объект Image и помещам его в новый аттрибут текстуры, вновь воспользовавшись возможностью JavaScript устанавливать любое свойство любому объекту.
По умолчанию объект текстура не имеет поля изображения image, но для нас это удобно, поэтому мы создали такое свойство.
Очевидно следующим шагом для получение объекта Image является загрузка в него фактического изображения,но перед этим мы присоединим к нему функцию обратного вызова, которая будет вызываться, когда изображение полностью загружено.
Таким образом, нам следует в первую очередь установить эту функцию. Как только это сделано, мы устанавливаем изображению свойство src, теперь всё готово.
Изображение загружается асинхронно, но код, который устанавливает свойство src
изображению немедленно возвращается, а в фоновом потоке происходит загрузка изображения с вебсервера.
Когда изображение загружено, вызывается наша функция обратного вызова, которя вызывает handleLoadedTexture:
function handleLoadedTexture(texture) {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.bindTexture(gl.TEXTURE_2D, null);
  }
Первое. что нам следует сделать, это сообщить WebGL, что наша текстура является текущей текстурой.
Все функции WebGL по работе с текстурой оперируют с текущей текстурой вместо того,
чтобы принимать указатель текстуру в качестве параметра.

В bindTexture показано как сделать текстуру текущей, это похоже на использование gl.bindBuffer, которое мы рассматривали ранее.
Далее мы сообщаем WebGL, что все изображения, загруженные нами в текстуры, необходимо отразить по вертикали.
Мы делаем это из-за разницы в координатах. Для наших текстурных координат мы используем координаты,похожие на те, что обычно используются в математике, их увеличение происходит по мере продвижения вверх по вертикальной оси. Эти координаты согласуются с координатами X, Y, Z, которые мы используем для указания позиций вершин. В отличие от многих других компьютерных систем - например формата GIF, который мы используем для изображения текстуры - используют координаты, которые увеличиваются по мере продвижения вниз по вертикальной оси. Горизонтальная ось одна и та же в обеих системах координат. Различие вертикальных осей означает, что GIF изображение, которое мы использем для нашей текстуры, уже отражено по вертикали и нам необходимо вернуть его обратно в исходное сосстояние.
(Спасибо Ilmari Heikkinen за разъяснение по этому вопроосу в комментариях.)
Следующим шагом является загрузка нашего только что полученного изображения на графическую карту при помощи texImage2D.
Перечислим параметры в порядке их следования:используемое изображение, уровень детализации(что это мы рассмотрим в одном из следующих уроков), формат, в котором мы хотим, чтобы изображение было сохранено на графической карте(повторяю второй раз по причинам, которые вы узнаете позже),размер каждого “канала” изображения(зависит от типа данных, используемого для хранения красной, зелёной и синей цветовой составляющей),
и, наконец, само изображение.
В следующих двух строках указываются параметры масштабирования для текстуры. Первый говорит WebGL каким образом заполнить большое пространство экрана текстурой, если заполняемое пространство больше размера изображения текстуры, другими словами,
в ней указано каким образом отмасштабировать текстуру.
Вторая является инструкцией как обрезать изображение.
Существуют различные способы наложения текстуры, которые вы можете указать, самый простой и непривлекательный из них NEARESTон говорит о том, что оригинальный образ используется "как есть",вследствие чего при просмотре крупным планом она будет выглядеть очень блочно. У этого способа есть преимущество - скорость отображения достаточно хорошая даже на медленных машинах.
В следющем уроке мы рассмотрим использование различных способ наложения текстуры, так что у вас будет возможность сравнить производительность и внешний вид каждого из них.
Как только всё это сделано, мы установим указатель на текущую структуру null.
Это не является строго необходимым, но является хорошей привычкой - очистка после использования.
Итак, это был весь код, который требуется для загрузки текстуры.
Теперь давайте перейдём к initBuffers. Естественно отсутствует весь код, относящийся к пирамиде из урока4, но более интересным изменением является замена буфера цветов вершин куба новым буфером текстурных координат.
Это выглядит следующим образом:
cubeVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    var textureCoords = [
      // Front face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
 
      // Back face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
 
      // Top face
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
 
      // Bottom face
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,
 
      // Right face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
 
      // Left face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
    cubeVertexTextureCoordBuffer.itemSize = 2;
    cubeVertexTextureCoordBuffer.numItems = 24;
В таком виде код должен показаться вам наглядным, заметьте - всё, что мы сделали - это указали новый аттрибут для каждой вершины в буфере массиве и этот новый аттрибут имеет 2 значения для каждой вершины.
Текстурыне координаты указывают положение x, y для вершины на изображении текстуры.
Размер текстуры нормирован таким образом, что высота и широта изображения принимаются за единицу, так что получаем (0, 0) в левом нижнем углу, (1, 1) в правом верхнем углу.
Это было единственное изменение в initBuffers, двигаемся дальше к функции drawScene.
Наиболее интересные изменения в этой функции естественно те, которые показывают использование текстуры.
Однако, прежде чем мы перейдем к ним, рассмотрим ряд более простых изменений изменений - первое - удаление пирамиды и второе - в настоящее время куб вращается в другом направлении.
Я не буду описывать их в деталях, тут всё довольно легко разобрать,изменения выделены красным цветом в верхней части функции drawScene:
var xRot = 0;
  var yRot = 0;
  var zRot = 0; 
  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
 
    perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
    loadIdentity();
 
    mvTranslate([0.0, 0.0, -5.0])
 
    mvRotate(xRot, [1, 0, 0]);
    mvRotate(yRot, [0, 1, 0]);
    mvRotate(zRot, [0, 0, 1]); 
 
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
Есть также соответствующие изменения в функции animate, которая обновленяет xRot , yRot и zRot , их я описывать не буду.

Итак, минуя эту функцию, рассмотрим код по работе с текстурой.
В функции initBuffers мы создали буфер, содержащий текстурные координаты, здесь нам необходимо связать их с соответсвующим аттрибутом так, чтобы шейдеры смогли их увидеть:
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
…теперь когда WebGL знает, какой бит текстуры использует каждая вершина, нам необходимо указать ему использовать текстуру, которую мы загрузили ранее и затем нарисовать куб:
gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, neheTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0); 
 
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
То, что происходит здесь имеет некоторую степень сложности.
WebGL может иметь дело с количеством текстур до 32 в течении любого вызова функции, такой как, например, gl.drawElements, текстуры пронумерованы от TEXTURE0 к TEXTURE31. То, что мы делаем, сказано в первых 2х строках - нулевая текстура - эта та, которая была загружена ранее, в третьей строке мы передаём нулевое значение в шейдерную форму (которую, как и другие формы,используемые нами для матриц, мы извлекаем из шейдерных программ в initShaders).
Она сообщает шейдеру, что мы используем нулевую текстуру. Давайте посмотрим каким образом она используется позднее.
Во всяком случае, как только эти три строки будут выполнены, мы готовы идти дальше, итак, мы просто используем тот же код, что и прежде для рисования треугольников, составляющих куб.
Единственный код, который остался нераобранным - изменения в шейдерах. Давайте сначала рассмотрим вершинный шейдер:
attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord; 
 
  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
 
  varying vec2 vTextureCoord; 
 
  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord; 
  }
Это очень похоже на вещи, связанные с цветом, которые мы передавали вершинному шейдеру в уроке2.
Всё, что мы делаем- это определяем текстурные координаты(снова, вместо цвета) в качестве аттрибута для каждой вершины и передаём их в какой-либо переменной.
После того, как это было проделано для каждой вершины, WebGL будет работать с фрагментами(которые, напоминаю, являются просто пикселями), которые находятся между вершинами, вычислив их при помощи линейной интерполяции аналогичному тому, как было проделано с цветами в уроке 2.
Итак, серединный фрагмент между вершинами с текстрными координатами (1, 0) и (0, 0) получит текстурные координаты (0.5, 0), и конечный фрагмент между вершинами (0, 0) and (1, 1) получит текстурные координаты (0.5, 0.5).
Следующая остановка, фрагментный шейдер:
#ifdef GL_ES
  precision highp float;
  #endif
 
  varying vec2 vTextureCoord;
 
  uniform sampler2D uSampler; 
 
  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); 
  }
Таким образом, мы забрали интерполированные значения текстурных координат, и у нас есть переменная типа sampler, которая является способом представления текстур для шейдера.
В функции drawScene, наша текстура это gl.TEXTURE0, а форма uSampler установлена в значение ноль, этот образец представляет нашу текстуру. Всё, что делает шейдер, это использование функции texture2D для получения соответсвующего цвета из текстуры при помощи координат.
Для текстурных координат традиционно использются обозначения s и t чаще. чем x и y, и шейдерный язык поддерживает эти обозначения; мы могли бы так же легко использовать vTextureCoord.x и vTextureCoord.y.
Теперь у нас есть цвет фрагмента! Мы видим на экране текстурированный объект. На сегодня всё. Теперь вы имеете представление о том, как добавлять текстуры к 3D объекту при помощи загрузки изображения, указав WebGL использовать его в качестве текстры, передав вашему объекту текстурные координаты и использование координат и текстур шейдерах.
Если у вас есть вопросы, комментарии или поправки, пожалуйста оставьте комментарий ниже!
Иначе, перейдите к следующему уроку,в котором я покажу, как можно получить простой ввод с клавиатруы в JavaScript, который анимирует вашу 3D сцену и делает её интерактивной для человека, просматривающего страницу.
Мы будем использовать это для предоставления пользователю возможности изменять угол поворота куба, приближать и удалять сцену, и регулировать настройки, переданные WebGL, отвечающие за наложение текстуры.

<< Урок 4

Урок 6 >>



Благодарности: Chris Marrin’s WebKit-only spinning box сильно помогло при написании данного материала, так же как и портирование Крисом демо Jacob Seidelin’s в Firefox.
Как всегда, я глубоко благодарен NeHe за их уроки OpenGL и за скрипты к этому уроку.

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 вдохновило меня на адаптирование этого урока для введения в массивы элементов.