Руководство по шейдерам

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

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


Сцена из Minecraft, до и после добавления нескольких шейдеров.

Задача этого туториала

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

Это общее руководство, поэтому всё изученное в нём можно применять в любой технологии, использующей шейдеры.

Что же такое шейдер?

Шейдер — это просто программа, выполняемая в графическом конвейере. Она сообщает компьютеру, как рендерить каждый пиксель. Эти программы называются шейдерами («затенителями»), потому что их часто используют для управления эффектами освещения и затенения, но ничего не мешает использовать их и для других спецэффектов.

Шейдеры пишут на специальном языке шейдеров. Не волнуйтесь, вам не придётся изучать совершенно новый язык: мы будем использовать GLSL (OpenGL Shading Language), который похож на C. (Существует несколько языков написания шейдеров для разных платформ, но поскольку все они адаптированы под выполнение в видеопроцессоре, то похожи друг на друга.)

Примечание: эта статья посвящена исключительно фрагментным шейдерам (fragment shader). Если вам любопытно, какие ещё виды шейдеров бывают, то можно почитать о различных этапах графического конвейера в OpenGL Wiki.

Приступаем!

В этом туториале мы будем использовать ShaderToy. Он позволяет нам начать программировать шейдеры прямо в браузере, без возни с установкой и настройкой! (Для рендеринга он использует WebGL, поэтому требуется браузер с поддержкой этой технологии.) Создавать учётную запись не обязательно, но удобно для сохранения кода.

Примечание: на момент написания статьи ShaderToy находился в состоянии беты [прим. пер.: статья написана в 2015 году]. Некоторые детали интерфейса/синтаксиса могут немного отличаться.

Нажав на New Shader, вы увидите что-то вроде этого:

Если вы не зарегистрировались, интерфейс может немного отличаться.

Маленькая чёрная стрелка внизу служит для компиляции кода.

Что происходит?

Я собираюсь описать то, как работают шейдеры, одним предложением. Приготовились? Вот оно!

Единственное предназначение шейдера — возврат четырёх чисел:r, g, b и a.

Именно это, и только это может делать шейдер. Функция выполняется для каждого пикселя на экране. Она возвращает эти четыре цветовых значения, которые становятся цветом пикселя. Это то, что называется пиксельным шейдером (иногда его называют фрагментным шейдером).

Запомнив это, давайте зальём экран сплошным красным цветом. Значения RGBA (red, green, blue и alpha, определяющая прозрачность) меняются от 0 до 1, поэтому единственное, что нужно сделать — вернуть r,g,b,a = 1,0,0,1. ShaderToy ожидает, что окончательный цвет пикселя будет храниться в fragColor.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    fragColor = vec4(1.0,0.0,0.0,1.0);
}

Поздравляю! Вот ваш первый готовый шейдер!

Задача: Попробуйте изменить цвет на сплошной серый.

vec4 — это просто тип данных, поэтому мы могли бы объявить цвет как переменную, вот так:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec4 solidRed = vec4(1.0,0.0,0.0,1.0);
    fragColor = solidRed;
}

Выглядит не очень вдохновляюще. У нас есть мощь, способная выполнять код для сотен тысяч пикселей одновременно, а мы закрашиваем их одним цветом.

Давайте попробуем отрендерить на экране градиент. Мы не сможем добиться многого, не зная пары вещей о пикселях, на которые мы влияем, например, об их положении на экране…

Входные данные шейдера

Пиксельный шейдер передаёт несколько переменных, которые мы можем использовать. Самая полезная — это fragCoord, содержащая координаты X и Y пикселя (и Z, если вы работаете в 3D). Давайте попробуем превратить все пиксели в левой части экрана в чёрные, а все пиксели справа — в красные:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 xy = fragCoord.xy; //Получаем координаты текущего пикселя
    vec4 solidRed = vec4(0,0.0,0.0,1.0);//Теперь он стал чёрным
    if(xy.x > 300.0){//Произвольное число, мы не знаем размер экрана!
        solidRed.r = 1.0;//Присваиваем красному компоненту значение 1.0
    }
    fragColor = solidRed;
}

Примечание: Получить доступ к компонентам любого vec4 можно с помощью obj.x, obj.y, obj.z и obj.w или с помощью obj.r, obj.g, obj.b, obj.a. Они эквивалентны, это просто удобный способ наименования, упрощающий чтение кода, потому что когда люди видят obj.r, то понимают, что obj представляет собой цвет.

Вы заметили проблему в приведённом выше коде? Попробуйте нажать на кнопку go fullscreen в правом нижнем углу окна предварительного просмотра.

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

Если что-то не является встроенной переменной, то можно отправить эту информацию из центрального процессора (из основной программы) в видеопроцессор (в ваш шейдер). ShaderToy берёт эту задачу на себя. Все передаваемые шейдеру переменные указаны во вкладке Shader Inputs. Переменные, передаваемые таким образом из ЦП в видеопроцессор, называются в GLSL uniform (глобальными).

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

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 xy = fragCoord.xy; //Получаем координаты текущего пикселя
    xy.x = xy.x / iResolution.x; //Делим координаты на размер экрана
    xy.y = xy.y / iResolution.y;
    // Теперь x для самого левого пикселя равен 0, а для самого правого равен 1
    vec4 solidRed = vec4(0,0.0,0.0,1.0); //Теперь он стал чёрным
    if(xy.x > 0.5){
        solidRed.r = 1.0; //Присваиваем красному компоненту значение 1.0
    }
    fragColor = solidRed;
}

Если попробовать теперь изменить размер окна предварительного просмотра, то цвета должны точно разделить экран пополам.

От разделения к градиенту

Превратить это в градиент будет довольно просто. Значения цветов изменяются от 0 до 1, и координаты тоже изменяются от 0 до 1.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 xy = fragCoord.xy; //Получаем координаты текущего пикселя
    xy.x = xy.x / iResolution.x; //Делим координаты на размер экрана
    xy.y = xy.y / iResolution.y;
    // Теперь x для самого левого пикселя равен 0, а для самого правого равен 1
    vec4 solidRed = vec4(0,0.0,0.0,1.0); //Теперь он стал чёрным
     solidRed.r = xy.x; //Присваиваем красному компоненту нормализованное значение x
    fragColor = solidRed;
}

Вуаля!

Задача: Можете превратить эту картинку в вертикальный градиент? А как насчёт диагонального? Как насчёт градиента из нескольких цветов?

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

Отрисовка изображений

Экспериментировать с цветами интересно, но если мы хотим сделать что-то впечатляющее, то наш шейдер должен научиться получать в качестве входных данных изображение и изменять его. Таким образом можно получить шейдер, воздействующий на весь игровой экран (например, «подводный» эффект или цветокоррекцию), или воздействовать нужным способом только на определённые объекты (например, для создания реалистичной системы освещения).

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


Четыре входных канала ShaderToy

Нажмите на iChannel0 и выберите любую текстуру (изображение).

В результате у вас будет изображение, передаваемое шейдеру. Однако есть одна проблема: отсутствие функции DrawImage(). Не забывайте, единственное, что может делать пиксельный шейдер — менять цвет каждого пикселя.

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


В зависимости от расположения (0,0) на экране может потребоваться отразить ось Y для правильной привязки текстуры. На момент написания статьи ShaderToy был обновлён и исходная точка находится в левом верхнем углу, так что ничего отражать не требуется.

Мы можем выполнить привязку с помощью функции texture(textureData,coordinates), получающей на входе данные текстуры и пару координат (x, y), возвращающую как vec4 цвет текстуры в этих координатах.

Вы можете привязывать координаты к экрану как вам угодно. Можно отрисовать всю текстуру на четверти экрана (пропуская пиксели, то есть уменьшив её масштаб) или просто отрисовать часть текстуры.

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

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 xy = fragCoord.xy / iResolution.xy;//Соединяем это в одну строку
    vec4 texColor = texture(iChannel0,xy);//Получаем от iChannel0 пиксель в координате xy
    fragColor = texColor;//Присваиваем экранному пикселю этот цвет
}

Так мы получили своё первое изображение!

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

Давайте попробуем наложить на изображение градиент, похожий на то, что мы делали раньше:

texColor.b = xy.x;

Поздравляю, вы только что написали свой первый эффект постпроцессинга!

Задача: Сможете написать шейдер, делающий изображение чёрно-белым?

Учтите, что хотя мы и выбрали статичное изображение, всё, что вы видите, выполняется в реальном времени. Чтобы убедиться в этом, замените статичное изображение на видео: нажмите на iChannel0 и выберите любое видео.

Добавляем движение

Пока что все наши эффекты были статичными. Мы можем делать гораздо более интересные вещи, воспользовавшись входными данными, предоставляемыми ShaderToy. iGlobalTime — это постоянно увеличивающаяся переменная. Мы можем использовать её как начальное число для создания периодических эффектов. Давайте попробуем немного поиграть с цветами:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 xy = fragCoord.xy / iResolution.xy; // Соединяем это в одну строку
    vec4 texColor = texture(iChannel0,xy); // Получаем от iChannel0 пиксель в координате xy
       texColor.r *= abs(sin(iGlobalTime));
    texColor.g *= abs(cos(iGlobalTime));
    texColor.b *= abs(sin(iGlobalTime) * cos(iGlobalTime));
    fragColor = texColor; // Присваиваем экранному пикселю этот цвет
}

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

Задача: Можете ли вы сделать шейдер, меняющий изображение с чёрно-белого на цветное и обратно?

Примечание об отладке шейдеров

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

Подводим итог

Это только самые основы работы с шейдерами, но освоившись с ними, вы сможете добиться гораздо большего. Посмотрите эффекты ShaderToy и проверьте, сможете ли вы понять или воспроизвести их!

В этом туториале я не упоминал вершинные шейдеры (Vertex Shaders). Они пишутся на том же языке, но выполняются для каждой вершины, а не пикселя, и возвращают вместе с цветом ещё и положение. Вершинные шейдеры обычно проецируют 3D-сцену на экран (эта функция встроена в большинство графических конвейеров). Пиксельные шейдеры обеспечивают большинство наблюдаемых на экране сложных эффектов, поэтому я рассматриваю их.

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

ShaderToy, с которым мы работали ранее, отлично подходит для быстрой проверки и экспериментов, но его возможности довольно ограничены. Например, вы не можете контролировать данные, передаваемые шейдеру. Если у нас будет собственная среда для запуска шейдеров, то мы сможем создавать всевозможные интересные эффекты и использовать их в собственных проектах! Для запуска шейдеров в браузере мы будем использовать в качестве фреймворка Three.js. WebGL — это Javascript API, позволяющий рендерить шейдеры, Three.js нужен только для упрощения работы.

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

Настройка

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

Можете создать форк и отредактировать проект на CodePen.

Hello Three.js!

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

Можете скачать файл HTML, который является простой сценой Threejs.

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

Для создания куба нам нужно определить его геометрию и материал, а затем добавить его в сцену. Добавьте этот фрагмент кода в поле 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;// Сдвигаем куб назад, чтобы его было видно

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

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

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

Задача: Сможете повращать куб по другой оси? А как насчёт двух осей одновременно?

Итак, всё готово, пора добавить шейдеры!

Добавляем шейдеры

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

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

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

Как и где пишется код GLSL? Основное правило: код GLSL загружается в качестве строки. Затем её можно передать для парсинга и выполнения в видеопроцессоре.

В JavaScript это можно сделать простой передачей кода внутри переменной, вот так:

var shaderCode = "Здесь код шейдера;"

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

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

Создадим новый тег <script> в HTML, который выглядит вот так:

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

</script>

Мы присвоим ему ID 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() {...}. Остальное — это просто boilerplate HTML.)

Можете создать форк и отредактировать проект на CodePen.

Шаг 2: применение шейдера

Способы применения шейдера могут отличаться, они зависят от используемой платформы и её интерфейса с видеопроцессором. С этим никогда не возникает трудностей, и быстрым поиском можно выяснить, как создать объект и применить к нему шейдеры с помощью 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;// Перемещаем его назад, чтобы его было видно

На этом этапе вы должны видеть белый экран:

Можете создать форк и отредактировать проект на CodePen.

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

Задача: Сможете ли вы сделать одну часть экрана красной, а другую синей? (Если не получится, то на следующем шаге я дам подсказку!)

Шаг 3: передача данных

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

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

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

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

Чтобы отправить его шейдеру, изменим формирователь экземпляров 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);
}

В результате у нас получится красивый градиент!

Можете создать форк и отредактировать проект на CodePen.

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

Шаг 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 и изменить размер окна, то вы увидите, как изменяются цвета, несмотря на то, что изначальный размер окна просмотра остался тем же). Проще всего это заметить, посмотрев на цвета в углах и убедившись, что они не меняются.

