Advertisement
  1. Game Development
  2. WebGL

Создание шейдеров на основе Babylon.js и WebGL: теория и примеры

by
Read Time:20 minsLanguages:
Sponsored Content

This sponsored post features a product relevant to our readers while meeting our editorial guidelines for being objective and educational.

Russian (Pусский) translation by Marat Amerov (you can also view the original English article)

Во время своего доклада на второй день конференции Build 2014 евангелисты Microsoft Стивен Гуггенхаймер и Джон Шевчук рассказали о реализации поддержки Babylon.js для Oculus Rift. Одним из ключевых пунктов их демонстрации было упоминание разработанной нами технологии имитации линз:

Lens simulation imageLens simulation imageLens simulation image

Я также присутствовал на докладе Фрэнка Оливье и Бена Констебля на тему использования графики в IE с применением Babylon.js.

Эти доклады напомнили мне об одном вопросе, который мне часто задают в отношении Babylon.js: «Что вы подразумеваете под шейдерами?» Я решил посвятить этому вопросу целую статью с целью объяснить принцип работы шейдеров и привести несколько примеров их основных типов.

Теория

Прежде чем начинать наши опыты, нужно понять, как всё функционирует.

Работая с аппаратно ускоренной 3D графикой, мы имеем дело с двумя разными процессорами: центральным (CPU) и графическим (GPU). Графический процессор – это всего лишь разновидность крайне специализированного центрального процессора. 

GPU – это конечный автомат, настраиваемый посредством CPU. К примеру, именно CPU дает GPU команду отображать линии вместо треугольников, включить прозрачность и т. д.

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

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

Вершина – это своего рода точка в 3D пространстве (в отличие от точки в 2D пространстве).

Существует 2 вида шейдеров: вершинные и пиксельные (фрагментные) шейдеры.

Графический пайплайн

Прежде чем перейти непосредственно к шейдерам, сделаем еще одно небольшое отступление. Для отображения пикселей GPU получает из CPU данные геометрии.

С помощью буфера индексов, содержащего список индексов вершин, 3 вершины объединяются в треугольник. Каждая запись в буфере индексов соответствует номеру вершины в буфере вершин (это позволяет избежать дублирования вершин).

К примеру, буфер индексов на примере ниже – это список из двух граней: [1 2 3 1 3 4].  Первая грань содержит вершины 1, 2 и 3. Вторая грань содержит вершины 1, 3 и 4. Таким образом, в данном случае геометрия состоит из четырех вершин: 

Chart showing four verticesChart showing four verticesChart showing four vertices

Вершинный шейдер выполняется на каждой вершине треугольника. Основное предназначение вершинного шейдера – отобразить пиксель для каждой вершины (то есть выполнить проекцию 3D вершины на 2D экран).

vertex shader is applied on each vertex of the trianglevertex shader is applied on each vertex of the trianglevertex shader is applied on each vertex of the triangle

Используя эти 3 пикселя (задающие параметры 2D треугольника на экране), GPU проанализирует все относящиеся к пикселю (по крайней мере, к его положению) значения и применит пиксельный шейдер, чтобы сгенерировать цвет для каждого пикселя данного треугольника.

pixel shader will be applied on every pixel included into the 2D trianglepixel shader will be applied on every pixel included into the 2D trianglepixel shader will be applied on every pixel included into the 2D triangle

То же самое выполняется для всех граней в буфере индексов.

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

GLSL

Как было сказано ранее, для рендеринга треугольников графическому процессору потребуются 2 шейдера: вершинный и пиксельный. Оба пишутся на специальном языке под названием GLSL (Graphics Library Shader Language), который немного похож на C.

Специально для Internet Explorer 11 мы разработали компилятор, преобразовывающий GLSL в HLSL (High Level Shader Language) – шейдерный язык DirectX 11. Это позволило нам повысить безопасность кода шейдера:

Flow chart of transforming GLSL to HLSLFlow chart of transforming GLSL to HLSLFlow chart of transforming GLSL to HLSL

Вот пример простого вершинного шейдера:

Структура вершинного шейдера

