Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

Введение в программирование шейдеров для начинающих: часть 2

Scroll to top
Read Time: 10 min
This post is part of a series called A Beginner's Guide to Coding Graphics Shaders.
A Beginner's Guide to Coding Graphics Shaders
A Beginner's Guide to Coding Graphics Shaders: Part 3

() translation by (you can also view the original English article)

Сервис ShaderToy, который мы использовали в предыдущем уроке отличный для простых тестов и экспериментов, но он довольно таки ограничен. К примеру, мы не можем контролировать какие данные, среди прочих вещей, мы будем засылать в шейдер. Имея собственную среду для запуска шейдеров, означает что мы сможем делать все виды причудливых эффектов, которые в последствии мы сможем применить в своих собственных проектах!

Для этого мы будем использовать Three.js как наш основной фреймворк для запуска шейдеров. _Технология WebGL - это Javascript API, которая позволит на отрисовывать шейдеры, Three.js просто поможет нам это делать проще._

Если Вам не интересен JavaScript или веб-платформа как таковая, не волнуйтесь, мы не будем акцентировать внимания на спецификах отрисовки под веб (хотя, если Вы хотите узнать больше об этом фреймворке, обратите внимание на этот урок). _Настройка шейдеров в браузере - это самый быстрый способ начать, и привыкнув к этому процессу позволит Вам быстро настраивать и использовать шейдеры по любую платформу._

Настройка

_Данный раздел поможет Вам настроить шейдеры локально_ Это можно сделать используя данный шаблон на CodePen:

Вы можете модифицировать шаблон на CodePen.

Привет, Three.js!

_Three.js - это JavaScript фреймворк, который заботится о большом количестве шаблонного кода для WebGL, необходимого нам для запуска наших шейдеров._ Самым простым способом начать - является использование версии, расположенной на CDN.

Вот HTML файл, который содержит базовую Three.js-сцену.

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

Что бы его создать, нам нужно определить его геометрию и материл, а после этого добавить на сцену. Добавьте эту часть кода после Add your code here:

1
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
2
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00} );//We make it green

