Цель статьи - дать возможность внезапно научится писать графические приложения с использованием OpenGL.
Предполагается, что читатель немного знаком с каким-то императивным языком программирования, в котором есть классы (С++, Java, C#, Delphi, Python, VB...).
Работа будет вестись на языке JavaScript, но, на самом деле, это не имеет особого значения. Мне доводилось пользоваться OpenGL в Delphi, Java и С++, и, могу сказать, что разница несущественна. Родным языком для себя считаю C++, так что код будет написан, как бы, из расчета на C++.
Приложение будет запускаться в web-браузере, но соединения с интернетом для этого не потребуется. Для написания и отладки нужен, соответственно, только браузер, который поддерживает WebGL. Такой, как Mozilla Firefox, например.
Чтобы отлаживать приложение в Firefox, нужно установить в нем дополнение Firebug (https://addons.mozilla.org/ru/firefox/addon/firebug/). После установки, в главном меню браузера под пунктом "Инструменты" появляется раздел "Веб-разработка->Firebug". Здесь нам интересен Debugger, которым можно дебажить JavaScript, а также полезна Веб-консоль, куда выводятся ошибки (переключатель "JS") и логи (переключатель "Logging").
В логи можно писать из приложения так:
console.log("log message!");
Шаблон
Приложение является Web-страницей. Это HTML файл, с одним элементом "canvas" (поверхностью для рисования) и одним скриптом JavaScript.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><head><title>Learning WebGL</title><meta http-equiv="content-type" content="text/html; charset=UTF-8"><script type="text/javascript" src="main.js"></script></head><body onload="webGLStart();"><canvas id="lesson-canvas" style="border: none;" width="320" height="240"></canvas></body></html>
которая прикрепляет наш скриптовый файл main.js к странице.
А также, еще одна:
<body onload="webGLStart();">
index.html
которая говорит, что когда страница загрузится, то будет запущена JavaScript-функция webGLStart из main.js:
function webGLStart() {
var canvas = document.getElementById("lesson-canvas");
initGL(canvas);
initShaders();
initBuffers();
drawScene();
}
main.js
из HTML будет получен объект canvas - область рисования и передан в функцию initGL.
Код функции initGL означает следующее: все, что мы будем рисовать через объект "gl", будет выводится на наш элемент canvas:
var gl;
function initGL(canvas) {
gl = canvas.getContext("experimental-webgl");
if(gl == null) {
alert("Could not initialize WebGL");
return;
}
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
}
main.js
Функции загрузки шейдеров (initShaders) и генерирования модели (initBuffers) оставим пока пустыми:
function initShaders() {
}
function initBuffers() {
}
main.js
А в функции рисования (drawScene) сделаем три вещи: установим размер области рисования по размерам элемента canvas, установим цвет заливки (4 компоненты RGBA: красный Red, зеленый Green, синий Blue, непрозрачность Alpha) и зальем всю область этим цветом:
Можно теперь открыть в браузере файл index.html. Если появилось окошко с надписью "Could not initialize WebGL" (оно из нашей функции initGL: alert("Could not initialize WebGL")), то это означает, что WebGL не активирован или не поддерживается в браузере. А, возможно, что существуют вообще какие-то проблемы с драйверами или видеокартой.
Должен получится прямоугольник, залитый цветом, который передали функции gl.clearColor. Размеры его - 320*240, как было ранее заявлено в HTML файле:
Чтобы что-то нарисовать, нужно разобраться в том, что делает графический конвейер:
На вход ему нужно подать Vertex Buffer - список координат вершин и указать, что из них надо собирать треугольники. После этого, драйвер для каждой вершины запустит шейдер вершин (Vertex Shader), который мы ему дадим. Он пишется на GLSL и предназначен для того, чтобы двигать вершины туда, куда нам нужно. Потом будут собраны треугольники и определены пиксели на экране, которые принадлежат этим треугольникам. Для каждого пикселя драйвер запустит фрагментный шейдер (Fragment Shader), который тоже нужно задать. Пишется на GLSL и предназначен для того, чтобы красить пиксели в нужный цвет.
Напишем вершинный шейдер, который на вход принимает двухмерную позицию вершины (in_Pos - это x, y) и выдает её без изменений (gl_Position - это x, y, 0, 1):
четвертая координата в gl_Position - это коэффициент, на который OpenGL делит все координаты. Вершинный шейдер должен возвращать четырехмерный вектор.
Вот как OpenGL сопоставляет координаты из gl_Position и положение в области рисования:
Центр координат расположен посредине canvas. Ось x направлена вправо, ось y - вверх.
Если хоть одна из трех x, y, z координат вершины меньше -1.0 или больше 1.0, то вершина будет за пределами canvas. Следует заметить, что деление на четвертый коэффициент из gl_Position происходит до этого.
OpenGL собирает из вершин треугольники, делит их на пиксели и запускает наш фрагментный шейдер для каждого пикселя. Фрагментный шейдер дает пикселю цвет:
Формат цвета здесь тоже RGBA. В самой первой строчке идет задание точности float-ов, поскольку комитет по стандартизации решил, что в этой версии фрагментных шейдеров программист должен указывать точность сам.
Мы убедились, что GLSL шейдеры - это обычные строки текста, которые нужны для работы OpenGL. Разместим их внутри HTML страницы, чтобы загружать их в нашей JavaScript функции initShaders:
Для доставания этого GLSL кода из HTML тегов, будет использоваться самописная функция getTextFromElement, которая на вход принимает id элемента (в данном случае "vertex-shader" или "fragment-shader"), и возвращает внутренний текст.
Наша функция загрузки шейдера будет принимать его текст (параметр str) и его тип (параметр type), создавать "шейдерный объект" (переменная shader), прикреплять к нему текст шейдера (функция gl.shaderSource), компилировать (функция gl.compileShader), проверять ошибки компиляции (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) и возвращать шейдерный объект:
Эти шейдерные объекты нужно будет собрать в один "программный объект". Итак, в функции initShaders создадутся два шейдерных объекта vertexShader и fragmentShader со скомпилированными шейдерами. Эти объекты прикрепятся функцией gl.attachShader к новому программному объекту shaderProgram. Потом произойдет компоновка программного объекта - gl.linkProgram(shaderProgram) и проверка на ошибки.
var shaderProgram;
var vertexPositionAttribute;
function initShaders() {
var vertexShader = makeShader(getTextFromElement("vertex-shader"), gl.VERTEX_SHADER);
var fragmentShader = makeShader(getTextFromElement("fragment-shader"), gl.FRAGMENT_SHADER);
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if(!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialize shaders");
}
vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "in_Pos");
gl.enableVertexAttribArray(vertexPositionAttribute);
}
main.js
Последние два вызова заботятся о том, чтобы на вход вершинному шейдеру можно было подать Vertex Buffer - список координат вершин. gl.getAttribLocation служит для получения идентификатора входного атрибута in_Pos, который мы объявили в шейдере. gl.enableVertexAttribArray сообщает драйверу, что он действительно будет использоваться для передачи данных.
Создаваться и заполняться буфер будет в функции initBuffers:
var squareVertexPositionBuffer;
var itemSize, numItems;
function initBuffers() {
squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [1.0, 1.0,
-1.0, 1.0,
1.0, -1.0,
-1.0, -1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
itemSize = 2;
numItems = 4;
}
main.js
gl.bindBuffer служит для выбора буффера squareVertexPositionBuffer, чтобы функция gl.bufferData записала в него массив вершин vertices. В переменные itemSize и numItems запомним размерность (2) и количество вершин (4).
vertices - массив. В JavaScript массивы можно определять так:
myArray = [element1, element2, element3];
Дополним функцию рисования drawScene.
Нужно задействовать шейдерную программу с помошью функции gl.useProgram, чтобы рисовать именно её шейдерами.
Дальше gl.bindBuffer активирует squareVertexPositionBuffer (буффер с координатами вершин) для того, чтобы присоединить его к входному атрибуту in_Pos (идентификатор атрибута in_Pos - это vertexPositionAttribute, который мы получили ранее в initShaders) с помощью функции gl.vertexAttribPointer.
Под конец, gl.drawArrays будет рисовать всё, что поступает через in_Pos: начиная с нулевого поступившего и в общей сложности numItems штук (numItems == 4). И из этого будет делаться полоска треугольников gl.TRIANGLE_STRIP. То есть, наши четыре двухмерные вершины пойдут на вход вершинному шейдеру, а из четырех вершин, которые мы из шейдера выйдадим, будут сложены треугольники.
Запускаем - видим прямоугольник на весь canvas.
Вершинный шейдер создан для того, чтобы организованно двигать вершины. То есть, можно начинать развлекаться:
* я буду использовать далее в статье diff-запись: строчки, которые удаляются помечены знаком "-", а те, которые добавляются - знаком "+" (те, которые не меняются, никак не помечены). В заголовках с символами @@ указаны номера и количества строчек, а иногда и имя функции, в которой они находятся. Эти заголовки не являются частью программы, а используются только в статье.
То есть заменим "gl_Position = vec4(in_Pos, 0.0, 1.0);" на "gl_Position = vec4(in_Pos * 0.5, 0.0, 1.0);" в вершинном шейдере. Это умножает на 0.5 обе входные координаты всех четырех вершин (уменьшая в 2 раза прямоугольник).
Чтобы делать полезные вещи, нужно уметь управлять этим, общим для всех вершин, значением из JavaScript, а не только из GLSL шейдера.
Масштабирование
Заведем переменную, которую можно задавать из скрипта. Их называют "юниформами" (и почему-то не в женском роде). Завернем её сразу в структуру:
uniform - значит общая для всех вершин, struct - значит структура.
nav_s - наш новый тип структур, а nav - это, собственно, общая для всех вершин структура, задающая навигацию. Ведь уменьшение прямоугольника - это уже настоящий зум (zoom), как в графическом редакторе.
В JavaScript cоздадим класс Uniforms, отвечающий за связь с nav.zoom.
В конструкторе "Uniforms()", функция gl.getUniformLocation будет извлекать из шейдерной программы идентификатор юниформа nav.zoom:
function Uniforms() {
this.zoomId = gl.getUniformLocation(shaderProgram, "nav.zoom");
}
Используя полученный в конструкторе идентификатор юниформа, метод обновления refresh будет принимать объект n и передавать в шейдер его поле zoom: