Быстрый старт в WebGL (Quickstart in WebGL)

Автор: | 02.08.2019

Tags:  WebGL 3D HTML  JavaScript GLSL GPU шейдер OpenGL

Введение
Пример простого WebGL приложения
Шаг 1. Определение WebGL контекста
Шаг 2. Создание 3D-модели и сохранение ее в буферах
Шаг 3. Создание, компиляция и подключение шейдеров
Шаг 4. Связывание шейдеров с буферами
Шаг 5. Отображение графики
Полезные ссылки

Введение

WebGL (Web-based Graphics Library) — кроссплатформенный API для 3D-графики в браузере. WebGL следует клиентскому подходу генерации цифрового изображения из модели  (рендеринга)  с использованием графического процессора (аппаратный рендеринг). Код  WebGL приложения представляет собой сочетание HTML5,  JavaScript (JS) и языка GLSL (OpenGL Shading Language):

  • WebGL интегрирован в JS, а JS интегрирован в HTML 5.
  • JS необходим для связи с центральным процессором (CPU — Central Processor Unit).
  • GLSL используется для связи с графическим процессором (GPU- Graphic Processor Unit).

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

WebGL обеспечивает возможности 3D графики библиотеки OpenGL. Он также преобразует геометрическую модель в пиксельный формат, используя шейдеры.

Шейдер — компьютерная программа на языке GLSL, предназначенная для исполнения на GPU с его тысячами параллельно работающих ядер. Шейдеры бывают вершинными и фрагментными (еще их называют пиксельными).

Вершинный шейдер предназначен для обработки каждой вершины и нахождения её координат с учетом матриц модели и проекций. Фрагментный шейдер предназначен для расчета цвета каждого пикселя (двумерной точки).

Архитектуру WebGL-приложения сегодня в некотором смысле можно сравнить с тем, как наши предки видели вселенную.

Графические приложения наиболее нуждаются оптимизации, особенно в Web программировании. Изучая каждую функцию OpenGL, стоит задуматься, как вызов этой функции выполняется и сколько времени он может занять (см. Оптимизация OpenGL приложений).

Возможности OpenGL определяются аппаратурой, которая, как известно, имеет тенденцию развиваться. В соответствии с этим в  OpenGL добавляются новые функции.  С появлением GPU  весь функционал с последовательной передачей и обработкой каждой вершины примитива практически перестал использоваться. Взамен GPU принимает на вход описание трехмерной сцены в виде массивов вершин и параметров наблюдателя, а затем строит по ним на экране двумерное изображение сцены.

По мере совершенствования GPU появилась возможность  хранить данные (вершины и их атрибуты) в различных буферах – непосредственно на GPU. Здесь они передаются вершинным шейдерам и могут отображаться напрямую, что, в свою очередь, повышает производительность.

Пример простого WebGL приложения

Ниже приводится текст кода приложения с использованием WebGL для рисования простого треугольника с 2D-координатами.  Краткое описание фрагмента кода рассматривается в последующих  разделах. С более подробным описанием этого и других приложений можно ознакомиться в статье  WebGL — Quick Guide.

