Из-за сравнительной молодости этой технологии, руководств по работе с ней не так уж и много. Почитать кое-что о работе с ней можно здесь (здесь — перевод на русский). Здесь можно узнать кое-что об основах WebGL.
Для облегчения работы с WebGL разработан ряд библиотек (правда, большинство из них еще довольно сырые). Применению одной из них — webGLU — для формирования простой сцены, освещенной одним источником-фонарем, и посвящена эта статья. Здесь можно посмотреть пример, а отсюда скачать полный архив для запуска его на своей машине.
Для начала немного напомню о разнице между OpenGL и WebGL. Так как WebGL базируется на GLES, эта технология значительно уступает OpenGL: в ней нет множества удобных расширений (ARB и т.п.), нет встроенной поддержки освещения, да даже GL_QUADS в ней не поддерживаются… Однако, что поделаешь: больше никаких технологий, позволяющих воплотить 3D в вебе без сторонних плагинов нет.
Для расчета положения вершин и их цвета на итоговом изображении используются шейдеры. При простейшем построении трехмерных сцен шейдеры вызываются для каждой вершины, с которой они связаны. Но более сложные сцены описать таким образом невозможно. В этом случае прибегают к написанию шейдеров, формирующих текстуру, которая и образует итоговую сцену (пример можно увидеть здесь. Возможно, в следующей статье я коснусь такого способа описания трехмерных сцен.
Библиотека WebGLU содержит скудную документацию и несколько примеров. Этого маловато для полноценной работы с библиотекой, поэтому приходится и ее исходники читать. Кроме того, библиотека довольно сырая, так что иногда приходится и поковыряться в ее коде.
Итак, для того, чтобы начать работу с WebGLU, нам необходимо подключить скрипт webglu.js и инициализировать его:
…
$W.initialize();
$W — основной объект пространства имен WebGLU. Бóльшую часть работы мы будем вести с ним.
Кроме этого объекта есть объект $G (пространство имен GameGLU), упрощающий работу с управлением сценой
при помощи мыши и клавиатуры. В WebGLU есть и кое-какие экспериментальные функции (например, CrazyGLU —
для работы с псевдобуфером выбора и кое-какой физики). Все исходные файлы подгружаются WebGLU при
запуске соответствующих функций (например, функция useControlProfiles();
подгружает
файл ControlProfiles.js), так что вручную подгружать скрипты помимо webglu.js не нужно.
После инициализации WebGLU мы можем приступить к созданию объектов сцены. Для создания объекта WebGLU предоставляет интерфейс
$W.Object(type, flags)
, где type — тип объекта (как в
OpenGL):
- $W.GL.POINTS - каждая вершина отображается точкой,
- $W.GL.LINES - каждая пара из вершин 2n-1 и 2n соединяется линией,
- $W.GL.LINE_LOOP - все вершины по порядку соединяются отрезками с замыканием на первую вершину,
- $W.GL.LINE_STRIP - все вершины соединяются отрезками без замыкания,
- $W.GL.TRIANGLES - каждая тройка вершин (по порядку) образует треугольник,
- $W.GL.TRIANGLE_STRIP - вершины по порядку соединяются треугольниками,
- $W.GL.TRIANGLE_FAN - вершины соединяются треугольниками вокруг общей — первой — вершины;
$W.RENDERABLE | $W.PICKABLE
,
однако, если мы рисуем объект, который является дочерним, то следует указать явно $W.PICKABLE
.
В простейшем случае для каждой вершины объекта необходимо указать цвет, однако, мы вольны написать и
сообственный шейдер (что мы и сделаем далее), позволяющий указывать общий цвет для всего объекта.
Итак, например, для создания цветных координатных осей, мы сделаем так:
var originLines = new $W.Object($W.GL.LINES);
originLines.vertexCount = 6;
originLines.fillArray("vertex",
[[0,0,0], [3,0,0], [0,0,0], [0,3,0], [0,0,0], [0,0,3]]);
with ($W.constants.colors){
originLines.fillArray("color",
[ RED, RED, GREEN, GREEN, BLUE, BLUE]);
}
Шейдер по-умолчанию использует следующие массивы для характеристики каждой вершины объекта:
- "vertex" - координаты вершин,
- "color" - цвета вершин,
- "normal" - нормали к вершинам,
- "texCoord" - координаты текстуры в данной вершине,
- "wglu_elements" - индексы координат вершин (для сложных объектов).
attribute
).
Подключение шейдеров в WebGL реализуется при помощи метода
Material
. Единственным
аргументом этого метода является путь к JSON-файлу описания шейдеров. Например, наши шейдеры,
реализующие освещение, подключаются так:
var lights = new $W.Material({path:$W.paths.materials + "light.json"});
Сам файл light.json выглядит так:
{
name: "light",
program: {
name: "light",
shaders: [
{name:"light_vs", path:$W.paths.shaders+"light.vert"},
{name:"light_fs", path:$W.paths.shaders+"light.frag"}
]
}
}
Здесь name — общее имя «материала»; program → name — по-видимому, характеризует имя программы (возможно,
создатель WebGL предполагал, что для одного материала может использоваться несколько программ, пока
же этот параметр особой роли не играет); shaders — используемые шейдеры с указанием пути к ним.
Переменные, являющиеся общими для каждого объекта или всей системы (с атрибутом
uniform
),
связываются с соответствующими переменными JavaScript при помощи метода setUniformAction(n, f)
объекта
Materal
. Аргументы этого метода имеют слеюдующее значение: n — имя переменной в шейдере (в методе
оно указывается как строка); f — функция типа function(u, o, m)
,
где u — объект uniform (), o — сам объект, m — материал. Например, связывание параметра «color» с
цветом объекта выполняется так:
lights.setUniformAction('color', function(uniform, object, material){
$W.GL.uniform4fv(uniform.location, object.color); });
Для того, чтобы задать объекту нестандартный материал, используется свойство объекта
setMaterial(mat)
,
где mat — нужный нам материал. Так как JavaScript позволяет «на лету» добавлять свойства в уже
определенные объекты, мы можем легко вносить изменения в объекты для координирования их со своими шейдерами.
Создадим при помощи WebGLU вот такую сцену:
Для использования освещения нам обязательно нужно правильно просчитать нормали ко всем вершинам, которые будут обрабатываться нашими шейдерами-«осветителями». Сферу мы можем нарисовать при помощи функции
genSphere(n1,n2,rad)
библиотеки WebGLU, а вот цилиндры придется рисовать самостоятельно.
Проще всего это сделать, заполнив боковую поверхность цилиндра связанными треугольниками:
function drawCylinder(R, H, n, flags){
var v = [], norm = [];
var C = new $W.Object($W.GL.TRIANGLE_STRIP, flags);
C.vertexCount = n * 2+2;
for(var i = -1; i < n; i++){
var a = _2PI/n*i;
var cc = Math.cos(a), ss = Math.sin(a);
v = v.concat([R*cc, R*ss,0.]);
v = v.concat([R*cc, R*ss,H]);
norm = norm.concat([-cc, -ss, 0.]);
norm = norm.concat([-cc, -ss, 0.]);
}
C.fillArray("vertex", v);
C.fillArray("normal", norm);
return C;
}
Этот способ, как вы убедитесь дальше, довольно примитивен: из-за того, что мы не помещаем вершины
на поверхность цилиндра между его торцами, освещение для него рассчитывается неверно: если «фонарь»
освещает только середину поверхности цилиндра, не захватывая его торцы, цилиндр отображается
неосвещенным. Чтобы нарисовать цилиндр правильно, нужно добавить дополнительные промежуточные вершины
и заполнить массив индексов для правильного отображения треугольников. Другой вариант — нарисовать
объект составным (с дочерними объектами), из нескольких прямоугольников, каждый из которых состоит
из набора треугольников.
Окружности мы будем рисовать при помощи функции
function drawCircle(R, n, w, flags){
var v = [];
var C = new $W.Object($W.GL.LINE_LOOP, flags);
C.vertexCount = n;
for(var i = 0; i < n; i++){
var a = _2PI/n*i;
v = v.concat([R*Math.cos(a), R*Math.sin(a),0.]);
}
C.fillArray("vertex", v);
if(typeof(w) != "undefined") C.WD = w;
else C.WD = 1.;
C.draw = function(){ // переопределяем для возможности изменения ширины линии
var oldw = $W.GL.getParameter($W.GL.LINE_WIDTH);
$W.GL.lineWidth(this.WD);
this.drawAt(
this.animatedPosition().elements,
this.animatedRotation().matrix(),
this.animatedScale().elements
);
$W.GL.lineWidth(oldw);
};
return C;
}
Чтобы иметь возможность менять толщину линий, которыми рисуются окружности, нам нужно будет переопределить
функцию draw()
данного объекта (т.к. функция $W.GL.lineWidth(w)
устанавливает
толщину линии w глобально — вплоть до следующего вызова этой функции). Если поменять $W.GL.LINE_LOOP
у этого объекта на $W.GL.POINTS
, окружности будут нарисованы точками. Размер точек будет
зависеть от свойства WD
объекта благодаря тому, что мы используем для него «материал» points,
в котором указано
gl_PointSize = WD;
Здесь можно посмотреть код фрагментного
шейдера для отображения точек разного размера, а здесь
— шейдера вершин.
Итак, объекты мы создали. Настала очередь создать шейдеры для расчета освещения. В любом справочнике по OpenGL для расчета итогового цвета вершины при освещении несколькими источниками можно найти следующую формулу:
result_Color = mat_emission
+ lmodel_ambient * mat_ambient
Sum_i(D * S * [l_ambient * mat_ambient + max{dot(L,n),0}*l_diffuse*mat_diffuse
+ max{dot(s,n),0}^mat_shininess * l_specular * mat_specular
)
Здесь
- result_Color - итоговый цвет вершины,
- mat_X - свойства материала,
- l_X - свойства i-го источника света
- X:
- emission - излучаемый свет (т.е. материал - источник света),
- lmodel_ambient - общий рассеянный свет модели освещения (не зависит от источников, т.е. это - фоновый свет)
- ambient - фоновый свет (цвет материала вне источников света, рассеянная световая составляющая i-го источника света)
- D - коэффициент ослабления света, D = 1/(kc + kl*d + kq*d^2),
- d - расстояние от источника до вершины,
- kc - постоянный коэффициент ослабления ("серый фильтр"),
- kl - линейный коэффициент ослабления,
- kq - квадратичный коэффициент ослабления,
- S - эффект прожектора, вычисляется так:
= 1, если источник света - не прожектор (бесконечно удаленный параллельный пучок),
= 0, если источник - прожектор, но вершина вне конуса излучения,
= max{dot(v,dd),0}^GL_SPOT_EXPONENT в остальных случаях, здесь v - нормированный вектор от прожектора (GL_POSITION) к вершине, dd (GL_SPOT_DIRECTION) - ориентация прожектора - L = -v (нормированный вектор от вершины к источнику),
- n - нормаль к вершине,
- diffuse - рассеянный свет, не зависит от угла падения/отражения,
- s - нормированный вектор, равный сумме L и вектора от вершины к глазу,
- shininess - степень блеска (от 0 до 128, чем больше, тем более "блестящей" является поверхность),
- specular - цвет зеркального компонента.
Теперь нам остается определить свойства нашего «фонаря»:
light = {
position: [0.,2.,1.5],
target: [0.,0.,-2.],
color: [1.,.5,0.,1.],
fieldAngle: 60.,
exponent: 5.,
distanceFalloffRatio: .02
};
при помощи setUniformAction(…)
связать свойства «фонаря» и свойства объектов с переменными
шейдеров и задать индивидуальные свойства каждому объекту, использующему данный «материал».
После того, как мы все это сделаем, анимируем сцену при помощи функции
$W.start(T);
,
где T — минимальный интервал между отрисовкой сцены. Если наша сцена слишком сложная, нам придется
отрисовывать ее после каждого изменения вручную при помощи функций $W.util.defaultUpdate();
и $W.util.defaultDraw();
. Эти функции не влияют на компиляцию шейдеров (которую необходимо
выполнять лишь при внесении кардинальных изменений в сами шейдеры), поэтому наша сцена будет «подвисать»
лишь в момент начальной загрузки (при инициализации), а также немного притормаживать при изменении размера
окна.
Напоследок скажу, что функция вращения сцены (точнее — перемещения камеры вокруг сцены) из WebGLU не очень удобная, поэтому стоит определить свою функцию перемещения. Здесь (а также по указанному в самом начале адресу примера) вы можете посмотреть, как выглядит итоговый html-файл.
Статья получилась довольно большой, при том, что я не упомянул о работе с буфером выбора (если нам необходимо реализовать отождествление объектов по щелчку мыши), отображении объектов при смешивании (а это нужно для использовании компонента прозрачности в цвете объекта), отсечении «тыльных» поверхностей фигур и многом другом. Надеюсь, эта моя статья о WebGL не последняя (а может быть мое начинание продолжит кто-нибудь еще).
Journal information