Попробуем разобрать и понять, как работает динамичная программа на WebGL в конце предыдущего шага №1. Для этого, постепенно напишем ее заново.
В компьютере, как и в любом современном устройстве типа мобильного, есть центральный процессор (CPU — Central Processing Unit) и видеокарта – графический процессор (GPU — Graphics Processing Unit). GPU в определенных задачах работает значительно быстрее, чем CPU. Простой способ понять разницу между GPU и CPU — сравнить то, как они выполняют задачи. CPU состоит из нескольких ядер, оптимизированных для последовательной обработки данных, в то время как GPU имеет тысячи более мелких и энергоэффективных ядер, созданных для выполнения многих задач одновременно.
Функции WebGL являются частью JavaScript, то есть, прописаны в коде браузеров. В определенный момент выполнения программы, когда необходимо рассчитать кадры 3D-модели для вывода на экран, CPU передает специальные программные коды (называемые шейдерами) для расчета кадров в GPU. Результаты выводятся в элемент HTML <canvas>.
Шейдеры пишутся на языке GLSL (OpenGL Shading Language), подобном C++ и компилируемом перед выполнением. Набор скомпилированных шейдеров называется программой для GPU.
В нашей первой программе “WebGL Hello World” (рассмотрена подробно на 1-ом шаге) мы не использовали шейдеры. Мы только определили, поддерживают ли GPU и браузер работу с шейдерами, создаваемыми нами в WebGL.
Примитивы (теория)
Элементарной составляющей изображения является «Точка». Точка считается минимальным логическим элементом без размера. Даже если при увеличенном отображении точка визуально занимает много пикселей, логически она все равно неделимая точка.
Из двух точек можно составить примитив «Линия». Если необходимо сохранить в памяти отдельные линии между 6-ю соседними последовательными точками, что случается довольно часто (gl.LINES), то понадобится 10 точек в памяти для 5 линий (по 2 точки для каждой линии). Но координаты точек конца и начала совпадают, поэтому можно сохранить эти 5 линий с помощью только 6 точек (gl.LINE_STRIP). То есть, в 2 раза меньше памяти и времени. Это увеличивает производительность.
Из трех точек можно составить примитив «Треугольник». Как и с линиями, если необходимо сохранить в памяти отдельные треугольники между 6-ю соседними последовательными точками, что случается довольно часто (gl.TRIANGLES), то понадобится 12 точек для 4 треугольников. Но координаты многих точек совпадают, поэтому можно сохранить эти 4 треугольника с помощью только 6 точек (gl.TRIANGLE_STRIP). То есть, в 2 раза меньше памяти и времени. Это увеличивает производительность.
Буферы (теория)
В WebGL есть буферы для хранения данных (буфер – это по сути массив, блок памяти):
Допустим, мы хотим нарисовать треугольник. Для этого необходимо записать вершины в буфер вершин. Сначала разместим координаты XYZ вершин треугольника в массиве:
var data = [ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0 ];
VBO создается следующим образом:
var myVBO = gl.createBuffer();
Далее, выбираем текущий VBO, задавая при этом его тип:
gl.bindBuffer(gl.ARRAY_BUFFER, myVBO);
Теперь размещаем вершины data в текущем буфере VBO:
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
У вершин есть свойства, которые можно передать в скомпилированные шейдеры. Также можно передать константы, которые будут применимы к каждой вершине. Во время работы программы, чтобы найти и изменить данные свойств и констант в исполняемой программе GPU, используются getAttribLocation (program, name), getUniformLocation (program, name). Например:
var vertexPositionAttribute = gl.getAttribLocation(glProgram, "aVertexPosition"); gl.enableVertexAttribArray(vertexPositionAttribute); gl.bindBuffer(gl.ARRAY_BUFFER, myVBO); gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
В 4-ой строке мы задаем параметры данных, используя функцию vertexAttribPointer (index, size, type, normalized, stride, offset). Из параметров пока усвоим только два: size – число параметров свойства (например, для RGBA это число 4); type – тип данных.
Типы шейдеров (теория)
Вершинные шейдеры
Вершинный шейдер оперирует данными, связанными с вершинами многогранников, например, с координатами вершины, с текстурными координатами, с цветом вершины, с векторами касательной, бинормали, нормали. Вершинный шейдер также может использоваться для расчёта освещения.
Пиксельные шейдеры
Пиксельный шейдер работает с фрагментами растрового изображения и с текстурами — обрабатывает данные, связанные с пикселями (например, цвет, глубина, текстурные координаты). Пиксельный шейдер используется на последней стадии графического конвейера для формирования фрагмента изображения.
Рабочий пример (практика)
Нарисуем треугольник, используя полученные знания. Возьмем «шаблонную» пустую программу из предыдущего шага, которая только инициализировала WebGL, и добавим в нее шейдеры позиции и цвета, нарисуем элементарную фигуру – треугольник.
Готовим программу. Скопируем «шаблонную» программу в новую папку, переименуем файлы, изменим заголовок (тэг <head>):
<head> <title>WebGL Triangle 2D</title> <script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; void main(void) { gl_Position = vec4(aVertexPosition, 1.0); } </script> <script id="shader-fs" type="x-shader/x-fragment"> void main(void) { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } </script> <script src="js/vars.js"></script> <script src="js/02-triangle-2d.js"></script> </head>
Мы добавили 2 блока шейдеров: «shader-vs» и «shader-fs».
Первый, вершинный шейдер VS (Vertex Shader), будет задавать позицию каждой поступившей вершины.
Наш шейдер принимает координаты вершины (x,y,z) и задает позицию в однородных координатах (x,y,z,1.0).
Второй, пиксельный шейдер FS (Fragment Shader), будет задавать цвет каждого поступившего пикселя.
Все пиксели станут белыми и непрозрачными (1.0, 1.0, 1.0, 1.0), так как RGBA установлены в 1.
Пока что программа ничего не делает. Добавим в нашу JavaScript программу файл vars.js с глобальными переменные, которые нам понадобятся:
/*GLOBAL variables that will store: */ var gl = null; var canvas = null; var glProgram = null; var fragmentShader = null; var vertexShader = null; var tianglesVerticeBuffer = null; //Triangle VBO. var vertexPositionAttribute = null; //Will be passed to the VS. var trianglesColorBuffer = null; var vertexColorAttribute = null;
В блок успешной инициализации программы добавим функции начальной настройки программы:
if(gl) { setupWebGL(); //[1] initShaders(); //[2] setupBuffers(); //[3] drawScene(); //[4] }
[1].
В функцию setupWebGL () запишем действия из программы предыдущего шага:
setupWebGL() { //The default value to set the color buffer. gl.clearColor(0, 0.5, 0.5, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
[2].
В функции initShaders () мы создаем шейдеры.
var vertexShader = gl.createShader(gl.VERTEX_SHADER); var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
и указываем шейдерам источники, из которых в них записывается наш код.
var fs_source = document.getElementById('shader-fs').innerHTML; var vs_source = document.getElementById('shader-vs').innerHTML; gl.shaderSource(vertexShader, vs_source); gl.shaderSource(fragmentShader, fs_source);
Теперь компилируем шейдеры (переводится текст программы в коды бинарных процессорных инструкций)
gl.compileShader(vertexShader); gl.compileShader(fragmentShader);
Создаем программу для GPU, в которую мы вносим шейдеры:
var glProgram = gl.createProgram(); gl.attachShader(glProgram, vertexShader); gl.attachShader(glProgram, fragmentShader);
Линкуем программу GPU (сшиваются скомпилированные коды между собой и с кодами из библиотек в цельную программу) и подключаем ее к WebGL.
gl.linkProgram(glProgram); gl.useProgram(glProgram);
Итак, функции initShaders () и вспомогательная makeShader (src, type) будут такими:
function initShaders() { //Set shader source. var fs_source = document.getElementById('shader-fs').innerHTML; var vs_source = document.getElementById('shader-vs').innerHTML; //1.Create and compile shaders. vertexShader = makeShader(vs_source, gl.VERTEX_SHADER); fragmentShader = makeShader(fs_source, gl.FRAGMENT_SHADER); //2. Create program. glProgram = gl.createProgram(); //3. Attach shaders to the program. Link program. gl.attachShader(glProgram, vertexShader); gl.attachShader(glProgram, fragmentShader); gl.linkProgram(glProgram); if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) alert("Unable to initialize the shader program."); //4. Use program. gl.useProgram(glProgram); } //1. function makeShader(src, type) { //Call the API function createShader to create a WebGLShader object. var shader = gl.createShader(type); gl.shaderSource(shader, src); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) alert("Error compiling shader: " + gl.getShaderInfoLog(shader)); return shader; }
[3].
В следующей функции setupBuffers () мы помещаем в буфер VBO «trianglesVerticeBuffer» позиции вершин. VBO будем потом отправлять в вершинный шейдер.
function setupBuffers() { var triangleVertices = [ //Triangle's points coodinates. -0.5, 0.5, 0.0, 0.0, 0.0, 0.0, -0.5, -0.5, 0.0 ]; trianglesVerticeBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, trianglesVerticeBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangleVertices), gl.STATIC_DRAW); }
[4].
Наконец, в функции drawScene () мы передадим координаты вершин треугольника, то есть наш VBO «trianglesVerticeBuffer» в вектор aVertexPosition вершинного шейдера.
//Move data into shader. vertexPositionAttribute = gl.getAttribLocation(glProgram, "aVertexPosition"); gl.enableVertexAttribArray(vertexPositionAttribute); gl.bindBuffer(gl.ARRAY_BUFFER, trianglesVerticeBuffer); gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
И второе действие в функции drawScene () – это собственно рисование.
//Make draw. //TYPE OF PRIMITIVE; FIRST POINT; NUM POINTS AFTER FIRST. gl.drawArrays(gl.TRIANGLES, 0, 3);
Теперь, при запуске программы, мы получим желаемый результат:
Напомню, координаты точек нашего треугольника в формате (x,y,z):
-0.5, 0.5, 0.0,
0.0, 0.0, 0.0,
-0.5, -0.5, 0.0
Заметим, что значение Z для всех точек равно 0. Вторая точка (0.0, 0.0) – в самом центре нашего холста.
Ура! Все получилось, идем пока по домам 🙂
А на ближайших шагах мы продолжим совершенствовать программу. Установим цвет, камеру, движение, 3D.
Собрал программу, работает! Буду изучать дальше, у меня идея сделать программу по просмотру татуировок в 3D.