суббота, 28 августа 2010 г.

WebGL Урок 2 - Добавление цвета



Добро пожаловать в мой второй WebGL урок! На этот раз мы собираемся посмотреть, как добавить цвета на сцену. Он основан на уроке №3 из NeHe OpenGL учебника.
Вот как урок выглядит в браузере, который поддерживает WebGL:


Нажмите здесь, что бы посмотреть WebGL версию, если у вас есть браузер, который поддерживает ее; если у вас нет WebGL браузера, - вам поможет руководство по установке.

Подробнее о том, как все это работает:

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

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

Вы можете получить код для этого примера через "View Source" браузера, еще можно скачать код с GitHub, этот и другие уроки расположены в этом репозитарии. В любом случае, когда получите код, открывайте его в вашем любимом текстовом редакторе и посмотрите.

Большая часть должна выглядеть так же как в первом уроке. Пробежимся по коду:
Определяем вершинный и пиксельный шейдеры, используя HTML <script> теги типа "x-shader/x-vertex" и "x-shader/x-fragment"
Инициализируем WebGL контекст в initGL
Загружаем шейдеры в объект программы WebGL с помощью getShader и initShaders .
Определяем model-view матрицу mvMatrix наряду с функциями loadIdentity , multMatrix , mvTranslate для манипулирования матрицей.
Определяем матрицу проекции pMatrix и функцию perspective для манипулирования этой матрицей.
Определяем setMatrixUniforms для перемещения model-view матрицы и матрицы проекции из JavaScript в WebGL с тем, чтобы шейдеры могли их увидеть.
Загружаем буферы, содержащие информацию об объектах в сцене с использованием initBuffers
Рисуем эту сцену, в drawScene.
Определим функцию webGLStart для установки первоочередных настроек
Наконец, мы добавляем минимально HTML’а, необходимого для отображения всего этого.

Единственное, что изменилось в этом коде с первого урока, это шейдеры, а так же функции initBuffers и drawScene. Для того чтобы объяснить, в чем заключаются изменения, вам нужно немного узнать о WebGL рендеринг конвейере. Вот схема:
Диаграмма показывает, в очень упрощенной форме, как данные, передаваемые функциям JavaScript в drawScene, превращаются в пиксели, отображаемые в WebGL canvas на экране. Она только показывает шаги, необходимые для объяснения этого урока, и мы увидим на более подробные версии в дальнейших уроках.

На самом высоком уровне, этот процесс работает следующим образом: каждый раз при вызове функции, такой как drawArrays, WebGL обрабатывает данные, которые вы ранее загрузили в виде атрибутов (например, буферы мы использовали для вершин в уроке №1) и uniform переменных (который мы использовали для матриц model-view и проекции), и передает их в вершинный шейдер.

Происходит это путем вызова вершинного шейдера по одному разу для каждой вершины, каждый раз с атрибутами соответствующими этой вершине; uniform переменные также передаются, но, как следует из их названия, они не меняются от вызова к вызову. Вершинный шейдер обрабатывает эти данные - в уроке 1, он применил матрицы проекции и model-view так, что вершины все будут в перспективе и перемещены в соответствии с нашим текущим состоянием model-view - и помещает результаты в, так называемые, varying переменные. Результатом его работы может быть ряд varying переменных, в частности одна из них является обязательной, gl_Position , которая содержит координаты вершины после того как шейдер закончил возиться с ней.

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

После того как пиксельный шейдер отработает, его результаты немного размываются WebGL (опять же, мы рассмотрим это в дальнейшем), и они помещаются в кадровый буфер, который в конечном счете является тем, что отображается на экране.

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

Нам помогает тот факт, что мы можем передать не только позицию, но и ряд varying переменных из вершинного шейдера, а затем прочитать их в пиксельном шейдере. Итак, передадим цвет в вершинный шейдер в varying переменную, которую затем прочтем в пиксельном шейдере.