<!doctype html>
<html>
   <body>
      <canvas width = "300" height = "300" id = "my_Canvas"></canvas>
		
      <script>
          /* Step1: Prepare the canvas and get WebGL context */

             var canvas = document.getElementById('my_Canvas');
             var gl = canvas.getContext('experimental-webgl');

         /* Step2: Define the geometry and store it in buffer objects */

         // Put the vertices data into an array
         var vertices = [-0.5, 0.5, -0.5, -0.5, 0.0, -0.5,];

         // Create a new buffer object
         var vertex_buffer = gl.createBuffer();

         // Bind an empty array buffer to it
         gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
         
         // Pass the vertices data to the buffer
         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

         // Unbind the buffer
         gl.bindBuffer(gl.ARRAY_BUFFER, null);

          /* Step3: Create and compile Shader programs */
        
        // Vertex shader source code
         var vertCode =
            'attribute vec2 coordinates;' + 
            'void main(void) {' + ' gl_Position = vec4(coordinates,0.0, 1.0);' + '}';

         //Create a vertex shader object
         var vertShader = gl.createShader(gl.VERTEX_SHADER);

         //Attach vertex shader source code
         gl.shaderSource(vertShader, vertCode);

         //Compile the vertex shader
         gl.compileShader(vertShader);

         //Fragment shader source code
         var fragCode = 'void main(void) {' + 'gl_FragColor = vec4(0.0, 0.0, 0.0, 0.1);' + '}';

         // Create fragment shader object
         var fragShader = gl.createShader(gl.FRAGMENT_SHADER);

         // Attach fragment shader source code
         gl.shaderSource(fragShader, fragCode);

         // Compile the fragment shader
         gl.compileShader(fragShader);

         // Create a shader program object to store combined shader program
         var shaderProgram = gl.createProgram();

         // Attach a vertex shader
         gl.attachShader(shaderProgram, vertShader); 
         
         // Attach a fragment shader
         gl.attachShader(shaderProgram, fragShader);

         // Link both programs
         gl.linkProgram(shaderProgram);

         // Use the combined shader program object
         gl.useProgram(shaderProgram);

         /* Step 4: Associate the shader programs to buffer objects */
         
         //Bind vertex buffer object
         gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);

         //Get the attribute location
         var coord = gl.getAttribLocation(shaderProgram, "coordinates");

         //point an attribute to the currently bound VBO
         gl.vertexAttribPointer(coord, 2, gl.FLOAT, false, 0, 0);

         //Enable the attribute
         gl.enableVertexAttribArray(coord);

        /* Step5: Drawing the object (triangle) */

         // Clear the canvas
         gl.clearColor(0.5, 0.5, 0.5, 0.9);

         // Enable the depth test
         gl.enable(gl.DEPTH_TEST); 
         
         // Clear the color buffer bit
         gl.clear(gl.COLOR_BUFFER_BIT);

         // Set the view port
         gl.viewport(0,0,canvas.width,canvas.height);

         // Draw the triangle
         gl.drawArrays(gl.TRIANGLES, 0, 3);
      </script>
   </body>
</html>

В программе были выполнены 5 следующих шагов:

  1. Определяется контекст элемента canvas, а через него — WebGL контекст
  2. Определяется геометрия и сохраняется в буферных объектах
  3. Создаются и компилируются шейдерные программы
  4. Шейдерные программы связываются с буферными объектами
  5. Рисуется  объект (треугольник)

Все эти шаги подробно описаны в разделах далее.

Определение WebGL контекста

WebGL-приложения написаны на языке JavaScript. Через него вы можете напрямую взаимодействовать с элементами HTML документа . В HTML5 определен элемент <canvas> как «растровый холст, который может быть использован для отображения 2D графики. Через объект canvas можно получить WebGL контекст, который обеспечивает возможности 3D графики библиотеки OpenGL.

<!DOCTYPE html>
<html>
   <canvas id = 'my_canvas'></canvas>
	
   <script>
      var canvas = document.getElementById('my_canvas');
      var gl = canvas.getContext('experimental-webgl');
      gl.clearColor(0.9,0.9,0.8,1);
      gl.clear(gl.COLOR_BUFFER_BIT);
   </script>
</html>

Для создания контекста рендеринга WebGL в элементе canvas вы должны передать строчку экспериментальный-webgl вместо 2d в метод canvas.getContext (). Некоторые браузеры поддерживают только «webgl».

Чтобы написать приложение WebGL, все, что вам нужно  — это текстовый редактор и веб-браузер. Также для тестирования приложения, можно использовать online редакторе CodePen. При тестировании приведенного выше кода пока лишь получим вывод закрашенного в голубой цвет окна:

Создание 3D-модели и сохранение ее в буферах

Трехмерная модель создается  с использованием следующих примитивов:

Вершины модели определяются в пространственной системе координат, плоскость xy которой совпадает с плоскостью окна:

В OpenGL WinApi C++ приложении (см. 3D графика на основе OpenGL WinApi C++) для отрисовки квадрата использовался бы такой код:

glBegin(GL_TRIANGLES);
        // v1-v3-v4
        glVertex3f(0.5,0.5,0.0);
        glVertex3f(-0.5,-0.5,0.0);
        glVertex3f(-0.5,0.5,0.0);
glEnd();
glBegin(GL_TRIANGLES);
         // v1-v3-v2 
         glVertex3f(0.5,0.5,0.0);
         glVertex3f(-0.5,-0.5,0.0);
         glVertex3f(0.5,-0.5,0.0);
glEnd();

В WebGL это уже не работает. Здесь используется современная  OpenGL технология. Мы определяем атрибуты геометрии, такие как вершины, индексы, цвет и т. д. Сохраняем их в массивах JavaScript. Затем создаем один или несколько буферных объектов и передаем массивы с данными в соответствующий буферный объект.

Через буфер данные передаются в шейдеры. Для обработки геометрии существует два типа буферных объектов:

  • Vertex Buffer Object (VBO) — содержит данные для каждой вершины .
  • Index Buffer Object (IBO) — содержит индексы вершин.

