Pull to refresh
91.25
2ГИС
Главные по городской навигации

WebGL для всех

Reading time 11 min
Views 52K


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

WebGL, в отличие от Javascript, имеет высокий порог вхождения, его до сих пор мало кто использует, а ещё меньше тех, кто об этом пишет. Большинство руководств или статей перепрыгивают сразу на использование какой-нибудь библиотеки. Но мы-то с вами знаем, что большие универсальные инструменты не всегда пригодны для наших задач или, возможно, делают это на неприемлемом уровне: проигрывают в скорости, поставляются с ненужным багажом и т.д.

Этой статьёй хочется облегчить порог вхождения в чистый WebGL, дать начальное представление и подсказать, куда двигаться дальше.

Технология WebGL позволяет рисовать графику в браузере, используя возможности видеокарты, тогда как раньше мы могли использовать только процессор. Если вы не понимаете, что это даёт, советую посмотреть эту небольшую демонстрацию.

WebGL основан на OpenGL ES 2.0, который, в свою очередь, является специальной версией для работы на мобильных устройствах. Спецификация WebGL была выпущена в 2011 году, разрабатывается и поддерживается некоммерческой организацией Kronos Group, сайт которой частенько лежит, что ещё более усложняет изучение. Известно, что в настоящее время идёт разработка спецификации версии 2.0.


Статистика поддержки WebGL разными браузерами с сайта caniuse.com

WebGL доступен в большинстве современных браузеров и поддерживается у 83% пользователей. Приятным бонусом разработки на WebGL является то, что вы будете поддерживать только современные браузеры и забудете о кошмарах ECMAScript 3.

Если вы думаете, что WebGL рисует 3D, вы ошибаетесь. WebGL ничего не знает о 3D, это скорее низкоуровневый 2D API, и всё что он умеет делать, это рисовать треугольники. Но он умеет рисовать их очень много и очень быстро.

Хотите нарисовать квадрат? Пожалуйста, соедините два треугольника. Нужна линия? Без проблем, всего лишь несколько последовательно соединенных треугольников.

Как нарисовать треугольник


Поскольку все фигуры в WebGL состоят из треугольников, поэтапно разберём, как отобразить один треугольник.

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

Шейдер — это программа, выполняемая на видеокарте и использующая язык GLSL. Этот язык достаточно простой, и его изучение не представляет проблемы.

Всего есть два вида шейдеров: вершинный и фрагментый, и для отрисовки абсолютно любой фигуры всегда используются оба. Разберёмся с каждым по очереди.

Чтобы понять суть работы вершинного шейдера, абстрагируемся от задачи с треугольником и предположим, что вы хотите нарисовать куб или любую другую фигуру со множеством вершин. Для этого вам нужно задать её геометрию, а геометрия в свою очередь задаётся с помощью указания координат вершин. Было бы накладно самим каждый раз вычислять новые координаты всех вершин при изменении положения куба в пространстве. Такую работу лучше переложить с процессора на видеокарту, для этого и существует вершинный шейдер.

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

Вершинный шейдер всегда вычисляет положение вершин, но попутно он может выполнять и другую работу, например, подсчёт угла падения света. Энтузиасты делают потрясающие вещи, используя возможности вершинных шейдеров.

Знания положения фигуры недостаточно, чтобы её нарисовать. Необходима также информация о том, как должна быть раскрашена фигура, для этого служит фрагментный шейдер. Он вызывается для каждой точки поверхности фигуры и на основе переданной информации вычисляет цвет пикселя на экране.


Если вершинный шейдер определяет геометрию фигуры, то фрагментный — её цвет

Как уже было сказано выше, код шейдеров пишется на языке GLSL. Рассмотрим код шейдеров для треугольника:

Пример вершинного шейдера:
attribute vec3 a_position;
attribute vec3 a_color;
uniform vec3 u_position;
varying vec3 v_color;

void main(void) {
    v_color = a_color;
    gl_Position = vec4(u_position + a_position, 1.0);
}

Пример фрагментного шейдера:
precision mediump float;
varying vec3 v_сolor;

void main(void) {
    gl_FragColor = vec4(v_color.rgb, 1.0);
}

