что такое вершинный шейдер

learnopengl. Урок 1.5 — Shaders

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

В предыдущем уроке мы кратко коснулись темы “поверхностных шейдеров” и того, как их использовать. В данном уроке мы рассмотрим шейдеры подробнее и в частности шейдерный язык OpenGL (OpenGL Shading Language).

Шейдеры (как упоминалось выше, шейдеры — это программы) программируются на C подобном языке GLSL. Он адаптирован для использования в графике и предоставляет функции работы с векторами и матрицами.

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

Ниже представлена обобщенная структура шейдера:

Входные переменные вершинного шейдера называются вершинными атрибутами. Существует максимальное количество вершин, которое можно передать в шейдер, такое ограничение накладывается ограниченными возможностями аппаратного обеспечения. OpenGL гарантирует возможности передачи по крайней мере 16 4-х компонентных вершин, иначе говоря в шейдер можно передать как минимум 64 значения. Однако стоит учитывать, что существуют вычислительные устройства значительно поднимает эту планку. Так или иначе, узнать максимальное количество входных переменных-вершин, передаваемых в шейдер, можно узнать обратившись к атрибуту GL_MAX_VERTEX_ATTRIBS.

В результат выполнения данного кода, в результате мы увидим цифру >= 16.

GLSL, как и любой другой язык программирования, предоставляет определенный перечень типов переменных, к ним относятся следующие примитивные типы: int, float, double, uint, bool. Также GLSL предоставляет два типа-контейнера: vector и matrix.

Vector

Vector в GLSL — это контейнер, содержащий от 1 до 4 значений любого примитивного типа. Объявление контейнера vector может иметь следующий вид (n — это количество элементов вектора):

vecn (например vec4) — это стандартный vector, содержащий в себе n значений типа float
bvecn (например, bvec4) — это vector, содержащий в себе n значений типа boolean
ivecn (например, ivec4) — это vector, содержащий в себе n значений типа integer
uvecn (нарпример, uvecn) — это vector, содержащий в себе n значений типа unsigned integer
dvecn (например dvecn) — это vector, содержащий в себе n значений типа double.

В большинстве случаев будет использоваться стандартный vector vecn.

Для доступа к элементам контейнера vector мы будем использовать следующий синтаксис vec.x, vec.y, vec.z, vec.w (в данном случае мы обратились ко всем элементам по порядку, от первого к последнему). Также можно итерироваться по RGBA, если вектор описывает цвет, или stpq если вектор описывает координаты текстуры.

P.S. Допускается обращение к одному вектору через XYZW, RGBA, STPQ. Не нужно воспринимать данную заметку как руководство к действию, пожалуйста.

P.P.S. Вообще, никто не запрещает итерироваться по вектору при помощи индекса и оператора доступа по индексу []. https://en.wikibooks.org/wiki/GLSL_Programming/Vector_and_Matrix_Operations#Components

Из вектора, при обращении к данным через точку, можно получить не только одно значение, но и целый вектор, используя следующий синтаксис, который называется swizzling

Для создания нового вектора вы можете использовать до 4 литералов одного типа или вектор, правило только одно — в сумме нужно получить необходимое нам количество элементов, например: для создания вектора из 4 элементов мы можем использовать два вектора длиной в 2 элемента, или один вектор длиной в 3 элемента и один литерал. Также для создания вектор из n элементов допускается указание одного значения, в этом случае все элементы вектора примут это значение. Также допускается использование переменных примитивных типов.

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

In и out переменные

Мы знаем что шейдеры — это маленькие программы, но в большинстве случаев они являются частью чего-то большего, по этой причине в GLSL есть in и out переменные, позволяющие создать “интерфейс” шейдера, позволяющий получить данные для обработки и передать результаты вызывающей стороне. Таким образом каждый шейдер может определить для себя входные и выходные переменные используя ключевые слова in и out.

Вершинный шейдер был бы крайне неэффективным, если бы он не принимал никаких входных данных. Сам по себе это шейдер отличается от других шейдеров тем, что принимает входные значения напрямую из вершинных данных. Для того, чтобы указать OpenGL, как организованы аргументы, мы используем метаданные позиции, для того, чтобы мы могли настраивать атрибуты на CPU. Мы уже видели этот прием ранее: ‘layout (location = 0). Вершинных шейдер, в свою очередь, требует дополнительных спецификаций для того, чтобы мы могли связаться аргументы с вершинных данными.

Можно опустить layout (location = 0), и использовать вызов glGetAttributeLocation для получения расположения атрибутов вершин.

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

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

Vertex shader

Fragment shader

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

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

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

Uniforms

