Сервис ShaderToy, который мы использовали в предыдущем уроке отличный для простых тестов и экспериментов, но он довольно таки ограничен. К примеру, мы не можем контролировать какие данные, среди прочих вещей, мы будем засылать в шейдер. Имея собственную среду для запуска шейдеров, означает что мы сможем делать все виды причудливых эффектов, которые в последствии мы сможем применить в своих собственных проектах!
Для этого мы будем использовать Three.js как наш основной фреймворк для запуска шейдеров. _Технология WebGL - это Javascript API, которая позволит на отрисовывать шейдеры, Three.js просто поможет нам это делать проще._
Если Вам не интересен JavaScript или веб-платформа как таковая, не волнуйтесь, мы не будем акцентировать внимания на спецификах отрисовки под веб (хотя, если Вы хотите узнать больше об этом фреймворке, обратите внимание на этот урок). _Настройка шейдеров в браузере - это самый быстрый способ начать, и привыкнув к этому процессу позволит Вам быстро настраивать и использовать шейдеры по любую платформу._
Настройка
_Данный раздел поможет Вам настроить шейдеры локально_ Это можно сделать используя данный шаблон на CodePen:
_Three.js - это JavaScript фреймворк, который заботится о большом количестве шаблонного кода для WebGL, необходимого нам для запуска наших шейдеров._ Самым простым способом начать - является использование версии, расположенной на CDN.
Вот HTML файл, который содержит базовую Three.js-сцену.
Сохраните этот файл на диск, для дальнейшего открытия в браузере. Вы увидите черный экран. Это не очень интересно, так что давайте попробуем добавить куб, просто чтобы убедиться, что все работает.
Что бы его создать, нам нужно определить его геометрию и материл, а после этого добавить на сцену. Добавьте эту часть кода после Add your code here:
1
vargeometry=newTHREE.BoxGeometry(1,1,1);
2
varmaterial=newTHREE.MeshBasicMaterial({color:0x00ff00});//We make it green
3
varcube=newTHREE.Mesh(geometry,material);
4
//Add it to the screen
5
scene.add(cube);
6
cube.position.z=-3;//Shift the cube back so we can see it
_Мы не станем вдаваться в подробности этого кода, потому как нам более интересна шейдерная часть._ Если всё пошло как надо, мы должны увидеть зеленый квадрат по центру экрана:
Пока мы тут, давайте начнем его вращать. Функция render вызывается каждый фрейм. Мы можем получить доступ к вращению кубе через cube.rotation.x (или .y, или .z). Попробуем инкрементировать эти значения, в итоге наша функция станет выглядеть следующим образом:
1
functionrender(){
2
cube.rotation.y+=0.02;
3
4
requestAnimationFrame(render);
5
renderer.render(scene,camera);
6
}
Задача: сможете ли Вы заставить куб вращаться вдоль другой оси координат? А что на счёт двух осей в одно и то же время?
Итак, теперь у нас всё настроено, давайте же напишем какие-нибудь шейдеры!
_Написание шейдеров_
С этого момента, мы можем начать думать о процессе написание шейдера. _Вы, вероятно, находите себя в ситуации не взирая на платформу, которую собрались использоват: вы всё подготовили, что-то начало рисоваться на экране, но как мне теперь получить доступ к GPU?_
Шаг 1: Загрузка в GLSL коде
Для построения нашей сцены мы используем JavaScript. В других случаях это может быть C++, Lua или любой другой язык. Так или иначе, для написания шейдеров используется специальный язык – Shading Language. Для OpenGL таким языком является GLSL (OpenGLShading Language). Учитывая, что WebGL основывается на OpenGL, нам придется иметь дело с GLSL.
Как и где писать GLSL-код? Как правило, GLSL-код загружается в виде string. Затем вы можете отправить его для парсинга и выполнения GPU.
В JavaScript для этого нужно просто добавить весь код в переменную:
1
varshaderCode="All your shader code here;"
Такой способ работает, но поскольку в JavaScript не так просто создавать многострочные строки, он нам не подходит. Большинство разработчиков пишут код шейдера в текстовом файле, меняют его расширение на .glsl или .frag (сокр. от fragment shader) и только потом загружают его.
Мы пойдем другим путем: напишем код нашего шейдера внутри тега <script> и оттуда загрузим его в JavaScript. Таким образом, мы сможем для удобства хранить все в одном файле.
Добавим внутрь нашего HTML-файла тег <script>:
1
<script id="fragShader"type="shader-code">
2
3
</script>
Чтобы потом было легко найти тег, присвоим ему идентификатор fragShader. На самом деле, типа shader-code не существует. (вместо него можно указать любое другое название) Нам это нужно для того, чтобы код не выполнялся и не отображался в HTML.
Теперь добавим самый простой шейдер, возвращающий только белый цвет.
1
<script id="fragShader"type="shader-code">
2
voidmain(){
3
gl_FragColor=vec4(1.0,1.0,1.0,1.0);
4
}
5
</script>
В этом случае компоненты vec4 соответствуют значениям rgba, как описано в предыдущем уроке.
Наконец, загрузим наш код. В JavaScript это делается с помощью простой строки, которая находит HTML-файл и разбирает код внутри него:
Эта строка должна располагаться ниже кода для куба.
Помните: только если код загружен в виде строки символов, он будет распознан как корректный GLSL-код (то есть void main() {...}. Остальное – это лишь стереотипный HTML-код).
Нам нужно создать специальный материал и отдать ему код нашего шейдера. Создадим плоскость (хотя для этого подошел бы и куб) и наложим на нее шейдер: Должен появиться белый экран:
Если изменить текущий цвет в коде шейдера на другой, новый цвет появится после обновления.
Задача: Как сделать одну часть экрана красной, а другую – синей? (Если возникли трудности, изучите следующий шаг).
Шаг 3: Отправка данных
Сейчас мы можем делать с шейдером что угодно. Но, откровенно говоря, делать с ним пока особо нечего. У нас есть только встроенная позиция пикселя gl_FragCoord для работы, и, если вы помните, это не нормализовано. Мы должны иметь по крайней мере размеры экрана.
Данные следует отправлять в шейдер в виде так называемой uniform-переменной. Для этого нужно создать объект под названием uniforms и добавить в него наши переменные. Вот пример синтаксиса для отправки данных о разрешении экрана:
Каждая uniform-переменная должна иметь два параметра: type и value. В данном случае мы имеем двумерный вектор, где в роли координат выступают ширина и высота экрана. Ниже приведена таблица (из спецификаций Three.js) со всеми типами и идентификаторами данных, которые можно отправить:
Единая форма записи
GLSL типы
JavaScript типы
'i', '1i'
int
Number
'f', '1f'
float
Number
'v2'
vec2
THREE.Vector2
'v3'
vec3
THREE.Vector3
'c'
vec3
THREE.Color
'v4'
vec4
THREE.Vector4
'm3'
mat3
THREE.Matrix3
'm4'
mat4
THREE.Matrix4
't'
sampler2D
THREE.Texture
't'
samplerCube
THREE.CubeTexture
Для отправки данных в шейдер добавляем их в ShaderMaterial:
И это еще не все! Теперь мы можем использовать эту переменную. Давайте создадим градиент так же, как и в предыдущем уроке: отрегулируем координаты и настроим значение цвета.
Измените код следующим образом:
1
uniformvec2resolution;//Uniform variables must be declared here first
Если вам не совсем ясно, как нам удалось получить такой симпатичный градиент всего двумя строчками кода, загляните в первый урок. В нем мы подробно разбирали все нюансы.
Задача: Как разделить экран на четыре одинаковых сектора с разными цветами? Примерно так:
Шаг 4: Обновление данных
Теперь мы умеем отправлять данные в шейдер. Но что если потребуется их обновить? Например, если вы откроете предыдущий пример в новой вкладке и измените размер окна, градиент не обновится, ведь он по-прежнему использует начальные параметры экрана.
Обычно для обновления переменных нужно заново отправить uniform-переменную. Но в Three.js достаточно просто обновить объект uniforms в функции render:
Итак, вот как выглядит наша функция render после внесения такого изменения:
1
functionrender(){
2
cube.rotation.y+=0.02;
3
uniforms.resolution.value.x=window.innerWidth;
4
uniforms.resolution.value.y=window.innerHeight;
5
6
requestAnimationFrame(render);
7
renderer.render(scene,camera);
8
}
Если вы откроете обновленный код на CodePen и поменяете размер окна, цвета изменятся, хотя начальная область просмотра остается неизменной. в этом легко убедиться, если посмотреть на цвета в каждом углу
Примечание: Такой способ отправки данных в GPU весьма ресурсоемкий. При отправке нескольких переменных за один кадр вы не почувствуете разницу. Но если переменных сотни, частота кадров заметно снизится. Это может прозвучать неправдоподобно, но если на экране несколько сотен объектов, и ко всем нужно применить разное освещение, ситуация может быстро выйти из-под контроля. В следующих статьях мы обязательно поговорим об оптимизации работы шейдеров.
Задача: Как сделать, чтобы цвета менялись со временем? Если возникли трудности, посмотрите, как мы справились с этим в первом уроке.
Шаг 5: Работа с текстурами
Независимо от того, какую платформу вы используете и в каком формате загружаете текстуры, они отправляются в шейдер в виде uniform-переменных.
Для справки: загружать файлы в JavaScript очень просто из внешнего URL (что мы и будем делать). При загрузке изображения с локального компьютера могут возникнуть проблемы с правами доступа, так как JavaScript не может и не должен иметь доступ к файлам вашей системы. Самый простой способ обойти это – настроить локальный сервер для Python. Но не волнуйтесь: это гораздо проще, чем кажется.
Three.js оснащен очень удобной функцией для загрузки изображения в виде текстуры:
1
THREE.ImageUtils.crossOrigin='';//Allows us to load an external image
Это стандартное тестовое изображение для компьютерной графики, предоставленное Институтом обработки сигналов и изображений университета Южной Калифорнии (англ. Signal and Image Processing Institute, отсюда аббревиатура IPI). Оно прекрасно подходит для тестирования наших графических шейдеров.
Задача: Как сделать постепенный переход от полноцветной текстуры к оттенкам серого? Повторюсь, если возникнут трудности, обратитесь к первому уроку.
Бонусный шаг: Наложение шейдеров на другие объекты
В созданной нами плоскости нет ничего особенного. Все это можно было бы наложить и на куб. По сути, для этого нужно изменить всего одну строчку геометрии, с такой:
Вы можете сказать: Секундочку, но это же не совсем правильная проекция текстуры на куб! – и будете правы. Если внимательно посмотреть на шейдер, станет ясно, что мы всего лишь наложили все пиксели тестового изображения на экран. То есть изображение плоско проецируется на куб, а все пиксели за пределами куба обрезаются.
Для полноценной проекции на грани куба пришлось бы переделывать 3D-движок. Звучит немного глупо, учитывая, что у нас и так есть 3D-движок, который мы могли бы использовать, чтобы нарисовать текстуру на каждой грани отдельно. Но эта серия уроков в большей степени посвящена использованию шейдеров для получения эффектов, которых не удалось бы достичь другим путем. Поэтому мы пока не будем разбирать данный вопрос. Если вам интересно, на Udacity есть отличный курс по основам 3D графики.
Дальнейшие шаги
На этом этапе вы уже знаете, как использовать ShaderToy, и вдобавок можете накладывать любые текстуры на любые поверхности и практически на любой платформе.
А значит, мы готовы перейти к более сложной теме – настройке системы освещения с реалистичными тенями. Именно этим мы займемся в следующем уроке!
Currently a student at St. Olaf College in Northfield, Minnesota. Originally from Egypt. I've been making games for over 7 years. I'm super passionate about building tools or crafting experiences that make life a little better. I enjoy teaching, and am usually far more eloquent on paper.