Вершинный шейдер содержит следующие элементы:

  • Атрибуты: Атрибут определяет часть вершины. По умолчанию вершина должна иметь, по крайней мере, данные о положении (vector3:x, y, z). Но вы, как разработчик, можете предоставить больше данных. К примеру, в коде выше есть vector2 под названием uv (координаты текстуры, позволяющие нам применять 2D текстуру на 3D объект).
  • Uniform-переменные: Определяются центральным процессором и используются шейдером. Единственная uniform-переменная, которая есть у нас в данном случае, – это матрица, используемая для проекции положения вершины (x, y, z) на экран (x, y)
  • Varying-переменные: Представляют собой значения, которые создаются вершинным шейдером и передаются в пиксельный. В нашем случае вершинный шейдер передаст в пиксельный шейдер значение vUV (простая копия uv). Следовательно, здесь определяются координаты текстуры и положение пикселя. GPU добавит эти значения, а использовать их будет непосредственно пиксельный шейдер. 
  • main: Функция main() – это код, который выполняется в GPU для каждой вершины. Он должен как минимум давать значение для gl_position (положение текущей вершины на экране).

Как видно из примера выше, нет ничего сложного в вершинном шейдере. Он генерирует системную переменную (начинается на gl_) под названием gl_position, чтобы определить положение конкретного пикселя, а также задает varying-переменную под названием vUV

Волшебство в основе матриц

Матрица в нашем шейдере называется worldViewProjection. Она проецирует положение вершины в переменную gl_position.  Но как же нам получить значение этой матрицы? Поскольку это uniform-переменная, нам нужно определить её на стороне CPU (с помощью JavaScript).

Это трудный для понимания аспект работы с 3D графикой. Нужно неплохо разбираться в сложных математических вычислениях (или пользоваться 3D движком вроде Babylon.js, о чем мы поговорим позже).

Матрица worldViewProjection состоит из трех отдельных матриц:

The worldViewProjection matrix is the combination of three different matricesThe worldViewProjection matrix is the combination of three different matricesThe worldViewProjection matrix is the combination of three different matrices

В результате получается матрица, позволяющая преобразовывать 3D вершины в 2D пиксели, учитывая при этом позицию точки обзора и всё, что относится к положению, масштабу и повороту текущего объекта.

Задача 3D дизайнера – создать эту матрицу и поддерживать актуальность её данных.

И снова шейдеры

После того как вершинный шейдер выполнится на каждой вершине (то есть 3 раза), мы получим 3 пикселя с правильным значением vUV и gl_position. Далее GPU перенесет эти значения на каждый пиксель внутри треугольника, образованного тремя основными пикселями.

Затем к каждому пикселю будет применен пиксельный шейдер:

Структура пиксельного (или фрагментного) шейдера

По своей структуре пиксельный шейдер похож на вершинный:

  • Varying-переменные: Представляют собой значения, которые создаются вершинным шейдером и передаются в пиксельный шейдер. В нашем случае пиксельный шейдер получит из вершинного шейдера значение vUV.
  • Uniform-переменные: Определяются центральным процессором и используются шейдером. Единственная uniform-переменная, которая есть у нас в данном случае – это семплер, который нужен для считывания цветов текстуры.
  • main: Функция main – это код, который выполняется в GPU для каждого пикселя. Он должен как минимум давать значение для gl_FragColor (цвет текущего пикселя).

Этот пиксельный шейдер очень простой: он считывает цвет текстуры, используя координаты текстуры из вершинного шейдера (который, в свою очередь, получил их из вершины).

Вы хотите увидеть результат такого шейдера? Вот:

Вот что получилось в итоге. Рендеринг выполняется в реальном времени; вы можете двигать сферу мышкой.

Чтобы получить такой результат, нужно хорошенько поработать с кодом WebGL. Конечно, WebGL – это очень мощный API. Но он низкоуровневый, поэтому придется всё делать самостоятельно: от создания буфера до определения структуры вершин. Вам также нужно будет выполнять множество математических вычислений, настраивать состояния, управлять загрузкой текстуры и так далее.

Слишком сложно? BABYLON.ShaderMaterial спешит на помощь

Я знаю, о чем вы подумали: «Шейдеры – это, конечно, круто, но я не хочу разбираться во всех тонкостях WebGL и самостоятельно производить все вычисления».

Не проблема! Именно поэтому мы и создали Babylon.js.

Позвольте представить вам код, используемый предыдущем демо. Для начала вам понадобится простая веб-страница:

Шейдеры здесь задаются тегами <script>. В Babylon.js их также можно задавать в отдельных файлах формата .fx.

Babylon.js доступен для скачивания по ссылке здесь или в нашем репозитории на GitHub. Для получения доступа к объекту BABYLON.StandardMaterial нужна версия 1.11 и выше.