Uniforms (будем называть их формами) — это еще один способ передачи информации от нашего приложения, работающего на CPU, к шейдеру, работающему на GPU. Формы немного отличаются от атрибутов вершин. Для начала: формы являются глобальными. Глобальная переменная для GLSL означает следующее: Глобальная переменная будет уникальной для каждой шейдерной программы, и доступ к ней есть у каждого шейдера на любом этапе в этой программе. Второе: значение формы сохраняется до тех пор, пока оно не будет сброшено или обновлено.

Для объявления формы в GLSL используется спецификатор переменной unifrom. После объявления формы его можно использовать в шейдере. Давайте посмотрим как установить цвет нашего треугольника с использованием формы:

Мы объявили переменную формы outColor типа вектора из 4 элементов в фрагментном шейдере и используем ее для установки выходного значение фрагментного шейдера. Т.к. форма является глобальной переменной, то ее объявление можно производить в любом шейдере, а это значит что нам не нужно передавать что-то из вершинного шейдера во фрагментный. Таким образом мы не объявляем форму в вершинном шейдере, т.к. мы её там не используем.

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

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

Сначала мы получаем время работы в секундах вызвав glfwGetTime(). После мы изменяем значение от 0.0 до 1.0 используя функцию sin и записываем результат в переменную greenValue.

Так как OpenGL реализован с использованием языка C, в котором нет перегрузки функций, вызов функций с разными аргументами невозможен, но в OpenGL определены функции для каждого типа данных, которые определяются постфиксом функции. Ниже приведены некоторые постфиксы:

f: функция принимает float аргумент;
i: функция принимает int аргумент;
ui: функция принимает unsigned int аргумент;
3f: функция принимает три аргумента типа float;
fv: функция принимает в качестве аргумента вектор из float.

Таким образом, вместо использования перегруженных функций, мы должны использовать функцию, реализация которой предназначена для определенного набора аргументов, на что указывает постфикс функции. В приведенном выше примере мы использовали функцию glUniform…() специализированную для обработки 4 аргументов типа float, таким образом, полное имя функции было glUniform4f() (4f — четыре аргумента типа float).

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

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

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

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

Больше атрибутов богу атрибутов.

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

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

Сейчас нам не нужна форма ourColor, но выходной параметре ourColor нам пригодится для передачи значения фрагментному шейдеру:

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

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Зная текущую схему мы можем обновить формат вершин используя функцию glVertexAttribPointer:

Первые несколько атрибутов функции glVertexAttribPointer достаточно просты. В данном примере мы используем вершинный атрибут с позицией 1. Цвет состоит из трех значений типа флота и нам не нужна нормализация.

Т.к. теперь мы используем два атрибута шейдеров, то нам следует пересчитать шаг. Для доступа к следующему атрибуту шейдера (следующий x вектора вершин) нам нужно переместиться на 6 элементов float вправо, на 3 для вектора вершин и на 3 для вектора цвета. Т.е. мы переместимся 6 раз вправо, т.е. на 24 байта вправо.

Теперь мы разобрались со сдвигами. Первым идет вектор с координатами вершины. Вектор со значением цвета RGB идет после вектора с координатами, т.е. после 3 * sizeof(GLfloat) = 12 байт.

Запустив программу вы можете увидеть следующий результат:

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Полный исходный код, творящий это чудо можно посмотреть тут:

Может показаться что результат не соответствует проделанной работе, ведь мы задали лишь три цвета, а не палитру, которую мы видим в результате. Такой результат дает фрагментная интерполяция фрагментного шейдера. При отрисовке треугольника, на этапе растеризации, получается намного больше областей, а не только вершины, которые мы используем в качестве аргументов шейдера. Растеризатор определяет позиции этих областей на основе их положения на полигоне. На основании этой позиции происходит интерполяция всех аргументов фрагментного шейдера. Предположим, мы имеем простую линию, с одного конца она зеленая, с другого она синяя. Если фрагментный шейдер обрабатывает область, которая находится примерно посередине, то цвет этой области будет подобран так, что зеленый будет равен 50% от цвета, используемого в линии, и, соответственно, синий будет равен 50% процентов от синего. Именно это и происходит на нашем треугольнике. Мы имеем три цвета, и три вершины, для каждой из которых установлен один из цветов. Если приглядеться, то можно увидеть, что красный, при переходе к синему, сначала становится фиолетовым, что вполне ожидаемо. Фрагментная интерполяция применяется ко всем атрибутам фрагментного шейдера.

ООП в массы! Делаем свой класс шейдера

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

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

Давайте будем молодцами и будем использовать директивы ifndef и define, чтобы избежать рекурсивного выполнения директив include. Этот совет относится не к OpenGL, а к программированию на C++ в целом.

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

Считывание файла шейдера. Для считывания будем использовать стандартные потоки C++, помещая результат в строки:

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

Ну а вишенкой на торте будет реализация метода Use:

А вот и результат нашей работы:

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