3
var cube = new THREE.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
function render() {
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 (OpenGL Shading Language). Учитывая, что WebGL основывается на OpenGL, нам придется иметь дело с GLSL.

Как и где писать GLSL-код? Как правило, GLSL-код загружается в виде string. Затем вы можете отправить его для парсинга и выполнения GPU.

В JavaScript для этого нужно просто добавить весь код в переменную:

1
var shaderCode = "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
void main() {
3
    gl_FragColor = vec4(1.0,1.0,1.0,1.0);
4
}
5
</script>

В этом случае компоненты vec4 соответствуют значениям rgba, как описано в предыдущем уроке.

Наконец, загрузим наш код. В JavaScript это делается с помощью простой строки, которая находит HTML-файл и разбирает код внутри него:

1
var shaderCode = document.getElementById("fragShader").innerHTML;

Эта строка должна располагаться ниже кода для куба.

Помните: только если код загружен в виде строки символов, он будет распознан как корректный GLSL-код (то есть void main() {...}. Остальное – это лишь стереотипный HTML-код).

Вы можете форкнуть и редактировать его на CodePen.

Шаг 2: Наложение шейдера

Методы наложения шейдеров могут отличаться в зависимости от платформы и способа взаимодействия с GPU. Однако ничего сложного здесь нет. В том же Google можно легко найти, как создать объект и применить к нему шейдеры с помощью Three.js.

Нам нужно создать специальный материал и отдать ему код нашего шейдера. Создадим плоскость (хотя для этого подошел бы и куб) и наложим на нее шейдер: Должен появиться белый экран:

1
//Create an object to apply the shaders to

2
var material = new THREE.ShaderMaterial({fragmentShader:shaderCode})
3
var geometry = new THREE.PlaneGeometry( 10, 10 );
4
var sprite = new THREE.Mesh( geometry,material );
5
scene.add( sprite );
6
sprite.position.z = -1;//Move it back so we can see it

Должен появиться белый экран:

Вы можете форкнуть и редактировать его на CodePen.


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

Задача: Как сделать одну часть экрана красной, а другую – синей? (Если возникли трудности, изучите следующий шаг).

Шаг 3: Отправка данных

Сейчас мы можем делать с шейдером что угодно. Но, откровенно говоря, делать с ним пока особо нечего.  У нас есть только встроенная позиция пикселя gl_FragCoord для работы, и, если вы помните, это не нормализовано. Мы должны иметь по крайней мере размеры экрана.

Данные следует отправлять в шейдер в виде так называемой uniform-переменной. Для этого нужно создать объект под названием uniforms и добавить в него наши переменные. Вот пример синтаксиса для отправки данных о разрешении экрана:

1
var uniforms = {};
2
uniforms.resolution = {type:'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)};
Каждая 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
var material = new THREE.ShaderMaterial({uniforms:uniforms,fragmentShader:shaderCode})

И это еще не все! Теперь мы можем использовать эту переменную. Давайте создадим градиент так же, как и в предыдущем уроке: отрегулируем координаты и настроим значение цвета.

Измените код следующим образом:

1
uniform vec2 resolution;//Uniform variables must be declared here first

2
void main() {
3
    //Now we can normalize our coordinate

4
	vec2 pos = gl_FragCoord.xy / resolution.xy;
5
    //And create a gradient!

6
    gl_FragColor = vec4(1.0,pos.x,pos.y,1.0);
7
}

И вы увидите красивый градиент!

Вы можете форкнуть и редактировать его на CodePen.

Если вам не совсем ясно, как нам удалось получить такой симпатичный градиент всего двумя строчками кода, загляните в первый урок. В нем мы подробно разбирали все нюансы.

Задача: Как разделить экран на четыре одинаковых сектора с разными цветами? Примерно так:

Шаг 4: Обновление данных

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

Обычно для обновления переменных нужно заново отправить uniform-переменную. Но в Three.js достаточно просто обновить объект uniforms в функции render:

Итак, вот как выглядит наша функция render после внесения такого изменения:

1
function render() {
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

2
var tex = THREE.ImageUtils.loadTexture( "https://tutsplus.github.io/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg" );

Первая строка вводится всего один раз. Сюда можно вставить URL-адрес любого изображения.

Теперь добавим текстуру в объект uniforms.

1
uniforms.texture = {type:'t',value:tex};

Наконец, объявим нашу uniform-переменную в коде шейдера и нарисуем ее тем же способом, что и в предыдущем уроке, – с помощью функции texture2D:

1
uniform vec2 resolution;
2
uniform sampler2D texture;
3
void main() {
4
	vec2 pos = gl_FragCoord.xy / resolution.xy;
5
    gl_FragColor = texture2D(texture,pos);
6
}

Вы должны увидеть растянутую на весь экран картинку с разноцветным драже:

Вы можете форкнуть и редактировать его на CodePen.

Это стандартное тестовое изображение для компьютерной графики, предоставленное Институтом обработки сигналов и изображений университета Южной Калифорнии (англ. Signal and Image Processing Institute, отсюда аббревиатура IPI). Оно прекрасно подходит для тестирования наших графических шейдеров.

Задача: Как сделать постепенный переход от полноцветной текстуры к оттенкам серого? Повторюсь, если возникнут трудности, обратитесь к первому уроку.

Бонусный шаг: Наложение шейдеров на другие объекты

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

1
var geometry = new THREE.PlaneGeometry( 10, 10 );

на такую:

1
var geometry = new THREE.BoxGeometry( 1, 1, 1 );

Вуаля! Теперь драже отображается на кубе:

Вы можете форкнуть и редактировать его на CodePen.

Вы можете сказать: Секундочку, но это же не совсем правильная проекция текстуры на куб! – и будете правы. Если внимательно посмотреть на шейдер, станет ясно, что мы всего лишь наложили все пиксели тестового изображения на экран. То есть изображение плоско проецируется на куб, а все пиксели за пределами куба обрезаются.

Для полноценной проекции на грани куба пришлось бы переделывать 3D-движок. Звучит немного глупо, учитывая, что у нас и так есть 3D-движок, который мы могли бы использовать, чтобы нарисовать текстуру на каждой грани отдельно. Но эта серия уроков в большей степени посвящена использованию шейдеров для получения эффектов, которых не удалось бы достичь другим путем.  Поэтому мы пока не будем разбирать данный вопрос. Если вам интересно, на Udacity есть отличный курс по основам 3D графики.

Дальнейшие шаги

На этом этапе вы уже знаете, как использовать ShaderToy, и вдобавок можете накладывать любые текстуры на любые поверхности и практически на любой платформе.

А значит, мы готовы перейти к более сложной теме – настройке системы освещения с реалистичными тенями.  Именно этим мы займемся в следующем уроке!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
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.