Сохраняем вершины треугольника в массиве JavaScript и передаем этот массив в VBO.

// Put the vertices data into an array
var vertices = [
   0.5,0.5,0.0,
   0.5,-0.5,0.0,
   -0.5,-0.5,0.0,
   -0.5,0.5,0.0,
];
// Create a new buffer object
 var vertex_buffer = gl.createBuffer();

// Bind an empty array buffer to it
 gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
 
 // Pass the vertices data to the buffer
 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// Unbind the buffer
 gl.bindBuffer(gl.ARRAY_BUFFER, null);

 Мы можем точно также сохранить индексы вершин треугольника в массиве и передать этот массив в IBO. Принципиальное отличие между созданием буферов для VBO и IBO лишь в выделенном красным цветом параметре:

var indices = [0,2,3,0,2,1]; // v1-v3-v4, v1-v3-v2
//Index buffer
var index_buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
...

Подробнее см. WebGL Geometry

Создание, компиляция и подключение шейдеров 

Схема ниже  показывает необходимые шаги для создания, компиляции и подключения шейдеров.

Шейдерные программы (Sources)

Программа вершинного шейдера  — код, выполняемый для каждой вершины:

attribute vec2 coordinates;
void main(void) {
   gl_Position = vec4(coordinates, 0.0, 1.0);
};

Переменная coordinates объявлена с классификатором attribute. Значение такой переменной доступно в  JS и в вершинном шейдере. Переменная coordinates будет связана с VBO через метод getAttribLocation (см. Шаг 4).

gl_Position — это предопределенная переменная, которая доступна только в программе вершинного шейдера. Содержит позицию вершины.  Поскольку вершинная шейдерная программа — операция  для каждой вершины, значение gl_position рассчитывается для каждой вершины.

Шейдерные программы — независимые программы, Поэтому их можно написать как отдельный скрипт или можно хранить непосредственно в строковом формате:

var vertCode =
   'attribute vec2 coordinates;' +
	
   'void main(void) {' +
      ' gl_Position = vec4(coordinates, 0.0, 1.0);' +
   '}';

Программа фрагметного шейдера — код, выполняемый для каждого пикселя:

void main(void) {
   gl_FragColor = vec4(0, 0.8, 0, 1);
}

В предопределенной переменной gl.FragColor сохраняется значение цвета  пикселя.

Вершинный шейдер принимает информацию для каждой вершины через переменную с квалификатором attribute  – это данные, передаваемые программой вершинному шейдеру (другим шейдерам данные не доступны). Причем данные приходят шейдеру на каждую вершину. Эти данные доступны только для чтения.

Оба шейдера принимают пользовательское состояние от приложения через переменные с квалификатором uniform – это данные, посылаемые в шейдер приложением.  В отличии от attribute они глобальны, как для шейдеров, так и для вершин. То есть, если объявить uniform переменную с одинаковым именем в вершинном и фрагментом шейдере, эта переменная будет общей для них. Также данные не зависят от того, какая сейчас вершина обрабатывается, они остаются неизменными пока их не изменит приложение.

От вершинного шейдера информация передается во фрагментный с помощью меняющихся переменных (с квалификатором varying), чьи объявления в обоих шейдерах должны совпадать. Фиксированная функциональность между вершинным и фрагментным процессорами интерполирует повершинные значения, записанные в эти переменные. При чтении фрагментным шейдером тех же varying переменных, он получает уже интерполированное значение для фрагмента, обрабатываемого в текущий момент.

Компиляция шейдеров

Компиляция включает следующие три шага:

  • Создание объекта-шейдера.
  • Прикрепление шейдерной программы к объекту-шейдеру.
  • Компиляция шейдерной программы.

Создание объекта-шейдера

Чтобы создать пустой шейдер, WebGL предоставляет метод с именем createShader () . Создает и возвращает шейдерный объект. Его синтаксис выглядит следующим образом:

Object createShader (enum type)

Как указано в синтаксисе, этот метод принимает предопределенное значение перечисления в качестве параметра:

  • gl.VERTEX_SHADER — для создания вершинного шейдера
  • gl.FRAGMENT_SHADER — для создания фрагментного шейдера.

Прикрепление шейдерной программы к объекту-шейдеру

Вы можете присоединить исходный код к созданному объекту шейдера, используя метод shaderSource () . Его синтаксис выглядит следующим образом —

void shaderSource(Object shader, string source)

Этот метод принимает два параметра:

  • shader — Вы должны передать созданный объект шейдера как один параметр.
  • source — Вы должны передать программный код шейдера в строковом формате.