1. Модифицируйте вершинный шейдер так, чтобы в результате треугольник перевернулся: решение.

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

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

Источник

Шейдеры. Что и как

Расскажу, как в общем случае они работают, что умеют и как их используют

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

Слово «шейдер» в контексте разработки игр очень популярно, слышать его могли и те, кто игры не делает. Само слово изначально появилось от англ. shading (затенение) — первые шейдеры использовались, чтобы передавать глубину с помощью работы со светом, блеском, тенями и прочим. Со временем шейдеры стали использоваться для совершенно разного вида постобработки и вообще отрисовки примерно всего.

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

Теперь разберемся, как это все работает.

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

Процесс отрисовки, если его достаточно упростить (что я и сделаю в рамках этой статьи), можно поделить на несколько шагов:

1. Получение входных данных из памяти.
2. Выполнение шейдера вершин.
3. Растеризация.
4. Выполнение шейдера пикселей (фрагментов).
5. Проведение тестов «глубины».
6. Отрисовка на текстуру для экрана.

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

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

1. Шейдер вершин.
2. Шейдер фрагментов.

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

На картинке начало координат немного не соответствует реальным, что все так же не влияет на понимание процесса 🙂

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

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

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

Теперь достаточно в каждом пикселе просто брать цвет из текстуры. Но чтобы это сделать, нужно добавить для каждой точки куба еще информацию: UV канал. Это координат вида (u, v). Отсюда и название, так как x и y были заняты. Она присваивается вершине объекта и обозначает точку на текстуре, которая ей соответствует. Чтобы было понятнее, если мы хотим на каждую грань куба нарисовать знакомое нам лицо, то UV координаты для каждой грани будут выглядеть предельно просто:

Модифицировать их никак не надо. Более того, считать координаты для каждой точки — тоже не нужно. Этим занимается GPU, самостоятельно интерполируя точки из вершин. Тогда достаточно просто сказать для каждой точки что-то вроде

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

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

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

Источник

Shader — это не магия. Написание шейдеров в Unity. Вертексные шейдеры

Всем привет! Меня зовут Дядиченко Григорий, и я основатель и CTO студии Foxsys. Сегодня мы поговорим про вершинные шейдеры. В статье будет разбираться практика с точки зрения Unity, очень простые примеры, а также приведено множество ссылок для изучения информации про шейдеры в Unity. Если вы разбираетесь в написании шейдеров, то вы не найдёте для себя ничего нового. Всем же кто хочет начать писать шейдеры в Unity, добро пожаловать под кат.

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Немного теории

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

Примеры где используются вершинные шейдеры

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Деформация объектов — реалистичные волны, эффект ряби от дождя, деформация при попадании пули, всё это можно сделать вершинными шейдерами, и это будет выглядеть реалистичнее, чем тоже самое сделанное через Bump Mapping в фрагментной части шейдера. Так как это изменение геометрии. В шейдерах уровня 3.0 на эту тему есть техника под названием Dispacement Mapping, так как в них появился доступ к текстурам в вершинной части шейдера.

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

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

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Мультяшное освещение или стилизованное. Во многих играх с точки зрения стиля значительно интереснее выглядит не pbr освещение, а стилизация. При этом не имеет смысла рассчитывать ничего в фрагментной части.

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

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

Простые примеры работы с вертексами

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Не хочется чтобы получилось, как в старых уроках по рисованию совы, поэтому пойдём последовательно по этапам. Создадим стандартный surface шейдер. Это можно сделать по правой кнопке мыши в Project View или в верхней панели во вкладке Assets. Create->Shader->Standard Surface Shader.

И получим такую стандартную заготовку.

Shader «Custom/SimpleVertexExtrusionShader»
<
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
>
SubShader
<
Tags < "RenderType"="Opaque" >
LOD 200

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

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)

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
>
FallBack «Diffuse»
>

Как в ней что работает и в целом подробно мы её разберём в статье после базовой практики, плюс частично будем разбираться по ходу реализации шейдеров. Пока пусть часть вещей останутся, как данность. Если коротко, тут нет никакой магии (в плане того, как подцепляются параметры и прочее) Просто по определённым ключевым словам юнити генерирует код за вас, чтобы не писать его с нуля. Поэтому этот процесс достаточно не очевиден. Подробнее про surface шейдер и его свойства в Unity можно прочитать тут. docs.unity3d.com/Manual/SL-SurfaceShaders.html

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

Shader «Custom/SimpleVertexExtrusionShader»
<
Properties
<
_Color («Color», Color) = (1,1,1,1)
>
SubShader
<
Tags < "RenderType"="Opaque" >
LOD 200

#pragma surface surf Standard fullforwardshadows

struct Input
<
float4 color : COLOR;
>;

void surf (Input IN, inout SurfaceOutputStandard o)
<
fixed4 c = _Color;
o.Albedo = c.rgb;
>
ENDCG
>
FallBack «Diffuse»
>

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

Просто цвет на модели с освещением. За расчёт освещения в данном случае отвечает Unity.

Для начала добавим самый простой эффект из примеров Unity. Экструзия по нормали, и на его примере разберём, как это работает.

Для этого добавим в строчку #pragma surface surf Standard fullforwardshadows модификатор vertex:vert. Если в качестве параметра функции мы передаём inout appdata_full v то в сущности эта функция является модификатором вертексов. По своей сути — это часть вертексного шейдера, который создан кодогенерацией юнити, которая осуществляет предварительную обработку вертексов. Так же в блок Properties добавим поле _Amount принимающее значения от 0 до 1. Для использования поля _Amount в шейдере, нам так же нужно определить его там. В функции мы будем просто сдвигать на нормаль в зависимости от _Amount, где 0 — это стандартная позиция вертекса (нулевой сдвиг), а 1 — сдвиг ровно на нормаль.

Shader «Custom/SimpleVertexExtrusionShader»
<
Properties
<
_Color («Color», Color) = (1,1,1,1)
_Amount («Extrusion Amount», Range(0,1)) = 0.5
>
SubShader
<
Tags < "RenderType"="Opaque" >
LOD 200

#pragma surface surf Standard fullforwardshadows vertex:vert

struct Input
<
float4 color : COLOR;
>;

fixed4 _Color;
float _Amount;

void vert (inout appdata_full v)
<
v.vertex.xyz += v.normal * _Amount;
>
void surf (Input IN, inout SurfaceOutputStandard o)
<
fixed4 c = _Color;
o.Albedo = c.rgb;
>
ENDCG
>
FallBack «Diffuse»
>

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

Shader «Custom/SimpleVertexExtrusionWithTime»
<
Properties
<
_Color («Color», Color) = (1,1,1,1)
_Amplitude («Extrusion Amplitude», float) = 1
>
SubShader
<
Tags < "RenderType"="Opaque" >
LOD 200

#pragma surface surf Standard fullforwardshadows vertex:vert

struct Input
<
float4 color : COLOR;
>;

fixed4 _Color;
float _Amplitude;

Итак, это были простые примеры. Пора рисовать сову!

Деформация объектов

На тему одного эффекта деформации у меня уже написана целая статья с подробным разбором математики процесса и логики мышления при разработке подобного эффекта habr.com/ru/post/435828 Это и будет нашей совой.

Все шейдеры в статье написаны на hlsl. У этого языка на самом деле есть своя объёмная документация, о которой многие забывают и удивляются откуда берётся половина зашитых функций, хотя они определены в HLSL docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions

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

Низкоуровневые шейдеры

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

По старой доброй традиции работы с шейдерами здесь и в дальнейшем будем мучать стенфордского кролика.

В целом так называемый ShaderLab в юнити — это по сути визуализация инспектора с полями в материалах и некоторое упрощение написания шейдеров.

Возьмём общую структуру Shaderlab шейдера:

Такие директивы компиляции как
#pragma vertex vert
#pragma fragment frag
определяют какие функции шейдера компилировать в качестве вершинного и фрагментного шейдера соответственно.

Скажем возьмём один из самых частых примеров — шейдер для вывода цвета нормалей:

Shader «Custom/SimpleNormalVisualization»
<
Properties
<
>
SubShader
<
Pass
<
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

struct v2f <
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
>;

v2f vert (appdata_base v)
<
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + 0.5;
return o;
>

fixed4 frag (v2f i) : SV_Target
<
return fixed4 (i.color, 1);
>
ENDCG
>
>
FallBack «VertexLit»
>

что такое вершинный шейдер. Смотреть фото что такое вершинный шейдер. Смотреть картинку что такое вершинный шейдер. Картинка про что такое вершинный шейдер. Фото что такое вершинный шейдер

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

Функция UnityObjectToClipPos — это вспомогательная функция Unity (из файла UnityCG.cginc), которая переводит вертексы объекта в позицию связанную с камерой. Без неё объект, при попадании в зону видимости (фруструм) камеры, будет рисоваться в координатах экрана вне зависимости от положения трансформа. Так как первоначально позиции вертексов представлены в координатах объекта. Просто значения относительно его пивота.

Этот блок.
struct v2f <
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
>;
Это определение структуры, которая будет обрабатываться в вертексной части и передаваться в фрагментную. В данном случае в ней определено, чтобы из меша забиралось два параметра — позиция вершины и цвет вершины. Подробнее про то, какие данные возможно прокидывать в юнити, можно прочитать по этой ссылке docs.unity3d.com/Manual/SL-VertexProgramInputs.html

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

Заключение

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

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *