Подумать только

logoas CogITas

Сомневаться - Трудиться - Любить

× Error from canvas.getContext(): Maybe your browser or hardware (GPU) does not appear to support WebGL. The Canvas for WebGL below will be empty.
Mouse и Num Lock

Просто о сложном, или как работает WebGL

Строим 3D проекты – Шаг 2

Взгляд со стороны

Взгляд со стороны

 

   Попробуем разобрать и понять, как работает динамичная программа на 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 раза меньше памяти и времени. Это увеличивает производительность.

   Можно даже сказать, что треугольник – это точка, у которой есть размер. И многие элементы в 3D-модели будут собираться из элементарных масштабируемых треугольников. Это эффективнее, ведь в треугольник включается количество точек, равное площади треугольника, а хранятся и обрабатываются при этом только 3 точки, а то и меньше. Учитывая, что прямоугольная проекция треугольника на экран будет под разными углами поворота треугольника, то площадь проекции будет от площади  линии до площади треугольника:

Проекция на плоскость экрана

Проекция на плоскость экрана

 

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

Классификация проекций

Классификация проекций

 

Изометрическая, диметрическая, триметрическая

Изометрическая, диметрическая, триметрическая

 

Основные виды (principle views)

Основные виды (principle views)

 

Буферы (теория)

   В WebGL есть буферы для хранения данных (буфер – это по сути массив, блок памяти):

  • буфер цвета,
  • буфер глубины,
  • буфер шаблона.
  1. В буфере цвета (Color Buffer) хранятся оттенки пикселей RGBA (красный, зеленый, синий, прозрачность). По умолчанию, прозрачность А (альфа) каждого пикселя разрешена, то есть учитывается при обработке.
  2. В буфере глубины (Depth Buffer) хранятся координаты пикселей по оси Z. При проекции 3D-объекта на плоскость монитора, может несколько точек проецироваться в одну и ту же точку (X,Y). Значения Z в буфере глубины для таких точек сравниваются и отрисовывается только ближайшая к монитору. Это увеличивает производительность.

Направленная на зрителя ось Z

Направленная на зрителя ось Z

 

  1. Буфер шаблона (Stencil Buffer) служит для хранения информации о скрытых пикселях, которые не надо выводить на монитор. Это увеличивает производительность.
  2. Буфер вершин. Вершины – точки, по которым образуются фигуры. Чтобы задать расположение вершины, используются шейдеры, которые передаются в GPU. В WebGL мы задаем объекты буфера вершин VBO (Vertex Buffer Objects), в которых по отдельности хранятся одни из данных про вершины: позиция, цвет, вектор нормали, координаты текстур. VBO отправляются в шейдер.

   Допустим, мы хотим нарисовать треугольник. Для этого необходимо записать вершины в буфер вершин. Сначала разместим координаты 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);
   ARRAY_BUFFER — используется для VBO, содержащих позицию и цвет вершин;
   ELEMENT_ARRAY_BUFFER – используется для VBO, содержащих индексы вершин.

   Теперь размещаем вершины data в текущем буфере VBO:

gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
   STATIC_DRAW – задает данные, которые при выполнении программы не меняются;
   DYNAMIC_DRAW – данные задаются каждый раз перед использованием;
   STREAM_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);
Для рисования мы можем использовать один  из 3 вариантов:
clear (mask)                                                  //Mask parameter determines which buffer (s) are cleared.
   (Использовался нами в функции setupWebGL ()).
drawArrays (mode, first, count)                   //Called on each enabled VBO array.
drawElements (mode, count, type, offset)    //Called on a VBO of indices (of type ELEMENT_ARRAY_BUFFER).

   Теперь, при запуске программы, мы получим желаемый результат:

Треугольник в 3D, параллельный экрану

Треугольник в 3D, параллельный экрану

 

    Напомню, координаты точек нашего треугольника в формате (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.

 

<< Предыдущий шаг

Следующий шаг >>

Все статьи


Комментарии к “Просто о сложном, или как работает WebGL”

  • marfoldi говорит:

    Собрал программу, работает! Буду изучать дальше, у меня идея сделать программу по просмотру татуировок в 3D.

    Ответить
    • Андрей Свирский говорит:

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

      Ответить
  • Оставить комментарий

    Ваш email не будет отображаться. Обязательные поля помечены *

    (Чтобы установить аватар, необходимо зарегистрировать свой e-mail на gravatar.com. Как это сделать, написано в статье http://cogitas.ru/robots-avatar-icon-wordpress.html)