Знакомство фронтендера с WebGL (часть 1)
Реальных моделей пока не было, поэтому на первое время мне предоставили модель яблока.
Основная проблема заключалась в том, что у меня не было опыта работы с 3D, я очень плохо знал математику и геометрию, а также у меня никогда не было опыта работы с WebGL. В общем, в свои силы я верил слабо. По итогу с задачей я справился, и я хочу рассказать об этом опыте в небольшом цикле статей.
Почему вообще WebGL?
Слово WebGL у меня ассоциируется с 3D. Я считаю, что больше нет нормальных способов отрендерить что-то в 3D без WebGL. Помимо того, что слово WebGL само по себе звучит очень круто, нашлись и другие причины выбрать эту технологию:
Мы хотели интерактивности. Например, чтобы моделька реагировала на движение курсора мыши по двум осям. С помощью видео такое сделать невозможно.
Адаптивность. Мы могли рисовать одну модельку под любые экраны и устройства (поддержка WebGL была подавляющей). Нам не нужно рендерить заранее кучу видюх под разные экраны и переживать о том, что на каком-то экране появится пикселизация.
Проблемы с iOS. На каждой версии были свои приколы с фоновыми видео на телефоне. На одной из версий видео вообще могло не запуститься из-за политик Apple, а иногда нужно было проводить специальный обряд, чтобы фоновое видео заработало. Таких проблем с WebGL пока нет и, надеюсь, не будет.
IE11 тоже поддерживает WebGL. Да, есть нюанс, но он даже не стоит внимания.
В SVG невозможно сделать полноценный 3D, только псевдо. Также браузеры плохо себя чувствуют, когда SVG элементов в DOM больше 10 000. Это показали мои попытки отрендерить модельку в SVG.
Я думаю, первого пункта было бы достаточно, чтоб принять решение в пользу webGL.
С чего начал поиск решения задачи
Модель яблока была в формате OBJ, насчет других форматов я решил не думать. Этот формат текстовый и это придавало мне какой-то уверенности, что с решений в интернете должно быть много.
Я знал про существования библиотек three.js, Babylon.js и PixiJS (это вообще 2D-рендерер). Вес 3D-библиотек огромный, как бы они не были сжаты. Я не хотел пускать таких монстров к себе на сайт, у меня и так был 100 кб react-dom, куда еще больше? А чтобы разобраться в 3D-библиотеках, все равно нужно было иметь какие-то представления о 3D-графике. Поэтому я гуглил «webgl obj model render». Я находил только онлайн-просмотрщики или какие-то узко специфичные решения, которые я даже запустить не смог. Также искал демки на CodePen, но там мало что находилось под мою задачу и критерии. А если что-то и находил, я просто не мог понять, что вообще происходит и что мне делать. В итоге я решил, что если не получу базу в WebGL, я не смогу решить задачу.
Погружение в WebGL
Не знаю как так получилось, но в интернете на глаза ресурсы по WebGL мне не попадались, поэтому я пошел Телеграм-чат @webgl_ru (найти его было просто) и спросил как начать в WebGL фронтендщику? Похоже, что в чат постоянно заходили подобные мне фронтендщики с аналогичными вопросами, поэтому у ребят из чата уже был заготовлен список ресурсов, который они мне и скинули. Впоследствии участники данного чата мне еще не раз помогли, за что им огромное спасибо.
Я из списка, который мне скинули, я выбрал ресурс WebGL Fundamentals, у которого было довольно говорящее название, а так же перевод на русский. Обычно я не вижу ничего ужасного в английской документации, но WebGL казался мне чем-то инородным и страшным, а также состоящим из подходов, которые доселе не были мне знакомы. То, что WebGL рендерит это всё через Canvas — это было единственное, что я знал об этой технологии.
Что вообще такое WebGL
Первое, что бросается в глаза, — это необычный API. Привычное нам браузерное API — это просто вызов методов у каких-то встроенных объектов/классов, тогда как апи WebGL это как будто вы программно настраиваете repl node.js, а потом прокидываете в этот repl строки javascript кода и получаете какой-то результат из этого. В случаи webgl вы настраиваете внутри браузера обрезанную версию OpenGL (библиотека которая заставляет наши видео-карты что-то рисовать) и прокидываете в нее код на языке GLSL. GLSL к счастью обрезанный C подобный язык и изучать новый синтаксис не придется, как будто пишешь на es3 javascript.
Если обобщить, то работа на webgl выглядит так:
Получаете доступ к webgl (по сути к openGL, но версия обрезанная и потому это и называется webgl).
Выставляете какие-нить специальные флаги, которые могут менять работу рендера.
Пишите программу на GLSL. Сама программа это просто функции которые принимает данные и выплевывает какой-то результат. Данные это просто точки в координатной системе, а так же это может быть цветом. Тип как указать положение div через absolute и поставить его центр на высоте 300 пикселей.
Пространство в webgl имеет координаты от -1 до 1, потому, нам нужно преобразовывать координаты разных фигур в координаты от -1 до 1, если они за пределами.
Прокидываете через специальный апи эти координаты и цвета и другие параметры.
Мы получаем 2D/3D картинку.
Шейдеры
Выше я говорил про программы на GLSL, программа всегда состоит из 2 шейдеров. Шейдер есть функция.
Каждая программа состоит из вершинного шейдера (Vertex Shader) и фрагментого шейдера (Fragment Shader).
Вершинный шейдер — позволяет разметить пространство, а фрагментный — закрашивает это пространство. Так работают видео карты. Сначала им нужно выставить точки в пространстве, потом соединить эти точки невидимыми линиями, а потом закрасить каждый пиксель внутри получившийся фигуры.
Если приводить пример из жизни, у вас есть стенка 1 м х 1 м и есть художник по имени Видеокарта. Вот вы и ему говорите: «Поставь мне точку на 30 сантиметров от верха и 50 см слева, потом точку в 50×50, потом в 70×80, соедини мне это линиями и закрась получившееся пространство красным».
Сами шейдеры выглядит так:
Вершинный шейдер (Vertex Shader)
// атрибут, который будет получать данные которые мы передали.
attribute vec4 a_position;
// все шейдеры имеют функцию main
void main() {
// gl_Position - специальная переменная вершинного шейдера,
// которая отвечает за установку положения
gl_Position = a_position;
}
Фрагментный шейдер (Fragment Shader)
// фрагментные шейдеры не имеют точности по умолчанию, поэтому нам необходимо её
// указать. mediump подойдёт для большинства случаев. Он означает "средняя точность"
precision mediump float;
void main() {
// gl_FragColor - специальная переменная фрагментного шейдера.
// Она отвечает за установку цвета.
gl_FragColor = vec4(1, 0, 0, 1); // вернёт красный
}
В вершинном шейдере вы увидели attribute
. Всего в шейдеры можно прокинуть несколько типов переменных (дальше копипаста с webglfundamentals.org):
Атрибуты и буферы
Буферы — это массивы бинарных данных, загруженных в графический процессор. Обычно буферы содержат вещи вроде положений вершин, нормалей, координат текстур, цветов вершин и т.д., хотя вы вольны положить в них что угодно.
Атрибуты определяют, каким образом данные из ваших буферов передаются в вершинный шейдер. Например, вы можете поместить положения вершин в буфер как три 32-битных числа с плавающей точкой на одно положение. Вы указываете конкретному атрибуту, откуда брать положения вершин, какой тип данных используется (три 32-битных числа с плавающей точкой), начиная с какого индекса в буфере начинаются положения вершин и какое количество байтов нужно получить от одного положения до следующего.
Доступ к буферам не произвольный. Вместо этого вершинный шейдер выполняется заданное количество раз и каждый раз, когда он выполняется, выбирается следующее значение каждого из указанных буферов и назначается атрибуту.
Uniform-переменные
Uniform-переменные — это глобальные переменные, которые устанавливаются перед выполнением программы шейдера.
Текстуры
Текстуры — это массивы данных, к которым есть произвольный доступ в программе шейдера. Чаще всего в текстуру помещается картинка, но текстура — это просто набор данных и вы можете запросто поместить в неё что-то отличное от набора цветов.
constying-переменные
constying-переменные позволяют передавать данные из вершинного шейдера фрагментному шейдеру. Во фрагментном шейдере мы получим интерполированные значения вершинного шейдера — зависит от того, отображаем ли мы точки, линии или треугольники.
Вершинный шейдер выполняется на каждую порцию x, y, z (z может не указываться, если рисуем в 2D) координат. Каждые такие координаты создают вершину. А дальше уже эти вершины объединяются в треугольники (полигоны) и потом фрагментным шейдером эти треугольники закрашиваются.
Вы спросите, почему именно треугольники?
В процессе обучения я на это не обращал внимания, но когда начал предпринимать попытки нарисовать модельку тоже удивился, но оказывается любую фигуру можно нарисовать через ТРЕУГОЛЬНИКИ (ПОЛИГОНЫ) и потому, добавлять другие фигуры бессмысленно.
Треугольник есть абсолют.
В webgl можно рисовать только треугольниками, линиями и точками.
Если рисовать через линии, то будут только отрисовываться грани между вершинами, но все что внутри фигуры не будет закрашиваться. Так же можно просто рисовать линии указывая сколько нужно точек прежде чем запустить фрагментный шейдер.
Если рисовать через точки, то будут отрисовываться только вершины.
Матрицы
В процессе изучения, я узнал про матрицы. Это пришло из математики и для js разраба это выглядит как массив из 9 или 12 чисел (12 для 3D). Матрицы решают вопросы того как трансформировать модель (а точней вершины), чтоб поставить модельку в нужное место в пространстве, увеличить или покрутить. Матрицы так же позволяют создавать камеры, то есть менять вид обзора и другое. Вы могли с ними встречаться, если работали transform: matrix(...n)
в css.
Матрицы один из фундаментов 2D/3D графики, а так же наверно одна из немногих вещей которой можно пользоваться не разбираясь как она работает.
Достаточно запомнить, что чтоб применить несколько трансформаций нужно просто матрицы друг на друга перемножить и результат передать в шейдер, а так же, что матрица 3×3 для 2D трансформаций, а 4×4 для 3D трансформаций.
А дальше использовать знакомые нам названия из библиотеки gl-matrix. Более подробно про матрицы можно узнать на webgl fundumentals.
Hello world на webgl
Так как все же выглядит hello world код на webgl? Что вообще требуется для того, чтоб это запустить и нарисовать треугольник?
Нужно получить ссылку на canvas элемент.
Получить из него webgl контекст, то есть то что нам позволит обращаться к webgl и рисовать через него.
Создать программу из вершинного шейдера и фрагментного шейдера.
Получить ссылки на переменные из шейдеров.
Присвоить переменным данные
Запустить функцию drawArrays у webgl.
Вот наш треугольник
Песочница с комментариями.
И после такого количества кода (по ссылке), мы получаем треугольник.
Честно говоря, это безумное количества кода ради треугольника немного остудило желание, но автор объяснил, что все это можно убрать под хелперы, а в будущем это позволяет создавать удивительные вещи внутри браузера, которые вы можете увидеть в примерах от threejs.
А также это объясняет безумные размеры 3D либ.
Мне и моему коллеге дизайнеру была поставлена задача разработать новую версию сайта-визитки компании. Коллега полгода учился работать с 3D-редакторами (в нерабочее время на Maxon Cinema 4D), поэтому он хотел использовать свои новые навыки при создании новой версии сайта. Его идея заключалась в том, что на каждой странице на первом экране будет крутиться какая-нибудь непонятная фигура с каким-нибудь красивым текстом. Выглядеть это должно было примерно так: