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

в 11:01, , рубрики: glsl, GPUGame, graphics, html, javascript, Platform Agnostic, programming, shaders, three.js, Web Platform, Блог компании Plarium, Программирование, Работа с 3D-графикой, разработка игр

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

Введение в программирование шейдеров: часть 2 - 1

Первую часть материала «Введение в программирование шейдеров» можно прочитать по ссылке.

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

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

Если вас не интересует JavaScript или разработка для веб-платформ, не волнуйтесь: мы не будем вплотную заниматься спецификой веб-рендеринга (если вы хотите узнать больше о Three.js, ознакомьтесь с этим уроком). Дело в том, что настройка шейдеров в браузере – это самый быстрый способ начать работу, но по мере ознакомления с процессом вы без труда сможете настраивать и использовать шейдеры на любой другой платформе.

Настройка

В этом разделе мы поговорим о том, как настраивать шейдеры локально. Исходный код доступен для просмотра на CodePen:

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

Знакомьтесь, Three.js!

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

Здесь можно скачать HTML-код базовой сцены Three.js.

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

Прежде чем добавить куб в сцену, нужно определить его геометрию и материал. Вставьте следующий фрагмент кода под надписью Add your code here:

var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00} );//Сделаем его зеленым
var cube = new THREE.Mesh( geometry, material );
// Добавим на экран
scene.add( cube );
cube.position.z = -3;//Отодвинем назад, чтобы видеть его

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

Введение в программирование шейдеров: часть 2 - 3

Теперь давайте добавим вращение. Функция render вызывается для каждого кадра. Вращение куба задается с помощью cube.rotation.x (или .y или .z). Здесь можно поиграть со значениями, но конечная функция рендера должна выглядеть примерно так:

function render() {
    cube.rotation.y += 0.02;
     
    requestAnimationFrame( render );
    renderer.render( scene, camera );
}

Задача: Как сделать, чтобы куб вращался по другой оси? А по двум осям одновременно?
Итак, все готово, пора добавлять шейдеры!

Добавление шейдеров

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

Шаг 1: Загрузка в GLSL-код

Для построения нашей сцены мы используем JavaScript. В других случаях это может быть C++, Lua или любой другой язык. Так или иначе, для написания шейдеров используется специальный язык – Shading Language. Для OpenGL таким языком является GLSL (OpenGL Shading Language). Учитывая, что WebGL основывается на OpenGL, нам придется иметь дело с GLSL.

Как и где писать GLSL-код? Как правило, GLSL-код загружается в виде строки символов (string), которая затем парсится и выполняется в GPU.
В JavaScript для этого нужно просто добавить весь код в переменную:

var shaderCode = "All your shader code here;"

Такой способ работает, но поскольку в JavaScript не так просто создавать многострочные строки, он нам не подходит. Большинство разработчиков пишут код шейдера в текстовом файле, меняют его расширение на .glsl или .frag (сокр. от “fragment shader”) и только потом загружают его.

Мы пойдем другим путем: напишем код нашего шейдера внутри тега script и оттуда загрузим его в JavaScript. Таким образом, мы сможем для удобства хранить все в одном файле.

Добавим внутрь нашего HTML-файла тег script:

<script id="fragShader" type="shader-code">
 
</script>

Чтобы потом было легко найти тег, присвоим ему идентификатор fragShader. На самом деле, типа shader-code не существует (вместо него можно указать любое другое название). Нам это нужно для того, чтобы код не выполнялся и не отображался в HTML.
Теперь добавим самый простой шейдер, возвращающий только белый цвет.

<script id="fragShader" type="shader-code">
void main() {
    gl_FragColor = vec4(1.0,1.0,1.0,1.0);
}
</script>

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

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

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

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

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

// Создадим объект, к которому применим шейдер
var material = new THREE.ShaderMaterial({fragmentShader:shaderCode})
var geometry = new THREE.PlaneGeometry( 10, 10 );
var sprite = new THREE.Mesh( geometry,material );
scene.add( sprite );
sprite.position.z = -1; // Отодвинем назад, чтобы видеть его

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

Введение в программирование шейдеров: часть 2 - 4

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

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

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

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

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

var uniforms = {};
uniforms.resolution = {type:'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)};

Каждая uniform-переменная должна иметь два параметра: type и value. В данном случае мы имеем двумерный вектор, где в роли координат выступают ширина и высота экрана. Ниже приведена таблица (из спецификаций Three.js) со всеми типами и идентификаторами данных, которые можно отправить:

Введение в программирование шейдеров: часть 2 - 5

Для отправки данных в шейдер добавляем их в ShaderMaterial:

var material = new THREE.ShaderMaterial({uniforms:uniforms,fragmentShader:shaderCode})

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

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

uniform vec2 resolution; //Сначала объявляются uniform-переменные
void main() {
    // Теперь можем отрегулировать координаты
    vec2 pos = gl_FragCoord.xy / resolution.xy;
    // И создать градиент!
    gl_FragColor = vec4(1.0,pos.x,pos.y,1.0);
}

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

Введение в программирование шейдеров: часть 2 - 6

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

Введение в программирование шейдеров: часть 2 - 7

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

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

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

function render() {
    cube.rotation.y += 0.02;
    uniforms.resolution.value.x = window.innerWidth;
    uniforms.resolution.value.y = window.innerHeight;
 
    requestAnimationFrame( render );
    renderer.render( scene, camera );
}

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

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

Задача: Как сделать, чтобы цвета менялись со временем? Если возникли трудности, посмотрите, как мы справились с этим в первом уроке.

Шаг 5: Работа с текстурами

Независимо от того, какую платформу вы используете и в каком формате загружаете текстуры, они отправляются в шейдер в виде uniform-переменных.

Для справки: загружать файлы в JavaScript очень просто из внешнего URL (что мы и будем делать). При загрузке изображения с локального компьютера могут возникнуть проблемы с правами доступа, так как JavaScript не может и не должен иметь доступ к файлам вашей системы. Самый простой способ обойти это – настроить локальный сервер для Python. Но не волнуйтесь: это гораздо проще, чем кажется.
Three.js оснащен очень удобной функцией для загрузки изображения в виде текстуры:

THREE.ImageUtils.crossOrigin = ''; // Позволяет загружать изображения из внешних источников
var tex = THREE.ImageUtils.loadTexture( "https://tutsplus.github.io/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg" );

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

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

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


uniform vec2 resolution;
uniform sampler2D texture;
void main() {
    vec2 pos = gl_FragCoord.xy / resolution.xy;
    gl_FragColor = texture2D(texture,pos);
}

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

Введение в программирование шейдеров: часть 2 - 8

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

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

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

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

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

на такую:

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

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

Введение в программирование шейдеров: часть 2 - 9

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

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

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

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

Автор: Plarium

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js