Примечание: Отправка данных в видеопроцессор обычно является затратной задачей. Отправка нескольких переменных за один кадр вполне нормальна, но при передаче сотен переменных за кадр частота смены кадров значительно снизится. Кажется, что такое маловероятно, но если на экране есть несколько сотен объектов, и ко всем ним применено освещение с разными свойствами, то всё быстро выходит из под контроля. Позже мы узнаем про оптимизацию шейдеров.

Задача: Попробуйте постепенно изменять цвета.

Шаг 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);
}

На экране должно появиться растянутое изображение конфет:

(Это изображение является стандартным тестовым изображением в компьютерной графике, оно взято у Института обработки сигналов и изображений (SIPI) (поэтому на нём показана аббревиатура IPI) Университета Южной Калифорнии. Думаю, оно нам подходит, ведь мы как раз изучаем графические шейдеры!)

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

Дополнительный шаг: применяем шейдеры к другим объектам

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

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

на:

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

Вуаля, конфеты нарисованы на кубе:

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

Если бы мы хотели применить текстуру так, чтобы она выглядела физически нарисованной на кубе, то наша работа напоминала бы изобретение заново 3D-движка (что было бы довольно глупо, учитывая, что мы уже используем 3D-движок и мы можем просто попросить его отрисовать текстуру отдельно на каждой стороне). В этом туториале мы используем шейдеры для того, что иначе достичь было бы невозможно, так что мы не будем вдаваться в такие подробности. (Если вы жаждете узнать больше, на Udacity есть отличный курс по основам 3D-графики!)

Следующие шаги

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

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

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

Начиная с этого момента мы будем рассматривать общие концепции графических шейдеров без привязки к конкретной платформе. (Для удобства во всех примерах кода по-прежнему будет использоваться JavaScript/WebGL.)

Для начала найдите подходящий вам способ выполнения шейдеров. (JavaScript/WebGL — это простейший способ, но я рекомендую вам поэкспериментировать со своей любимой платформой!)

Цели

К концу этого туториала вы не только начнёте хорошо ориентироваться в системах освещения, но и с нуля создадите собственную.

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

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

Отличным примером этого является Chroma. Игрок может бегать по динамическим теням, создаваемым в реальном времени:

Приступаем к работе: наша исходная сцена

Мы многое пропустим в первоначальной настройке, потому что она подробно рассмотрена выше. Начнём с простого фрагментного шейдера, рендерящего нашу текстуру:

Здесь не происходит ничего сложного. Код на JavaScript задаёт сцену и отправляет шейдеру текстуру для рендеринга и размеры экрана.