Наконец, основной JavaScript-код выглядит следующим образом:

Как видно, я использую BABYLON.ShaderMaterial, чтобы избавиться от необходимости компилировать шейдеры, линковать их или управлять ими.

При создании объекта BABYLON.ShaderMaterial нужно указать элемент DOM, используемый для хранения шейдеров или базовое имя файлов, в которых находятся шейдеры. Для второго варианта потребуется также создать по файлу для каждого шейдера, используя следующий принцип именования: basename.vertex.fx и basename.fragment.fx. Затем нужно будет создать материал вроде этого:

Нужно также указать имена любых используемых атрибутов и uniform-переменных. Затем можно напрямую задать значения uniform-переменных и семплеров с помощью функций setTexture, setFloat, setFloats, setColor3, setColor4, setVector2, setVector3, setVector4 и setMatrix.

Довольно просто, правда?

Помните матрицу worldViewProjection? С Babylon.js и BABYLON.ShaderMaterial вам не придется о ней волноваться. Объект BABYLON.ShaderMaterial вычислит всё автоматически, так как мы объявляем матрицу в списке uniform-переменных.

Объект BABYLON.ShaderMaterial может самостоятельно управлять следующими матрицами:

  • world
  • view
  • projection
  • worldView
  • worldViewProjection

Никаких сложных расчетов. К примеру, при каждом выполнении sphere.rotation.y += 0.05 матрица world данной сферы генерируется и передается в GPU.

CYOS: Создайте шейдер своими руками

Мы пойдем еще дальше и создадим страницу, на которой можно будет динамически редактировать шейдеры и наблюдать за результатом в реальном времени. Для этого нам понадобится прежний код, а также объект BABYLON.ShaderMaterial для компиляции и выполнения созданных шейдеров.

Я использовал для CYOS редактор кода под названием ACE. Он невероятно удобен и оснащен функцией подсветки синтаксиса. Не стесняйтесь взглянуть на него здесь. Здесь вы можете найти CYOS.

В поле Templates можно выбирать предустановленные шейдеры, мы поговорим о них немного позже.

Вы также можете изменить 3D объект, используемый для предпросмотра шейдеров в поле Meshes.

Кнопка Compile используется для создания нового объекта BABYLON.ShaderMaterial из шейдеров. Вот её код:

Подозрительно просто, правда? Итак, осталось только получить 3 предварительно вычисленных матрицы: world, worldView и worldViewProjection. Данные о вершинах будут содержать значения положения, нормали и координат текстур. Также загрузятся 2 следующие текстуры:

amiga textureamiga textureamiga texture
amiga.jpg
ref textureref textureref texture
ref.jpg

И, наконец, вот renderLoop, где я обновляю две удобные формы:

  • переменную time – чтобы получать забавные анимации
  • переменную cameraPosition – чтобы получать данные о положении камеры в шейдерах (что очень пригодится при расчете освещения)

К тому же, CYOS теперь доступен и для Windows Phone благодаря проделанной нами работе для Windows Phone 8.1:

CYOS on Windows PhoneCYOS on Windows PhoneCYOS on Windows Phone

Basic

Начнем с базового шейдера в CYOS.

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

Чтобы высчитать положение пикселя, нужна матрица worldViewProjection и положение вершины:

Координаты текстуры (uv) передаются в пиксельный шейдер неизмененными.

Обратите внимание на первую строчку: precision mediump float; – её обязательно нужно добавить в вершинный и пиксельный шейдер для правильной работы в Chrome. Она отвечает за то, чтобы для улучшения производительности не использовались числа высокой точности.

С пиксельным шейдером всё обстоит еще проще: нужно всего лишь использовать координаты текстуры и получить цвет текстуры:

Как было видно ранее, uniform-переменная textureSampler заполнена текстурой amiga, поэтому результат выглядит так:

Basic Shader resultBasic Shader resultBasic Shader result

Black and white

Перейдем ко второму шейдеру, черно-белому.

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

Самый простой способ добиться данного эффекта – взять всего один компонент, например, как показано ниже:

Мы использовали .ggg вместо .rgb (в компьютерной графике эта операция называется swizzle).

Но если нужно получить настоящий черно-белый эффект, лучше всего вычислить относительную яркость, которая учитывает все компоненты цвета:

Скалярное произведение вычисляется следующим образом:

result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z

В нашем случае:

luminance = r * 0.3 + g * 0.59 + b * 0.11 (эти значения рассчитываются с учетом того, что человеческий глаз более чувствителен к зеленому цвету)

Звучит круто, не так ли?

Black and white shader resultBlack and white shader resultBlack and white shader result

Cell Shading Shader

Следующий по списку – шейдер с заливкой ячеек, он немного сложнее.

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

Учите, что данные положения вершины и нормали к вершине передаются неизмененными, потому нам также нужно применить матрицу world, чтобы учитывать вращение объекта.

Вот как будет выглядеть пиксельный шейдер:

Этот шейдер предназначен для симуляции света, поэтому, чтобы не вычислять плавное затенение по всей поверхности объекта, мы будем высчитывать интенсивность света на основе нескольких порогов яркости. Например, если интенсивность равна от 1 (максимум) до 0.95, цвет объекта, взятый из текстуры, будет накладываться напрямую, без изменений. Если же интенсивность будет от 0.95 до 0.5, к значению цвета будет применен множитель 0.8 и так далее.

В итоге процесс создания такого шейдера можно разбить на 4 шага:

  • Сначала объявляем пороги яркости и константы для каждой степени интенсивности.
  • Рассчитываем освещение на основе алгоритма Фонга (исходя из соображения, что источник света не движется).

Интенсивность света, падающего на пиксель, зависит от угла между нормалью к вершине и направлением света.

  • Получаем цвет текстуры для пикселя.
  • Проверяем порог яркости и применяем константу соответствующей степени интенсивности.

В итоге мы получим нечто похожее на мультипликационный эффект:

Cell shading shader resultCell shading shader resultCell shading shader result

Phong Shader

Мы уже использовали алгоритм Фонга в предыдущем примере. Теперь рассмотрим его подробнее.

С вершинным шейдером всё будет довольно просто, так как основная часть работы придется на пиксельный:

Согласно алгоритму, нужно вычислить диффузную и зеркальную составляющие, используя направления света и нормаль к вершине:

В предыдущем примере мы использовали только диффузную составляющую, так что осталось только добавить зеркальную. Эта картинка из статьи на Википедии объясняет принцип работы шейдера:

Diffuse plus Specular equals Phong ReflectionDiffuse plus Specular equals Phong ReflectionDiffuse plus Specular equals Phong Reflection
Автор: Brad Smith aka Rainwarrior

Результат:

Phong shader resultPhong shader resultPhong shader result

Discard Shader

Для этого типа шейдера я бы хотел ввести новое понятие: ключевое слово discard. Такой шейдер будет игнорировать любой пиксель не красного цвета, создавая в результате иллюзию полого объекта.

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

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

Результат выглядит весьма забавно:

Discard shader resultDiscard shader resultDiscard shader result

Wave Shader

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

Для данного примера нам пригодится пиксельный шейдер с затенением по Фонгу.

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

Синус умножается на position.y, и это дает следующий результат:

Wave shader resultWave shader resultWave shader result

Spherical Environment Mapping

На создание данного шейдера нас вдохновил этот прекрасный туториал. Я дам вам прочитать эту прекрасную статью и поиграть с соответствующим шейдером.

Spherical environment mapping shaderSpherical environment mapping shaderSpherical environment mapping shader

Fresnel Shader

И напоследок мой любимый шейдер, Fresnel.

Он меняет интенсивность в зависимости от угла между направлением просмотра и нормалью к вершине.

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

Fresnel Shader resultFresnel Shader resultFresnel Shader result

Собственный шейдер?

Думаю, теперь вы готовы создать собственный шейдер. Не стесняйтесь использовать комментарии здесь или на форуме Babylon.js, чтобы поделиться своими экспериментами!

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

И еще несколько моих статей на ту же тему:

А также уроки по JavaScript от нашей команды:

И, конечно же, вы всегда можете воспользоваться некоторыми нашими бесплатными инструментами для оптимизации работы в вебе: Visual Studio Community, пробную версию Azure и кроссбраузерные инструменты для тестирования на Mac, Linux или Windows.

Эта статья является частью web dev tech серии от Microsoft. Мы рады поделиться с вами Microsoft Edge и новым механизмом рендеринга EdgeHTML. Получить бесплатно виртуальные машины или удаленное тестирование на вашем устройстве Mac, iOS, Android или Windows @ http://dev.modern.ie/.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.