WebGL — стандарт, спецификация которого разрабатывается в настоящее время, предназначенная для отображения 3D графики в web браузерах. Данная технология позволяет внедрять аппаратно-ускоренную 3D графику в веб-страницы без необходимости использовать специальные плагины веб-браузера на любой платформе, поддерживающей OpenGL или OpenGL ES. Технически, это будет представлять собой привязку JavaScript-скриптов к функциям, определенным в библиотеках OpenGL ES 2.0, реализованную на уровне браузера.
EnergizeGL - JavaScript-библиотека, позволяющая работать с WebGL без знаний OpenGL и матричных преобразований.
В связи с тем, что на данный момент (16 ноября 2010 года) спецификация WebGL все еще является черновой, ведущие производители браузеров Google Chrome и Mozilla Firefox не включают поддержку этой технологии в стабильные ревизии. Для работы с этой технологией следует скачать последнюю бета-версию указанных браузеров: Chromium или Minefield.
Прежде всего создадим html-файл, подключим к нему библиотеку EnergizeGL, создадим тег canvas, запустим EnergizeGL.
Подпрограмма StartEnergizeGL принимает три параметра:
Теперь мы готовы рисовать простейшие примитивы, используя функцию draw(), однако, перед этим нам нужно сориентироваться в пространстве. Дело в том, что в WebGL используется левосторонняя система координат (смотри рисунок), в то время как камера строго зафиксированна в точке (0,0,0) и смотрит по направлению оси Z в отрицательную сторону. Таким образом, если нам нужно переместиться правее камеры - мы должны увеличивать координату X, левее - уменьшать X, выше - увеличивать Y, ниже - уменьшать Y, глубже (дальше) - уменьшать Z, а если нам вдруг понадобится размещать объекты за точкой обзора (за нашей спиной) - то увеличивать координату Z.
Таким образом, чтобы нарисовать видимый примитив, сперва нам следует переместиться немного вперед, вдоль оси Z в отрицательную сторону. Добавим внутрь функции draw() код:
В результате мы увидим куб со сторонами 1-1-1 сверху. Функция translate(x, y, z); перемещает координаты центра сцены на указанный вектор. Это значит, что если мы были в позиции (0, 0, -20) и использовали функцию translate(5, 0, 0); - мы переместили центр рисуемой сцены на вектор {+5, +0, +0} в координаты (5, 0, -20). Функция cube(); без параметров рисует простейший примитив - куб в центре сцены со сторонами 1-1-1. Функция cube(x, y, z, dx, dy, dz); рисует куб с центром в векторе {+x,+y,+z} относительно центра сцены и с параметрами: длинна - dx, ширина - dy, высота - dz. Оставим первый куб без изменений, переместим центр сцены правее и нарисуем куб с другими параметрами:
В результате получилось два объекта на сцене. Возникает желание посмотреть на них с другого ракурса, для этого у нас есть функция rotate(x, y, z);, которая изменяет ориентацию нашей сцены на указаные углы. Углы (x, y, z) указываются в радианах.
Обратите внимание, что после поворота сцены на какой-либо угол, функция translate перестает перемещать вас на координаты, соответствующие левосторонней системе координат относительно камеры (например, второй объект поднялся выше). Дело в том, что вместе с поворотом сцены, поворачиваются и ее несущие XYZ-оси, по которым в дальнейшем и работает функция translate. Для того чтобы лучше понять происходящее, поперемещайте функцию rotate по коду, сравните разницу между результатами выполнения.
Добавим на сцену еще парочку простейших примитивов: пирамиду и сферу.
Функция sphere(); без параметров рисует сферу в центре текущей сцены с радиусом 1. Функция sphere(x, y, z, rx, ry, rz); рисует сферу, смещенную на вектор {+x,+y,+z} от центра сцены с радиусами по X, Y и Z - соответсвенно - rx, ry, rz. Функция pyramid(); без параметров рисует пирамиду в центре текущей сцены с высотой и шириной 1. Функция pyramid(x, y, z, длинна, высота, ширина); рисует пирамиду, смещенную на вектор {+x,+y,+z} от центра сцены с указанными длинной, высотой и шириной.
Немного изменим параметры сферы и пирамиды.
Окрашивать примитивы в разные цвета позволяет функция setColor(r, g, b, a);, задающая цвет для всех последующих после нее примитивов. Параметры r, g и b - RGB-составляющие цвета, параметр a - прозрачность. Параметры - нецелые числа в интервале [0..1]. Например, если вам нужно задать непрозрачный красный цвет, используйте setColor(1, 0, 0, 1);
Возникает желание осмотреть нашу сцену с разных сторон. Чтобы это сделать, можно менять параметры первых двух операторов - translate и rotate вручную, но очень хочется делать это динамически. Для этого нужно организовать управление положением камеры, например, с помощью мыши. На помощь приходят функции mouseDrag() и mouseWheel(), вызываемые библиотекой каждый раз, когда пользователь двигает мышкой с зажатой левой кнопкой по области тега canvas или крутит колесико мыши. Создадим глобальные переменные rotX (вращение по оси X), rotY (вращение по оси Y), zoom (удаленность), подставим их как параметры в первые две функции, а в функциях mouseDrag() и mouseWheel() будем их изменять с помощью глобальных переменных mouseX, lastMouseX, mouseY, lastMouseY, wheelSpeed, предоставляемых библиотекой EnergizeGL.
Обратите внимание, после рисования первого куба добавлено рисование мировой сетки с помощью функции grid();. Так как сетка рисуется сразу же после куба, она рисуется с тем же цветом и с теми же координатами, что и куб.
Продолжаем работать с полученной сценой. Поступила задача повернуть второй примитив по осям X и Y на один радиан. Для выполнения этой задачи можно пойти двумя путями. Первый и не самый удачный, это: после перемещения в позицию второго примитива повернуть сцену на {+1; +1; +0}, нарисовать примитив, повернуть сцену обратно на {-1; -1; 0}, продолжить рисование остальных примитивов. Этот способ плох тем, что если понадобится поворот на нецелый градус, точный поворот в обратную сторону будет затруднен. На помощь приходят функции pushMatrix() и popMatrix(). Функция pushMatrix(); сохраняет матрицу текущей сцены (координаты, поворот, масштабирование) в стеке матриц. Функция popMatrix(); восстанавливает последнюю сохраненную матрицу сцены из стека матриц. Используя эти две функции можно решить задачу следующим образом: сохранить матрицу сцены, переместиться и повернуть сцену, нарисовать примитив, восстановить последнюю сохраненную матрицу сцены.
Сохранять матрицу сцены можно неограниченное количество раз.
Практически все трехмерные объекты в WebGL описываются набором точек-вершин, которые в свою очередь описывают плоские треугольники, из которых и состоит объект. Точки называются вертексами (vertex), а треугольники - сторонами (face). Линии между точками, разделяющие стороны, называются гранями (edge); если несколько сторон лежат в одной плоскости и смежны, они логически объединяются в полигон (polygon); и, наконец, смежные полигоны объединяются в поверхности (surface).
В большинстве случаев в конечном счете визуализируются лишь стороны-треугольники (faces), логически объедененные в полигоны (polygons), в случае отладки или разработки иногда визуализируются так же вершины и грани. В этой главе рассказывается как создать полигональную сетку (mesh), описывая ее координатами вершин треугольников, и отобразить ее на сцене. На этот раз нам понадобится так же функция setup(), вызываемая один раз в процессе запуска библиотеки EnergizeGL. В ней мы опишем нашу полигональную сетку, загрузим ее в память и будем ее в дальшейшем использовать в функции рисования draw().
Функция startMesh(name); создает полигональную сетку (mesh) в памяти, обращаться в дальнейшем к которой можно будет по указанному имени. Все последующие вызовы функции addVertex будут относиться к указанной новой полигональной сетке (mesh).
Функция addVertex(x, y, z); добавляет к полигональной сетке (mesh) вершину (vertex) с указанными координатами.
Функция endMesh(); завершает создание полигональной сетки.
Рекомендуется использовать указанный выше функционал как можно реже, так как загрузка в память набора вершин - операция трудоемкая. Однако, в некоторых случаях приходится формировать полигональную сетку "на лету", например, когда объект изменяется во времени.
Функция mesh(name, method); распологает созданную ранее полигональную сетку (mesh) на текущей сцене (позиция, поворот, масштабирование). Как правило эта функция вызывается внутри функции draw(). Параметры: name - имя полигональной сетки, которую требуется разместить на сцене; method - метод отрисовки полигональной сетки. В следующей главе будут подробней рассмотрены методы отрисовки, в этой главе мы будем использовать метод TRIANGLES, это означает что полигональная сетка формируется из отдельных треугольников, вершины для которых сгруппированы тройками в списке вершин (vertex list) полигональной сетки (mesh).
В примере 2а я создал полигональную сетку из трех вершин и, соответсвенно, одной стороны, автоматически сформированной из этой тройки вершин, а затем разместил ее на сцене. Вершины расположены в координатах ( 0, 0, 0), ( 1, 0, 0), ( 0, 0, 1). Все три вершины Y=0, в результате получается плоский треугольник, расположенный в плоскости XZ перпендикулярно оси Y. Попробуйте поизменять координаты в примере, понаблюдайте за перемещением вершин.
Дорисуем треугольник до квадрата, добавив еще один треугольник в нашу полигональную сетку. Для этого добавим описание еще трех вершин.
Обратите внимание на то, что в описании вершин второго треугольника вторая и третья вершины совпадают с предыдущим треугольником, и лишь первая вершина отлична. В результате получились две стороны (два треугольника) в одной плоскости со смежной гранью (1,0,0)--(0,0,1). Логически они объединяются в полигон.
При построении сложной полигональной сетки не обязательно, чтобы треугольники были смежны друг с другом, имели общие грани. Треугольники могут быть в разных местах и могут, даже, пересекаться друг с другом. Продолжим создание нашей полигональной сетки, добавив еще два треугольника, аналогичных предыдущим, но сдвинутым по оси Y вверх на еденицу.
В результате получилась полигональная сетка с 12тью вершинами, 12тью гранями, 4мя сторонами и 2мя полигонами.
При построении сложной полигональной сетки так же не обязательно использовать перпендикулярные осям или друг другу треугольники. Можно создавать наклонные плоские поверхности. Необязательно же и использование целых чисел в координатах, можно использовать дробные. Добавим в нашу полигональную сетку еще два треугольника.
В результате получилась полигональная сетка с 18тью вершинами, 18тью гранями, 6тью сторонами и 4мя полигонами. Визуально это похоже на каркас будущего самолета: пол, потолок, носовая кабина. Можно продолжить дополнять нашу полигональную сетку деталями.
Неотъемлемой частью трехмерного моделирования является текстуирование, то есть - процесс описания наложения текстуры на трехмерный объект. Для того, чтобы загрузить текстуру в память мы будем использовать функции loadTexture(name, path) и startLoading(). Для того, чтобы назначить загруженную текстуру каким-либо объектам используются функции useTexture(name) и noTexture(). Параметры: name - имя для текстуры (словно текстура - новая переменная), path - путь к текстуре. Стороны у загружаемых текстур должны быть кратны степеням двойки, т.е. 2, 4, 8, 16, 32... 512, 1024 и так далее.
Давайте рассмотрим полученную сцену внимательней. Текстура наложилась шесть раз на парралелепипед, по разу на каждую сторону. Текстура наложилась один раз на сферу, "размазавшись" по ней. Текстура на пирамиду визуально не наложилась, фактически, по пирамиде был лишь размазан первый (оранжевый) пиксел из текстуры. Дело в том, что стандартные примитивы библиотеки на самом деле так же состоят из полигональной сетки, но, в полигональных сетках парралелепипеда и сферах - указаны UV-координаты текстуирования, а в полигональной сетке пирамиды - нет. Не существует общепринятого представления как наложить квадратную текстуру на пирамиду, но мы можем выйти из этого затруднения, создав собственную полигональную сетку в форме пирамиды и указав для каждой вершины UV-координаты текстуры.
UV-преобразование или развёртка в трёхмерной графике (англ. UV map) — соответствие между координатами на поверхности трёхмерного объекта (X, Y, Z) и координатами на текстуре (U, V). Координаты U и V обычно изменяются от 0 до 1, 0 соответствует началу рисунка по оси U или V, а 1 - концу рисунка.
С помощью функции addTextureCoord( U, V); расположенной между функциями startMesh(...) и endMesh() можно задать UV-коорданаты текстуры для каждой точки (vertex) полигональной сетки.
Мы создали полигон из двух треугольников и для первого треугольника описали UV-координаты, сопоставив их с каждой вершиной. В результате описаный треугольник (0,0)-(1,0)-(0,1) из текстуры наложился на первый треугольник.
Продолжим текстуирование и наложим на следующий треугольник не эти же текстурные координаты ((0,0)-(1,0)-(0,1)), а другую треугольную половину текстуры.
В результате мы идеально положили квадратную текстуру на два прямоугольных треугольника. Попробуйте поизменять UV-координаты, посмотрите, как при этом изменяется наложение текстуры. Текстуру можно наложить неверно запутавшись в том, какой точке в трехмерном пространстве какая точка соответствует из двухмерного пространства текстуры.
В качестве UV-координат можно указывать не только целые числа 0-1. Можно использовать дробные числа, тогда текстура будет положена на треугольник неполностью по указанному краю. Можно использовать числа больше еденицы или даже меньше нуля, тогда текстура будет наложена на поверхность несколько раз. Отмасштабируем нашу текстуру: в первом треугольнике положим текстуру в два раза плотнее по оси X (U=[0..2]), а во втором - в два раза реже по оси Y (V=[0..0.5]).
Зачастую стоит задача растянуть одну текстуру равномерно на несколько связанных полигонов. Для демонстрации я создам еще один полигон под небольшим углом к предыдущему и равномерно распределю текстуру: по первому полигону U=[0..0.5], по второму полигону U=[0.5..1], и по обоим V=[0..1].
В результате первая половина текстуры наложилась на первый полигон, а вторая - на второй.
Для имитации кривых поверхностей используют сложные полигональные сетки. Так, например, в трехмерном виде цилиндр отображается с помощью последовательных плоских прямоугольников, связанных по окружности. Степень кривизны и релеастичность зависят от сегментации цилиндра: чем больше сегментов в составе цилиндра, тем мягче углы между сегментами. Вак правило хватает 32х сегментов для успешного оптического обмана. В этой главе я создам две функции, одна будет создавать полигональную сетку цилиндра (без дна), вторая - конуса (без дна).
Полигональные сетки могут быть отображены по разному на сцене. В конечном счете полигональная сетка состоит из последовательного индексированного массива вершин, а в каком виде этот массив вывести на экран - решает уже пользователь. Есть несколько вариантов вывода: вершины (POINTS), линии (LINES), треугольники. Выводы треугольников в свою очередь делятся на три режима:
Я создам функцию cylinder(name, radius, height, segments), которая будет создавать полигональную сетку с заданым именем и остальными параметрами. Основные соображения в этой функции это: 1) первым делом создать две первые "опорные точки"; 2) в последующем создавать по паре точек снизу и сверху цилиндра, с рассчетом на то, что цилиндр будет выводиться режимом TRIANGLE_STRIP, а точки автоматически будут линковаться на предыдущие две; 3) координаты точек по кругу цилиндра рассчитываются с помощью математических функций и зависят от радиуса цилиндра и количества желаемых сегментов.
Конус строится еще проще. Я создам функцию cone(name, radius, height, segments), которая будет создавать полигональную сетку конуса с заданым именем и остальными параметрами. Сетка заполняется точками на основе следующих соображений: 1) первым делом нужно создать опорные первые точки, одну - в центре наверху конуса, вторую - у основания на краю; 2) в дальнейшем создавать точки по кругу, с рассчетом на то, что выводиться они будут в режиме TRIANGLE_FAN, автоматически линкуясь на предыдущую точку и на самую первую, опорную, в вершине конуса; 3) координаты точек по кругу конуса рассчитываются с помощью математических функций и зависят от радиуса конуса и количества желаемых сегментов.
1. Монета, состоящая из 32х сегментов по радиусу. На лицевой стороне должна быть наложена текстура как указано на рисунке. |
2. Пирог, состоящий из 8ми сегментов по радиусу. Один сегмент должен отсутствовать. На все полигоны должна быть наложена текстура как указано на рисунке. |
Анимирование в общем случае реализуется с помощью последовательного бесконечного вызова специальной функции. Например, создана функция animate(), которая на каких то внутренних критериях а так же на внешних глобальных переменных принимает решения по изменению этих переменных. Эти переменные в свою очередь используются в функции draw(), а их изменение от вызова к вызову функции animate() порождает анимацию. Обе функции вызываются раз в 15мс например, для этого используется встроенная JavaScript функция: setInterval(animate, 15); setInterval(draw, 15);
В случае работы с библиотекой EnergizeGL все несколько проще: при инициализации библиотеки функция draw() уже входит в бесконечный цикл вызовов, она вызывается по умолчанию 60 раз в секунду, обеспечивая производительность 60fps (частоту вызовов можно изменить с помощью библиотечной функции fps(fps)), кроме этого библиотека обеспечивает нас внешней глобальной переменной frame в которой содержится номер текущего проигрываемого кадра. Таким образом анимацию можно определить прямо внутри функции draw() опираясь на номер текущего кадра.
В результате рисуемый куб завертелся. Завертелся он вокруг себя по оси X на значение переменной a, изменяющейся по времени в зависимости от текущего кадра. В следующем примере я закручу куб не вокруг себя, а вокруг центра мировой сетки, для этого я просто поменяю местами вызовы функции translate и rotate. Так как поворот рисуемой сцены будет происходить до перемещения, то само перемещение будет учитывать новый угол поворота сцены и перемещать по новой оси Y (повернутой на угол) на +2 от центра сцены.
Наиболее легким способом вычисления циклического изменения координаты объекта относительно бесконечно возрастающего номера кадра является математический синус или косинус. Например, конструкция b = 5*Math.sin(frame/30); предоставит мне переменную b, переодически изменяющуюся по времени в промежутке [-5..+5]. Я назначу смещение куба на переменную b, в результате получу куб, перемещающийся из стороны в сторону.
Вариант | ||||||||||||||||
Задание | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
1. Размещение простейших примитивов на сцене | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 |
2. Создание сложных объектов: полигональная сетка | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 3 | 3 | 1 | 1 | 1 | 2 | 2 | 2 | 3 |
3. Текстуирование, UV-маппинг | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
4. Создание кривых поверхностей | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 |
5. Анимация | 2 | 2 | 1 | 1 | 2 | 2 | 1 | 1 | 2 | 2 | 1 | 1 | 2 | 2 | 1 | 1 |