Удобно то, что это дает нам градиенты цветов автоматически. Все varying переменные установленные вершинным шейдером линейно интерполируются при создании пикселей между вершинами, не только на позиции. Линейная интерполяция цвета между вершинами дает плавный градиент, вы можете это увидеть в треугольнике на картинке выше.

Давайте посмотрим на код, мы будем работать, изменяя 1 урок. Во-первых, вершинный шейдер. В нем изменений довольно много, вот новый код:

attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec4 vColor;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}

В коде создаются два атрибута - значения, которые варьируются от вершины к вершине - называются aVertexPosition и aVertexColor, две uniform переменные называются uMVMatrix и uPMatrix и одна varying переменная vColor .

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

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

#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main(void) {
gl_FragColor = vColor;
}

Здесь, устанавливается точность плавающей точки, и мы принимаем varying переменную vColor содержащую гладко смешанный цвет, полученный линейной интерполяцией, и просто возвращаем его, как цвет для данного пикселя.

Вот и все различия между шейдерами этого урока и предыдущего. Есть еще два других изменения. Первое очень маленькое, в initShaders мы сейчас получаем ссылки на два атрибуты, а не один; новые строки выделены красным цветом ниже:

var shaderProgram;
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");

shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}

gl.useProgram(shaderProgram);

shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

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

Остальные изменения в этом уроке находятся в функции initBuffers, которой теперь необходимо создать буферы для двух атрибутов, и в drawScene, которая должна передать оба атрибута в WebGL.
Посмотрим на initBuffers сперва, мы определяем новые глобальные переменные для хранения буферов цветов для треугольника и квадрата:

var triangleVertexPositionBuffer;
var triangleVertexColorBuffer;
var squareVertexPositionBuffer;
var squareVertexColorBuffer;

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

function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;

triangleVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
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,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
triangleVertexColorBuffer.itemSize = 4;
triangleVertexColorBuffer.numItems = 3;

Таким образом, значения, которые мы обеспечиваем для цвета находятся в списке, один набор значений для всех вершин, как для позиций вершин. Однако, есть одно интересное отличие между этими двумя массивами буферов: в то время как позиции вершин задаются в виде трех цифр для каждой вершины, X, Y и Z координаты, их цвета задаются в виде 4 элементов - красный, зеленый, синий и альфа. Альф является мерой прозрачности (0 прозрачный, 1 полностью непрозрачный) и будет полезна в следующих уроках. Это изменение в количестве элементов для каждой вершины в буфере требует изменения, связанного с ним itemSize.

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

squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;

squareVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
colors = []
for (var i=0; i <>
colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
squareVertexColorBuffer.itemSize = 4;
squareVertexColorBuffer.numItems = 4;

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

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([-1.5, 0.0, -7.0])

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

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

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

mvTranslate([3.0, 0.0, 0.0])
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

setMatrixUniforms();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}

Следующее изменение... подождите, а больше изменений нет! Это было все, что необходимо, что бы добавить цвет на нашу WebGL сцену, и, надеюсь, вы теперь знакомы с основами шейдеров и каким образом данные передаются между ними.

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


Благодарности: разобраться, что происходит в конвейере рендеринга стало гораздо легче, с помощью книги OpenGL ES 2.0 Programming Guide, которую Джим Пик рекомендовал в его WebGL блоге. Как всегда, я благодарен NeHe за его OpenGL учебник, за скрипты для этого урока.

пятница, 20 августа 2010 г.

WebGL Урок 1 - треугольник и квадрат


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

A static picture of this lesson's results

Нажмите здесь, что бы посмотреть WebGL версию, если у вас есть браузер, который поддерживает ее; если у вас нет WegGL браузера, вам поможет руководство по установке.

Подробнее о том, как все это работает:

На заметку: Эти занятия ориентированы на людей обладающих хорошими знаниями в области программирования, но не имеющих реального опыта в 3D-графике; целью ставим себе поскорее ввести в курс дела и прояснить, что происходит в коде, с тем, что бы Вы смогли начать делать собственные 3D веб-страницы как можно быстрее. Но используйте этот текст на свой страх и риск, так как я тоже только разбираюсь в WebGL и по ходу дела составляю эти уроки. Однако я исправляю ошибки, как только узнаю о них, так что если вы видите что-либо, то, пожалуйста, дайте мне знать в комментариях.

Вы можете получить код для этого примера через "View Source" браузера, еще можно скачать код с GitHub, этот и другие уроки расположены в этом репозитарии. В любом случае, когда получите код, открывайте его в вашем любимом текстовом редакторе и посмотрите. На первый взгляд это довольно сложно, даже если у вас есть опыт работы с OpenGL. С начала мы определим пару шейдеров, которые можно расценить как сложные...но не отчаивайтесь, на самом деле они гораздо проще, чем кажутся.

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

Вы увидите следующий код HTML:

<body onload="webGLStart();">
<a href="http://russian-webgl.blogspot.com/2010/08/webgl-1.html">Назад к уроку 1</a>

<canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>

<br/>
<a href="http://russian-webgl.blogspot.com/2010/08/webgl-1.html">Назад к уроку 1</a><br />
</body>

Это вся html раметка на странице - все остальное находится в JavaScript (хотя, если вы получили код, используя "View Source", вы увидите несколько дополнительных тегов необходимых для google analytics, их можно игнорировать). Мы могли бы добавить любое количество обычных тегов HTML внутри <body> и встроить наш WebGL холст как в обычную веб-страницу, но для этой простой демонстрации, мы только добавили WebGL библиотеки, ссылки на этот блог, а также <canvas> теги, в которых живет 3D-графика. canvas - это нововведение HTML 5.0 - с помощью этого тега можно с помощью JavaScript добавлять новые элементы на веб-странице, как 2D, так и (через WebGL) 3D. Мы не перегружаем canvas тэг настройками, вместо этого весь конфигурационный код для WebGL поместим в JavaScript функцию webGLStart, которая вызывается один раз во время загрузки страницы.

Давайте на неё взглянём:

function webGLStart() {
var canvas = document.getElementById("lesson01-canvas");
initGL(canvas);
initShaders();
initBuffers();

gl.clearColor(0,0, 0,0, 0,0, 1,0);
gl.clearDepth(1,0)
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);

setInterval(drawScene, 15);
)

Она вызывает функции для инициализации WebGL и шейдеров, передавая canvas элемент, на котором мы хотели бы рисовать 3D-графику в initGL, затем инициализирует буферы в initBuffers; в буферах будут храниться данные о треугольнике и квадрате, которые мы собираемся рисовать - мы поговорим о них позже. Далее выполняются базовые настройки движка GL, - цвет холста устанавливается в чёрный, clearDepth очистит холст, и включаем depth testing, для того что бы фигуры находящиеся позади других фигур были скрыты. Эти вещи осуществляются вызовами методов объекта gl - подробности позже. Наконец, вызываем setInterval для того, что бы функция drawScene вызывалась каждые 15 миллисекунд, drawScene рисует объекты на холсте, используя буферы.

Мы вернемся к initGL и initShaders позже, так как они важны для понимания, как работает страница, но сначала давайте взглянем на initBuffers и drawScene.

Сначала рассмотрим initBuffers, построчно:

var triangleVertexPositionBuffer;
var squareVertexPositionBuffer;

Мы объявляем две глобальные переменные для хранения буферов. (В любой странице WebGL из реального мира вам бы не следовало создавать отдельные глобальной переменной для каждого объекта сцены, но мы используем их здесь, что бы не усложнять первый урок :-)

Далее:

function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();