var uniforms = {
  tex : {type:'t',value:texture},// Текстура
  res : {type: 'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}// Хранит разрешение
}

В коде на GLSL мы объявляем и используем эти uniform-переменные:

uniform sampler2D tex;
uniform vec2 res;
void main() {
    vec2 pixel = gl_FragCoord.xy / res.xy;
    vec4 color = texture2D(tex,pixel);
    gl_FragColor = color;
 }

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

Просто чтобы убедиться, что вы всё понимаете, вот вам небольшое задание на разогрев:

Задача: Отрендерите текстуру, не изменяя соотношения её сторон (Попробуйте сделать это самостоятельно, мы рассмотрим решение ниже.)

Довольно очевидно, почему текстура растянута, но если это непонятно, то вот подсказка: посмотрите на строку, в которой мы нормализуем координаты:

vec2 pixel = gl_FragCoord.xy / res.xy;

Мы делим vec2 на vec2, что аналогично делению каждого отдельного компонента. Другими словами, написанное выше эквивалентно следующему:

vec2 pixel = vec2(0.0,0.0);
pixel.x = gl_FragCoord.x / res.x;
pixel.y = gl_FragCoord.y / res.y;

Мы делим x и y на разные числа (на ширину и высоту экрана). Естественно, что изображение будет растянутым.

Что произойдёт, если мы разделим x и y gl_FragCoord только на x res? Или только на y?

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

Шаг 1: добавление источника света

Прежде чем создать что-то интересное, нам нужен источник света. «Источник света» — это просто точка, передаваемая шейдеру. Для этой точки мы создадим новую uniform:

var uniforms = {
  //Добавляем переменную источника света
  light: {type:'v3', value:new THREE.Vector3()},
  tex : {type:'t',value:texture},// Текстура
  res : {type: 'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}// Хранит разрешение
}

Мы создали вектор с тремя измерениями, потому что мы хотим использовать x и y в качестве положения источника на экране, а z — в качестве радиуса.

Давайте присвоим в JavaScript значения нашему источнику света:

uniforms.light.value.z = 0.2;// Радиус

Мы будем использовать радиус как процент от размеров экрана, поэтому 0.2 будет составлять 20% экрана. (В этом выборе нет ничего особенного. Мы могли бы задать размер в пикселях. Это число ничего не значит, пока мы не начнём с ним делать что-нибудь в коде GLSL.)

Чтобы получить положение мыши, нужно просто добавить получатель события (event listener):

document.onmousemove = function(event){
	// Обновляем источник света, чтобы он следовал за мышью
	uniforms.light.value.x = event.clientX; 
	uniforms.light.value.y = event.clientY; 
}

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

На GLSL это может выглядеть примерно так:

uniform sampler2D tex;
uniform vec2 res;
uniform vec3 light;// Не забывайте объявлять здесь uniform!
void main() {
    vec2 pixel = gl_FragCoord.xy / res.xy;
    vec4 color = texture2D(tex,pixel);
    // Расстояние от текущего пикселя до источника света
    float dist = distance(gl_FragCoord.xy,light.xy);
    
    if(light.z * res.x > dist){// Проверяем, находится ли пиксель внутри радиуса
      gl_FragColor = color;
    } else {
      gl_FragColor = vec4(0.0);
    }
}

Здесь мы сделали следующее:

  • Объявили uniform-переменную источника света.
  • Использовали встроенную функцию distance для вычисления расстояния между источником света и текущим пикселем.
  • Проверили, больше ли это расстояние (в пикселях) 20% ширины экрана; если это так, то возвращаем цвет пикселя, в противном случае возвращаем чёрный.

Ой-ёй! Источник света следует за мышью как-то странно.

Задача: Сможете это исправить? (Попробуйте снова разобраться самостоятельно, прежде чем мы решим эту задачу ниже.)

Исправление движения источника света

Возможно, вы помните, что ось Y здесь перевёрнута. Вы можете поторопиться просто ввести:

light.y = res.y - light.y;

Это математически верно, но если поступить так, то шейдер не скомпилируется! Проблема в том, что uniform-переменные невозмжоно изменять. Чтобы понять, почему, нужно помнить, что этот код выполняется для каждого отдельного пикселя параллельно. Представьте, что все процессорные ядра попытаются изменить единственную переменную одновременно. Плохая ситуация!

Мы можем исправить ошибку, создав новую переменную вместо uniform. Или ещё лучше — мы можем просто сделать этот шаг до передачи данных в шейдер:

uniforms.light.value.y = window.innerHeight - event.clientY;

Теперь мы успешно определили видимый радиус нашей сцены. Однако он выглядит слишком резким…

Добавление градиента

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

Вместо присвоения всем пикселям в пределах радиуса цвета текстуры:

gl_FragColor = color;

мы можем умножать его на коэффициент расстояния:

gl_FragColor = color * (1.0 - dist/(light.z * res.x));

Это сработает, потому что dist — это расстояние в пикселях между текущим пикселем и источником света. (light.z * res.x) — это длина радиуса. Поэтому когда мы смотрим на пиксель ровно под источником света, dist равно 0, то есть мы умножаем color на 1 и получаем полный цвет.


На этом рисунке dist вычисляется для произвольного пикселя. dist меняется в зависимости от того, в каком пикселе мы находимся, а значение light.z * res.x постоянно.

Если мы посмотрим на пиксель на границе круга, то dist равно длине радиуса, то есть в результате мы умножаем color на 0 и получаем чёрный цвет.

Шаг 2: добавляем глубину

Пока мы не сделали ничего особенного, просто добавили нашей текстуре градиентную маску. Всё по-прежнему выглядит плоским. Чтобы понять, как это исправить, давайте посмотрим, что сейчас делает система освещения, и сравним с тем, что она должна делать.

В представленном выше случае стоит ожидать, что точка A будет освещена сильнее всего, потому что источник света находится прямо над ней, а B и C будут темнее, потому что на боковых сторонах практически нет лучей.

Однако вот что видит наша система освещения сейчас:

Все точки обрабатываются одинаково, потому что единственный фактор, который учитывает система — это расстояние на плоскости xy. Вы можете подумать, что нам всего лишь нужна высота каждой их этих точке, но это не совсем так. Чтобы понять, почему, рассмотрите этот рисунок:

A находится наверху фигуры, а B и C — по бокам. D — это ещё одна точка на земле. Мы видим, что A и D должны быть самыми яркими, причём D немного темнее, потому что свет достигает её под углом. С другой стороны, B и C должны быть очень тёмными, потому что до них почти не доходит свет, ведь они направлены от источника света.

Не так важна высота, как направление, в котором повёрнута поверхность. Оно называется нормалью поверхности.

Но как передать эту информацию шейдеру? Мы ведь наверно не можем передавать огромный массив из тысяч чисел для каждого отдельного пикселя? На самом деле, мы так и делаем! Только мы называем это не массивом, а текстурой.

Именно это делает карта нормалей: она просто является изображением, в котором значения r, g и b каждого пикселя представляют не цвет, а направление.

На рисунке выше показана простая карта нормалей. Если воспользоваться инструментом «пипетка» мы увидим, что направление по умолчанию («плоское») представлено цветом (0.5, 0.5, 1) (синий цвет, занимающий бо́льшую часть изображения). Это направление, указывающее прямо вверх. Значения x, y и z присваиваются значениям r, g и b.

Наклонная сторона справа повёрнута вправо, поэтому её значение x выше. Значение x также является значением красного, именно поэтому сторона выглядит немного красноватой или розоватой. То же самое относится ко всем остальным сторонам.

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

Давайте загрузим эту простую карту нормалей для теста:

var normalURL = "https://raw.githubusercontent.com/tutsplus/Beginners-Guide-to-Shaders/master/Part3/normal_maps/normal_test.jpg"
var normal = THREE.ImageUtils.loadTexture(normalURL);

И добавим её как одну из uniform-переменных:

var uniforms = {
  norm: {type:'t', value:normal},
  //.. делаем всё остальное
}

Чтобы проверить, что мы загрузили её правильно, давайте попробуем отрендерить её вместо текстуры, изменив код на GLSL (помните, что мы на этом этапе используем просто фоновую текстуру, а не карту нормалей):

Шаг 3: применение модели освещения

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

Простейшей для реализации моделью является модель Фонга. Вот как она работает: пусть у нас есть поверхность с данными о нормалях:

Мы просто вычисляем угол между источником света и нормалью поверхности:

Чем меньше угол, тем ярче пиксель.

Это значит, что когда пиксель находится непосредственно под источником света, где разность углов равна 0, он будет самым ярким. Самые тёмные пиксели будут указывать в том же направлении, что и источник света (это будет похоже на заднюю часть объекта).

Давайте реализуем эту модель.

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

Поэтому вместо:

vec4 color = texture2D(...);

Давайте сделаем сплошной белый цвет (или любой другой цвет):

vec4 color = vec4(1.0); // белый цвет

Это сокращение GLSL для создания vec4 со всеми компонентами, равными 1.0.

Вот как выглядит алгоритм:

  1. Получаем вектор нормали текущего пикселя.
  2. Получаем вектор направления света.
  3. Нормализуем векторы.
  4. Вычисляем угол между ними.
  5. Умножаем конечный цвет на этот коэффициент.

1. Получаем вектор нормали текущего пикселя

Нам нужно знать, в каком направлении «смотрит» поверхность, чтобы можно было вычислить количество света, попадающего на этот пиксель. Это направление хранится в карте нормалей, поэтому получение вектора нормали заключается в получении цвета текущего пикселя в текстуре нормалей:

vec3 NormalVector = texture2D(norm,pixel).xyz;

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

2. Получаем вектор направления света

Теперь нам нужно знать, в каком направлении указывает свет. Можно представить, что поверхность освещения — это фонарик, направленный на экран в точке расположения курсора мыши. Поэтому можно вычислить вектор направления света просто с помощью расстояния между источником света и пикселем:

vec3 LightVector = vec3(light.x - gl_FragCoord.x,light.y - gl_FragCoord.y,60.0);

Он должен иметь и координату Z (чтобы можно было вычислить угол относительно трёхмергого вектора нормали поверхности). С этим значением можно поэкспериментировать. Вы заметите, что чем оно меньше, тем резче контраст между яркими и тёмными областями. Можно представить, что это высота фонарика над сценой: чем он дальше, тем равномернее распространяется свет.

3. Нормализуем векторы

Теперь нам нужно нормализировать:

NormalVector = normalize(NormalVector);
LightVector = normalize(LightVector);

Чтобы оба вектора имели длину 1.0, мы воспользуемся встроенной функцией normalize. Это необходимо, потому что мы хотим вычислить угол с помощью скалярного произведения. Если вы не очень понимаете, как оно работает, то стоит немного изучить линейную алгебру. Для наших целей нам нужно знать только, что скалярное произведение возвращает косинус угла между векторами одинаковой длины.

4. Вычисляем угол между векторами

Давайте сделаем это с помощью встроенной функции dot:

float diffuse = dot( NormalVector, LightVector );

Я назвал переменную diffuse потому что этот термин используется в модели освещения по Фонгу, ведь она определяет количество света, достигающее поверхности сцены.

5. Умножаем конечный цвет на этот коэффициент

Вот и всё. Теперь умножим цвет на значение. Я создал переменную distanceFactor, чтобы наше уравнение легче читалось:

float distanceFactor = (1.0 - dist/(light.z * res.x));
gl_FragColor = color * diffuse * distanceFactor;

И мы получили работающую модель освещения! (Попробуйте увеличить радиус источника света, чтобы эффект был сильнее заметен.)

Хм, кажется, что-то не так. Похоже, что источник как-то наклонён.

Давайте ещё раз посмотрим на наши вычисления. У нас есть вектор света:

vec3 LightVector = vec3(light.x - gl_FragCoord.x,light.y - gl_FragCoord.y,60.0);

Что, как мы знаем, даст нам (0, 0, 60), когда источник света находится над текущим пикселем. После нормализации он будет равен (0, 0, 1).

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

Задача: Понимаете ли вы, в чём заключается решение? Сможете реализовать его?

Проблема в том, что в текстуре в качестве значений цвета нельзя хранить отрицательные значения. Нельзя обозначит направленный влево вектор как (-0.5, 0, 0). Поэтому при создании карт нормалей нужно прибавлять ко всему 0.5. (Или, выражаясь более обще, нужно смещать систему координат). Нужно понимать это, чтобы знать, что перед использованием карты нужно вычесть из каждого пикселя 0.5.

Вот как демо выглядит после вычитания 0.5 из координат x и y вектора нормали:

Нам нужно внести ещё одно исправление. Помните, что скалярное произведение возвращает косинус угла. Это значит, что выходные данные ограничены интервалом от -1 до 1. Значения цвета не могут быть отрицательными, и поскольку WebGL автоматически отбрасывает отрицательные значения, в некоторых случаях поведение будет странным. Для решения этой проблемы можно использовать встроенную функцию max и превратить это:

float diffuse = dot( NormalVector, LightVector );

в это:

float diffuse = max(dot( NormalVector, LightVector ),0.0);

И у нас получилась работающая модель освещения!

Можно поставить на фон каменную текстуру, а настоящую карту нормалей взять в репозитории этого туториала на GitHub (а именно здесь):

Нам нужно только изменить одну строку на JavaScript, с:

var normalURL = "https://raw.githubusercontent.com/tutsplus/Beginners-Guide-to-Shaders/master/Part3/normal_maps/normal_test.jpg"

на:

var normalURL = "https://raw.githubusercontent.com/tutsplus/Beginners-Guide-to-Shaders/master/Part3/normal_maps/blocks_normal.JPG"

И одну строку на GLSL:

vec4 color = vec4(1.0);// белый цвет

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

vec4 color = texture2D(tex,pixel);

И вот окончательный результат:

Советы по оптимизации

Видеопроцессор выполняет свою работу очень эффективно, но очень важно знать, что может её замедлять. Вот несколько советов:

Ветвление

В шейдерах обычно желательно по возможности избегать ветвления. Хотя большое количество конструкций if редко бывает проблемой в любом коде для ЦП, в шейдерах для видеопроцессора они могут стать узким местом.

Чтобы понять, почему, стоит снова вспомнить, что код на GLSL выполняется для каждого пикселя на экране параллельно. Графическая карта может выполнить множество оптимизаций, исходя из того, что для всех пикселей нужно выполнять одинаковые операции. Однако если в коде будет куча if, то некоторые оптимизации выполнить не удастся, потому что теперь для разных пикселей выполняется разных код. Будут ли конструкции if замедлять выполнение, или нет, зависит от реализации на конкретном оборудовании и в графической карте, но неплохо помнить об этом, если вы хотите ускорить шейдер.

Отложенный рендеринг

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

Освещение — это основная причина выбора того или иного пути. В стандартном прямолинейном конвейере рендеринга вычисления освещения должны выполняться для каждой вершины и каждого фрагмента видимой сцены для каждого источника света в сцене.

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

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

Следующие шаги

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

  • Попробуйте изменять высоту (значение z) вектора освещения, чтобы понаблюдать за его воздействием
  • Поэкспериментируйте с интенсивностью освещения. (Можно сделать это, умножая значение diffuse на коэффициент.)
  • Добавьте в уравнение вычисления освещения значение ambient (окружающего освещения). (Это значит, что мы присваиваем минимальное значение, то есть даже в тёмных областях цвет не будет полностью чёрным. Такое освещение позволяет сделать сцену реалистичной, потому что объекты в реальной жизни всегда освещены, даже если на них не падает прямой свет)
  • Попробуйте реализовать шейдеры из этого туториала по WebGL. Он сделан в Babylon.js, а не в Three.js, но вы можете читать только части, относящиеся к GLSL. В частности, вас могут заинтересовать сэл-шейдинг и затенение по Фонгу.
  • Вдохновляйтесь демо из GLSL Sandbox и ShaderToy

Ссылки

Каменная текстура и карта нормалей для этого туториала взяты с OpenGameArt: http://opengameart.org/content/50-free-textures-4-normalmaps.

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

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

Версия: C# 7.3, Unity 2019.3, Unity

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

В этом руководстве вы узнаете:

  • Что такое шейдеры.
  • Как отображать цвета вершин.
  • Как анимировать в шейдерах.

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

Примечание. Некоторые модели и текстуры, которые использует начальный проект, взяты с сайтов Sharegc.com и Textures.com.

В примере проекта используется Unity 2019.3, хотя все, что описано в этом руководстве, должно работать и в более старых версиях.

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

Что такое шейдеры?

Компьютерная графика, особенно 3D-графика, использует множество различных видов информации для построения визуальных элементов: сетки, текстуры, источники света и так далее. Эта информация передается графическому оборудованию, которое затем обрабатывает изображение и выводит его на экран.

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

Как компьютеры визуализируют графику

Понимание типов шейдеров

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

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

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

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

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

Вычислительные шейдеры на самом деле ничего не обрабатывают, а представляют собой просто программы, работающие на видеооборудовании. Эти шейдеры являются недавним нововведением — более старое видеооборудование может даже не поддерживать их.

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

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

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

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

Выбор из меню Shader окна Inspector в Unity

Хорошо, достаточно объяснений. Пришло время написать шейдеры!

Написание пользовательского шейдера

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

Сцена песчанного острова в начале создания шейдеров в Unity

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

Вы можете решить обе проблемы с помощью пользовательских шейдеров. Однако, прежде чем решать эти проблемы, вам необходимо понять, как создавать шейдеры.

Для начала создайте ассет Surface Shader в папке Shaders, щелкнув правой кнопкой мыши и выбрав Create > Shader > Standard Surface Shader. Назовите его MyFirstShader.

Перейдите в папку Materials, выберите cartoon-sand и щелкните раскрывающийся список Shader в верхней части Inspector. Выберите Custom > MyFirstShader, чтобы переключить материал на этот шейдер.

Выбор MyFirstShader в качестве шейдера для песчанного материала в Unity

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

Рассмотрение шаблона по умолчанию для настраиваемого шейдера

Дважды нажмите на ассет Shader, чтобы открыть его в редакторе кода, и изучите код.

Шейдер состоит из различных разделов внутри основного блока кода шейдера.

Весь код находится внутри блока шейдера в фигурных скобках с именем Custom/MyFirstShader. Эта запись просто сообщает Unity, что показывать при просмотре меню шейдеров материала. Глядя на разные блоки кода…

Properties

	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}

В блоке Properties вверху вы объявляете различные определяемые пользователем свойства. Это значения, которые вы можете редактировать в инспекторе при выборе материала.

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

Блок SubShader

	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200

Блок кода Subshader — это то место, где находится большая часть кода шейдера. Первые две строчки в этом блоке объявляют идентифицирующие теги, распознаваемые Unity, котоыре устанавливают значение, используемое системой уровня детализации (LOD) Unity.

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

Блок CGPROGRAM

		CGPROGRAM
		// Physically-based standard lighting model,
                // and enable shadows on all light types
		#pragma surface surf Standard fullforwardshadows

		// Use Shader model 3.0 target to get nicer looking lighting
		#pragma target 3.0

Эти строчки кода декларируют несколько важных аспектов, которым нужно следовать. Во-первых, они указывают, что следующий код использует язык Cg. Для написания шейдеров существует несколько разных языков программирования. До этого момента все использовалось на языке Unity, ShaderLab.

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

surf — это название основной функции затенения, представленной ниже. Standard объявляет желаемую модель освещения — другие модели освещения включают Lambert и Blinn-Phong, но стандартное физическое освещение выглядит лучше всего. fullforwardshadows активирует динамические тени для этого шейдера.

Переменная свойства MainTex

		sampler2D _MainTex;

Эта строчка объявляет переменную, соответствующую одному из свойств. Хотя это кажется немного избыточным, вы должны объявить здесь переменную, чтобы код Cg использовал это свойство.

Эти переменные могут быть одного из нескольких типов, включая sampler2D для изображения текстуры и fixed / half / float для чисел. Эти три числа являются числами возрастающей точности, и вы должны использовать наименьшую точность, которая работает.

Числовые значения могут иметь номер суффикса для создания вектора. Например, fixed4 обозначает четыре числа. Вы получаете доступ к значениям в векторе с помощью свойств .xyzw или .rgba.

Например, c.rgb в коде шейдера извлекает первые три числа из fixed4, вызываемого c.

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

Вводные данные и переменные свойства

		struct Input {
			float2 uv_MainTex;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		// Add instancing support for this Shader. You need to check
                // 'Enable Instancing' on materials that use the Shader.
		// See https://docs.unity3d.com/Manual/GPUInstancing.html for
                // more information about instancing.
		// #pragma instancing_options assumeuniformscaling
		UNITY_INSTANCING_BUFFER_START(Props)
			// put more per-instance properties here
		UNITY_INSTANCING_BUFFER_END(Props)

Этот блок кода объявляет структуру данных с именем Input и перечисляет в ней значения. Единственными входными значениями в коде шаблона являются UV-координаты основной текстуры, но есть несколько входных значений, к которым шейдеры могут получить доступ. Графические данные передадут объявленные здесь входные значения в шейдер.

Главная функция шейдера

		void surf (Input IN, inout SurfaceOutputStandard o) {
			// Albedo comes from a texture tinted by color
			fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}

surf — это основная функция затенения, которую вы объявляете в строке #pragma выше. Первый параметр — это структура input, а другой параметр — это результат, в который функция записывает данные.

Вы заметите, что структура вывода имеет такие параметры, как .Metallic и .Smoothness, которые вы устанавливаете с помощью свойств шейдера.

Единственная строка в surf, которая не является прямым присвоением номера входа выходному значению, — это fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color ;.

Unity предоставляет tex2D для поиска цвета в текстуре по заданной координате. При написании фрагментного шейдера необходимо явно включить библиотеку шейдеров Unity, но шейдеры поверхности включают библиотеку автоматически.

FallBack шейдер

	FallBack "Diffuse"
}

После кода Cg вы объявляете резервный шейдер, хотя он и не требуется. Это указывает Unity использовать резервный шейдер, если графическое оборудование не может запускать какие-либо пользовательские субшейдеры, обычно потому, что это старая видеокарта, которая не поддерживает функции шейдера.

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

Добавление цвета вершин в шейдер поверхности

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

Сейчас вы можете довольно четко видеть края сетки, но кажется, что настоящие пляжи переходят в цвет воды.

Есть много способов добиться этого в игре, но в одном простом подходе используется цвет вершин.

Чтобы понять, что такое цвета вершин, представьте, что «вершина» — это просто набор данных. Эти данные всегда включают положение вершины, но вы также можете включить дополнительные параметры.

Координаты текстуры являются обычным дополнением, при этом координаты (обозначенные буквами UV вместо XY) предоставляют числа, которые вы используете при работе с текстурами. Что ж, цвет — это еще один вариант, предоставляющий числа, которые вы используете при расчете цветового вывода шейдера.

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

Цвета, интерполированные на грань треугольника

Этому островному мешу уже назначены цвета вершин, но вы не можете их видеть, потому что стандартный шейдер Unity не обрабатывает цвета вершин.

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

Создание пользовательского шейдера для цвета вершин

Создайте новый шейдер поверхности и назовите его LitVertexColor. Установите материалу cartoon-sand этот шейдер как используемый, а затем откройте ресурс шейдера, чтобы отредактировать код.

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

struct Input {
	float2 uv_MainTex;
	float4 vcolor : COLOR; // vertex color
};

Далее включите vcolor в расчет цвета, который происходит в surf:

fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color * IN.vcolor;

И это все! Сохраните шейдер и вернитесь в сцену Unity. После компиляции шейдера вы увидите, как края острова сливаются под водой.

Сцена песчанного острова со смешанными краями

Анимация текстуры воды

Теперь песок выглядит намного привлекательнее! Между тем, вы обнаружили еще одну проблему, заключающуюся в том, что вода полностью статична.

Вы можете изменить это с помощью другого настраиваемого шейдера, хотя на этот раз вам не нужно освещение. Помните, что поверхностные шейдеры обрабатывают освещение за вас, поэтому на этот раз вы не будете создавать поверхностные шейдеры.

Создание неосвещенного шейдера

Вместо этого создайте Unlit Shader с помощью контекстного меню: Create > Shader > Unlit Shader.

Назовите новый ассет шейдер CartoonWater и откройте его в редакторе кода.

Первым делом обновите имя шейдера в самом верху.

Shader "Custom/CartoonWater"

Имя по умолчанию для этого шейдера — "Unlit / CartoonWater". Изменение этого имени на "Custom / CartoonWater" упрощает поиск ваших пользовательских шейдеров в меню.

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

Properties
{
   _MainTex ("Texture", 2D) = "white" {}
   _Opacity ("Opacity", Range(0,1)) = 0.5
   _AnimSpeedX ("Anim Speed (X)", Range(0,4)) = 1.3
   _AnimSpeedY ("Anim Speed (Y)", Range(0,4)) = 2.7
   _AnimScale ("Anim Scale", Range(0,1)) = 0.03
   _AnimTiling ("Anim Tiling", Range(0,20)) = 8
}

По умолчанию Unlit Shader имеет только одно свойство текстуры _MainTex, поэтому вы добавили дополнительные свойства для управления как непрозрачностью воды, так и тем, как анимируется текстура поверхности.

Затем обновите раздел Tags и сразу после строки LOD 100 добавьте несколько новых параметров:

Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

Теги по умолчанию предполагают непрозрачный материал, поэтому вы добавили и настроили некоторые флаги для поддержки прозрачности. В частности, обратите внимание, что отключение ZWrite означает, что вода не будет закрывать другие объекты в буфере глубины, в то время как вы устанавливаете Blend для использования альфа-значений.

Теперь обратите внимание на раздел CGPROGRAM с его набором директив pragma.

Раньше вы работали с поверхностным шейдером, но теперь вы имеете дело с прямыми вершинными и фрагментными шейдерами. Таким образом, в то время как директива #pragma ранее объявляла функцию Surface Shading, на этот раз директивы #pragma объявляют функции Vertex и Fragment Shading.

Также обратите внимание на инструкцию #include. Unity предоставляет здесь библиотеку полезных функций (которые эта строка импортирует для использования в шейдере). Они делают полезные вещи, например, создают образцы текстуры и отслеживают время.

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

unity варит полезные инструменты шейдеров

UnityCG.cginc include в Unity содержит предопределенные переменные и вспомогательные функции.

Теперь взгляните на включенные структуры:

struct appdata
{
   float4 vertex : POSITION;
   float2 uv : TEXCOORD0;
};

struct v2f
{
   float2 uv : TEXCOORD0;
   UNITY_FOG_COORDS(1)
   float4 vertex : SV_POSITION;
};

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

Чуть ниже sampler2D _MainTex; В строке основного кода Cg добавьте следующие новые свойства:

float4 _MainTex_ST;
half _Opacity;
float _AnimSpeedX;
float _AnimSpeedY;
float _AnimScale;
float _AnimTiling;

Как и в предыдущем Surface Shader, вы должны объявить все свойства как переменные в Cg-коде. Вы будете использовать эти свойства, чтобы изменить текстуру и анимировать ее в основной функции шейдера.

Теперь в основной функции фрагмента шейдера, прямо вверху (над комментарием кода // sample the texture добавьте:

// distort the UVs
i.uv.x += sin((i.uv.x + i.uv.y) * _AnimTiling + _Time.y * _AnimSpeedX) * _AnimScale;
i.uv.y += cos((i.uv.x - i.uv.y) * _AnimTiling + _Time.y * _AnimSpeedY) * _AnimScale;

Эти две строки кода — основа анимированного водного эффекта. Этот код смещает UV-координаты, которые он получает, причем величина смещения изменяется со временем.

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

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

Затем вы добавляете время к переданному числу, заставляя возвращаемое значение перемещаться по синусоиде с течением времени. Код библиотеки из более раннего включает переменную, которая отслеживает время.

_Time — это вектор из четырех чисел, при этом четыре значения представляют собой время, масштабированное для удобства в различной степени. Вот почему в коде используется _Time.y, а не просто _Time; y — второе число в векторе.

Наконец, вы умножаете все на _AnimScale, свойство, которое контролирует, насколько сильным будет эффект. Чем больше масштаб, тем больше волнистость.

Наконец, чуть выше return col; в строке кода функции frag добавьте:

col.a = _Opacity;

Здесь код просто устанавливает альфа-значение для свойства _Opacity. Обязательно сделайте это после выборки текстуры; в противном случае текстура перезапишет альфа-значение.

Теперь, когда вы написали свой шейдер, вы можете назначить его материалу cartoon-water.

По умолчанию поверхность воды не будет анимирована в режиме просмотра Scene, но будет отображаться в режиме просмотра Game, когда вы нажмете Play.

шейдер анимирует волны для волнообразного эффекта воды

Куда двигаться дальше?

Вы можете скачать готовые файлы проекта, нажав кнопку «Скачать материалы урока» вверху урока.

Теперь вы знаете основы написания собственных шейдеров в Unity! Конечно, есть еще много чего, что стоит изучить, и хороший ресурс для изучения — это руководство по шейдерам Unity.

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

Если вам интересно узнать о шейдерах с помощью Unity, ознакомьтесь с нашим другим руководством по Shader Graph в Unity здесь.

Надеюсь, вам понравился этот урок! Если у вас есть какие-либо вопросы, комментарии или предложения, не стесняйтесь оставлять их в комментариях.

Автор перевода: Jean Winters

Источник: Introduction to Shaders in Unity

Смотрите также:

Основы Shader Graph в ЮнитиShader Graph в Unity для начинающих

Введение в скрипты UnityВведение в скрипты Unity

Вам кажутся ноды в Cycles немного запутанным? Вы не одиноки. Несмотря на то, что они предоставляют практически безграничный контроль над материалами, многие пользователи с трудом понимаю, что делать с этим лабиринтом вариантов. Именно поэтому эта статья осмеливается сделать невозможное: объяснить действие каждого шейдера простым русским языком.

Наслаждайтесь! -Greg Zaal
Перевод урока с сайта BlenderGuru

  • Diffuse BSDF
  • Glossy BSDF
  • Anisotropic BSDF
  • Glass BSDF
  • Refraction BSDF
  • Transparent BSDF
  • Translucent BSDF
  • Velvet BSDF
  • Toon BSDF
  • Subsurface Scattering
  • Emission
  • Background
  • Hair BSDF
  • Ambient Occlusion
  • Holdout
  • Volume Absorption
  • Volume Scatterv
  • Mix Shader
  • Add Shader

Diffuse BSDF

shader1
Что он делает: Получает свет и распространяет его без видимых отражений.
Используйте его для: Не отражающих поверхностей, таких как бумага или стены. Или для смешивания с другими шейдерами.

Свойства

shader2

Шероховатость (Roughness)

Вы, возможно, регулировали этот слайдер уставившись на экран и были в замешательстве, потому как не могли заметить практически никаких изменений. На самом то деле изменения происходят, но они незначительны:
shader3
Шероховатость добавляет очень тонкие, микроскопические уровни шероховатости поверхности. Где это использовать? Везде, где нужен очень мелкий уровень шероховатости, который едва виден на глаз. Это могут быть такие объекты как ткань, необработанные лесоматериалы или песок:
shader4

Glossy BSDF

shader5
Что он делает: Отражает свет и окружающюю среду.
Используйте его для: Добавления отражения к любому объекту.

Это очень распространенный шейдер, который часто используется в сочетании с диффузным шейдером (с использованием Mix Shader, обсудим позже), чтобы создать некоторые общие материалы, такие как пластик, металл, керамика и дерево:
shader6

Свойства

shader7

Функция распределения

Проще говоря, это математика. Это способ рассчета «размытости». Какой Вы должны использовать? Ну, это зависит от ситуации, а также Ваших предпочтений. Я думаю, что Beckmann хорошо работает для металлов, а GGX хорош для всего остального. Ashikhmin-Shirley был добавлен сравнительно недавно и, кажется, это своего рода что-то среднее между ними. Стоит отметить, что Вы не увидите никаких изменений при значении шероховатости равным 0.
shader8

Шероховатость (Roughness)

Управляет тем, как размывается отражение, имитируя микроскопические неровности на поверхности:
shader9

Anisotropic BSDF

shader10
Что он делает: Ведет себя точно также же, как и шейдер Glossy, но искажает отражение в одном направлении.
Используйте его для: Полированных металлов или материалов, где свет должен отражаться равномерно, как на сковородке.
shader11

Свойства

shader13

Функция распределения

Тоже самое, что и для шейдера Glossy, только разница между ними в данном случае более заметна:
shader12

Анизотропия (Anisotropy)

Значение от -1,0 до 1,0 контролирует степень растяжения. Отрицательные значения растягивают отражения по горизонтали, а положительные по вертикали. При значении 0 результат будет таким же, как если бы Вы просто использовали шейдер Glossy.
shader14

Вращение (Rotation)

Используется для поворота направления отражений. Диапазаон от 0 до 1 вращает от 0 до 360 градусов. Обычно регулируют данный параметр в диапазоне от 0 до 0,5, потому как при значении 180 отражение такое же как и при 360.
shader15

Тангенс (Tangent)

Tangent контролирует в направлении какой из осей использовать искажение отражения.
shader16

Glass BSDF

shader17
Что он делает: Ведет себя как стекло. Искажает и отражает свет от поверхности в соответствии с индеком преломлления (IOR).
Используйте его для: Стекла, воды или любые другие отражающие и преломляющие свет материалы, такие как драгоценные камни.
shader18

Свойства

shader19

Функция распределения

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

Шероховатость (Roughness)

Имитация мельчайших неровностей и царапин на поверхности, которые могут создать видимость матового стекла.
shader21

Шероховатость 0 Шероховатость 0.2
IOR

Индекс преломления света контролирует насколько свет преломляется, как он проходит через поверхность, а также, насколько хорошо видны отражения.
shader22
Вы можете найти несколько списков значений индекса преломления для различных материалов в Интернете. Большинство из них немного отличаются, потому что IOR на самом деле изменяется в зависимости от того, насколько горячий материал. Вот список самых распространеных значений:

  • Вода: 1.33
  • Стекло: 1.5
  • Алмаз: 2.4

Refraction BSDF

shader23
Что он делает: Ведет себя точно так же, как шейдер Glass, но без компонента отражения.
Используйте его для: Особых случаев, когда Вам нужно преломлять свет, но не отражать его. Используется для создания тепловой деформации и черных дыр.
shader24
shader25

Transparent BSDF

shader26
Что он делает: Полностью прозрачный когда белый, окрашивает когда цветной.
Используйте его для: Сочетания с другими материалами, чтобы создать прозрачные части объекта.
shader27
Сам по себе он невидим и довольно бесполезен, но в сочетании с изображениями с альфа каналом он может быть использован для создания сложных объектов, таких как листья или волосы.
shader28

Translucent BSDF

shader29
Что он делает: Позволяет свету проходить сквозь объект.
Используйте его для: Создания тонких объектов, таких как трава или бумага. Смешивают его с шейдером Diffuse для достижения более реалистичного результата.
shader30
Этот шейдер смущал меня в течение долгого времени, но это было до тех пор, пока я не понял, что он не предназначен для использования в одиночку, а должен быть объединен с другими шейдерами, чтобы позволить свету проходить сквозь них.
shader31
Обратите внимание на то, как плоскость светится только тогда, когда свет попадает на нее сзади. Что здесь действительно важно, так это количество отскоков света. После того как свет попадает внутрь и мы позволяем ему «скакать» внутри объекта, часть света в последствии покинет объект и подсветит его передние грани:
shader32
Еще раз, он не особо полезен само по себе, но если сочетать его с диффузным шейдером, мы можем создать действительно интересные материалы.
shader33

Только Diffuse Diffuse и Translucent Только Translucent

Не следует путать его с Sub-Surface Scatting (рассмотрим ниже).

Используйте полупрозрачный шейдер только тогда, когда материал тонкий (бумага, трава, листья…).

Velvet BSDF

shader34
Что он делает: Изгибает свет вокруг объекта как настоящий бархат. Используйте для одежды и ткани.
Используйте его для: Одежды и ткани.
Вы можете думать о нем, как о шейдере Diffuse, но с большим темным пятном в середине:
shader35

Свойства

shader36

Сигма (Sigma)

Контролирует значение шероховатости, тем самым изменяя размер темного пятна. Чем выше значения, тем меньше черное пятно.
shader37

Toon BSDF

shader38
Что он делает: Создает мультяшное затенение.
Используйте его для: Особых случаев, когда Вам нужно создать что-либо в нефотореалистичном мультяшном стиле.

Свойства

shader39

Компонент

Выбор между Diffuse и Glossy — разница в том, что диффузный вид независимая составляющая (появление определенной точки на поверхности не меняется, когда камера движется вокруг), а глянцевая зависит от вида.
shader40

Размер (Size)

Настраивает размер формы круга. Обычно большой размер, используется для Diffuse, и поменьше для Glossy.
shader41

Diffuse с большим/маленьким размером круга Glossy с большим/маленьким размером круга
Сглаживание (Smooth)

При использовании Glossy работает как контроль шероховатости и размывает отражения. Для Diffuse составляющей смягчает/размывает очертания круга.
shader42

Diffuse с большим/маленьким размытием Glossy с большим/маленьким размытием

Subsurface Scattering

shader43
Что он делает: Имитация рассеивания света под поверхностью объекта.
Используйте его для: Кожи, воска, молока и многих видов продуктов питания.
shader44

Свойства

shader45

Спад

Определяет какой алгоритм используется для вычисления постепенного спада света, когда он проходит через материал. Существует очень мало разницы между этими двумя вариантами (Cubic и Gaussian), поэтому, как правило, устанавливается функции Гаусса, а Cubic дает нам дополнительную опцию «Четкость».
shader46

Масштаб (Scale)

Управляет тем, насколько далеко свет может рассеиваться сквозь поверхность.
shader48

Радиус (Radius)

Еще один способ контролировать рассеивание света, только на этот раз с отдельными значениями для красного, зеленого и синего каналов. Это означает, что мы можем подкрасить рассеянный света определенным цветом, например, как кожа рассеивает более красный свет. Обратите внимание, что Вы можете подключить изображения к данному разъему.
shader49

Справа радиус для красного канала больше

Четкость (Sharpness)

Данная опция доступна только при использовании Cubic Falloff. Она не делает острые края слишком мягкими, но может уменьшить количество темных кромок.
shader50

Размытие текстуры (Texture Blur)

Очевидно, что это используется, чтобы размыть текстуру (все что подключено к входу цвет), но это, вероятно, не то, что вы ожидаете. Радиус размытия остается постоянным, так же, как и на входе Scale. Этот ползунок контролирует, сколько размытия смешивается с исходной текстурой. При значении 0,0 у Вас обычная чистая текстура, а при 0,5 у Вас половина чистой текстуры и половина размытой исчезающей на вершине объекта. При 1,0 Вы можете видеть только размытую текстуру. Это означает, что цвета текстуры будут размытыми под поверхностью объекта. Скорее всего Вам не придется часто использовать этот параметр так, как кожа создается с помощью фотографий, которые уже включают в себя эффект размытия.
shader51

Emission

shader52
Что он делает: Излучает свет и бросает его на окружающие предметы.
Используйте его для: Объектов которые должны излучать свет. Такие как: лампочки, искры, огонь или просто в качестве источника света.

Свойства

shader53

Сила (Strength)

Интенсивность/яркость излучаемого света.
shader54

Background

shader55

Одна и та же сцена с разными HDR

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

Hair BSDF

shader57
Что он делает: Поглощает и отражает свет, но специфическим образом.
Используйте его для: Волос и меха.

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

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

Свойства

shader58

Компонент

Компонент Reflection отражает свет от поверхности волос. Transmission пропускает свет который проходит через волосы и выходит с другой стороны. Большую часть времени вы будете использовать два узла Hair BSDF: один с Reflection, другой с Transmission и смешать их вместе.
shader59

Смещение (Offset)

Помните свойство Rotation анизотропного шейдера? Это тоже самое, но с другим именем и измеряется в градусах. Волосы по своей природе являются направленной вещью. И таким образом отражение/пропускание света будет искажено в одном направлении. Смещение позволяет нам контролировать вращение этого направления.
shader60

Вверху Reflection с увеличенным смещением Внизу Transmission с увеличенным смещением
Шероховатость U/V (Roughness U/V)

Другое несоответствие с шейдером Anisotropic — эти два свойства контролируют шероховатости в направлении перекоса света и перпендикулярно к нему. Инвертирование данных значений будет иметь тот же эффект, что и установка смещения на 90 градусов. Но, конечно, это также позволяет нам сделать волосы более или менее блестящая и контролировать силу эффекта анизотропии.

Ambient Occlusion

shader61
Что он делает: Рассчитывает темное затенение в углах и щелях.
Используйте его для: Подчеркивания точек соприкосновения и фальсификации освещения.

AO означает Ambient Occlusion и обычно используется для целых сцен с использованием прохода рендера. Но вы также можете использовать его лишь для какого-то одного объекта.
shader62

Данным способом можно удалить AO у конкретного объекта

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

Свойства

shader63

Цвет (Color)

Это цвет светлой части AO (затенение всегда черное).
shader64

Holdout

shader65
shader66
Что он делает: Создает прозрачную дыру в Вашем рендере.
Используйте его для: Используется в композитинге. Например создать объект, который отбрасывает тень, но при этом не блокирует остальные 3D-объекты за ним.

Чтобы объект бросал тень, но оставался при это прозрачным, необходимо отметить пункт Transparent на вкладке рендера в меню Film.

Volume Absorption

shader67
Что он делает: Влияет на объем материала постепенно поглощая освещение (чем глубже, тем темнее).
Используйте его для: Грязной воды, окрашенной жидкости или стекла.

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

Свойства

shader68

Плотность (Density)

Управляет толщиной объема. Чем выше плотность, тем больше света поглощается и тем темнее и насыщеннее цвет.
shader69

Volume Scatter

shader70
Что он делает: Рассеивает свет который проходит через объект.
Используйте его для: Облаков, дыма и тумана.

Как и в предыдущем случае, данный шейдер влияет не на поверхность, а на объем. Подключив данный шейдер к Вашему объекту Вы превратите его в облако и свет будет проходить сквозь него и вокруг него. Но будьте осторожны с ним. Он значительно увеличивает время рендеринга :)
shader71

Свойства

shader72

Плотность (Density)

Данная опция контролирует плотность объема. Чем выше значение, тем меньше света будет проходить сквозь объект.
shader73
При очень высоких значениях объект будет выглядеть практически как с диффузным шейдером.

Анизотропия (Anisotropy)

Это свойство управляет видимостью объема в зависимости от положения камеры и источника света. Отрицательные значения рассеивают свет позади объекта, а положительные в его передней части. Это означает, что если свет и камера находятся в одном месте (например, оба спереди), то объем будет лучше заметен с отрицательными значениями. Если свет и камера находятся в разных местах (как если бы Вы смотрели на солнце), объем будет более заметен с положительными значениями. Это легче понять визуально:
shader74

Mix Shader

shader75
Что он делает: Объединяет два шейдера вместе.
Используйте его для: Почти всего. Он смешивает свойства двух шейдеров вместе, что имеет важное значение в реальном мире.

В реальном мире практически не существует материалов обладающих свойствами одного шейдера. Всегда присутствуют различные свойства. Например, керамическая кружка помимо рассеивания света (diffuse), также его отражает (glossy).
shader76
Тоже самое применимо и к дереву. В основном оно состоит из диффузного шейдера, но также присутствует отражение.
shader77
Даже кусок стекла не только отражает и пропускает свет, но немного поглощает его (volume absorption).
shader78

Add Shader

shader79
Что он делает: Сочетает в себе шейдеры вместе + добавляет их значения света (фактически нарушая законы физики).
Используйте его для: Специальных материалов, в которых необходимо объединить два световых значения и они не должны быть фотореалистичными.

В реальном мире, количество света, которое отражается от поверхности не может быть больше, чем количество света, которое попало на поверхность. Но с помощью Add Shader мы можем добиться этого. Если мы соединим Diffuse и Glossy шейдеры вместе, мы в два раза увеличим количество энергии, что физически невозможно, но полезно для некоторых материалов. Например, если Вы хотите объединить красные, зеленые и синие шейдеры стекла для создания эффект дисперсии.
shader80

Add Shader: Diffuse + Glossy (нереалистично) Mix Shader: Diffuse + Glossy

Он может быть использован в некоторых случаях, но только там, где Вы знаете, что Вы делаете или, если намеренно хотите нарушить правила для творческой свободы. Случаи в которых необходимо использовать Add Shader:

  • Чтобы придать цвет шейдеру Volume Scatter с помощью Volume Absorption.
  • Для двух шейдеров, которые излучают свет, такие как Emission и Background.
  • Там где энергия должна сохраняться, как в случае с дисперсией света.

Ну вот и все! Надеюсь теперь у Вас есть четкое понимание того, что делают шейдеры в Cycles.

Всем привет. Сегодня я хотел бы задеть такую тему, как рендеринг и шейдеры в Unity. Шейдеры — простыми словами это инструкции для наших видео-карт, которые говорят, как правильно отрисовывать и трансформировать объекты в игре. Итак, welcome to the club buddy.

(Осторожно! Пост является километровой портянкой)

Как работает рендеринг в Unity?

В текущей версии юнити у нас есть три различных пайплайна для отрисовки графики — Built-in, HDRP и URP. Прежде чем разбираться с рендерами, нам нужно понять саму концепцию пайплайнов, которые предлагает нам Unity.

Каждый из рендер-пайплайнов выполняет ряд этапов, которые выполняют более значимую операцию и формируют из этого полноценный процесс отрисовки. И когда мы загружаем на сцену модель (к примеру .fbx), до попадания на наши мониторы она проходит большой путь, словно передвигаясь из Москвы во Владивосток по разным дорогам.

Каждый рендер-пайплайн обладает своими свойствами, с которыми мы будем работать: свойства материалов, источники света, текстуры и все функции, которые происходят внутри шейдера, будут влиять на внешний вид и оптимизацию объектов на экране.

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

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

Прикладные функции

Первое, что у нас происходит — это обработка стадий работы приложения (прикладные функции), которая начинается на CPU и происходит в пределах нашей сцены. Сюда можно включить:

  • Обработка физики и просчет столкновений;
  • Анимации текстур;
  • Ввод с клавиатуры и мыши;
  • Наши скрипты;

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

Процессинг геометрии

Когда компьютер запрашивает через CPU у нашего GPU изображения, которые мы видим на экране, это производится в два этапа:

  • Когда состояние рендера настроено и пройдены этапы от обработки геометрии до обработки пикселей;
  • Когда объект отрисовывается на экране;

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

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

  • Просчитать позицию вершин у объекта;
  • Преобразовать положение в другие пространственные координаты (с локальных на мировые, как пример), для того чтобы их можно было отрисовать на экране;

Также во время выполнения данного этапа мы можем дополнительно выбрать свойства, которые будут нужны для следующих этапов отрисовки графики. Сюда входят нормали, тангентсы, а так же UV-координаты и другие параметры.

Проецирование и клиппинг работают как дополнительные этапы и зависят от настроек камеры на нашей сцене. Обратите внимание, что весь процесс рендеринга производится относительно Camera Frustum (поля обзора).

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

Процесс клиппинга отрезает геометрию за пределами поля обзора камеры

Растеризация и работа с пикселями

Следующий этап работы рендеринга — растеризация. Он заключается в том, чтобы найти в нашей проекции пиксели, соответствующие нашим 2D координатам на экране. Процесс поиска всех пикселей, которые заняты экранным объектом и называется растеризацией. Этот процесс можно рассматривать как шаг синхронизации между объектами в нашей сцены и пикселями на экране.

Для каждого объекта на экране выполняются следующие этапы:

  • Настройка треугольника — отвечает за генерацию данных по нашим объектам и передачи для обхода;
  • Обход треугольника — перечисляет все пиксели, которые входя в группу полигона. В данном случае эта группа пикселей называется фрагментом (fragment);

Далее следует последний этап, когда мы собрали все данные и готовы к выводу пикселей на экран. В этот момент запускается фрагментный шейдер (еще известный как пиксельный шейдер), который отвечает за видимость каждого пикселя. В основном он отвечает за цвет каждого пикселя для вывода на экране.

Forward и Deferred шейдинг

Как мы уже знаем, у Unity есть три вида пайплайнов рендеринга: Built-In, URP и HDRP. С одной стороны у нас есть Built-In (самый старый вид рендера, соответствующий всем критериям Unity), а с другой более современные, оптимизированные и гибкие пайплайны HDRP и URP (называемые Scriptable RP).

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

Примерами путей отрисовки могут быть прямой рендеринг (forward path), отложенный шейдинг (deferred path), а также устаревшие (legacy deferred и legacy vertex lit). Каждый из них поддерживает определенные возможности, ограничения и обладает своей производительностью.

В Unity по умолчанию рендеринг производится прямым путем (forward path). Это связано с тем, что он поддерживается наибольшим количеством видео-чипов, однако обладает своими ограничениями на освещение и другие возможности.

Обратите внимание, что URP поддерживает только прямой рендеринг (forward path), в то время как HDRP обладает большим выбором и может сочетать как прямой путь рендеринга, так и отложенный.

Чтобы лучше понять эту концепцию, нам стоит рассмотреть пример, когда у нас есть некий объект и прямое освещение (directional light). То, как будут взаимодействовать эти объекты и определяет наш путь отрисовки (модель освещения).

Также на результат работы будут влиять:

  • Характеристики материала;
  • Характеристики источников освещения;

Базовая модель освещения соответствует сумме трех различных свойств, таких как: ambient color, diffuse reflection и specular reflection.

Расчет освещения выполняется в шейдере, он может быть выполнен на вершину или на фрагмент. Когда освещение рассчитывается по вершинам, это называется вершинным освещением (per-vertex lighting) и выполняется на этапе вершинного шейдера, аналогично, если освещение рассчитывается по фрагментам, то оно называется per-fragment или per-pixel shader и выполняется на этапе фрагментного (пиксельного) шейдера.

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

Матрицы в Unity

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

В Unity матрицы представляют собой пространственные преобразования, и среди них мы можем найти:

  • UNITY_MATRIX_MVP;
  • UNITY_MATRIX_MV;
  • UNITY_MATRIX_V;
  • UNITY_MATRIX_P;
  • UNITY_MATRIX_VP;
  • UNITY_MATRIX_T_MV;
  • UNITY_MATRIX_IT_MV;
  • unity_ObjectToWorld;
  • unity_WorldToObject;

Все они соответствуют матрицам четыре на четыре (4×4), то есть каждая из них имеет четыре строки и четыре столбца числовых значений. Примером матрицы может быть следующий вариант:

Как и говорилось ранее — наши объекты обладают двумя узлами (к примеру в некоторых графических редакторах они называются transform и shape) и оба отвечают за положение наших вершин в пространстве (объектном). Объектное пространство в свою очередь определяет положение вершин относительно центра объекта.

И каждый раз, когда мы будем изменять положение, поворот или масштаб вершин объекта — мы будем умножать каждую вершину на матрицу модели (в случае с Unity — UNITY_MATRIX_M).

Чтобы переводить координаты из одного пространства в другое и работать внутри него — мы будем постоянно работать с различными матрицами.

Свойства полигональных объектов

Продолжая тему работы с полигональными объектами, можно сказать, что в мире 3D графики каждый объект состоит из полигональной сетки. Объекты на нашей сцене обладают свойствами и каждый из них всегда содержит вершины, тагненсы, нормали, UV-координаты и цвет все это вместе формирует Mesh. Всем этим и управляют такие подпрограммы, как шейдеры.

Полигональная сетка объекта

При помощи шейдеров мы можем получать доступ и изменять каждый из этих параметров. При работе с этими параметрами мы как правило будем использовать вектора (float4). Далее разберем каждый из параметров нашего объекта.

Подробнее про вершины

Вершины объекта, соответствующие набору точек, определяющих площадь поверхности в двухмерном или трехмерном пространстве. В 3D редакторах, как правило, вершины представлены как точки пересечения сетки и объекта.

Вершины характеризуются, как правило двумя моментами:

  • Они являются дочерними компонентами компонента transform;
  • Они имеют определенное положение в соответствии с центром общего объекта в локальном пространстве.

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

Нормали у объектов

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

Тангенсы

Обратившись к документации Unity, мы получим следующее описание:

Тангент — это вектор единичной длинны, следующий за поверхностью меша вдоль направления горизонтальной текстуры

Документация Unity

Что я щас такое блин прочитал? Если простым языком — тангентсы следуют по U координатам в UV для каждой геометрической фигуры.

UV-координаты

Наверное, многие ребята смотрели на скины в GTA Vice City и, возможно, как и я даже пытались рисовать там что-то свое. И UV-координаты как раз таки связаны с этим. С их помощью мы можем расположить 2D текстуру на 3D объект, словно дизайнеры одежды, создавая выкройки, называемые UV-развертками.

Эти координаты действуют как опорные точки, которые управляют тем, какие тексели в текстурной карте соответствуют каждой вершине в сетке.

Область UV-координат равна диапазону между 0,0 (float) и 1,0 (float), где «ноль» означает начальную точку, а «1» — конечную точку.

Цвета вершин

Помимо позиций, вращения, размера, у вершин есть еще и свои цвета. Когда мы экспортируем объект из 3D-программы, она присваивает объекту цвет, на который нужно воздействовать, либо освещением, либо копированием другого цвета.

По умолчанию цвет вершин — белый (1,1,1,1), а цвета кодируются в RGBA. При помощи цветов вершин можно, к примеру, работать со смешиванием текстур, как показано на картинке выше.

Что же такое шейдер?

Итак, на основе всего того, что было описано выше — шейдер
это небольшая программа, которая может быть использована для
создания интересных эффектов в наших проектах
. Внутри нее содержатся математические вычисления и списки инструкций (команд), которые позволяют обрабатывать цвет для каждого пикселя в области покрывающей объект на экране нашего компьютера, либо работать с трансформациями объекта (к примеру для создания динамической травы или воды).

Эта программа позволяет нам рисовать элементы (используя системы координат) на основе свойств нашего полигонального объекта. Шейдеры выполняются на GPU, поскольку он имеет параллельную архитектуру, состоящую из тысяч небольших, эффективных ядер, предназначенных для решения задач одновременно, в то время как CPU был разработан для последовательной серийной обработки.

Обратите внимание, что в Unity есть три типа файлов, связанных с шейдерами:

Во-первых, у нас есть программы с расширением «.shader», которые способны компилироваться в различные типы пайплайнов рендеринга.

Во-вторых, у нас есть программы с расширением «.shadergraph», которые могут компилироваться только в либо в URP, либо в HDRP. Кроме того, у нас есть файлы с расширением «.hlsl», которые позволяют нам создавать настраиваемые функции; обычно они используются в типе узла под названием
Custom Function, который находится в Shader Graph.

Существует также другой тип шейдеров с расширением «.cginc» — Compute Shader, который связан с «.shader» CGPROGRAM, а «.hlsl» связан с «.shadergraph» HLSLPROGRAM.

В Unity существует по крайней мере четыре типа структур, определенных для генерации шейдеров, среди которых мы можем найти комбинацию вершинного и фрагментного шейдера, шейдера поверхностей для автоматического расчета освещения и compute-шейдера для более продвинутых концепций.

Небольшой экскурс в язык шейдеров

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

  • HLSL (High-Level Shader Language — Microsoft);
  • Cg (C for Graphics — NVIDIA) — устаревший формат;
  • ShaderLab — декларативный язык — Unity;

Мы же быстро пробежимся по Cg, ShaderLab и немного заденем HLSL. Итак…

Cg — это язык программирования высокого уровня, разработанный для компиляции на большинстве графических процессоров. Он был разработан NVIDIA в сотрудничестве с Microsoft и использует синтаксис, очень похожий на HLSL. Причина, по которой шейдеры работают с языком Cg, заключается в том, что они могут компилировать как HLSL и GLSL (OpenGL Shading Language), ускоряя и оптимизируя процесс создания материалов для видеоигр.

Все шейдеры в Unity (за исключением Shader Graph и Compute) написаны на декларативном языке под названием ShaderLab. Синтаксис этого языка позволяет отображать свойства шейдера в инспекторе Unity. Это очень интересно, поскольку мы можем манипулировать значениями переменных и векторов в реальном времени, настраивая наш шейдер для получения желаемого результата.

ShaderLab в Unity

В ShaderLab мы можем вручную определить несколько свойств и команд, среди них блок Fallback, который совместим с различными типами пайплайнов рендеринга, которые существуют в Unity.

Fallback — это фундаментальный блок кода в мультиплатформенных играх. Он позволяет нам скомпилировать другой шейдер вместо того, который сгенерировал ошибку. Если шейдер сломался в процессе компиляции
Fallback возвращает другой шейдер, и графическое оборудование может продолжить свою работу. Это необходимо, чтобы нам не писать различные шейдера под XBox и PlayStation, а использовать единые шейдеры.

Базовые типы шейдеров в Unity

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

Разберем, за что отвечает каждый тип:

  • Standart Surface Shader — Этот тип шейдера характеризуется оптимизацией написания кода, который взаимодействует с базовой моделью освещения и работает только с Built-In RP.
  • Unlit Shader — Относится к первичной цветовой модели и будет базовой структурой, которую мы обычно используем для создания наших эффектов.
  • Image Effect Shader — Структурно он очень похож на Unlit шейдер. Эти шейдеры используются в основном в эффектах постобработки в Built-In RP и требуют функции «OnRenderImage()» (C#).
  • Compute Shader — Этот тип характеризуется тем, что выполняется на видеокарте и структурно сильно отличается от ранее упомянутых шейдеров.
  • RayTracing Shader — Экспериментальный тип шейдеров, позволяющий собирать и обрабатывать трассировку лучей в реальном времени, работает только с HDRP и DXR.
  • Blank Shader Graph — Пустой шейдер на основе графов, с которым вы можете работать без знаний языков шейдеров, вместо этого используя ноды.
  • Sub Graph — Подшейдер, который можно использовать в других шейдерах Shader Graph.

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

Для анализа структуры шейдеров, достаточно создать простой шейдер на основе Unlit и проанализировать его.

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

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

Shader «Unlit/OurSampleShaderUnlit»
{
Properties
{
_MainTex («Texture», 2D) = «white» {}
}
SubShader
{
Tags {«RenderType»=»Opaque»}
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include «UnityCG.cginc»

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};

sampler 2D _MainTex;
float4 _MainTex;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o, o.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}

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

Shader «InspectorPath/shaderName»
{
Properties
{
// Здесь у нас находятся параметры шейдера
}

SubShader
{
// Здесь у нас конфигурируется сабшейдер
Pass
{
CGPROGRAM
// Здесь у нас расположена Cg программа — HLSL
ENDCG
}
}

Fallback «ExampleOfOtherShaderForFallback»
}

С текущим примером и его основной структурой становится несколько понятнее. Шейдер начинается с пути в инспекторе редактора Unity (InspectorPath) и имени (shaderName), затем свойства (например.
текстуры, векторы, цвета и т.д.), затем SubShader и в конце необязательный параметр Fallback для поддержки различных вариантов.

Таким образом, мы уже понимаем что, где и зачем начать писать.

Работа с ShaderLab

Большинство наших шейдеров, написанных в коде, начинаются с объявления шейдера и его пути в инспекторе Unity, а также его имени. Оба свойства, такие как SubShader и Fallback, записываются внутри поля «Shader» в декларативном языке ShaderLab.

Shader «OurPath/shaderName»
{
// Код шейдера будет здесь
}

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

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

PropertyName («display name», type) = defaultValue.

Где «PropertyName» означает имя свойства (например, _MainTex), «display name» задает имени свойства в инспекторе Unity (например. Texture), «type» указывает на его тип (например, Color, Vector, 2D и т.д.) и, наконец, «defaultValue» — это значение по умолчанию, присвоенное свойству (например, если свойство свойство является «Color», мы можем установить его как белый следующим образом (1, 1, 1, 1, 1).

Эти свойства отобразятся в параметрах материала для нашего шейдера

Вторым компонентом шейдера является Subshader. Каждый шейдер состоит как минимум из одного SubShader для идеальной загрузки. Когда имеется более одного SubShader, Unity будет обрабатывать каждый из них и выбирать наиболее подходящий в соответствии с аппаратными характеристиками, начиная с первого и заканчивая последним в списке (к примеру для того, чтобы разделить шейдер под iOS и Android). Когда SubShader не поддерживается, Unity попытается использовать компонент Fallback, соответствующий стандартному шейдеру, чтобы аппаратное обеспечение могло продолжить выполнение своей задачи без графических ошибок.

Shader «OurPack/OurShader»
{
Properties { … }
SubShader
{
// Здесь будет конфигурация шейдера
}
}

Прочитать подробнее про параметры и сабшейдеры можно здесь и здесь.

Блендинг

Блендинг нужен нам для процесса смешивания двух пикселей в один. Блендинг поддерживается как в Built-In, так и SRP.

Блендинг происходит на этапе, который объединяет конечный цвет пикселя с его глубиной. Этот этап, который происходит в конце пайплайна рендеринга, после этапа фрагментного (пиксельного) шейдера, при выполнении
stencil-буфера, z-буфера и смешивания цветов.

По умолчанию это свойство не записано в шейдере, так как это необязательная функция и используется в основном при работе с прозрачными объектами, например, когда мы должны нарисовать пиксель с
низким уровнем непрозрачности перед другим (часто такое используется в UI).

Мы можем включить смешивание здесь:

Blend [SourceFactor] [DestinationFactor]

Z-Buffer и тест глубины

Чтобы понять обе концепции, мы должны сначала узнать, как работают Z-буфер (также известный как Depth Buffer) и тест глубины.

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

С другой стороны, тестирование глубины — это условие, которое определяет, будет ли пиксель обновлен или нет в буфере глубины.

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

Чем ближе объект к камере, тем меньше значение Z-буфера, и пиксели с меньшими значениями буфера перезаписывают пиксели с большими значениями.

Чтобы понять концепцию, предположим, что у нас есть камера и некоторые примитивы в нашей сцене, и все они расположены на оси пространства «Z».

Слово «буфер» относится к «пространству памяти», в котором данные будут временно храниться, поэтому Z-буфер относится к значениям глубины между объектами в нашей сцене и камерой, которые присваиваются каждому пикселю.

Мы можем управлять Depth-тестом, благодаря параметрам ZTest в Unity.

Culling

Это свойство, совместимое как c Built-In RP, так и в URP/HDRP, управляет тем, какая из граней многоугольника будет удалена при обработке глубины пикселя.

Что это значит? Вспомните, что многоугольный объект имеет внутренние грани и внешние. По умолчанию внешние грани видны (CullBack);

Однако мы можем активировать внутренние грани:

  • Cull Off — Отрисовываются обе грани объекта;
  • Cull Back — По умолчанию отображаются задние грани объекта;
  • Cull Front — Отрисовываются передние грани объекта;

Эта команда имеет три значения, а именно: Back, Front и Off. По умолчанию активна команда «Back», однако, как правило, строка кода, связанная с culling, не видна в шейдере в целях оптимизации. Если мы хотим изменить параметры, мы должны добавить слово «Cull» после которого следует режим, который мы хотим использовать.

Shader «Culling/OurShader»
{
Properties
{
[Enum(UnityEngine.Rendering.CullMode)]
_Cull («Cull», Float) = 0
}
SubShader
{
// Cull Front
// Cull Off
Cull [_Cull]
}
}

Мы также можем динамически настраивать параметры Culling в инспекторе Unity через зависимость «UnityEngine.Rendering.CullMode», которая является Enum и передается в качестве аргумента в функции.

Использование Cg / HLSL

В нашем шейдере мы можем найти как минимум три варианта директив по умолчанию. Это директивы процессора и включаются в Cg или HLSL. Их функция заключается в том, чтобы помочь нашему шейдеру распознать и скомпилировать определенные функции, которые иначе не могут быть распознаны как таковые.

  • #pragma vertex vert — позволяет скомпилировать этап вершинного шейдера, называемый vert, в GPU как вершинный шейдер;
  • #pragma fragment frag — Директива выполняет ту же функцию, что и pragma vertex, с той разницей, что она позволяет этапу фрагментного шейдера под названием «frag» компилироваться в коде как фрагментный шейдер.
  • #pragma multi_compile_fog — В отличие от предыдущих директив, имеет двойную функцию. Во-первых, multi_compile относится к варианту шейдера, который позволяет нам генерировать варианты с различными
    функциональными возможностями в нашем шейдере. Во-вторых, слово «_fog» включает функциональные возможности тумана из окна Lighting в Unity, это означает, что если мы перейдем на вкладку Environment / Other
    Setting
    , мы можем активировать или деактивировать опции тумана нашего шейдера.

Помимо этого мы можем подключать файлы Cg / HLSL в наш шейдер. Как правило мы делаем это, когда подключаем UnityCG.cginc, который в свою очередь включает в себя координаты тумана, позиции объекта для клиппинга, трансформации текстур, перенос и принятие тумана и многое другое, включая константы UNITY_PI.

Самое важное, что мы можем делать с Cg / HLSL — это написание непосредственных функций обработки вертексных и фрагментных шейдеров, использовать переменные этих языков и различные координаты, вроде текстурных (TEXCOORD0).

#pragma vertex vert
#pragma fragment frag

v2f vert (appdata v)
{
// Возможнность работы с вертексным шейдером
}

fixed4 frag (v2f i) : SV_Target
{
// Возможность работы с фрагментным шейдером
}

Подробнее о Cg / HLSL можно почитать здесь.

Shader Graph

Shader Graph — новое решение для юнити, позволяющая без знания шейдерного языка мастерить свои решения. Для работы с ним используются визуальные ноды (однако никто не запрещает сочетать их с шейдерным языком). Shader Graph работает только с HDRP и URP.

Мы должны учитывать, что при работе с Shader Graph версии, разработанные для Unity 2018 являются BETA-версиями и не получают поддержки, в то время как версии, разработанные для Unity 2019.1+ являются активно совместимыми и получают поддержку.

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

Итак, является ли Shader Graph хорошим инструментом для разработки шейдеров? Конечно. И с ним может разобраться не только графический программист, но и технический дизайнер или художник.

Чтобы создать граф, достаточно выбрать нужный нам тип в редакторе Unity:

Перед началом работы, сделаем небольшое введение в вершинный/фрагментный шейдер на уровне Shader Graph.

Как мы видим, на этапе вершинного шейдера есть три определенных точки входа, а именно: Position(3), Normal(3), Tangent(3), как в шейдере Cg или HLSL. Если сравнивать с обычным шейдером, то это означает, что Position(3) = POSITION[n], Normal(3) = NORMAL[n] и Tangent(3) = TANGENT[n].

Почему Shader Graph имеет три измерения, а Cg или HLSL — четыре?

Вспомним, что четвертое измерение вектора соответствует его компоненту W, который в большинстве случаев равен «единице или нулю». Когда W = 1, это означает, что вектор соответствует положению в пространстве или точке. В то время как, когда W = 0, вектор соответствует направлению в пространстве.

Итак, чтобы настроить наш шейдер, первое, что мы сделаем, это перейдем в редактор и создадим два параметра: цвет — _Color, и Texture2D — _MainTex.

Чтобы создать связь между свойствами ShaderLab и нашей программой, мы должны создать переменные в поле CGPROGRAM. Однако, в Shader Graph этот процесс происходит иначе. Мы должны перетащить свойства, которые мы хотим использовать в область работы с нодами.

Все, что нам нужно сделать, чтобы текстура типа Texture2D работала в сочетании с нодой Sample Texture 2D, это соединить выход свойства _MainTex с входом типа Texture(T2).

Чтобы перемножить обе ноды (с цветом и текстурой), мы должны просто вызвать ноду Multiply и передать оба значения в качестве точки входа. Наконец, output у цвета (коэффициент) выходящий из множителя нам нужно прокинуть в Base Color, найденному на этапе фрагментного шейдера. Сохраняем шейдер — и все готово. Наш первый шейдер готов.

Также мы можем обратиться к общей настройке графа, разделенной на две секции под названием Node и Graph, которые обладают настраиваемыми свойствами, позволяющими изменять цветопередачу. Мы можем найти опции блендинга, обводки, и альфа-клиппинга и др. Кроме того, мы можем настроить свойства нодов в нашей конфигурации Shader Graph.

Сами же ноды предоставляют аналоги тех или иных функций, которые мы пишем в ShaderLab. Как пример, код функции Clamp:

void Unity_Clamp_float4(float4 In, float4 Min, float4 Max, out float4 Out)
{
Out = clamp(In, Min, Max);
}

Превратиться в ноду:

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

Итоги

Говорить на тему шейдеров можно очень много и очень долго, как и затрагивать сам процесс рендеринга. Здесь я не затронул шейдеры рейтрейсинга и Compute-Shading, капнул по шейдерным языкам поверхностно и описал процессы лишь с верхушки айсберга. Работа с графикой — это целые дисциплины, о которых в интернете можно найти тонны исчерпывающей информации, например:

  • Мануал по шейдерам от Unity;
  • Разбор рендеринга в играх;
  • Информация по языку HLSL;
  • Книга — библия шейдеров по Unity, из которой частично брался материал, на английском;

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

Алена расскажет про шейдера в целом понятным для всех языком

Иван расскажет про оптимишейдера в Unity

Интересно было бы послушать о вашем опыте работы с шейдерами и рендерингом в рамках Unity, а так же услышать ваше мнение — что же лучше SRP или Built-In :-)

Спасибо за внимание!

Автор: Greg Zaal

Ноды Cycles кажутся вам сложными и запутанными? Вы в этом не одиноки.

В то время как ноды Cycles предлагают нам безграничный контроль над материалами, многие пользователи всё ещё блуждают в лабиринтах его опций, не понимая многих из них.

Именно по этой причине была написана данная статья в попытке сделать невозможное: объяснить каждый шейдер Cycles простым для пониманий языком.

Diffuse BSDF (Материал рассеивания)

Что он делает: Поглощает свет и рассеивает (Diffuse в переводе с англ.) с нулевым отражением.

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

Свойства

Roughness (Шероховатость)

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

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

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

Glossy BSDF (Глянцевый материал)

Что он делает: Отражает свет и окружающие объекты

Используется для: Добавления отражений любым предметам

Это очень часто используемый шейдер, особенно в комбинации с diffuse шейдером (используется Mix Shader, который будет описан позже) при создании таких распространённых материалов, как пластик, металл, керамика и дерево:

Модель стола от 1DInc, дракон — Stanford University

Свойства

Distribution Function (Функция распределения)

Определяет математически как рассчитывается «размытие».

Какой лучше использовать? Зависит от ситуации, а также от ваших личных предпочтений.

Я считаю, что Beckmann отлично подходит для металлов, а GGX для всего остального. Ashikhmin-Shirley (модель Ашихмина-Ширли) был добавлен намного позже, и имеет что-то среднее между двумя предыдущими. Также, вы не увидите разницы между материалами, если параметр Roughness установлен на 0.

Roughness (шероховатость)

Этот параметр просто управляет размытием отражений, являясь по сути имитацией микроскопических неровностей на поверхности материала:

Anisotropic BSDF (Анизотропия)

Что он делает: Ведёт себя практически также, как и шейдер Glossy, но искажает отражения в определённом направлении.

Используется для: Шлифованный металл или материалы, где свет отражается неравномерно, как например, дно сковороды.

Урок по шейдеру Anisotropic

Свойства

Distribution Function (Функция распределения)

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

Anisotropy (Анизотропия)

Значения от -1.0 до 1.0, которые управляют степенью растяжения. Отрицательные значения растягивают отражения по горизонтали, а положительные по вертикали. При значении 0.0, получится тот же самый шейдер Glossy.

Rotation (Вращение)

Используется для поворота направления искажённого отражения. Начинается от 0.0 и заканчивается 1.0, что является поворотом от 0 до 360 градусов. Чаще всего используются значения между 0.0 и 0.5 (180 градусный поворот, выглядит будто ничего не произошло).

Tangent (Тангенс)

Фактически, Tangent управляет осью, которая будет использована для искажения отражений. Используйте ноду Tangent, чтобы задать направление.

Анизотропия с тангенсами: ось Z, ось Y, UV

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

Glass BSDF (Стекло)

Что он делает: Ведёт себя как реальное стекло, преломляет и отражает свет, когда он попадает на поверхность, в соответствии с IOR (Коэффициент преломления).

Используется для: Стекла, воды или других отражающих, преломляющих материалов, таких как драгоценные камни.

Урок: как создать бокал пива и Черепаха с кристаллами (Автор: Jeepster)

Свойства

Distribution Function (Функция распределения)

И снова, это просто функции, определяющие алгоритм, который будет использован для расчёта вида размытых отражений и преломлений, но в этот раз опция Ashikhmin-Shirley (модель Ашихмина-Ширли) не доступна.

Разница между Beckmann и GGX

Roughness (Шероховатость)

Имитирует незримый рельеф и царапинки на поверхности, которые могут выглядеть как заиндевевшее/матовое стекло.

Разница между параметрами Roughness: 0 и 0.2

IOR

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

Вы можете найти несколько таблиц со значениями коэффициентов преломления для различных материалов в интернете (cgsociety.org, pixelandpoly.com и IOR) и многие из них будут слабо отличаться, потому что IOR изменяется в зависимости от того насколько нагрет материал, но вот вам для начала несколько материалов:

    • Вода: 1.33
    • Стекло: 1.5
  • Алмаз: 2.4

Refraction BSDF (Преломление)

Что он делает: Ведёт себя, как и шейдер Glass, за исключением отсутствия настроек отражения.

Используется в: Особых случаях, когда нужно преломление света без отражений. Например, искажения раскалённого воздуха и чёрные дыры.

Спасибо gandalf3 за этот великолепный Урок создания чёрной дыры.

Transparent BSDF (Прозрачность)

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

Используется: Вместе с другими материалами для создания прозрачных частей материала.

Сам по себе невидимый и весьма бесполезный — но в сочетании с изображениями, содержащими альфа канал, может принимать вид сложных объектов, как например, листья или волосы.

Стекло без преломлений, Sponza Atrium автор: Marko Dabrovic (скачать blend файл)

Translucent BSDF (Просвечивающий материал)

Что он делает: Позволяет свету проходить насквозь.

Используется для: Тонких объектов, таких как трава или бумага. Совмещается с другими шейдерами, например, с diffuse, для достижения большего эффекта реализма.

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

Видите, как подсвечивается плоскость, когда свет направлен с обратной стороны? Именно так работает шейдер translucent в отдельности.

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

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

Только Diffuse, diffuse и translucent, только translucent

Не стоит путать его с шейдером Sub-Surface Scatting (о котором мы поговорим позже).

Используйте шейдер translucent только когда материал очень тонкий (бумага, трава, листья и т.п.).

Velvet BSDF (Вельвет)

Что он делает: Преломляет (отклоняет) свет, как это происходит в вельветовой ткани. Используется для ткани и одежды.

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

Сравнение Diffuse и Velvet

Свойства

Sigma

Сокращённое значение шероховатости, которое управляет размером тёмной области, где большее значение означает меньший размер тёмного пятна.

Toon BSDF (грубо говоря «Мультяшный»)

Что он делает: Создаёт шейдинг в стиле мультфильма или комикса.

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

Свойства

Component (компонента)

Выбор между Diffuse и Glossy — различие в том, что diffuse это независимая от ракурса составляющая (внешний вид отдельной точки на поверхности не изменяется от изменения положения камеры относительно объекта), а glossy зависит от ракурса.

Компоненты Toon (diffuse и glossy)

Size (размер)

Выбор размера формы окружности — обычно для Diffuse используется большой размер, а для Glossy маленький.

Слева: diffuse с маленьким и большим размером. Справа: glossy с маленьким и большим размером

Smooth (сглаживание)

При использовании составляющей Glossy, этот параметр схож с управлением шероховатости и размытием отражений. Для diffuse, он размывает/сглаживает форму окружности.

Слева: diffuse с маленьким и большим значением сглаживания. Справа: glossy с маленьким и большим значением сглаживания.

Subsurface Scattering (Подповерхностное рассеивание)

Что он делает: Имитирует рассеивание света под поверхностью объекта.

Используется для: Кожи, воска, молока и многих видов пищи.

Скан головы от Ten 24

Свойства

Falloff (затухание)

Аналог функций распределения, определяющий, каким алгоритмом будет производится просчёт затухания света, который проходит сквозь материал. Существует очень незаметное различие между двумя опциями (Cubic и Gaussian), однако информация об измерениях обычно приравнивается к Гауссовой функции (Gaussian) (смотреть: NVidia, Arnold, Matt Heimlich), а кубическая (Cubic) даёт дополнительный параметр «Sharpness».

Scale (Масштаб)

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

Radius (Радиус)

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

Справа радиус больше для красного цвета

Sharpness (Резкость)

Эта опция доступна только при выборе Cubic Falloff. Она позволяет рёбрам оставаться более чёткими, а также может понизить эффект тёмных краёв.

Texture Blur (Размытие текстуры)

Вполне очевидно, что эта опция используется для размытия текстуры (не важно подключена ли она на вход Colour), но возможно результат не оправдает ваших ожиданий. Радиус размытия остаётся постоянным, и он аналогичен входу Scale. Всё что контролирует слайдер Texture Blur, это в каком соотношении размытая текстура будет смешиваться с оригинальной, таким образом, при 0.0 вы получите полностью чёткую текстуру, а при 0.5 вы получите наполовину чёткую текстуру, плавно переходящую в размытую версию поверх неё. При значении 1.0, вы получите только размытую текстуру.

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

Emission (Излучение)

Что он делает: В прямом смысле излучает свет и освещает окружающие объекты.

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

Свойства

Strength (Сила)

Интенсивность/яркость излучения света.

Background (Задний план)

Одна и также сцена, освещённая различными HDR картами

Этот шейдер доступен только в нодах World, и используется для излучения света от окружающей среды.

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

Hair BSDF (Материал волос)

Что он делает: Поглощает и отражает свет — но специфическим для волос способом.

Используется для: волос и шерсти.

Этот интересный шейдер похож на некую смесь шейдеров diffuse, translucent и anisotropic.

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

Преимущество данного шейдера в том, что нам нет необходимости заботиться о том, как будет выглядеть каждая прядь, нам остаётся думать лишь о том, чтобы наш лохматый персонаж выглядел хорошо. Да, конечно вы можете создать отличный материал для волос при помощи шейдеров diffuse, glossy и translucency или SSS, но это потребует очень много времени на рендеринг. Поэтому, данный шейдер урезает и делает очень умное усреднение того, как будет выглядеть каждая прядь, глядя издалека. Пряди могут выглядеть довольно ужасающе, если посмотреть на них с близи, и даже не думайте использовать этот шейдер на обычной геометрии (ну ладно, вот здесь это смотрится очень даже круто), но если использовать его на волосах в нормальном масштабе, то он работает превосходно.

Свойства

Component (компонента)

Компонента Reflection — это свет, который отразился от поверхности волос. Transmission — это свет, который проходит сквозь волосы и выходит с другой стороны. Чаще всего приходится использовать две ноды — одна с Reflection, а другая с Transmission, смешанные между собой.

Компоненты волос (transmission и reflection)

Offset (смещение)

Помните параметр Rotation в шейдере Anisotropic? Это практически тоже самое, только с разницей в названии и единицах измерения в градусах. Небольшая несогласованность в интерфейсе :)

По природе, волосы имеют направленную структуру. И отражение/пропускание света будет искажаться в определённом направлении. Параметр Offset позволяет нам вращать направление искажения.

Вверху: reflection с увеличенным смещением. Внизу: transmission с увеличенным смещение.

Roughness U/V (Шероховатость)

Ещё одно различие с шейдером Anisotropic — эти два параметра управляют шероховатостью в направлении искажения света, и перпендикулярно ему. Перестановка значений приведёт к тому же эффекту, что и Offset в 90 градусов. Но также, это позволяет нам придать волосам больший или меньший блеск, и управлять силой эффекта анизотропии.

Ambient Occlusion (Затенение в углах)

Что он делает: Просчитывает тёмные участки в углах и щелях.

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

AO или сокращённо — Ambient Occlusion, обычно используется для всей сцены с использованием прохода AO. Но если вы захотите большего контроля или же добавить только один объект, то этот шейдер также может вам пригодиться.

Использование метода, описанного выше, для удаления AO на определённом объекте.

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

Расстояние AO управляется теми же настройками что и для World AO.

Свойства

Color (Цвет)

Это цвет яркой части AO, тёмные же участки всегда будут чёрного цвета.

Holdout (вырезание)

Что он делает: Проделывает прозрачное отверстие в вашем рендере.

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

Объект всё также отбрасывает тень, но та часть рендера где был объект будет прозрачной — конечно же если вы включили опцию «Transparent» на панели Film.

Volume Absorption (Поглощающий объём)

Что он делает: Воздействует на объём материала, плавно поглощая свет, становясь темнее в зависимости от уровня глубины объекта.

Используется для: Имитации мутной воды и цветных жидкостей или стекла.

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

Свойства

Density (Плотность)

Определяет густоту объёма. Более высокая плотность поглощает больше света, при этом делая цвета насыщеннее и темнее.

Volume Scatter (Рассеивающий объём)

Что он делает: Рассеивает свет, проходящий сквозь объект.

Используется для: Облаков, дыма и дымки.

Как и предыдущий шейдер, он воздействует на объём объекта, а не на поверхность — и на этот раз, он рассеивает свет, вместо того, чтобы поглощать его. Применение шейдера Volumetric Scatter на ваш объект это другими словами, придание ему вида облака: свет проходит сквозь и вдоль объекта.

Предупреждаю, это может значительно увеличить время вашего рендеринга :)