Компиляция шейдерной программы

Чтобы скомпилировать шейдерную программу, вы должны использовать метод compileShader () . Его синтаксис следующий:

compileShader(Object shader)

Этот метод принимает программный объект шейдера в качестве параметра.

Фрагмент кода  компиляции шейдерной программы

В следующем фрагменте кода показано, как создать и скомпилировать шейдерные программы.

// Vertex Shader
var vertCode =
   'attribute vec3 coordinates;' +
	
   'void main(void) {' +
      ' gl_Position = vec4(coordinates, 1.0);' +
   '}';

var vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, vertCode);
gl.compileShader(vertShader);
 
// Fragment Shader
var fragCode =
   'void main(void) {' +
      ' gl_FragColor = vec4(0, 0.8, 0, 1);' +
   '}';

var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, fragCode);
gl.compileShader(fragShader);

Комбинированная шейдерная программа

После создания и компиляции обеих шейдерных программ вам необходимо создать из них комбинированную программу в следующей последовательности:

  • Создать пустой программный объект
  • Присоединить к нему оба шейдера
  • Связать оба шейдера
  • Использовать программу

Создание программного объекта

Создается пустой программный объект с помощью метода createProgram ():

createProgram();

Присоединение шейдеров к  программному объекту

Шейдеры присоединяются к созданному программному объекту, используя метод attachShader () :

attachShader(Object program, Object shader);

Этот метод принимает два параметра —

  • program — пустой программный объект.
  • shader — один из скомпилированных шейдеров (вершинный или фрагментный шейдер)

 С помощью метода attachShader() необходимо подключить оба шейдера.

Связывание шейдеров

Шейдеры связываются, используя метод linkProgram () :

linkProgram(shaderProgram);

shaderProgram — объект программы, к которому прикреплены шейдеры

Использование программы

Для использования программы WebGL предоставляет метод с именем useProgram ():

useProgram(shaderProgram);

shaderProgram — объект программы, к которому вы прикрепили шейдеры, после связывания.

Фрагмент кода

Код для создания, компиляции и использования комбинированной шейдерной программы:

var shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertShader);
gl.attachShader(shaderProgram, fragShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);

Связывание шейдеров с буферами

Атрибут программы вершинного шейдера необходимо связать с Vertex Buffer Object (VBO) в следующей последовательности:

  • Получить местоположение атрибута.
  • Указать атрибуту на VBO.
  • Активировать атрибут.

Получение расположения атрибута

WebGL предоставляет метод getAttribLocation (), который возвращает местоположение атрибута. Его синтаксис выглядит следующим образом

ulong getAttribLocation(Object program, string name)

Этот метод принимает объект программы вершинного шейдера и значения ее атрибута.

Следующий фрагмент кода показывает, как используется getAttribLocation () в программе::

var coordinatesVar = gl.getAttribLocation(shader_program, "coordinates"); 

Указание атрибуту на VBO

Чтобы назначить VBO переменной атрибута, WebGL предоставляет метод:

void vertexAttribPointer(location, int size, enum type, bool normalized, 
                                                        long stride, long offset)

Этот метод принимает шесть параметров:

  • location — указывает место хранения переменной атрибута. При использовании этой опции вы должны передать значение, возвращаемое методом getAttribLocation () .
  • size — определяет количество компонентов на вершину в объекте буфера.
  • type — указывает тип данных.
  • normalized — это логическое значение. Если true, неплавающие данные нормализуются до [0, 1]; в противном случае он нормализуется до [-1, 1].
  • stride — указывает количество байтов между различными элементами данных вершины или ноль для шага по умолчанию.
  • offset — указывает смещение (в байтах) в объекте буфера, чтобы указать, из какого байта хранятся данные вершины. Если данные сохраняются с самого начала, offset равно 0.

Следующий фрагмент кода показывает, как используется vertexAttribPointer () в программе:

gl.vertexAttribPointer(coordinatesVar, 3, gl.FLOAT, false, 0, 0);

Активация атрибута

Для активации атрибута вершинного шейдера с получением доступа к VBO используется метод enableVertexAttribArray (). Этот метод принимает в качестве параметра переменную, которая хранит местоположение атрибута.

Следующий фрагмент кода показывает, как используется метод  enableVertexAttribArray () в программе:

gl.enableVertexAttribArray(coordinatesVar);

Отображение графики

