WebGL туториал 1 (Белый квадрат)

Не так давно, я решил заменить отрисовку средствами canvas на WebGL в своем небольшом проекте. Первой ссылкой в гугле выпал этот замечательный туториал http://learningwebgl.com/blog/?page_id=1217. Разобравшись с интересующими меня уроками пришел к выводу, что примеры к этим урокам можно значительно упростить, а некоторые понятия требуют иллюстраций. Решил написать небольшую заметку о том как нарисовать квадрат и затем текстурировать его средствами WebGL.
Полный код примера тут


Введение 

Сейчас существует множество библиотечек, облегчающих работу с WebGL, берущие на себя все рутинные настройки, загрузку моделей и отрисовку сцены. Я не думаю что прямая работа с WebGL, самостоятельная загрузка объектов, матричные операции и прочее, оправданы. Однако, человек всегда стремится понять, как работает то, чем он пользуется каждый день. Эта статья родилась как итог моих экспериментов с WebGL. В своих проектах я использую Three.js, но смысл и принцип работы этой библитеки я понял только разобравшись с шейдерами и программированием WebGL.
Туториал состоит из трех статей, все они подводят к рендерингу куба и наложению текстур. Все это должно дать общее понимание работы WebGL. На счет оформления кода, я старался его сократить и пренебрегаю обработкой ошибок в некоторых местах.
Первая часть туториала, на мой взгляд должна быть максимально простой, что бы не отбить охоту разбираться дальше. Я приложил все усилия что бы сделать её таковой. Вторая статья акцентируется на программировании шейдеров и типах переменных. Третья статья должна дать понимание uv-развертки и текстурировании объектов. Возможно, в дальнейшем появятся и отдельные статьи, базирующиеся на первых трех.

Теория

Начну пожалуй с того, что секрет успеха WebGL в отрисовке средствами видеокарты, параллельно с потоком выполнения программы да еще и на куче шейдерных ядер. Логично предположить что для этого видеокарте нужно дать понять, что да как рисовать.
В простейшем случае, достаточно передать информацию о наборе вершин. Координаты всех вершин объекта складываются одна за другой в одномерный массив, затем скармливаются видеокарте. Но что же будет делать с ними видеокарта? Этим вопросом занимаются шейдеры. Шейдер - это программа, которая выполняется на шейдерном процессоре видеокарты и определяет то, как будут отрисовываться точки и плоскости. Шейдеры мы пишем на Си-подобном языке GLSL. В принципе, это и есть Си с некоторыми оговорками. Визуально, параллельную работу шейдеров можно представить так:


Ближе к делу

К сожалению, даже чтобы заработало хоть что-то недостаточно написать 3 строчки, в этом WebGL проигрывает Canvas'у. Я постарался упростить код, насколько это было возможно.
Как только страничка загрузилась, вызывается функция webGLStart и мы можем получить контекст:
var canvas = document.getElementById("main_canvas");
gl = canvas.getContext("webgl");
Контекст мы поместим в глобальную переменную gl. Он предоставляет доступ к функциям WebGL в рамках холста "main_canvas". Если бы мы хотели рисовать на 2d canvas'е привычными средствами, тип контекста изменился бы с "webgl" на "2d".
Прекрасно, осталось решить, что мы будем передавать видеокарте. Выше я говорил о двух вещах:
  1. Набор вершин
  2. Шейдеры для их обработки
Мы напишем две функции для выполнениях этих задач.

Инициализация шейдеров initShaders()