Можете посмотреть примеры: Создание сцены с травяным лугом, Как создать облака при помощи Cycles

Свойства

Density (Плотность)

Это свойство определяет насколько густым будет выглядеть объект. Чем значение больше, тем меньше света пройдёт насквозь.

При самом большом значении он будет выглядеть как шейдер diffuse. Это потому что свет еле проникает в объект, из-за его густоты.

Anisotropy (Анизотропия)

Это может показаться более запутанным, чем в первом случае, но просто запомните, что анизотропия — это «различие свойств среды в различных направлениях внутри этой среды» (wikipedia). Это свойство определяет то как выглядит объект в зависимости от направления света и камеры.

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

Это значит, что если свет и камера повёрнуты в одном направлении (например, фотовспышка), объём будет лучше всего виден при отрицательном значении анизотропии. Если свет и камера обращены в противоположные направления (как, например, когда вы смотрите на солнце), объём будет более видимым при положительном значении. Это проще усвоить на зрительном примере:

Для подробностей смотрите Blender Wiki.

Mix Shader (Смешивающий шейдер)

Что он делает: Объединяет два шейдера.

Используется: Практически для всего. Он смешивает свойства двух шейдеров вместе, что существенно для реального мира.

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

К примеру, керамическая чашка кофе определённо имеет шейдинг diffuse, но она также и блестящая, поэтому здесь присутствует и шейдер glossy.