После связывания буферов с шейдерами, последний шаг — отображение графики. Этот шаг включает в себя такие операции, как очистка цвета, очистка буфера, включение теста глубины, настройка порта просмотра и т. д (см. Пример простого WebGL приложения). Наконец, вам нужно нарисовать необходимые примитивы. Для этого WebGL предоставляет два метода, а именно: drawArrays () и drawElements ().

Метод drawArrays ()

drawArrays () — это метод, который используется для рисования моделей с использованием вершин. Вот его синтаксис:

void drawArrays(enum mode, int first, long count)

Этот метод принимает следующие три параметра:

  • mode — в WebGL модели отрисовываются с использованием примитивных типов. Используя режим, программисты должны выбрать один из примитивных типов, предоставляемых WebGL. Возможные значения для этого параметра — gl.POINTS, gl.LINE_STRIP, gl.LINE_LOOP, gl.LINES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN и gl.TRIANGLES.
  • first — эта опция указывает начальный элемент во включенных массивах. Это не может быть отрицательным значением.
  • count — эта опция указывает количество элементов для визуализации.

Если вы рисуете модель, используя метод drawArrays () , то WebGL создает геометрию в порядке, в котором определены координаты вершины.

Примеры

Если вы хотите нарисовать один треугольник, используя метод drawArrays (), то вам нужно передать ему три вершины и вызвать метод drawArrays () , как показано ниже.

var vertices = [-0.5,-0.5, -0.25,0.5, 0.0,-0.5,];
gl.drawArrays(gl.TRIANGLES, 0, 3);

Это создаст треугольник, как показано ниже:

Предположим, что вы хотите нарисовать смежные треугольники, тогда вы должны передать следующие три вершины по порядку в буфере вершин и указать количество элементов, которые будут отображаться как 6.

var vertices = [-0.5,-0.5, -0.25,0.5, 0.0,-0.5, 0.0,-0.5, 0.25,0.5, 0.5,-0.5,];
gl.drawArrays(gl.TRIANGLES, 0, 6);

Это создаст 2 смежных треугольника, как показано ниже:

drawElements ()

drawElements () — это метод, который используется для рисования моделей с использованием вершин и индексов. Его синтаксис выглядит следующим образом —

void drawElements(enum mode, long count, enum type, long offset)

Этот метод принимает следующие четыре параметра:

  • mode — модели WebGL отрисовываются с использованием примитивных типов. Используя режим, программисты должны выбрать один из примитивных типов, предоставляемых WebGL. Список возможных значений для этого параметра — gl.POINTS, gl.LINE_STRIP, gl.LINE_LOOP, gl.LINES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN и gl.TRIANGLES.
  • count — эта опция указывает количество элементов для визуализации.
  • type — эта опция указывает тип данных индексов, который должен быть UNSIGNED_BYTE или UNSIGNED_SHORT.
  • offset — эта опция указывает начальную точку рендеринга. Обычно это первый элемент (0).

Если вы рисуете модель, используя метод drawElements () , тогда объект индексного буфера также должен быть создан вместе с объектом буфера вершин. Если вы используете этот метод, данные вершин будут обрабатываться один раз и использоваться столько раз, сколько указано в индексах.

Пример

Если вы хотите нарисовать один треугольник, используя индексы, вам нужно передать индексы вместе с вершинами и вызвать метод drawElements (), как показано ниже.

var vertices = [ -0.5,-0.5,0.0, -0.25,0.5,0.0, 0.0,-0.5,0.0 ];
var indices = [0,1,2];
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT,0);

Это даст следующий результат:

Если вы хотите нарисовать 2 смежных треугольника, используя метод drawElements (), просто добавьте другие вершины и упомяните индексы для остальных вершин.

var vertices = [
   -0.5,-0.5,0.0,
   -0.25,0.5,0.0,
   0.0,-0.5,0.0,
   0.25,0.5,0.0,
   0.5,-0.5,0.0 
];
var indices = [0,1,2,2,3,4];
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT,0);

Это даст следующий результат:

 

Полезные ссылки:

  1. OpenGL
  2. WebGL — Quick Guide
  3. Введение в WebGL
  4. Настройка буфера вершин и буфера индексов
  5. Объекты как ассоциативные массивы
  6. Язык JavaScript
  7. Opengl-tutorial
  8. Введение в программирование шейдеров
  9. Learning WebGL по-русски
  10. Lightcycle demo using WebGL
  11. Основы WebGL: разбираемся в магическом коде
  12. Основы WebGL
  13. Как работает WebGL
  14. Первая программа на WebGL
  15. Создание шейдеров
  16. UI-компоненты на пиксельных шейдерах: пишем ваш первый шейдер

 

Автор: Николай Свирневский