Начнем с шейдеров. Шейдерные программы для начала нужно написать, скомпилировать, собрать и наконец загрузить на видеокарту методами WebGL.
У нас в наличие имеется две однострочных программы - заглушки, написанные на GLSL. Чтобы их использовать, необходимо создать экземпляр шейдера с указанием его типа (FRAGMENT_SHADER или VERTEX_SHADER).
var fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, "void main(void) {gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);}");
gl.compileShader(fShader);
var vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader,"attribute vec4 aVertexPosition; void main(void) {gl_Position =  aVertexPosition;}");
gl.compileShader(vShader);
Первый шейдер определяет цвета фрагментов (пикселей). Результатом работы шейдера будет установка цвета пикселя в переменную gl_FragColor в формате RGBA. Пока у нас только белый цвет.
void main(void) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
Второй шейдер определяет координаты точек, резульатом работы этой программки будет установка значения переменной gl_Position. Мы просто записываем в неё координаты, которые шейдер получил на вход. Как получается так, что координаты точек попадают в переменную aVertexPosition мы разберемся ниже.
attribute vec4 aVertexPosition; 
void main(void) {
    gl_Position = aVertexPosition;
} 
Теперь прицепим шейдеры к шейдерной программе, слинкуем и загрузим.
var shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vShader);
gl.attachShader(shaderProgram, fShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
Чтобы использовать атрибуты шейдеров, на них нужно получить указатели.
shaderMemory["aVertexPosition"] = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderMemory["aVertexPosition"]);
Теперь в глобальном массиве shaderMemory появился элемент, значение которого указывает на переменную aVertexPosition. Этот указатель мы используем чтобы передать в шейдер координаты фигуры.

Отрисовка сцены drawScene

Первым делом, настройка вьюпорта и очистка цветового буфера. В принципе, ничего сложного, название функций говорит само за себя.
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
Предположим, благодаря функции initShaders, шейдеры уже залиты на видеокарту и готовы к работе, пришло время скормить шейдерам координаты квадрата. Как я упоминал выше, набор точек нужно передать видеокарте в виде массива. Так как мы пока рисуем в 2d, координаты точек пока придется задавать в однородных координатах, вместо декартовых. Позже мы разберемся, как переходить от привычных декартовых координат к однородным и о других важных вещах. 
А пока, координаты квадрата в однородных координатах:
var vertices = [
    0.5, 0.5, 0.0, 1.0,
    0.5, -0.5, 0.0, 1.0,
    -0.5, 0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 1.0
]; 
Полагаю, все в курсе что видеокарта рисует треугольники, а не квадраты, но у нас тут 4 точки. Так вот, наш случай не исключение. В массиве записаны координаты двух треугольничков с одной общей гранью. На рисунке ниже стрелками показана последовательность записи точек в массив. В качестве эксперимента предлагаю добавить еще одну точку и посмотреть на результат.


Последовательность точек у нас есть, но пока они размещены в нетипизированном массиве. Аскетичный С не позволяет нам такого шика, а потому мы создаем массив 32 битных чисел с плавающей точкой и скармливаем WebGL'лю.
var triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
Буфер точек готов, теперь пора привязать его к атрибуту шейдера aVertexPosition. Тут нам нужно указать кол-во компонент координаты одной точки нашего массива (4 штуки x,y,z,w) и тип координат (FLOAT)
var itemSize = 4;
gl.vertexAttribPointer(shaderMemory["aVertexPosition"], itemSize, gl.FLOAT, false, 0, 0);
И наконец пришло время рисовать. Для начала отрисовки, вызывается метод drawArrays. Одним из его аргументов передается количество вершин в буфере, чтобы видеокарта знала, сколько раз нужно вызывать вершинный шейдер. При вызове, он получит не весь список вершин квадрата, а координаты только одной точки, в этом и заключается параллельность обработки вершин.
var numItems = vertices.length / itemSize;
gl.drawArrays(gl.TRIANGLE_STRIP, 0, numItems);  
После окончания работы шейдера вершин, происходит растеризация плоскостей, а затем, для каждого пикселя каждой плоскости  вызывается шейдер  фрагмента, задающий цвет точки. Способ растеризации можно изменить заданием аргумента mode метода drawArrays. Приведу формат вызова drawArrays:
void drawArrays(GLenum mode,  GLint first,  GLsizei count);
mode - режим отрисовки
first - первый элемент списка
count - последний элемент списка
Режимы отрисовки проиллюстрирует картинка ниже:


Поэкспериментируйте с режимами отрисовки и координатами вершин. В следующей части туториала мы рассмотрим фрагментные шейдеры, типы переменных, афинные преобразования и перейдем наконец в третье измерение.
Если все верно, а в приведенном примере все именно так, то во вьюпорте должен появиться белый квадрат. Тут полный код примера.
Следующая статья >> 
А вот живой пример:


Комментарии

Популярные сообщения из этого блога

Siege Up! Editor (beta)

STM32F4 и программный выход в DFU

Git и Yandex.Disk