Код состоит из переменных и главной функции, возвращающей основной результат работы шейдера: gl_Position передаёт координаты, а gl_FragColor устанавливает цвет.

Шейдеры имеют три типа переменных, которые передаются из основной программы:
  1. attributes — доступны только в вершинном шейдере, разные для каждой из вершин;
  2. uniforms — доступны в обоих шейдерах и одинаковы для всех вызовов шейдера;
  3. varying — служат для передачи информации от вершинного шейдера к фрагментному.

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


Значения varying переменных внутри треугольника вычисляются
на основе значений этих переменных в вершинах


Попробуем инициализировать данные шейдеры. Для начала получим контекст WebGL:
var gl = canvas.getContext(‘webgl’);

Код шейдеров представляется обычной строкой и для использования его нужно скомпилировать:
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, document.getElementById('vertexShader').text);
gl.compileShader(vertexShader);

var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, document.getElementById('fragmentShader').text);
gl.compileShader(fragmentShader);

Для связывания двух типов шейдеров вместе используется шейдерная программа:
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

Если uniform-переменные связываются напрямую с переменными из js, то для атрибутов нужно использовать ещё одну сущность — буферы. Данные буферов хранятся в памяти видеокарты, что даёт значительный прирост в скорости рендеринга.

В нашем случае нам понадобятся:
  1. буфер вершин, который хранит всю информацию о расположению вершин геометрии;
  2. буфер цветов с информацией о цвете вершин.

Зададим буфер вершин:

Геометрия нашего треугольника

Вершины имеют координаты:
  • (0, 0, 0);
  • (0.5, 1, 0);
  • (1, 0, 0).

Стоит отметить, что при работе с буферами следует учитывать несколько особенностей:
  1. данные в буфер передаются одним массивом без вложенности, в случае нашего треугольника данные будут выглядеть следующим образом: [0, 0, 0, 0.5, 1, 0, 1, 0, 0];
  2. передаваться должны только типизированные массивы;
  3. прежде чем передать данные, вы должны точно указать, какой буфер будет использоваться, методом gl.bindBuffer.

Как это будет выглядеть в программе:
var vertexBuffer = gl.createBuffer();
var vertices = [0, 0, 0, 0.5, 1, 0, 1, 0, 0];
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices),  gl.STATIC_DRAW);

Создадим аналогичным образом буфер цветов. Цвет указываем для каждой из вершин в формате RGB, где каждая компонента цвета от 0 до 1:
var colorBuffer = gl.createBuffer();
var colors = [1, 0, 0, 0, 1, 0, 0, 0, 1];
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

Всё, что нам осталось, чтобы нарисовать треугольник, — это связать данные с переменными шейдерной программы и вызвать методы отрисовки. Для этого:
// Получим местоположение переменных в программе шейдеров
var uPosition = gl.getUniformLocation(program, 'u_position');
var aPosition = gl.getAttribLocation(program, 'a_position');
var aColor = gl.getAttribLocation(program, 'a_color');

// Укажем какую шейдерную программу мы намерены далее использовать
gl.useProgram(program);

// Передаем в uniform-переменную положение треугольника
gl.uniform3fv(uPosition, [0, 0, 0]);

// Связываем данные цветов
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.enableVertexAttribArray(aColor);
// Вторым аргументом передаём размерность, RGB имеет 3 компоненты
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);

// И вершин
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);

// Очищаем сцену, закрашивая её в белый цвет
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// Рисуем треугольник
// Третьим аргументом передаём количество вершин геометрии
gl.drawArrays(gl.TRIANGLES, 0, 3);

Наш треугольник готов:

Полный код примера можно
посмотреть здесь


Как я и говорил, цвет пикселей внутри треугольника линейно интерполируется между разноцветными вершинами. Мы смогли нарисовать самую простейшую фигуру с помощью WebGL и познакомились с шейдерами и буферами. Перейдём к следующему этапу.

Как нарисовать куб и заставить его вращаться


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