Мы создаем буфер для вершин треугольника. Вершины являются точками в 3D пространстве, которые определяют фигуры, которые мы рисуем. Для нашего треугольника, у нас их будет три (установим их через минуту). Этот буфер, на самом деле, - кусок памяти видеокарты; положив координаты вершин в память видеокарты, а затем, когда обновляем изображение на холсте, сообщая WebGL`у "нарисуй опять эти фигуры", мы можем сделать наш код действительно эффективным. Конечно, если это всего, лишь три вершины, как в данном случае, нагрузка на видеокарту не будет узким местом, - но когда вы имеете дело с большими моделями с десятками тысяч вершин, можно здорово экономить ресурсы.

gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);

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

var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];

Определяем наши вершины в виде JavaScript массива. Получился равнобедренный треугольник с центром в точке (0, 0, 0).

gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(vertices), gl.STATIC_DRAW);

Сейчас мы создаем WebGLFloatArray объект, основанный на нашем JavaScript массиве, и говорим WebGL использовать его для заполнения текущего буфера, которым является, конечно, наш triangleVertexPositionBuffer. Мы поговорим подробнее о WebGLFloatArray объекте в следующем уроке, а сейчас просто знайте, что это способ превращения JavaScript массива в то, что мы можем передать WebGL для заполнения буфера.

triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;

И последнее, установим два новых свойства для буфера. Они не являются необходимыми для WebGL, но будут очень полезны в дальнейшем. Одно из преимуществ (кто-то бы сказал недостатков) JavaScript в том, что объекту не обязательно явно объявлять свойство, для того что бы установить ему значение. Таким образом, несмотря, на то, что у объекта буфера раньше не было itemSize и numItems свойств, теперь они есть. Мы используем их, чтобы сообщить, что этот буфер из 9 элементов фактически представляет собой три отдельных позиции вершин(numItems), каждая из которых состоит из 3 цифр(itemSize).

Буфер для треугольника готов, вот буфер для квадрата:

squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;
}

Происходящее в коде должно быть довольно очевидно - квадрат состоит из 4 вершин, поэтому массив больше и numItems отличается.

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

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);

Первым делом нужно сообщить WebGL немного о размере холста, используя функцию viewport; мы объясним, почему это важно в следующих уроках; на данный момент, вы просто должны знать, что эта функция должна вызыватся с указанием размеров холста перед началом рисования. Далее, мы очищаем холст, подготавливая его к началу рисования:

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

а после этого:

perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);

Здесь мы устанавливаем перспективу, с которой мы хотим смотреть на сцену. По умолчанию, WebGL не обращает внимания на удалённость фигур от камеры, не изменяет их размер (этот стиль 3D известен как ортогональная проекция). Для того чтобы удалённые объекты выглядели меньше, мы должны сказать ему, немного об используемой перспективе. Для этой сцены, мы говорим, что наше поле зрения по вертикали составляет 45°, мы сообщаем коэффициент ширины к высоте нашего холста, и говорим, что мы не хотим, видеть то, что ближе, чем 0,1 единиц к нашей точке зрения, и что мы не хотим видеть то, что дальше, чем 100 единиц.

Функция perspective очень полезна, но она не встроена в WebGL, так что мы ее определяем выше в коде. Я позже буду более подробно объяснять, как она работает, но будем надеяться, и так понятно, как ее использовать без необходимости знать подробности.

Теперь, когда у нас настроена перспектива, мы можем перейти к рисованию на холсте:

loadIdentity();

Первым делом передвинемся в центр 3D сцены. В OpenGL, когда вы рисуете сцену, вы говорите ему нарисовать каждую фигуру в "текущим" месте с "текущим" вращением - так, например, вы говорите, "передвинутся вперед на 20 единиц, повернутся на 32 градуса, и нарисовать робота". Это полезно, потому что вы можете инкапсулировать код "нарисовать робота" в одной функции, а затем легко перемещать робота, просто изменяя параметры перемещения/вращения перед вызовом этой функции.

Текущее положение и вращение хранится в матрице; как вы, вероятно, учили в школе, матрицы могут представлять перемещения, вращения и другие геометрические преобразования. По причинам, в которые я не буду сейчас вдаваться, вы можете использовать одну 4?4 матрицу (не 3?3) для представления любого числа преобразований в 3D пространстве. Вы начинаете с единичной матрицы - это матрица, представляющая преобразование, которое ничего не делает - затем умножаете на матрицу, которая представляет ваше первое преобразование, затем перемножаете с матрицей представляющей вторую трансформацию, и так далее. Комбинированная матрица представляет все ваши преобразования. Матрица, используемая нами для представления нынешнего состояния перемещения/вращения, называется model-view матрицей, и теперь вы, вероятно, поняли, что функция loadIdentity, которую мы только что вызвали, приравнивает model-view матрицу к единичной матрице с тем, чтобы мы были готовы перемножать её с перемещениями и поворотами.

Внимательные читатели уже заметили, что в начале рассказа про матрицы я сказал: "в OpenGL", а не "в WebGL". Это потому, что, как и с функцией perspective, WebGL не поддерживает эту схему из коробки; мы должны реализовывать её сами, или использовать сторонние библиотеки, реализующие это для нас. Опять же, я объясню подробнее, каким образом это работает позже, но их можно использовать без необходимости знать подробности.

Хорошо, давайте перейдем к коду, который рисует треугольник на левой стороне нашего холста.

mvTranslate([-1.5, 0.0, -7.0]);

Переехав в центр нашего 3D пространства с помощью loadIdentity, мы начинаем треугольник, перемещаясь на 1,5 единиц влево (то есть, вдоль отрицательной оси X), и на 7 единиц от сцены (то есть, от зрителя; вдоль отрицательной оси Z). (mvTranslate, как вы уже могли догадаться, на низком уровне, переводится как "умножить model-view матрицу на матрицу трансформации со следующими параметрами".)

Следующим шагом является непосредственно рисование на холсте:

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

Вы помните, что для того, чтобы использовать один из буферов, мы призываем gl.bindBuffer указать текущий буфер, а затем вызвать код, который его использует. Здесь мы, выбрав наш triangleVertexPositionBuffer, говорим WebGL, что значения в нем должны использоваться как позиции вершин. Я объясню, как это работает позже, а пока вы можете увидеть, что мы используем свойство itemSize, которое мы установили буферу, чтобы сказать WebGL, что каждый элемент в буфере состоит из 3 цифр.

Далее:

setMatrixUniforms();

Говорим WebGL учесть нашу нынешнюю model-view матрицу (а также матрицу проекции, о которой будет рассказано ниже). Это необходимо, потому что все, что касается матриц, - не встроено в WebGL. Это можно понимать так, что все перемещения на сцене вы делаете через mvTranslate, но это происходит в JavaScript, а setMatrixUniforms перемещает результат на видеокарту.

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

gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

Или, иначе говоря, "нарисовать массив вершин, что я дал вам ранее, как треугольники, начиная с индекса 0 в массиве и до numItems индекса".

Как только это будет сделано, WebGL нарисует наш треугольник. Следующий шаг, нарисовать квадрат:

mvTranslate([3.0, 0.0, 0.0])

Начнем с перемещения нашей model-view матрицы на 3 единицы вправо. Помните, мы в настоящее время уже в точке на 1,5 левее и на 7 глубже, так что в итоге мы находимся на 1,5 единиц правее и на 7 единиц глубже центра. Далее:

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

Таким образом, мы говорим WebGL использовать буфер нашего квадрата как позиции вершин...

setMatrixUniforms();

... мы снова загоняем в WebGL обновленную model-view матрицу и матрицу проекции, что означает, что мы можем, наконец:

gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

нарисовать точки. Вы можете спросить, что такое полоса треугольников(TRIANGLE_STRIP)? Ну, это полоса треугольников :-) Если подробнее, это полоса треугольников, где первые три предоставленные вершины - это первый треугольник, далее, последние две из этих вершин, а также следующая вершина - это следующий треугольник, и так далее. В данном случае, это быстрый и грязный способ обозначения квадрата. В более сложных случаях, он может быть очень полезен как способ описания комплексной поверхности с точки зрения треугольников, соответствующих ей.

Так или иначе, как только это будет сделано, мы завершаем нашу функцию drawScene.

}
Если вы дочитали до этого места, вы определенно готовы поэкспериментировать. Скопируйте код в локальный файл, либо с GitHub или непосредственно из live версии, в последнем случае нужны index.html, sylvester.js и glUtils.js. Откройте его локально, чтобы убедиться, что все работает, попробуйте изменить некоторые позиции вершин выше; например, сцена сейчас плоская, попробуйте изменить значения Z квадрата на 2 или -3, и увидите как он становится больше или меньше, т.к. перемещается вперед и назад. Или попробуйте изменить одну или две позиции вершин, и смотрите как фигура изменяется в перспективе. Поиграйте с этой функцией, и не обращайте на меня внимания. Я подожду.

...

Хорошо, а теперь, когда вы вернулись, давайте взглянем на вспомогательные функции, которые сделали весь код, что мы прошли, рабочим. Как я уже говорил ранее, если вы не вникали в детали и просто скопировали функции, которые приведены выше, над initBuffers, вы можете строить интересные WebGL страницы (Следующий урок). Но в деталях нет ничего сложного, а с пониманием как это устроено - вы, вероятно, будете писать более качественный WebGL код.

Все еще со мной? Спасибо :-) Давайте сперва займемся самыми скучными функциями, первая вызывается в webGLStart и называется - initGL . Она находится в верхней части веб-страницы, и вот копия:

var gl;
function initGL(canvas) {
try {
gl = canvas.getContext("experimental-webgl");
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
} catch(e) {
)
if (!gl) {
alert("Не удалось инициализировать WebGL, извините :-(");
}
}

Все очень просто. Как вы могли заметить, функции initBuffers и drawScene часто ссылаются на объект gl, который, в свою очередь, ссылается на контекст WebGL. Эта функция получает контекст WebGL из переданного холста по стандартному имени контекста. (Как вы можете догадаться, в какой-то момент название контекста измениться на "webgl", вместо "experimental-webgl") Как только мы получили контекст, мы снова воспользуемся особенностью JavaScript позволяющей нам установить любое свойство любому объекту и установим ширину и высоту холста новому контексту; это нужно, что бы мы могли использовать их в коде, который настраивает проекцию и перспективу в начале drawScene. Как только это будет сделано, наш контекст GL готов.

После вызова initGL, webGLStart вызывает функцию initShaders. Которая, конечно же, инициализирует шейдеры ;-). Мы вернемся к этому позже, потому что сначала мы должны взглянуть на вспомогательные функции, которые работают с model-view матрицей. Вот код:

var mvMatrix;

function loadIdentity() {
mvMatrix = Matrix.I(4);
}

function multMatrix(m) {
mvMatrix = mvMatrix.x(m);
}

function mvTranslate(v) {
var m = Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4();
multMatrix(m);
}

Мы определяем переменную mvMatrix для model-view матрицы, а затем определяем функции loadIdentity, mvTranslate и multMatrix. Если вы знаете JavaScript, вы в курсе, что алгебраические функции для работы с матрицами, что мы используем, не являются частью JavaScript API; на самом деле они доступны нам из 2 JavaScript файлов, которые мы подключаем в верхней части нашей HMTL страницы:

<script src="sylvester.js" type="text/javascript"></script>
<script src="glUtils.js" type="text/javascript"></script>

Первый из них - Sylvester, - свободная библиотека для работы с матричной и векторной алгеброй в JavaScript, а второй представляет собой набор расширений для Sylvester, которые были разработаны Владимиром Вукичевичем.

В любом случае, с помощью этих простых функций и вспомогательных библиотек, мы сможем работать с нашей model-view матрицей. Есть еще одна матрица, о которой мы поговорим, я упоминал её ранее - матрица проекции. Как вы помните, функция perspective не встроена в WebGL. Но, так же, как процесс перемещения моделей и их повороты, используя model-view матрицу, процесс уменьшения\увеличения моделей, которые расположены на разном расстоянии от нас, является хорошим вариантом для отражения в виде матриц. И, как вы, несомненно, догадались, матрица проекции делает именно это. Вот код:

var pMatrix;
function perspective(fovy, aspect, znear, zfar) {
pMatrix = makePerspective(fovy, aspect, znear, zfar);
}

makePerspective - еще одна функции, определенная в glUtils.js, она возвращает специальную матрицу, которая поможет применить указанную перспективу.

Да, теперь мы прошли все, кроме setMatrixUniforms функции, которая, как я уже говорил ранее, перемещает model-view матрицу и матрицу проекции из JavaScript в WebGL, и страшные вещи, связанные с шейдерами. Они взаимосвязаны, так что давайте начнем.

Что такое шейдер, спросите вы? Ну, в какой-то момент истории 3D-графики они были тем, чем могут показаться, - кусочки кода, которые сообщают системе, как затенять, или освещать части сцены, перед тем как её нарисовать. Однако с течением времени они развились в силу, которая лучше объясняется как кусочки кода, которые могут делать абсолютно все, что они хотят с частями сцены, пока она не отрисована. И это действительно очень полезно, потому что (а) они могут работать на видеокарте, поэтому они делают то, что они делают очень быстро и (б) варианты преобразований, которые они могут сделать, могут быть очень удобны даже в простых примерах, как этот.

Причиной, по которой мы представляем шейдеры в простом примере WebGL (в OpenGL учебниках о них рассказывают значительно позже) является то, что мы используем их, чтобы заставить ядро WebGL, как мы надеемся, работающее на видеокарте, просчитывать нашу model-view матрицу и матрицу проекции, вместо того, чтобы двигать каждую точку и каждую вершину в (относительно) медленном JavaScript. Это чрезвычайно полезно, и стоит дополнительных накладных расходов.

Итак, вот как они создаются. Как вы помните, webGLStart называет initShaders, поэтому давайте посмотрим на эту функцию:

var shaderProgram;
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");

shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}

gl.useProgram(shaderProgram);

Она использует функцию getShader что бы получить две вещи: "пиксельный шейдер" и "вершинный шейдер", а затем присоединяет их к такой WebGL штуке, как "программа". Программа представляет собой кусок кода, который живет на WebGL стороне системы, вы можете рассматривать это как способ указания того, что может исполняться на видеокарте. Вы можете связать с ней несколько шейдеров, каждый из которых можно считать фрагментом кода этой программы, в частности, каждая программа может содержать один пиксельный шейдер и один вершинный шейдер. Мы рассмотрим их в ближайшее время.

shaderProgram.vertexPositionAttribute = gl.getAttribLocation (shaderProgram ", aVertexPosition");
gl.enableVertexAttribArray (shaderProgram.vertexPositionAttribute);

После того, как функция создаст программу и присоединит шейдеры, она определяет новое свойство у объекта программы - vertexPositionAttribute . Снова мы пользуемся возможностью JavaScript устанавливать любые свойства на любой объект, у объекта программа по умолчанию нет свойства vertexPositionAttribute, но нам удобно держать два значения рядом, поэтому мы просто сделали новое свойство.

Итак, что же такое vertexPositionAttribute? Как вы помните, мы использовали его в drawScene , если вы сейчас посмотрите выше на код, который устанавливает позиции вершин треугольника из соответствующего буфера, то увидите, что то, что мы делали, связано с буфером и этим атрибутом. Вы увидите, что это значит чуть позже, а сейчас, давайте просто отметим, что мы также используем gl.enableVertexAttribArray, что бы сказать WebGL, что мы хотим представить значения атрибута с помощью массива.

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

Последнее, что делает initShaders, - получает еще две переменные из программы, - ссылки на две uniform переменные. Мы посмотрим на них позже; на данный момент, вы должны просто отметить, что, как и vertexPositionAttribute, мы храним их в объекте программы для удобства.

Теперь давайте взглянем на getShader :

function getShader(gl, id) {
var shaderScript = document.getElementById(id);
if (!shaderScript) {
return null;
}

var str = "";
var k = shaderScript.firstChild;
while (k) {
if (k.nodeType == 3)
str += k.textContent;
k = k.nextSibling;
}

var shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null;
}

gl.shaderSource(shader, str);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}

return shader;
}

Это еще одна из тех функций, которые гораздо проще, чем кажется. Все, что мы здесь делаем, это ищем элемент на HTML странице, содержащий ID, соответствующий параметру, вытаскиваем его содержание, создаем пиксельный или вершинный шейдер (подробнее о разнице между этими шейдерами в следующих уроках), а затем передаем его WebGL, который приведет их в вид, способный исполнятся на видеокарте. Затем обрабатываем ошибки и все! Конечно, мы могли бы просто определить шейдеры как строки в нашем JavaScript коде, а не возиться с их извлечения из HTML - но делая это таким образом, мы делаем их более простыми для чтения, потому что они определены как скрипты на веб-странице так же, как если бы они были JavaScript скриптами.

Посмотрим на код шейдеров:

<script id="shader-fs" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>

<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>

Первое, что нужно помнить о них, что они не написаны на JavaScript, хотя происхождение языков явно общее. На самом деле, они написаны на специальном языке шейдеров, который многим обязан C (как, и JavaScript, конечно). Первый из них, пиксельный шейдер, делает не много; он просто указывает, что все, что нарисовано, будет белого цвета. (Цвет - является темой следующего урока.) Второй шейдер немного более интересный. Это вершинный шейдер - он может сотворить все что угодно с вершиной. В его коде присутствуют две uniform переменные - uMVMatrix и uPMatrix . Uniform переменные полезны, тем, что они могут быть доступны вне шейдера - по сути, вне своей программы, как вы, вероятно, помните, мы извлекли ссылки на них в initShaders. Можно представить программу шейдеров как объект (в объектно-ориентированном смысле) а uniform переменные как свойства этого объекта. Теперь, шейдер вызывается для каждой вершины, а вершина, передается в шейдер как aVertexPosition , благодаря использованию vertexPositionAttribute в drawScene , когда мы связали атрибут с буфером. Код шейдера в main перемножает вершину с model-view матрицей и матрицей проекции и возвращает результат как конечную позицию вершины.

Получается, что webGLStart вызывает функцию initShaders, которая используя getShader, загружает пиксельные и вершинные шейдеры из скриптов на веб-странице, с тем, что бы они были скомпилированы, переданы в WebGL и в дальнейшем использовались для отрисовки нашей 3D-сцены.

Теперь осталось только рассмотреть функцию setMatrixUniforms, которую можно легко понять, если вы дочитали досюда :-)

function setMatrixUniforms() {
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, new WebGLFloatArray(pMatrix.flatten()));
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, new WebGLFloatArray(mvMatrix.flatten()));
}

Так что, используя ссылки на uniform переменные, которые представляют нашу матрицу проекции и model-view матрицу, которые мы получили в initShaders, мы отправляем в WebGL значения из наших JavaScript матриц.

Уф! Это было довольно много для первого урока, но, надеюсь, теперь вы (и я) понимаете все основы, которые понадобятся, что бы начать делать нечто более интересное, - красочные, движущиеся, 3х-мерные модели WebGL. Чтобы узнать больше, читайте урок 2.

Благодарности: я в долгу перед NeHe за его учебник по OpenGL за сценарии для этого урока, но я также хотел бы поблагодарить Benjamin DeLillo и Vladimir Vukicevic за их примеры WebGL кода, который я исследовал, анализировал, вероятно, все перепутав, использовал для написания этого поста :-). Кроме того, большое спасибо Джеймсу Коглану за то, что опубликовал как Open Source прекрасную библиотеку Sylvester.


Перевод статьи learning webgl lesson 1