пятница, 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

17 комментариев:

  1. Спасибо. Познавательный урок.

    ОтветитьУдалить
  2. СПАСИБО РЕШИЛ ЗАБИТЬ НА WEBGL ДО ВЫХОДА БИБЛИОТЕК

    ОтветитьУдалить
  3. v originale ispolzuyut mat4 vmesto mv , i v kode tozhe!

    ОтветитьУдалить
  4. Спасибо за перевод. познавательно. Нашлось несколько синтаксических ошибок в Вашем коде, но не смертельно. Код можно взять и из оригинала, а понимание того, как все устроено, Вы дали.

    ОтветитьУдалить
  5. Мда. Я бы на месте автора, первый урок посвятил описанию специфики 3Д разработки, матричным преобразованиям, системе координат, и.т.д.
    Лично я имею большой опыт написания кода на JS, но с 3Д вообще не знаком. Очень много вещей из урока не совсем понятны. Куча терминов, суть которых скрыта за работой сторонних библиотек, а ведь без понимания, что именно они делают и зачем далеко не уедешь...
    Но в любом случаи огромное спасибо автору, за потраченное время и старания. Возможно после гугления касательно терминов и повторного прочтения, что то прояснится :).

    ОтветитьУдалить
    Ответы
    1. Если хотите все познать досконально, тогда просто не используйте webgl, а сами постройте постройте 3d кубик (его скилет) по всем правилам, вот тогда многое узнаете про матрицы трансформации (перемещение, вращение, масштабирование, да и матрица камеры, инфа по которой мне как-то с трудом нашлась, при этом нужно учитывать направления осей, тк.к в одной статье матрицы для правой оси, в другой - для левой))... по мне так лучше б было изучить на том уровне, что тут описано, а потом по тихой пополнять знания о мелких деталях, вы ж для того чтоб писать скрипты на js assembler не учили ;)

      Удалить
  6. Вывести на канвас треугольник и квадрат можно 8-10 строчками кода без всяких либ. Разочаровался в WebGL ((

    ОтветитьУдалить
    Ответы
    1. Это ж как пример млин. На том же learningwebgl последние уроки посмотрите.
      Раз WebGL основан на OpenGl,то довольно неплохие проекты можно делать.

      Удалить
  7. Черт, как же это сложно...
    Но с другой стороны это открывает следующий этап развития браузерных игр.
    К тому же сейчас активно пишут движки для этой технологии, например вот:
    http://www.html5gamedevelopment.org/html5-engines/2011-10-j3d---javscript--webgl-engine

    ОтветитьУдалить
    Ответы
    1. Обычный OpenGL (4 версии) не легче.Тот же VBO и шейдеры.

      Удалить
  8. gl.clearColor(0,0, 0,0, 0,0, 1,0);
    gl.clearDepth(1,0)
    Я что то пропустил? RGBA теперь 8 параметрами задается? Исправте не 0,0 а 0.0!

    ОтветитьУдалить
  9. Упрощённая версия https://gist.github.com/dortonway/6235736

    ОтветитьУдалить
  10. WebGLFloatArray не во всех браузерах поддерживается! Вот такой откат прокатывает: https://code.google.com/p/glmatrix/issues/detail?id=1

    ОтветитьУдалить
  11. Спасибо большое :)

    Но замечу, некоторые вещи:
    1) "Одно из преимуществ (кто-то бы сказал недостатков) JavaScript в том, что объекту не обязательно явно объявлять свойство" - чтобы статья имела более профессиональный вид наверное следует заменить "JavaScript" на "динамической типизации".
    2) И второе нет возможности, код в статье, отделить от текста?

    ОтветитьУдалить
  12. жесть. код очень трудно читается, не могли бы вы использовать стили, чтобы сделать подсветку кода или рамку. А так очень устарело, по сравнению с оригиналом, хотелось бы обновления. Спасибо, за внимание.

    ОтветитьУдалить
  13. Примеры интерактивных 3D приложений, созданных с помощью отечественного WebGL движка Blend4Web.

    http://www.blend4web.com/ru/demo/

    ОтветитьУдалить
  14. Оформление говно не читабельно.

    ОтветитьУдалить