Таким же образом мы можем рассмотреть и деревянный стол — дерево по большей мере имеет свойства шейдера diffuse, но также содержит немного глянцевого отражения.

Даже кусок толстого стекла имеет не только свойства прозрачности и отражения — он также немного замутнён внутри (объём поглощения).

Add Shader (шейдер добавления)

Что он делает: Комбинирует шейдеры вместе + добавляет совместные значения их освещённости (успешно нарушая при этом законы физики).

Используется для: Особых материалов, которые должны совмещать два источника света и не подразумевают фотореализм.

В реальном мире количество света, отражённого от поверхности не может превышать его изначальное количество, попавшее на поверхность. Но шейдер Add позволяет это сделать.

Если мы совместим Diffuse и Glossy шейдеры вместе, мы получим двойное значение энергии по сравнению с изначальным, что физически невозможно, но полезно для некоторых материалов, например, когда мы соединяем красное, зелёное и голубое стекло для достижения эффекта дисперсии.

Слева: Шейдер Add объединяет Diffuse и Glossy (нарушение закона сохранения энергии). Справа: Шейдер Mix, физически правильный (так правильно).

Так если этот Add шейдер такой коварный, почему же он тогда существует? Он не коварный, на самом деле он просто недопонятый проказник :)

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

Некоторые случаи, когда вы можете использовать Add Shader:

  • Когда вы хотите раскрасить ваш Volume Scatter, добавив к нему Volume Absorption такого же цвета.
  • Для двух шейдеров, которые излучают свет, например, Emission или Background.
  • Когда вы уверены, что закон сохранения энергии будет соблюдаться — например, когда вы смешиваете шейдеры красного, зелёного и голубого стекла для создания дисперсии.

Что ж, вот и всё! Я надеюсь вам стало хотя бы чуточку понятнее, что из себя представляют шейдеры Cycles.

Урок был взят и переведён с сайта: blenderguru.com.

Понравилась статья? Поделить с друзьями:
  • Производственная стиральная машина вязьма инструкция по эксплуатации
  • Некст таблетки инструкция по применению взрослым от чего помогает отзывы
  • Руководство научно исследовательской работой студента
  • Как принимать черника форте в таблетках инструкция
  • Сахалин мчс руководство