Идея данной статьи родилась после нескольких мучительных недель изучения WebGL. На личном примере выяснилось, что люди, не сталкивающиеся до этого с 3D-графикой, имеют в основном ошибочные представления о работе данной технологии. К тому же была проблема с поиском информации в интернете.
WebGL, в отличие от Javascript, имеет высокий порог вхождения, его до сих пор мало кто использует, а ещё меньше тех, кто об этом пишет. Большинство руководств или статей перепрыгивают сразу на использование какой-нибудь библиотеки. Но мы-то с вами знаем, что большие универсальные инструменты не всегда пригодны для наших задач или, возможно, делают это на неприемлемом уровне: проигрывают в скорости, поставляются с ненужным багажом и т.д.
Этой статьёй хочется облегчить порог вхождения в чистый WebGL, дать начальное представление и подсказать, куда двигаться дальше.
Технология WebGL позволяет рисовать графику в браузере, используя возможности видеокарты, тогда как раньше мы могли использовать только процессор. Если вы не понимаете, что это даёт, советую посмотреть эту небольшую демонстрацию.
WebGL основан на OpenGL ES 2.0, который, в свою очередь, является специальной версией для работы на мобильных устройствах. Спецификация WebGL была выпущена в 2011 году, разрабатывается и поддерживается некоммерческой организацией Kronos Group, сайт которой частенько лежит, что ещё более усложняет изучение. Известно, что в настоящее время идёт разработка спецификации версии 2.0.
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 устанавливает цвет.
Шейдеры имеют три типа переменных, которые передаются из основной программы:
- attributes — доступны только в вершинном шейдере, разные для каждой из вершин;
- uniforms — доступны в обоих шейдерах и одинаковы для всех вызовов шейдера;
- 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, то для атрибутов нужно использовать ещё одну сущность — буферы. Данные буферов хранятся в памяти видеокарты, что даёт значительный прирост в скорости рендеринга.
В нашем случае нам понадобятся:
- буфер вершин, который хранит всю информацию о расположению вершин геометрии;
- буфер цветов с информацией о цвете вершин.
Зададим буфер вершин:
Вершины имеют координаты:
- (0, 0, 0);
- (0.5, 1, 0);
- (1, 0, 0).
Стоит отметить, что при работе с буферами следует учитывать несколько особенностей:
- данные в буфер передаются одним массивом без вложенности, в случае нашего треугольника данные будут выглядеть следующим образом: [0, 0, 0, 0.5, 1, 0, 1, 0, 0];
- передаваться должны только типизированные массивы;
- прежде чем передать данные, вы должны точно указать, какой буфер будет использоваться, методом 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, 0.5, 0.5)
- (0.5, 0.7, 1)
- (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, 45, window.innerWidth / window.innerHeight, 0.1, 1000);
Метод mat4.perspective(matrix, fov, aspect, near, far) принимает пять параметров:
- matrix — матрица, которую необходимо изменить;
- fov — угл обзора в радианах;
- aspect — cоотношение сторон экрана;
- near — минимальное расстояние до объектов, которые будут видны;
- 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.
ссылка на оригинал статьи http://habrahabr.ru/post/273735/
Добавить комментарий