суббота, 25 сентября 2010 г.

WebGL Урок 3 - немного движения


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

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

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

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

Прежде чем заняться описанием кода я проясню одну вещь. Идея анимации 3D сцены в WebGL очень проста - вам надо просто постоянно перерисовывать сцену, каждый раз рисуя её иначе. Это вполне может быть совершенно очевидно для многих читателей, но это было несколько неожиданным для меня, когда я учил OpenGL, и, возможно, удивит тех, кто знакомится с 3D-графикой впервые .Я был смущен, потому что первоначально мне представлялось, что будут использоваться абстракции более высокого уровня, которые работали бы в формате "рассказать 3D-системе, что есть (например) квадрат в точке X во время первой отрисовки, и затем что бы переместить квадрат, сказать 3D-системе, что квадрат упоминавшийся ранее переехал в точку Y". Вместо этого получается так: "сказать 3D-системе, что есть квадрат в точке X, в следующем кадре, сказать 3D-системе, что он в точке Y, а затем следующий раз, что он в точке Z" и т. д.

Я надеюсь, что последний параграф прояснил ситуацию для кого-то (дайте мне знать в комментариях, если он только запутал, и я удалю его :-)

А теперь обратим внимание на функцию drawScene, именно она перерисовывает сцену, с помощью вот этого кода:

setInterval(drawScene, 15);
... который указывает JavaScript вызывать drawScene с интервалом 15мс. Все, что нам нужно сделать, чтобы оживить сцену и получить движущиеся треугольник и квадрат, это изменить код так, чтобы при каждом вызове, drawScene рисовала фигуры немного по-другому.

Это означает, что большая часть изменений, относительно кода урока №2 будет в функции drawScene, так что давайте начнем с неё. Прежде всего, следует отметить, что прямо перед объявлением этой функции, мы добавили две новые глобальные переменные.

var rTri = 0;
var rSquare = 0;
Они используются для отслеживания вращения треугольника и квадрата соответственно. Они оба начинают вращаться на ноль градусов, а затем с течением времени значения этих переменных будет увеличиваться(увидите позже как) поворачивая их больше и больше. (К сведению - использование глобальных переменных для таких вещей в 3D-программе, которая не является простой демонстрацией было бы очень плохой практикой. Я покажу, как структурировать код более элегантно в уроке №9.)

Следующее изменение в drawScene добавляем в момент, когда мы рисуем треугольник. Я покажу весь код, что рисует его, в контексте; новые строчки выделены красным цветом:

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

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

mvPushMatrix();
mvRotate(rTri, [0, 1, 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);

mvPopMatrix();

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

Вспомните, "текущее" состояние сохраняется в model-view матрице. С учетом всего этого, цель вызова:

mvRotate(rTri, [0, 1, 0]);

должна быть довольно очевидной; мы меняем текущее состояние вращения которое хранятся в model-view матрице, поворачивая на rTri градусов вокруг вертикальной оси (которая обозначена вектором во втором параметре). Это означает, что, когда треугольник отрисуется, он будет повернут на rTri градусов. Функция mvRotate, как и mvTranslate, которую мы рассматривали в уроке 1, написана на JavaScript - мы рассмотрим её позже.

Теперь, что касается вызовов mvPushMatrix и mvPopMatrix? Как и следовало ожидать из названия этих функций, они также связаны с model-view матрицей. Возвращаясь к моему примеру отрисовки робота, предположим, что в коде необходимо перейти к точке A, нарисовать робота, а затем переместится на некоторое смещение от точки А, и нарисовать чайник. Код, рисующий робота может вносить любые изменения в model-view матрицу; он может начать с туловища, затем переместиться вниз к ногам, затем вверх к голове, а потом и руки нарисовать. Проблема в том, что если после этого вы пытаетесь переместится на ваше смещение, вы переместитесь не относительно точки А, а относительно последней нарисованной фигуры, а это означает, что если ваш робот поднимет свои руки, чайник начнет парить в воздухе. Что не правильно :)

Очевидно, что необходим какой-то способ сохранения состояния model-view матрицы перед началом рисования робота, и восстановление его впоследствии. И это, конечно, именно то, чем mvPushMatrix и mvPopMatrix занимаются. mvPushMatrix ставит матрицу в стек, а mvPopMatrix избавляется от текущей матрицы, достает последнюю из стека, и восстанавливает ее. Использование стека означает, что мы можем иметь любое количество вложенных рисующих кусков кода, каждый из которых изменяет model-view матрицу, а затем восстанавливает её после этого. Поэтому, как только мы закончили рисовать наш повернутый треугольник, мы восстанавливаем model-view матрицу с помощью mvPopMatrix, чтобы этот код:

mvTranslate([3.0, 0.0, 0.0])
... передвигался по сцене с точки отсчета, где еще не было поворота. (Если по-прежнему не ясно, что все это значит, я рекомендую скопировать код и посмотреть, что произойдет, если удалить push\pop код, наверняка довольно быстро поймете.)

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

mvPushMatrix();
mvRotate(rSquare, [1, 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);

mvPopMatrix();
}

... это все изменения, внесенные в код в drawScene .

Очевидно, есть еще кое-что, что нам нужно сделать, чтобы оживить нашу сцену: нужно изменять значения rTri и rSquare с течением времени, так что каждый раз, когда сцена перерисовывается, она будет немного изменятся. Мы сделаем это в новой функции, называемой animate , которая, как и drawScene, будет вызываться регулярно (вы увидите код, обеспечивающий это чуть позже). Выглядит это примерно так:

var lastTime = 0;
function animate() {
var timeNow = new Date().getTime();
if (lastTime != 0) {
var elapsed = timeNow - lastTime;

rTri += (90 * elapsed) / 1000.0;
rSquare += (75 * elapsed) / 1000.0;
}
lastTime = timeNow;
}

По-простому для анимации сцены достаточно просто увеличивать rTri и rSquare на фиксированную величину при каждом вызове animate (что оригинальный OpenGL урок и делает), но здесь я решил немного улучшить этот момент; величина, на которую мы повернем объекты зависит от того, насколько давно эта функция вызывалась последний раз. В частности, треугольник вращается на 90 градусов в секунду, а квадрат на 75 градусов в секунду. Сделано это для того что бы у всех анимация выглядела одинаково, независимо от того, как быстры их компьютеры; люди с медленными компьютерами просто увидят менее плавную анимацию. Это не так важно для простой демонстрации, но, очевидно, в играх значимость такого решения возрастает.

Следующее изменение должно обеспечить регулярный вызов функций animate и drawScene. Мы сделаем это, создав новую функцию tick, которая вызывает их обоих а сама будет вызывается каждые 15 миллисекунд, вместо drawScene :

function tick() {
drawScene();
animate();
}

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

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clearDepth(1.0);

gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);

setInterval(tick, 15);
}

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

var mvMatrixStack = [];

function mvPushMatrix(m) {
if (m) {
mvMatrixStack.push(m.dup());
mvMatrix = m.dup();
} else {
mvMatrixStack.push(mvMatrix.dup());
}
}

function mvPopMatrix() {
if (mvMatrixStack.length == 0) {
throw "Invalid popMatrix!";
}
mvMatrix = mvMatrixStack.pop();
return mvMatrix;
}

Тут не должно быть ничего необычного. У нас есть список, в котором будет хранится стек матриц, и функции манипулирующие им.

Теперь давайте посмотрим на mvRotate

function mvRotate(ang, v) {
var arad = ang * Math.PI / 180.0;
var m = Matrix.Rotation(arad, $V([v[0], v[1], v[2]])).ensure4x4();
multMatrix(m);
}

Опять же, все просто - работу по созданию матрицы для представления вращения выполняем с помощью библиотеки Sylvester.

Это все! Больше изменений нет. Теперь вы знаете, как оживить простую WebGL сцену. Если у вас есть какие-либо вопросы, комментарии или корректировки, пожалуйста, оставьте комментарий ниже.

В следующем уроке (процитирую предисловие NeHe к его пятому уроку) мы "привратим фигуры в настоящие 3D фигуры, вместо 2D фигур в 3D мире". Жмите здесь что бы узнать как.

суббота, 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

суббота, 3 июля 2010 г.

Урок 0: Приступая к работе

Первым шагом к WebGL является браузер, который поддерживает эту технологию; WebGL поддерживается в бета версиях всех основных браузерав, кроме Internet Explorer, так что первым делом нужно раздобыть подходящую версию браузера для вашего компьютера. Webgl браузер доступен для следующих операционных систем:
  • Windows:
    • Если у вас достаточно свежая ATI или Nvidia видеокарта, проблем не должно быть. Пробуем установить Firefox или Chromium, как вам будет угодно, - если возникают проблемы посмотрите эти пару советов.
    • Если у вас графический чипсет от Intel (часто бывает в ноутбуках), то готовьтесь к проблемам. Видео драйверы от Intel поддерживают OpenGL не полностью, а сейчас поддержка WebGL в браузерах обеспечивается силами OpenGL. Все же стоит попробовать инструкции для установки Firefox или Chromium, но вполне вероятно, что они не будут работать. Если не получится, можно попробовать особую версию Firefox с функцией программного рендеринга (3D-графика просчитывается на CPU, вместо GPU видеокарты). Это будет медленнее, но по крайней мере вы сможете попробовать какую то часть WebGL контента!
  • Macintosh: если вы работаете в Snow Leopard (OS X 10.6), все должно быть прекрасно, я рекомендую использовать бета версию WebKit, который будет работать в качестве альтернативной версии Safari. Если вы работаете в Leopard (OS X 10.5), то вы не сможете использовать эту версию WebKit, но вы можете использовать как Firefox так и Chromium. Пользователи Snow Leopard конечно, тоже могут использовать Firefox или Chromium. Если у вас старая версия OS X, к сожалению, я не знаю ни одного WebGL браузера который вы сможете использовать прямо сейчас :-(
  • Linux: ситуация в значительной степени такая же, как с Windows:
    • Если у вас ATI или Nvidia видеокарта, а также последние версии драйверов, то все должно работать нормально как в Firefox так и в Chromium (по состоянию на 22 июня 2010 я слышал о проблемах с Firefox WebGL в 64-битных Linux, так что Chromium может быть вашим лучшим вариантом).
    • Если видеокарта от Intel, как и пользователям Windows вам, вероятно, придется использовать медленный программный рендеринг, попробуйте Firefox илиChromium , но они, вероятно, не будет работать с настройками по умолчанию. Но ситуация лучше, чем у Windows пользователей, - нужно убедиться, что Mesa установлен (лучше всего установить последнюю версию с помощью менеджера пакетов вашего дистрибутива), а затем вы сможете использовать Mesa версию Firefox.
Firefox

Бета версия Firefox известна под кодовым именем Minefield. Он обновляется каждую ночь, и уже вполне стабилен. Он может быть установлен рядом с основной версией Firefox, так что в случае чего можно его без проблем удалить и продолжить работать в привычном Firefox.

Чтобы получить Minefield:
  • Перейдите на страницу nightly builds и скачайте подходящую версию для вашего компьютера.
  • Установите его (предварительно закрыв все запущенные окна Firefox).
  • Запустите Minefield.
  • Перейдите на страницу "about:config"
  • Фильтруем по "webgl"
  • Ставим значение "webgl.enabled_for_all_sites" в "true".
(Спасибо Владимиру Вукичевичу за эту информацию)
Далее, нажмите здесь, чтобы попробовать WebGL.

Запуск Minefield с программным рендером в Windows

Если ваше графическое оборудование не поддерживает OpenGL 2.0, то сейчас единственный способ получить работающий на вашей машине WebGL это использование библиотеки под названием Mesa. Mesa попросту программно эмулирует видеокарту, так что это немного медленно, - но это лучше, чем ничего! Как запустить это на Windows:
  • Скачать эту версию Minefield.
  • Установите его (предварительно закрыв все запущенные окна Firefox).
  • Скачать эту версию библиотеки Меsа, любезно предоставленную Владимиром Вукичевичем
  • Разархивируйте zip архив Меsа; он содержит один DLL файл, называемый OSMESA32.DLL,его вы должны положить куда-нибудь на вашей машине.
  • Запустите Minefield.
  • Перейдите на страницу "about:config"
  • Фильтруем по "webgl"
  • Ставим значение "webgl.enabled_for_all_sites" в "true".
  • Настраиваем поле "webgl.osmesalib" чтобы оно указывало на OSMESA32.DLL. Например, если вы извлекали его в "C:\Temp", установите "webgl.osmesalib" в "C:\Temp\osmesa32.dll"
  • Ставим значение "webgl.software_rendering" в "true".
(Еще раз спасибо Владимиру Вукичевичу за информацию)
На этом всё. Нажмите здесь, чтобы попробовать WebGL.

Запуск Minefield с программным рендером на Linux

Если ваше графическое оборудование не поддерживает OpenGL 2.0, то сейчас единственный способ получить рабочий на вашей машине WebGL - использование библиотеки под названием Mesa. Mesa попросту программно эмулирует видеокарту, так что это немного медленно, - но это лучше, чем ничего! Вот как можно его установить.
  • Скачайте последнюю версию Minefield.
  • Установите его (предварительно закрыв все запущенные окна Firefox).
  • С помощью менеджера пакетов вашего дистрибутива Linux убедитесь, что Mesa установлена и обновлена до последней версии.
  • Запускайте Minefield.
  • Перейдите на страницу "about:config"
  • Фильтруем по "webgl"
  • Ставим значение "webgl.enabled_for_all_sites" в "true".
  • Ставим значение "webgl.software_rendering" в "true".
  • В опции "webgl.osmesalib" указываем путь к вашей OSMesa shared library (обычно что-то вроде /usr/lib/libOSMesa.so).
На этом всё. Нажмите здесь, чтобы попробовать WebGL.

Safari

Помните, Safari поддерживает WebGL только на Маках с Snow Leopard (OS X 10.6), если вы используете Leopard (10.5), Windows или Linux, то вы должны будете использовать Firefox или Chromium. (Если вы используете старую версию OS/X, я не знаю ни одного браузера с поддержкой WebGL :-()
Если вы используете Snow Leopard, вам необходимо:
  • Убедится, что вы используете, по крайней мере 4 версию Safari.
  • Скачайте и установите WebKit nightly build.
  • Запуcтите терминал, и в нем команду:
  • defaults write com.apple.Safari WebKitWebGLEnabled -bool YES
  • Запустите только что установленный WebKit.
(Спасибо Крису Маррину за информацию)
Далее, нажмите здесь, чтобы попробовать WebGL .

Chromium

Разработчики Chrome в настоящее время рекомендуют использовать ночные сборки Chromium`а. Chromium - это браузер с открытым исходным кодом на котором основан Chrome. Процедура несколько отличается для каждой поддерживаемой операционной системы, здесь приведены инструкции для Windows , Macintosh и Linux . (Предупреждение - я пробовал только версии Windows, но говорят другие версии тоже работают хорошо.

Для Windows
  • Перейдите на эту страницу и скачайте chrome-win32.zip
  • Распакуйте файл куда удобно.
  • Откройте окно командной строки и перейдите в chrome-win32 каталог, который вы распаковали.
  • Убедитесь, что все окна Chrome закрыты
  • Выполните следующую команду:
  • chrome.exe --enable-webgl
Для Macintosh
  • Перейти на эту страницу и скачайте chrome-mac.zip
  • Распакуйте файл куда удобно.
  • Откройте окно терминала и перейдите в chrome-mac каталог, который вы распаковали.
  • Убедитесь, что все окна Chrome закрыты
  • Выполните следующую команду:
  • ./Chromium.app/Contents/MacOS/Chromium --enable-webgl
Для Linux

Если вы используете 32-разрядный Linux, перейдите на эту страницу и скачайте chrome-linux.zip. Если вы хотите 64-разрядную версию, вы можете перейти на эту страницу, скрольте до упора вниз, чтобы найти самый свежий каталог, перейдите в него, и скачайте chrome-linux.zip оттуда.
  • Распакуйте файл куда удобно, и перейдите в chrome-linux каталог.
  • Убедитесь, что все окна Chrome закрыты
  • Выполните следующую команду:
  • ./chromium --enable-webgl
(Спасибо Мохамеду Мансуру за информацию)
Далее, нажмите здесь, чтобы попробовать WebGL .

Некоторые примеры WebGL

Как только ваш браузер будет установлен, вы сможете видеть WebGL контент. Посмотрим на второй WebGL урок. Вы должны увидеть красно-зелено-синий треугольник, и голубой квадрат. Если вы не видите квадрат и треугольник, почитайте руководство по устранению неполадок ниже.
Если все-таки работает, то всё отлично! Вот несколько интересных страниц с WebGL контентом:Если вы хотите двигаться дальше и узнать о том, как создать свою собственную WebGL страницу, читайте первый урок по WebGL.
Урок 1>>

Решаем проблемы

Для Windows и Linux пользователей: самая распространенная причина, по которой WebGL не работает - проблемы с графическими драйверами. Все текущие реализации WebGL основаны на OpenGL (ожидается, что это изменение в будущем), а поддержка OpenGL обеспечивается поставщиком графического драйвера. Для WebGL необходимо наличие как минимум OpenGL 2.0 для запуска; проблемы с видеокартами Intel существуют от того, что Intel не обеспечил поддержку OpenGL в драйверах для большинства своих графических устройств.
Если у вас ATI или Nvidia видеокарта, первое, что нужно сделать, это проверить версию OpenGL на вашем компьютере. Для Windows, это можно сделать с помощью GLview. На Linux, нужно выполнить команду glxinfo и найти строку с заголовком "OpenGL version string". Если номер версии меньше, чем 2.0, вам необходимо обновить драйвер.
Если после обновления вы всё еще испытываете проблемы - есть вероятность что подходящий OpenGL не доступен для вашей видеокарты или операционной системы. Возможно имеет смысл попробовать программный рендер. Вот инструкции для Windows и для Linux.


Оригинал на английском языке: http://learningwebgl.com/blog/?p=11