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