Какой рейтинг вас больше интересует?
|
Введение в WebGL2016-07-29 08:00:10 (читать в оригинале)В данном обзоре мы создадим простую 3d-программу, используя WebGL. Сначала мы будем использовать
WebGL Api напрямую. Потом сделаем варианты на популярных WebGL-фреймворках Three.js и BabylonJS. Небольшое вступлениеWebGL позволяет использовать в браузере преимущества аппаратного ускорения трехмерной графики без установки плагинов. WebGL основана на OpenGL ES 2.0, которая в свою очередь базируется на спецификации OpenGL 2.0 и используется на мобильных устройствах. Название “WebGL” можно интерпретировать как “OpenGL для браузеров”. В состав рабочей группы WebGL, разрабатывающей стандарт, входит некоммерческая организация Khronos Group, а также разработчики ведущих браузеров. Первая версия WebGL была выпущена в 2011 году. На данный момент последней является версия 1.0.3, выпущенная в 2014, и ожидается выход версии 2.0. Версия 2.0 основана уже на OpenGL ES 3.0 API. Все популярные браузеры (Safari, Chrome, Firefox, IE, Edge) поддерживают WebGL, в том числе и на мобильных устройствах. Позже всех включили поддержку WebGL в своих браузерах Apple и Microsoft: Apple - начиная с Safari 8 в 2014 году, Microsoft - начиная с IE 11 в 2013 году. WebGL используется не только для создания 3d-программ. Многие 2d-фреймворки используют WebGL, получая все преимущества аппаратного ускорения. Используем WebGL Api напрямуюДаже если вы используете WebGL-фреймворк и не собираетесь участвовать в разработке оного, знания по WebGL необходимы для понимания того, что происходит в вашей программе, для решения возникающих задач, в том числе проблем производительности. И Three.js и BabylonJS предоставляют api низкого уровня, которое близко к использованию нативного api. Иногда возникает потребность переписать / дописать часть кода фреймворка специально для своего приложения. Долгое время на официальном сайте Three.js можно было прочитать, что для рисования куба, используя только нативные средства браузера, понадобилось бы написать сотни строк кода. На самом деле нам действительно понадобится больше чем сотня строк, но все не так сложно как то там звучит. Шаг первыйСначала создадим html. Нам нужен элемент canvas и его контекст “webgl”.
Для поддержки устаревших версий браузеров, в частности Internet Explorer 11, нужно проверить контекст “experimental-webgl”.
JavaScript:
Чтобы что-то нарисовать в WebGL, как и в любой программе OpenGL, необходимы шейдеры. По сути, шейдеры - это программы на C-подобном языке, которые выполняются графической картой. Язык, используемый в шейдерах (shading language), ограничен и специально разработан для решения типичных графических задач, например матричных/векторных операций. В WebGL используется язык шейдеров OpenGL ES SL. Есть два типа шейдеров: вершинный (vertex shader) и фрагментный (fragment shader). Вершинный используется в основном для описания геометрии. Он выполняется для каждой вершины, которую передали шейдеру. Фрагментный шейдер выполняется для каждого фрагмента изображения (своего рода “пикселя”). Часть данных фрагментный шейдер получает от вершинного шейдера и эти данные интерполируются. В основном он используется для применения освещения, текстур. Его задача - определить цвет каждого фрагмента. Как видно из кода ниже, цвет фрагмента определяется путем присвоения значения специальной переменной gl_FragColor. В вершинном шейдере нужно присвоить значение специальной переменной gl_Position для задания координаты вершины. При этом используются данные, которые передаются из javascript-части, т.е. в нашем случае - это переменная-атрибут aPosition. Переменные gl_FragColor и gl_Position имеют специальное предназначение и их не нужно объявлять самому. Вершинный шейдер, записанный в виде массива:
Фрагментный шейдер в виде массива:
Т.к. шейдеры мы задали массивом, получим код вершинного шейдера таким образом
Рассмотрим код функции createShader, которая создает и компилирует шейдер. Да, компилирует, а потом нас ждет и линкование.
Создание шейдера с указанием типа
Указание кода шейдера
Компиляция шейдера
После компиляции шейдера хорошо бы проверить статус компиляции и вывести информацию об ошибках, если таковые были.
Cозданный шейдер понадобится далее для создания объекта WebGLProgram в методе initProgram. Сначала метод initProgram вызывает createShader, чтобы создать оба шейдера.
Потом создается программа WebGLProgram
Указываем оба скомпилированных шейдера:
И, наконец, обещанное линкование:
Хорошей практикой является проверка статуса линкования и логирование возможных ошибок.
После линкования нужно “принять к использованию” созданную программу, вызвав метод контекста useProgram
На этом этапе программа успешно создана. Остается только передать в шейдеры все нужные данные. Выглядит несколько громоздко, но зато гибко. Можно менять программы во время выполнения. В этом же методе я добавил код для получения ссылок на используемые шейдерами переменные. Получим ссылку на переменную aPosition таким образом
Т.е. ссылка будет храниться в this.prg.aPosition. Приготовим куб, для чего создадим конструктор CubeMesh.
Этому объекту, как видно, нужно передать список vertices (вершины) и indices (индексы или номера вершин). Удобно задавать геометрию в WebGL с помощью списка индексов. В WebGL есть несколько режимов рисования, один из них - это рисование треугольниками. Таким образом, чтобы нарисовать прямоугольник понадобится 2 треугольника. Чтобы нарисовать куб нужно 12 треугольников. Используя индексы, нам достаточно определить 8 вершин и передать графической системе координаты этих 8 вершин, т.е. массив из 24 чисел типа float для задания vertices и 12 наборов по три числа для передачи indices, т.е. дополнительно массив из 36 чисел типа integer. Каждый индекс указывает на соответствующую вершину в массиве вершин. Без индексов при использовании метода рисования gl.TRIANGLES понадобится 12 (кол-во треугольников) * 3 (три вершины) * 3 (3 координаты для каждой вершины) = 108 чисел типа float. Начало системы координат находится в центре области рисования. Ось Y направлена вверх, ось X направлена вправо. Что касается оси Z, то тут можно выбирать на свой вкус. WebGL не навязывает ни правостороннюю ни левостороннюю систему, хотя так называемая усеченная система координат (clip coordinate system) является левосторонней. В усеченной системе, все точки выходящие за отрезок [-1.0, 1.0] удаляются. Все координаты в конце концов приводятся к усеченной системе координат. В Three.js система координат правосторонняя (ось Z направлена на наблюдателя от экрана), как принято в большинстве программ OpenGL, а в BabylonJS левосторонняя система (как долгое время было в DirectX).
halfSize здесь это половина размера куба. Покаместь мы задали координаты в усеченной системе. После задания геометрии, нужно создать буферы памяти и поместить в них заготовленные данные (см. метод initBuffers).
В следующем участке кода задаются некоторые параметры, такие как цвет области рисования и проверка теста глубины.
Окончательный вид конструктора:
В последней строчке конструктора идет вызов метода renderLoop, который вызывает this.render(). В методе render и происходит рисование. renderLoop вызывается в каждом фрейме с помощью requestAnimationFrame, также как и при использовании 2d контекста канваса. Рассмотрим теперь метод render. В нем сначала очищается область рисования, а точнее буфер цвета и глубины.
Потом нужно активировать данные конкретного геометрического объекта. В нашем случае он один.
Если используются индексы, то рисование производится методом drawElements контекста
На данном этапе нарисован красный куб, хотя мы видим квадрат из-за его расположения. Можно было подобрать другие значения координат вершин, чтобы куб был повернут по другому. Итак, имеем всего около 128 строк. Что получилось на данный момент - демо шаг 1. Шаг второй - мировая система координат, вращающаяся камераПерейдем к более удобной системе координат, она будет, кстати, правосторонняя. Для этого создадим объект Camera. Камера будет ответственна за направление взгляда на сцену и за определение области видимости (frustrum), т.е. той области пространства за пределами, которой точки будут отброшены. Это достигается путем матричных преобразований, поэтому нам понадобится вспомогательный объект Matrix. Для удобства введем также объект Vector. Камера будет использовать перспективную проекцию, т.е. frustrum выглядит как усеченная пирамида. Конструктору будем передавать элемент canvas (используется для подсчета aspect ratio - отношения ширины кадра к высоте), угол обзора по оси y, расстояние до ближней и дальней плоскостей области видимости, а также позицию камеры в пространстве. Камера смотрит на начало координат. Реализованную камеру программно можно вращать по поверхности сферы с центром в начале координат, а также перемещать.
Матрицу преобразования камеры нужно передать в вершинный шейдер. Эта матрица состоит из двух матриц: матрицы вида и матрицы проекции. В данной программе в шейдер передаются обе матрицы отдельно и уже там они умножаются. Теперь вершинный шейдер выглядит так:
В initProgram добавлены две строчки для получения ссылок на переменные:
Чтобы передать данные в шейдер, в методе render добавлены такие две строчки перед прорисовкой сцены:
Поскольку сейчас используется камера, размеры куба можно увеличить, иначе он будет слишком маленький. В css я внес некоторые изменения, теперь область рисования занимает всю клиентскую часть браузера. Чтобы изображение оставалось пропорциональным добавлен метод handleSize, который вызывается при ресайзе окна и вначале работы программы. Наконец, в renderLoop перед вызовом метода render, добавим вызов this.camera.update() с несколькими строчками, которые обеспечивают вращение камеры вокруг начала координат и куба. Итак, вращающийся куб готов (на самом деле вращается камера) - демо шаг 2. Да, нам понадобилось почти три с половиной сотни строк, но выглядит это не как “страшные многие сотни”. Поэтому, считаю выражение в документации Three.js несколько преувеличенным. Заметно, что часть кода легко выносится отдельно для повторного использования: объект Matrix для операций с матрицами, Vector для операций с векторами, создание и компиляция шейдеров, функционал камеры. Шаг третий - важность светаДобавим теперь простенькое освещение, которое придаст объем трехмерной сцене. Будем использовать точечный источник цвета. Освещение в нашем случае будет вычисляться в вершинном шейдере. Вычисления во фрагментном шейдере дают более реалистичное затенение. Таким образом, в вершинном шейдере вычислим цвет каждой вершины. Чтобы передавать данные между шейдерами нужно использовать varying-переменные. Цвет будет передаваться во фрагментный шейдер с помощью varying-переменной vColor. Значения цвета при этом интерполируются. В вершинный шейдер добавлен следующий участок кода:
Как в вершинном, так и во фрагментном шейдере нужно объявить varying-переменную vColor. В вершинном шейдере понадобится еще uniform-переменная uLightPosition (позиция источника цвета), переменная-атрибут aNormal (нормаль к вычисляемой вершине) и матрица нормалей uNMatrix.
Во фрагментном шейдере все остается просто:
Получаем ссылки на переменные в initProgram:
В методе render передаем информацию о позиции источника света и матрицу нормалей:
Поскольку куб не двигается, в нашем случае его матрица нормалей будет всегда единичной матрицей и ее можно не использовать. Иначе, матрицу нормалей нужно пересчитывать для каждого освещаемого объекта на сцене при каждом перемещении. Для передачи данных о нормалях нужно создать буфер нормалей. Конструктор CubeMesh теперь принимает массив нормалей. В функции initBuffers добавлено создание буфера нормалей,
а в функции render:
И функция создания куба createCube конечно же претерпела изменения, т.к. нужно задать массив нормалей:
Итого, добавилось еще около 50 строчек кода и освещение готово. Можно заметить, что благодаря строчке
источник света перемещается вместе с камерой - демо. Используем Three.jsThree.js - одна из самых первых и самых популярных библиотек. Итак, реализуем функционал на Three.js.
Самые важные понятия в 3d-фреймворках это: renderer или engine, scene и camera. Начнем
с инициализации и настройки этих трех элементов в конструкторе приложения.
Как видно, дополнительные опции передаются в конструкторе в виде объекта. Таким образом можно указать renderer, что канвас должен занимать всю клиентскую область экрана:
Последний параметр называется updateStyle и заставляет поменять у канваса свойства style.width и style.height. Устанавливаем цвет фона:
Создаем сцену, которая будет содержать все графические объекты:
В Three.js есть несколько камер. Мы будем использовать камеру с перспективной проекцией.
Аргументы конструктора практически те же, что нужны нам были при создании оригинального приложения на нативном WebGL. Нам нужно указать fov (field of view) в градусах (в самостоятельно реализованной камере в радианах), aspect ratio (в самостоятельно реализованной камере высчитывался в коде камеры), near - расстояние до ближней плоскости отсечения области видимости, far - расстояние до дальней плоскости отсечения. Чтобы указать, на какую точку должна смотреть камера, нужно вызвать метод lookAt:
Для указания позиции камеры используем просто свойство position:
Кстати, position является экземпляром THREE.Vector3. Чтобы добавить камеру к сцене нужно выполнить следующий код:
В оригинальном приложении используется точечный источник света. Добавим его и здесь
Цвет света - это первый аргумент. Второй - интенсивность света. Третий аргумент позволяет влиять на эффект затухания света при удалении от него и это расстояние, где интенсивность равна нулю. Если указать ноль, то затухание будет отсутствовать. Именно такое поведение без эффекта затухания в оригинальном приложении, поэтому я указал 0. Следующий участок кода привяжет источник света к камере:
Этого мы достигали в первом приложении с помощью this.light.position = this.camera.position;. Задача сохранения пропорций изображения и его разрешения решается таким образом:
Последние две строчки очень важны. Они позволяют обновить свойство aspect камеры и ее матрицу проекций. В методе createCube, как и прежде, создается главный и единственный геометрический объект сцены - куб.
Как можно догадаться, все три аргументы - это размеры куба по соответствующим осям: x, y и z. Создание материала:
Названия параметров говорят сами за себя. Теперь можно создать экземпляр THREE.Mesh и добавить его к сцене:
Рассмотрим немного метод run.
Я думаю, можно легко увидеть соответствие между методами run в нативном приложении и приложении на Three.js. В методе update мы изменяем свойства rotation.x и rotation.y у cubeMesh, чтобы куб вращался вокруг осей x и y соответственно. Вращение задается в радианах.
Методы update и checkRotateLimits обеспечивают нужный характер вращения с ограничениями. Порт оригинального приложения на Three.js готов, для чего понадобилось около 85 строчек кода. Используем BabylonJSBabylonJS моложе Three.js. Первый релиз состоялся в 2013 году. Но BabylonJS стремительно развивается и сейчас является одним из самым популярных WebGL-фреймворков. Ссылка на официальный сайт BabylonJS. Настал черед портировать приложение на BabylonJS. На самом деле большинство фреймворков используют одинаковые понятия и многое окажется сходным с реализацией на Three.js. Структура приложения осталась та же, методы update и checkRotateLimits вообще не нужно менять. Проверить поддержку WebGL браузером в BabylonJS можно следующим образом:
После этой проверки создадим экземпляр BABYLON.Engine, который является аналогом THREE.WebGLRenderer. Первым аргументом передается элемент canvas. Второй включает / отключает поддержку сглаживания (antialias).
Создание и настройка сцены
В качестве камеры я выбрал ArcRotateCamera. Нужно заметить, что BabylonJS камера отвечает и за ее управление. Данная камера вращается вокруг указанной точке по сфере с указанным радиусом.
Первый аргумент - название камеры. Никто не мешает использовать несколько камер в одной сцене, существуют методы для
получения камеры по ее имени. Создание точечного источника света в указанной позиции и настройка некоторых его параметров:
Позиция источника света, указанная в конструкторе неважна, т.к. с помощью
мы привязываем источник света к камере. Таким образом, через свойство parent можно связать объекты в BabylonJS. Для того, чтобы картинка не искажалась при изменении размеров браузера, в BabylonJS достаточно сделать следующее
Т.е. все, что нужно - это вызвать метод resize у объекта engine. Создание Mesh в BabylonJS выглядит следующим образом
В BabylonJS, как в Three.js заготовлено много различных геометрических объектов для использования и различные виды материалов. Так выглядит метод run:
Нет необходимости использовать requestAnimationFrame. Вместо этого используется engine.runRenderLoop c указанием функции, которая должна выполняться в каждом фрейме. Для прорисовки сцены необходимо вызвать render
Можно зарегистрировать функции с помощью scene.registerBeforeRender и scene.registerAfterRender (названия говорят сами за себя). Как замечено выше, методы update и checkRotateLimits не поменялись. Каждый Mesh в BabylonJS содержит свойства с такими же именами, как в Three.js для вращения и изменения позиции: rotation и position. Реализация на BabylonJS готова, понадобилось около 80 строчек кода. Как видим, реализации на различных фреймворках оказались очень схожими. Тем не менее, нужно сказать, что BabylonJS позиционируется как полноценный игровой движок и в нем есть много удобных средств для разработки игр:
А Three.js позиционируется как 3D-библиотека общего назначения:
В то же время, в Three.js многое компенсируется плагинами. В целом, оба рассматриваемых средства разработки WebGL-приложений обладают хорошими возможностями и активно разрабатываются. Еще пару словТеперь пару слов о Three.js и BabylonJS с точки зрения организации этих проектов. Среди других средства разработки WebGL-приложений упомяну PlayCanvas, главным
достоинством которого является редактор с возможностью одновременной многопользовательской разработки. PlayCanvas бесплатен
только для публичных проектов.
|
Категория «Размышления»
Взлеты Топ 5
Падения Топ 5
Популярные за сутки
|
Загрузка...
взяты из открытых общедоступных источников и являются собственностью их авторов.