суббота, 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 мире". Жмите здесь что бы узнать как.