Нам придётся прописать каждую вершину каждого треугольника. Есть способы использовать более короткую запись, но для начала сделаем по-простому:
Это — просто
var vertexBuffer = gl.createBuffer();
var vertices = [
    // Передняя грань
    -1, -1, -1,
    1, -1, -1,
    -1, -1, 1,

    1, -1, 1,
    -1, -1, 1,
    1, -1, -1,

    // Задняя грань
    -1, 1, -1,
    -1, 1, 1,
    1, 1, -1,

    1, 1, 1,
    1, 1, -1,
    -1, 1, 1,

    // Нижняя грань
    -1, -1, -1,
    -1, 1, -1,
    1, -1, -1,

    1, 1, -1,
    1, -1, -1,
    -1, 1, -1,

    // Верхняя грань
    -1, -1, 1,
    1, -1, 1,
    -1, 1, 1,

    1, 1, 1,
    -1, 1, 1,
    1, -1, 1,

    // Левая грань
    -1, -1, -1,
    -1, -1, 1,
    -1, 1, -1,

    -1, 1, 1,
    -1, 1, -1,
    -1, -1, 1,

    // Правая грань
    1, -1, -1,
    1, 1, -1,
    1, -1, 1,

    1, 1, 1,
    1, -1, 1,
    1, 1, -1
];
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);


Аналогично запишем буфер цветов, раскрасив грани куба в три цвета:
  1. (1, 0.5, 0.5)
  2. (0.5, 0.7, 1)
  3. (0.3, 1, 0.3)

Тоже простой пример
var colorBuffer = gl.createBuffer();
var colors = [
    // Передняя грань
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,

    // Задняя грань
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,
    1, 0.5, 0.5,

    // Нижняя грань
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,

    // Верхняя грань
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,
    0.5, 0.7, 1,

    // Левая грань
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,

    // Правая грань
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3,
    0.3, 1, 0.3
];
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);


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

Известно, что матрица поворота в трёхмерном пространстве задаётся с помощью матрицы размером 3×3. К этой матрице добавляется вектор положения, таким образом, в итоге используется матрица 4×4.

WebGL никак не помогает нам работать с матрицами, поэтому, чтобы не тратить на них много времени, будем использовать довольно известную библиотеку glMatrix. Создадим с помощью неё единичную матрицу положения:
var cubeMatrix = mat4.create();

Чтобы отрисовать трёхмерный объект, нам нужно ввести понятие камеры. Камера, как и любой объект, имеет своё положение в пространстве. Она также определяет, какие объекты будут видны на экране, и отвечает за преобразование фигур так, чтобы на экране у нас создалась иллюзия 3D.


Перспектива куба на экране

За это преобразование отвечает матрица перспективы. C glMatrix она создаётся в две строчки:
var cameraMatrix = mat4.create();
mat4.perspective(cameraMatrix, 0.785, window.innerWidth / window.innerHeight, 0.1, 1000);

Метод mat4.perspective(matrix, fov, aspect, near, far) принимает пять параметров:
  1. matrix — матрица, которую необходимо изменить;
  2. fov — угл обзора в радианах;
  3. aspect — cоотношение сторон экрана;
  4. near — минимальное расстояние до объектов, которые будут видны;
  5. far — максимальное расстояние до объектов, которые будут видны.

Чтобы изображение куба попало в камеру, сдвинем камеру по оси Z:
mat4.translate(cameraMatrix, cameraMatrix, [0, 0, -5]);

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

Вершинный шейдер:
attribute vec3 a_position;
attribute vec3 a_color;
uniform mat4 u_cube;
uniform mat4 u_camera;
varying vec3 v_color;
void main(void) {
    v_color = a_color;
    gl_Position = u_camera * u_cube * vec4(a_position, 1.0);
}

Фрагментый шейдер:
precision mediump float;
varying vec3 v_color;
void main(void) {
    gl_FragColor = vec4(v_color.rgb, 1.0);
}

Инициализация шейдеров происходит точно так же, как и в случае треугольника:
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, document.getElementById('vertexShader').text);
gl.compileShader(vertexShader);

var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, document.getElementById('fragmentShader').text);
gl.compileShader(fragmentShader);

var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

var uCube = gl.getUniformLocation(program, 'u_cube');
var uCamera = gl.getUniformLocation(program, 'u_camera');
var aPosition = gl.getAttribLocation(program, 'a_position');
var aColor = gl.getAttribLocation(program, 'a_color');

Чтобы куб не стоял на месте, а вращался, необходимо постоянно менять его положение и обновлять кадр. Обновление происходит по средствам вызова встроенной функции requestAnimationFrame.

В отличие от других подобных методов, requestAnimationFrame вызывает переданную функцию только когда видеокарта свободна и готова к отрисовке следующего кадра.
// Создадим единичную матрицу положения куба
var cubeMatrix = mat4.create();

// Запомним время последней отрисовки кадра
var lastRenderTime = Date.now();

function render() {
    // Запрашиваем рендеринг на следующий кадр
    requestAnimationFrame(render);

    // Получаем время прошедшее с прошлого кадра
    var time = Date.now();
    var dt = lastRenderTime - time;

    // Вращаем куб относительно оси Y
    mat4.rotateY(cubeMatrix, cubeMatrix, dt / 1000);
    // Вращаем куб относительно оси Z
    mat4.rotateZ(cubeMatrix, cubeMatrix, dt / 1000);

    // Очищаем сцену, закрашивая её в белый цвет
    gl.clearColor(1.0, 1.0, 1.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Включаем фильтр глубины
    gl.enable(gl.DEPTH_TEST);

    gl.useProgram(program);

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.enableVertexAttribArray(aPosition);
    gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.enableVertexAttribArray(aColor);
    gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);

    gl.uniformMatrix4fv(uCube, false, cubeMatrix);
    gl.uniformMatrix4fv(uCamera, false, cameraMatrix);

    gl.drawArrays(gl.TRIANGLES, 0, 36);

    lastRenderTime = time;
}

render();

Получаем вращающийся куб:

Полный код примера можно
посмотреть здесь


Мы научились рисовать простой куб, поняли, как заставить его вращаться, и познакомились с понятиями матрицы положения и камеры.

Как отлаживать


Поскольку при работе с WebGL часть программы исполняется на стороне видеокарты, процесс отладки значительно усложняется. Нет привычных инструментов в виде DevTools и даже console.log. В интернете есть множество статей и докладов на эту тему, здесь же приведу лишь основные способы.

Чтобы понять, что код шейдеров был написан с ошибкой, после компиляции шейдеров можно использовать следующий метод:
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
    console.log(gl.getShaderInfoLog(vertexShader));
}

if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
    console.log(gl.getShaderInfoLog(fragmentShader));
}

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.log('Could not initialize shaders');
}

Также есть специальное расширение для браузеров WebGL-Inspector. Оно позволяет отследить загруженные шейдеры, буферы, текстуры в видеокарту, и вызовы методов WebGL.

Ещё есть Shader Editor, в Firefox DevTools этот функционал уже встроен, а для Chrome есть расширение, которое позволяет редактировать код шейдеров прямо в процессе работы приложения.

Куда двигаться дальше


В статье я попробовал осветить основные моменты, которые могут вызвать трудности во время изучения WebGL. Несмотря на то, что в работе требуется использовать разные векторы, матрицы и проекции, знать, как всё устроено внутри, необязательно. WebGL — отличный инструмент для решения целого ряда задач, и использовать его можно не только в геймдеве. Не бойтесь пробовать что-то новое, открывать для себя новые технологии и экспериментировать.

Напоследок — список полезных ресурсов, где можно продолжить изучение WebGL.
  • Полный код примеров с треугольником и кубом.
  • Краткая сводка WebGL с сайта Kronos Group.
  • Для более подробного изучения рекомендую пройти серию уроков WebGL Learning.
  • Бесплатный курс на Udacity по основам 3D. Хотя в курсе используется библиотека three.js, он будет полезен всем.
  • Доклад Владимира Агафонкина про WebGL и Mapbox c Frontend Dev Conf.
  • Слайды доклада Debugging and Optimizing WebGL Applications.
Tags:
Hubs:
+62
Comments 24
Comments Comments 24

Articles

Information

Website
2gis.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия