Что за файл shaders
Сразу оговорюсь, что материал рассчитан на тех, кто никогда не работал с шейдерами или вообще не знаком с разработкой игр, то есть это в некотором смысле научпоп.
Слово «шейдер» в контексте разработки игр очень популярно, слышать его могли и те, кто игры не делает. Само слово изначально появилось от англ. shading (затенение) — первые шейдеры использовались, чтобы передавать глубину с помощью работы со светом, блеском, тенями и прочим. Со временем шейдеры стали использоваться для совершенно разного вида постобработки и вообще отрисовки примерно всего.
Говоря общими словами, шейдер — это просто программа для графической карты. То есть то, что пишется школьниками на паскале (хипстерами на пайтоне) — это программы для вашего центрального процессора (CPU), а шейдеры — для графического (GPU). Особенность же этих программ выходит из особенностей GPU — они работают параллельно на сотнях маленьких ядех вместо нескольких больших, преимущественно осуществляя математические операции.
Теперь разберемся, как это все работает.
В общем случае цель шейдера — отрисовать некоторый объект. Поэтому возьмем куб, распишем процесс его отрисовки и посмотрим, где используются шейдеры и зачем. Сначала опишем сам куб. Для графической карты это 8 точек, между некоторыми из которых есть плоскость. Каждая из точек описывается тремя числами (правильно сказать, что это вершины). Помимо этого у кубика есть цвет и положение внутри мира.
Процесс отрисовки, если его достаточно упростить (что я и сделаю в рамках этой статьи), можно поделить на несколько шагов:
1. Получение входных данных из памяти.
2. Выполнение шейдера вершин.
3. Растеризация.
4. Выполнение шейдера пикселей (фрагментов).
5. Проведение тестов «глубины».
6. Отрисовка на текстуру для экрана.
В первом шаге видеокарта каким-то образом получает данные (вершины, плоскости, текстуры) в свою видеопамять, для нас это сейчас не так важно. Далее происходит конвертация координат относительно объекта в координаты на экране относительно камеры. После происходит растеризация — высчитывается, в каких пикселях уже на экране находится объект. Такие пиксели называют фрагментами. Отличие от пикселей заключается в том, что фрагмент помимо информации о пикселе, содержит еще и некоторую побочную информацию, полученную после растеризации. Для упрощения будем считать, что это все просто пиксели на экране. Далее для каждого пикселя выполняется шейдер фрагмента. А затем проверяется, что расстояние от камеры до фрагмента соответствует высчитанному заранее в нужном направлении в буфере глубины. Проще говоря, проверяется, нет ли перед объектом чего-либо еще, и нужно ли его отрисовывать на итоговое изображение.
Как видите, в процессе отрисовки можно заметить два вида шейдера. На самом деле, сейчас есть чуть больше видов, но они не столь важны для разбора, так как имеют более специфичный характер использования, а мы рассказываем на пальцах. Но вот те два, что нас волнуют:
1. Шейдер вершин.
2. Шейдер фрагментов.
Как сказано было ранее, этот шейдер (или группа шейдеров по очереди) занимается переводом координат относительно объекта, в координаты на текстуре.
На картинке начало координат немного не соответствует реальным, что все так же не влияет на понимание процесса :)
Пройдемся по состояниям. В первом у нас, очевидно, входные координаты без излишков. На втором они были перенесены в координаты относительно начала «мира». Потом они переносятся в координаты относительно точки смотрящего (видно на второй картинке), но заметно, что картинка плоская. Их проекция происходит далее и мы получаем наши итоговые координаты. Все эти операции производятся шейдером. Помимо прочего, он позволяет не только отобразить реальные координаты, но и модифицировать их так, чтобы исказить объект для эффекта. Например, я недавно писал шейдер, который переворачивал спрайт, чтобы отрисовать его тень:
Из занимательного — итоговые вершины располагаются на так называемой плоскости Clip Space и находятся в диапазоне от -1.0 до 1.0. Именно с такими координатами потом и работает видеокарта.
После преобразований вершин и растеризации нужно высчитать цвет каждого фрагмента (помним, что для упрощения это пиксели). Для примера возьмём наш куб: мы помним, что он залит одним цветом. Просто сделаем так, чтобы цвет каждого фрагмента стал цвета куба и все:
Выглядит немного странно, да? Проблема в том, что мы не видим ни теней, ни текстур. Будь на кубе какая-либо текстура, мы бы заметили переход между гранями. Вот возьмем текстуру:
Теперь достаточно в каждом пикселе просто брать цвет из текстуры. Но чтобы это сделать, нужно добавить для каждой точки куба еще информацию: UV канал. Это координат вида (u, v). Отсюда и название, так как x и y были заняты. Она присваивается вершине объекта и обозначает точку на текстуре, которая ей соответствует. Чтобы было понятнее, если мы хотим на каждую грань куба нарисовать знакомое нам лицо, то UV координаты для каждой грани будут выглядеть предельно просто:
Модифицировать их никак не надо. Более того, считать координаты для каждой точки — тоже не нужно. Этим занимается GPU, самостоятельно интерполируя точки из вершин. Тогда достаточно просто сказать для каждой точки что-то вроде
Это очень условный пример, но примерно так в простейшем случае оно и работает:
Помимо натягивания текстур в пиксельном шейдере можно, например, получить информацию об освещенности и добавить к цвету черного пропорционально затемнению в этой точке, тогда объект будет менее плоским. Это, конечно, если просчет освещенности где-то написан, потому что сама по себе видеокарта о понятиях освещения и теней мало чего знает. Вообще делать с цветами можно что угодно, например подменять их в зависимости от игрока:
Помимо UV канала в шейдер в зависимости от его вида приходят разные данные. Они зависят от игрового движка и графической библиотеки. Обычно туда входят данные о местоположении точки, нормаль (вектор исходящий от поверхности в точке), UV канал. Но также шейдер вершин может передавать данные в пиксельный шейдер. Если это координата, то она будет проинтеполирована на основе положения фрагмента относительно вершин, между которыми он находится, как, например, UV данные.
Двигателем прогресса в сторону фотореалистичности картинки в компьютерной графике я считаю именно компьютерные игры, поэтому давайте именно в разрезе видео-игр и поговорим о том, что такое “шейдеры”.
До того, как появились первые графические ускорители, всю работу по отрисовке кадров видеоигры выполнял бедняга центральный процессор.
Отрисовка кадра, довольно рутинная работа на самом деле: нужно взять “геометрию” – полигональные модели (мир, персонаж, оружие и т.д.) и растеризовать. Что такое растеризовать? Вся 3d модель состоит из мельчайших треугольников, которые растеризатор превращает в пиксели (то есть “растеризовать” значит превратить в пиксели). После растеризации взять текстурные данные, параметры освещенности, тумана и тп и рассчитать каждый результирующий пиксель игрового кадра, который будет выведен на экран игроку.
Так вот, центральный процессор (CPU – Central Processing Unit) слишком умный парень, чтобы заставлять его заниматься такой рутиной. Вместо этого логично выделить какой-то аппаратный модуль, который разгрузит CPU, чтобы тот смог заниматься более важным интеллектуальным трудом.
Таким аппаратным модулем стал – графический ускоритель или видеокарта (GPU – Graphics Processing Unit). Теперь CPU подготавливает данные и загружает рутинной работой коллегу. Учитывая, что GPU сейчас это не просто один коллега, это толпа миньонов-ядер, то он с такой работой справляется на раз.
Но мы пока не получили ответа на главный вопрос: Что такое шейдеры? Подождите, я подвожу к этому.
Хорошая, интересная и близкая к фото-реализму графика, требовала от разработчиков видеокарт реализовывать многие алгоритмы на аппаратном уровне. Тени, свет, блики и так далее. Такой подход – с реализацией алгоритмов аппаратно называется “Фиксированный пайплайн или конвейер” и там где требуется качественная графика он теперь не встречается. Его место занял “Программируемый пайплайн”.
Запросы игроков “давайте, завозите хороший графоний! удивляйте!”, толкали разработчиков игр (и производителей видеокарт соответственно) все к более и более сложным алгоритмам. Пока в какой-то момент зашитых аппаратных алгоритмов им стало слишком мало.
Наступило время видеокартам стать более интеллектуальными. Было принято решение позволить разработчикам программировать блоки графического процессора в произвольные конвейеры, реализующие разные алгоритмы. То есть разработчики игр, графические программисты отныне смогли писать программы для видеокарточек.
И вот, наконец, мы дошли до ответа на наш главный вопрос.
На устройствах Андроид можно наблюдать много установленных по умолчанию папок, которые не несут в себе серьезной функциональной нагрузки для системы. Другое дело, что определить их не так уж просто. Поэтому если системные файлы никак не мешают, и не забивают внутреннюю память смартфона, то лучше их все-таки не трогать.
Аналогичное правило распространяется и на Shaders. К тому же, удаление этой папки никак не способствует очистке памяти гаджета, поскольку повторный запуск игры приведёт к очередному кэшированию файлов.
Привет! Это небольшое введение в тему шейдеров, что это такое и как они используются при рендере графики в Unity.
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.
После мы запрашиваем индекс формы ourColor используя glGetUniformLocation. Данная функция принимает два аргумента: переменную программы-шейдера и название формы, определенной внутри этой программы. Если glGetUniformLocation вернул -1, это означает что такой формы с таким именем не было найдено. Последнем нашим действием является установка значения формы ourColor посредством использования функции glUniform4f. Заметьте, что поиск индекса формы не требует предварительного вызова glUseProgram, но для обновления значения формы сначала необходимо вызвать glUseProgram.
Так как 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% процентов от синего. Именно это и происходит на нашем треугольнике. Мы имеем три цвета, и три вершины, для каждой из которых установлен один из цветов. Если приглядеться, то можно увидеть, что красный, при переходе к синему, сначала становится фиолетовым, что вполне ожидаемо. Фрагментная интерполяция применяется ко всем атрибутам фрагментного шейдера.
Виды шейдеров
В зависимости от стадии конвейера шейдеры делятся на несколько типов: вершинный, фрагментный (пиксельный) и геометрический. А в новейших типах конвейеров есть еще шейдеры тесселяции. Подробно обсуждать графический конвейер мы не будем, я все думаю не написать ли об этом отдельную статью, для тех кто решит заняться изучением шейдеров и программирования графики. Напишите в комментариях если Вам интересно, я буду знать, стоит ли тратить время.
На чем пишут шейдеры?
Изначально шейдеры можно было писать на assembler-like языке, но позже появились шейдерные языки высокого уровня, похожие на язык С, такие как: Cg, GLSL и HLSL.
Такие языки намного проще чем C, ведь задачи решаемые с их помощью, гораздо проще. Система типов в таких языках отражает нужды программистов графики. Поэтому они предоставляют программисту специальные типы данных: матрицы, семплеры, векторы и тп.
Геометрический шейдер
Геометрические шейдеры способны создавать новую геометрию, и могут использоваться для создания частиц, изменения детализации модели «на лету», создание силуэтов и т.п. В отличие от предыдущего вершинного, способны обработать не только одну вершину, но и целый примитив. Примитивом может быть отрезок (две вершины) и треугольник (три вершины), а при наличии информации о смежных вершинах (англ. adjacency) для треугольного примитива может быть обработано до шести вершин.
“Что такое шейдеры?”
Ше́йдер (англ. shader — затеняющая программа) — это программа для видеокарточки, которая используется в трёхмерной графике для определения окончательных параметров объекта или изображения, может включать в себя описание поглощения и рассеяния света, наложения текстуры, отражения и преломление, затенение, смещение поверхности и множество других параметров.
Что такое шейдеры? Например, вот такой эффект можно получить, это шейдер воды примененный к сфере.
Папка Shaders — что за зверь?
Загадочная папка есть далеко не на всех Андроидах. Ее присутствие в смартфоне обычно видят любители мобильных игр и развлекательных приложений. Она предназначена для кэширования графики игры. За счёт этого утилита загружается быстрее, а графическое отображение не страдает от недостатка трафика или других нюансов.
Различные игры, изображения бонусов и все графические рисунки из геймплея так же хранятся в Shaders.
При этом после открытия папки можно будет видеть исключительно белые файлы, отобразить которые будет невозможно. В то же время именно они помогают играм быстро и качественно загружаться, становясь очень ценным подспорьем для геймеров.
Понять важность кэша можно оценив разницу в скорости загрузки игры в первый и во все последующие за ним разы. После установки приложение всегда загружается дольше, чем во время повторного открытия.
Полезная информация
Теперь Вы знаете что такое шейдеры, но помимо шейдеров, есть другие очень интересные темы в разработке игр и компьютерной графике, которые наверняка Вас заинтересуют:
-
,- техника создания потрясающих эффектов в современных видео-играх. Обзорная статья и видео с уроками создания эффектов в Unity3d ,- если Вы задумываетесь о разработке видеоигр, в качестве профессиональной карьеры или хобби, эта статья содержит отличный набор рекомендаций “с чего начать”, “какие книги читать” и т.д.
RenderMan
Все что мы обсудили выше относится к realtime графике. Но существуют non-realtime графика. В чем разница – realtime – реальное время, тоесть здесь и сейчас – давать 60 кадров в секунду в игре, это процесс реального времени. А вот рендерить комплексный кадр для ультрасовременной анимации по несколько минут это non-realtime. Суть во времени.
Например, графику такого качества как в последних мультипликационных фильмах студии Pixar получить в реальном времени мы сейчас получить не можем. Очень большие рендер-фермы обсчитывают симуляции света по совсем другим алгоритмам, очень затратным, но дающим почти фотореалистичные картинки.
Что такое шейдер?
Вершинный шейдер запускается для каждой вершины в меше и отвечает за преобразование трехмерных положений вершин (в пространстве объектов) в положения пространства отсечения (Геометрия отсекает невидимые камерой части объектов. Есть несколько дополнительных шагов, чтобы превратить это в положение на 2D экране. Надеюсь, я восполню пробелы в одном из следующих постов). Также он должен передавать из меша данные, которые потребуются для вычислений на этапе фрагментного шейдера (UV, нормали и т.д.).
В таких инструментах, как Shader Graph, все это обычно делается за нас, но важно понимать, что здесь происходит два отдельных этапа. В новых версиях есть Master Stack, чтобы четче разделить эти этапы. Он предоставляет нам порт Vertex Position, чтобы переопределить положение в пространстве объектов, прежде чем оно будет преобразовано в пространство отсечения, например, для Vertex Displacement. Мы также можем переопределить нормали и касательные, которые передаются на этап фрагментного шейдера, но Shader Graph обработает порты автоматически, если оставить поля пустыми.
Для каждого треугольника и вершины в этом треугольнике положения пространства отсечения, переданные из стадии вершин, используются для создания фрагментов — потенциальных пикселей на 2D-экране. Все данные для каждой вершины, переданные во фрагментный шейдер, также интерполируются по треугольнику. Вот почему у каждой вершины может быть указан один цвет, но треугольник получится градиентно окрашенным. (То же самое происходит с UV, что позволяет правильно наложить текстуру, а не просто брать цвет как один пиксель для каждой вершины)
Далее для этих фрагментов (потенциальных пикселей) запускается фрагментный шейдер. Он определяет цвет, который будет нарисован на экране (и в некоторых случаях выводит значение глубины). В результате может выводиться сплошной цвет с использованием цветов вершин, сэмплирование текстур и/или более сложный шейдинг с вычислениями освещения (название «шейдер» пошло от слова shade – тень).
В некоторых случаях бывает нужно отменить/отбросить пиксель из рендера (например, по alpha).
Графический пайплайн
Преимущество программируемого конвейера перед его предшественником в том, что теперь программистам можно создавать свои алгоритмы самостоятельно, а не пользоваться зашитым аппаратно набором опций.
Сначала видеокарты оснастили несколькими специализированными процессорами, поддерживающими разные наборы инструкций. Шейдеры делили на три типа в зависимости от того, какой процессор будет их исполнять. Но затем видеокарты стали оснащать универсальными процессорами, поддерживающими наборы инструкций всех трёх типов шейдеров. Деление шейдеров на типы сохранилось для описания назначения шейдера.
Помимо графических задач с такими интеллектуальными видеокартами появилась возможность выполнения на GPU вычислений общего назначения (не связанных с компьютерной графикой).
Впервые полноценная поддержка шейдеров появилась в видеокартах серии GeForce 3, но зачатки были реализованы ещё в GeForce256 (в виде Register Combiners).
Проходы шейдера
Обычно шейдеры включают основной этап, который либо не имеет тега LightMode, либо использует один из таких тегов как UniversalForward (URP), ForwardBase (Built-in Pipeline) или Forward (HDRP), если шейдер предназначен для использования в прямом, а не в отложенном рендеринге (я немного объясню это в следующем разделе).
Мы уже упоминали шейдеры в предыдущем уроке. Шейдеры — это небольшие программы выполняемые на графическом ускорителе (далее будем использовать более распространенное название — GPU). Эти программы выполняются для каждого конкретного участка графического конвейера. Если описывать шейдеры наиболее простым способом, то шейдеры — это не более чем программы преобразующие входы в выходы. Шейдеры обычно изолированы друг от друга, и не имеют механизмов коммуникации между собой кроме упомянутых выше входов и выходов.
В предыдущем уроке мы кратко коснулись темы “поверхностных шейдеров” и того, как их использовать. В данном уроке мы рассмотрим шейдеры подробнее и в частности шейдерный язык OpenGL (OpenGL Shading Language).
Шейдеры (как упоминалось выше, шейдеры — это программы) программируются на C подобном языке GLSL. Он адаптирован для использования в графике и предоставляет функции работы с векторами и матрицами.
Описание шейдера начинается с указания его версии, далее следуют списки входных и выходных переменных, глобальных переменных (ключевое слово uniform), функции main. Функции main является начальной точкой шейдера. Внутри этой функции можно производить манипуляции с входными данными, результат работы шейдера помещается в выходные переменные. Не обращайте внимания на ключевое слово uniform, к нему мы вернемся позднее.
Ниже представлена обобщенная структура шейдера:
В результат выполнения данного кода, в результате мы увидим цифру >= 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. Не нужно воспринимать данную заметку как руководство к действию, пожалуйста.
Из вектора, при обращении к данным через точку, можно получить не только одно значение, но и целый вектор, используя следующий синтаксис, который называется swizzling
Для создания нового вектора вы можете использовать до 4 литералов одного типа или вектор, правило только одно — в сумме нужно получить необходимое нам количество элементов, например: для создания вектора из 4 элементов мы можем использовать два вектора длиной в 2 элемента, или один вектор длиной в 3 элемента и один литерал. Также для создания вектор из n элементов допускается указание одного значения, в этом случае все элементы вектора примут это значение. Также допускается использование переменных примитивных типов.
Можно заметить, что вектор очень гибкий тип данных и его можно использовать в роли входных и выходных переменных.
Что такое меш?
Прежде чем перейти конкретно к шейдерам, важно сначала получить базовое представление о том, что такое меш.
Меш содержит данные для 3D-модели, состоящие из вершин и того, как те соединяются в треугольники. Каждый треугольник состоит из 3 вершин, но каждая вершина может быть общей для нескольких треугольников (поэтому количество вершин не всегда будет втрое больше, чем треугольников).
Пример кубического меша, состоящего из 12 треугольников. В Blender (ПО для 3D-моделирования) это 8 вершин, однако при импорте в Unity цифра может измениться в зависимости от того, являются ли данные для каждой вершины одинаковыми у общих вершин.
Термины «меш» и «модель» обычно используются как взаимозаменяемые. Но если углубиться в детали, «меш» всегда относится к геометрии (вершины/треугольники), а «модель» может относиться к импортированному файлу, в котором иногда содержатся несколько объектов меша, сабмеши, а также материалы, анимация и т.д.
Когда мы говорим «вершина», мы обычно имеем в виду ее положение в трехмерном пространстве, но каждая вершина в меше может содержать множество фрагментов данных. Сюда входят такие данные, как:
- Положение в 3D пространстве (пространстве объектов, поэтому (0,0,0) стоит в начале меша).
- UV, также известные как Текстурные Координаты, поскольку они чаще всего используются для наложения текстуры на модель. Обычно бывают координаты Vector2 (два значения с плавающей точкой, каждая ось помечена как xy или uv), но настоящие UV каналы могут быть Vector4 и содержать для передачи данных до 4 чисел с плавающей точкой. Смотрите Mesh.SetUVs.
- Нормали (Направления, используемые для шейдинга. Некоторые программы моделирования (например, Blender) могут дополнительно использовать для определения порядка поворота вершин, который показывает в какую сторону смотрит грань. Во время рендеринга в шейдере можно отбросить переднюю или заднюю сторону грани).
- Касательные (Направления перпендикулярные нормалям, которые огибают поверхность сетки вдоль горизонтальных координат текстуры (uv.x). Используются для построения Пространства Касательных, которое необходимо в таких разновидностях шейдинга как Normal/Bump Mapping)
- Цвета вершин (Цвет, который задан каждой вершине).
Две вершины могут занимать одинаковое положение в 3D пространстве, но если не совпадают другие относящиеся к ним данные, в вершинных данных для них будет две отдельных записи.
Пример кубических мешей. Оба имеют по 12 треугольников, но у левого 24 вершины, а у правого 8.
Достаточно распространено, когда модель имеет равномерное затенение (flat shading), а не плавное (smooth shading) как на изображении выше. Равномерное затенение увеличит количество вершин, потому что нормали вершин у разных граней должны указывать в разных направлениях. (Этого может не случиться в самой программе моделирования, но произойдет при экспорте в Unity). Для плавного затенения используется среднее значение направлений, поэтому вершины можно использовать как общие, при условии, что остальные данные также совпадают!
Пиксельный шейдер
Пиксельными шейдерами выполняют наложение текстур, освещение, и разные текстурные эффекты, такие как отражение, преломление, туман, Bump Mapping и пр. Пиксельные шейдеры также используются для пост-эффектов.
Пиксельный шейдер работает с фрагментами растрового изображения и с текстурами — обрабатывает данные, связанные с пикселями (например, цвет, глубина, текстурные координаты). Пиксельный шейдер используется на последней стадии графического конвейера для формирования фрагмента изображения.
Можно ли удалять и как это правильно сделать
Разработчики настоятельно рекомендуют не удалять системные файлы. Однако для любого правила действуют свои исключения. Вот и Shaders не относится к важным данным, которые нельзя трогать.
Важно! Среди геймеров бытует мнение, что удаление любого файла может расцениваться, как вмешательство в систему, что приведёт к бану в игре или невозможности дальнейшего гейминга. Однако таким влиянием рассматриваемая папка явно не обладает.
Тем не менее удалять данные, содержащиеся в Shaders, без крайней необходимости не стоит.
К тому же, все удаленные файлы будут восстановлены после повторного запуска утилиты. То есть никакого положительного эффекта достигнуто не будет, поскольку память смартфона заполнится снова. Перед удалением важно убедиться, что файлы действительно относятся к кэшу, и не нужны для более серьёзных действий, влияющих на приложение.
Сделать это можно двумя способами:
- посредством отслеживание объема;
- сменой имени.
При первом способе достаточно просто посмотреть, сколько весит хранилище до активной игры и по завершении процесса. Если вес существенно увеличивается, значит файлы – это всего лишь кэш.
Проверка важности содержимого также выполняется путём смены названия. Если после переименования утилита продолжит запускаться стабильно и без сбоев, значит файлы не имеют никакой системной важности, и их удаление не станет критичным для системы.
Супер-реалистичная графика в Sand piper
Например, посмотрите, на вот этот милый мультфильм, песчинки, перышки птички, волны, все выглядит невероятно реальным.
*Видео могут забанить на Youtube, если оно не открывается, погуглите pixar sandpiper – короткометражный мультфильм про храброго песочника очень милый и пушистый. Умилит и продемонстрирует насколько крутой может быть компьютерная графика.
Так вот это RenderMan от фирмы Pixar. Он стал первым языком программирования шейдеров. API RenderMan является фактическим стандартом для профессионального рендеринга, используется во всех работах студии Pixar и не только их.
Вершинный шейдер
Вершинными шейдерами делают анимации персонажей, травы, деревьев, создают волны на воде и многие другие штуки. В вершинном шейдере программисту доступны данные, связанные с вершинами например: координаты вершины в пространстве, её текстурные координатами, её цвет и вектор нормали.
Код шейдера
Шейдеры в Unity написаны на HLSL (High Level Shading Language), хотя обычно вы увидите, что его называют CG. Особенно, если имеете дело со встроенным конвейером (Built-in Render Pipeline).
Вы всегда увидите этот шейдерный код между тегами CGPROGRAM / HLSLPROGRAM и ENDCG / ENDHLSL. (А еще можете увидеть CGINCLUDE / HLSLINCLUDE, который включает код в каждый проход шейдера).
Шейдеры для URP / HDRP всегда должны использовать HLSL-версии этих тегов, так как CG-теги включают некоторые дополнительные файлы, которые не нужны этим конвейерам. В противном случае это приведет к ошибке из-за переопределения в библиотеках шейдеров.
Остальная часть файла .shader написана с использованием специфичного для Unity синтаксиса, известного как ShaderLab, и включает такие блоки, как «Shader», «Properties», «SubShader» и «Pass». Документацию по синтаксису Shaderlab вы можете найти здесь.
Технически Shaderlab имеет несколько устаревших способов создания шейдеров с фиксированными функциями, что означает отсутствие в надобности CG/HLSL PROGRAM, но я бы не стал беспокоиться об их изучении, поскольку программирование шейдеров намного полезнее.
В качестве альтернативы существуют редакторы шейдеров на основе нодов, такие как Shader Graph (официальный, доступен для URP или HDRP), Amplify Shader Editor (работает во всех конвейерах, но не бесплатный) и Shader Forge (больше не поддерживается, работает только в старых версиях Unity).
Если остались вопросы
Как обычно, если у Вас остались какие-то вопросы, задавайте их в комментариях, я всегда отвечу. За любое доброе слово или правку ошибок я буду очень признателен.
Пишет статьи о разработке игр. Не инди, — работает рендеринг-программистом в крупной ААА студии в Санкт-Петербурге. Большой поклонник игр Naughty Dog.
Пользователи часто сталкиваются с тем, что на устройствах, работающих под управлением операционной системы Android, по умолчанию содержится множество папок с совершенно непонятным названием, назначением и содержанием. И пока памяти на гаджете достаточно, их наличие, в общем-то, никого особо не волнует. Но как только устройство начинает сигнализировать о заполненном хранилище, возникает желание удалить все ненужное, хотя прежде не помешает разобраться, что именно относится к этой категории. И первой на пути возникает непонятная папка Shaders.
ООП в массы! Делаем свой класс шейдера
Код, описывающий шейдер, производящий его компиляцию, позволяющий производить настройку шейдера может быть весьма громоздким. Так давайте же сделаем нашу жизнь чуть проще, написав класс считывающий наш шейдер с диска, производящий его компиляцию, линковку, проверку на ошибки, ну и конечно, имеющий простой и приятный интерфейс. Таким образом ООП поможет нам инкапсулировать весь этот хаос внутри методов нашего класса.
Начнем разработку нашего класса с объявления его интерфейса и пропишем во вновь созданный заголовочник все необходимые include директивы. В результат получаем нечто подобное:
Давайте будем молодцами и будем использовать директивы ifndef и define, чтобы избежать рекурсивного выполнения директив include. Этот совет относится не к OpenGL, а к программированию на C++ в целом.
И так, наш класс будет хранить в себе свой идентификатор. Конструктор шейдера будет принимать в качестве аргументов указатели на массивы символов (иначе говоря текст, а в контексте класса, уместнее будет сказать — путь к файлу с исходным кодом нашего шейдера), содержащие путь к файлам, содержащим вершинный и фрагментный шейдеры, представленные обычным текстом. Также добавим утилитарную функцию Use, наглядно демонстрирующую преимущества использования классов-шейдеров.
Считывание файла шейдера. Для считывания будем использовать стандартные потоки C++, помещая результат в строки:
Теперь нам нужно скомпилировать и слинковать наш шейдер (давайте будем делать все хорошо, и сделаем функционал, сообщающей нам об ошибке при компиляции и линковки шейдера. Неожиданно, …но это очень полезно в процессе отладки):
Ну а вишенкой на торте будет реализация метода Use:
А вот и результат нашей работы:
В данном примере мы поместили наш вершинный шейдер в файл shader.vs, а фрагментный шейдер в файл shader.frag. В принципе, вы можете задать собственную конвенцию именования файлов, особой роли это не сыграет, но делайте это обдуманно, сохраняя семантику.
1. Модифицируйте вершинный шейдер так, чтобы в результате треугольник перевернулся: решение.
2. Передайте горизонтальное смещение с помощью формы и переместите треугольник к правой стороне окна с помощью вершинного шейдера: решение.
3. Передайте фрагментному шейдеру позицию вершины и установите значение цвета равное значению позиции (посмотрите, как позиция вершины интерполируется по всему треугольнику). После того, как сделаете это, постарайтесь ответить на вопрос, почему нижняя левая часть треугольника черная?: решение
Читайте также:
- Когда будет скидка на ark survival evolved на xbox one
- Можно ли в сбис отправлять в госорганы отчетность без эцп
- Сколько компьютеров можно подключить на один автомат
- Произошла ошибка загрузки файла код проблемы sd 116 null
- Не удалось получить доступ к файлу проверьте подключение или сократите имя файла android