Шейдеры. Или почему мир игр стал визуально так похож на наш!
Всем привет! Так как я поставила себе цель когда-нибудь выпустить блог о том, какие технологии улучшения трехмерной графики применяются в топовых играх в последнее время, то я решила сначала разобраться в том, как за последнее время плотного использования трехмерной графики в видеоиграх она вообще достигла таких высот. Или проще говоря засчет чего это происходит, что отвечает за детализацию изображений, которое мы видим на своих экранах, запуская очередную любимую игру.
Всем нам нравится, когда в играх реалистичные пейзажи: красиво переливающиеся блики солнца на водной глади, движения каждого листочка на ветке во время ветра, волосы персонажей выглядят лучше, чем у тебя и много другое. В общем все, что нас так притягивает в современных играх не взялось из неоткуда(ежу понятно). Так кто же «виновник торжества»
Может кто-то уже и догадался по названию поста(глупый вопрос, конечно, там ж написано), речь пойдет о шейдерах. Что это, для чего и почему. Постараюсь разложить все по полочкам. Начнем:)
Что вообще такое эти самые шейдеры. Шейдеры — это программки, которые исполняются на видеокарте и описывают эффекты, методы обработки и прорисовки графики. Изначально GPU преимущественно обрабатывали 2D графику. В это время появляются так называемые акселераторы(ускорители). Они берут на себя часть операций, связанную с обработкой графических данных, сами прорисовывают каждый пиксель и определяю его цвет. Все это использовали видеокарты NVIDIA Riva128 и 3Dfx Voodoo. Эти карты брадли на себя только такие задачи как текстурирование и закраску, все остальное выполнял СPU. Естественно картинка была соответствующей. Подумайте сами, процессор и так загружен другими системными задачами, а тут еще ему и графические данные прорисовывать приходится, которые, по сравнению с арифметическими расчетами, просто звери.
Но в те времена этого было достаточно конечному пользователю. Далее начали применяться графические библиотеки такие как Open Gl, Direct 3d. Они снимали с CPU значительную часть нагрузки и брали на себя. Теперь видеокарта строила сцену с нуля. Производителям видеоигр в те времена такое решение было удобно. Все применяемые в тех играх эффекты были уже аппаратно встроены в видеокарты. Их было достаточно для игр того поколения. Но ничего не стоит на месте, разработчики видеоигр стремились вырваться вперед по сравнению с конкурентами и появилась потребность как-то улучшать имеющиеся эффекты 3D графики. Тогда то и появилась проблема — для того, чтоб апгрейдить какой-то эффект, разработчикам видеоигр приходилось подождать годик, чтоб вышла видеокарта, поддерживающая данный эффект. Что было и дорого и длительно по времени. Тогда — то и появились шейдеры. Так началась новая эра развития трехмерной графики в игровой индустрии.


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

Пиксельные шейдеры или геометрические. В отличие от вершинных, геометрические шейдеры оперируют на уровне примитивов. Проще говоря, они делят изображение на примитивы(точки, отрезки, треугольники) и работают с каждым в отдельности. Сглаживают, освещают, анимируют, добавляют эффекты HDR и Motion Blur. Главное назначение пиксельных шейдеров заключается в комплексном смешивании текстур, просчете освещенности и оптических свойств моделей, постобработке кадра. В общем, почти все, что мы видим в современных играх, — плод трудов пиксельных шейдеров!


Естественно, как вы навреное уже поняли, шейдеры пишутся игровыми программистами. С развитием трехмерной графики, написание шейдеров усложняется. Раньше они вообще состояли из небольшого куска кода на ассемблероподобном языке, а сейчас же существует несколько шейдерных языков, которые ориентированые на достижение максимального качества визуализации. Ни одна современня игра не обходится без шейдеров, если вы видите потрясную картинку с ослепительным эффектом, то знайте, это все работа шейдеров. Без них мы бы до сих пор играли в игры типо первых Quake, Doom, Unreal. Так же необходимо отметить, что современные игры сочетают в себе много шейдеров одновременно, которые выполняются параллельно вашей видеокартой.
Однако, но хоть шейдеры — это программы, их количество напрямую связано с качеством вашей видеокарты. Их количество настроено изначально на заводе-производителе и самому изменить это параметр невозможно. Хотя, обладателям консолей не стоит этого бояться т.к игры изначально оптимизируют под консоли и она в любом случае пойдет на приемлемом качестве. И как раз из-за шейдеров обладателям ПК приходится сверять системные характеристики и постоянно увеличивать мощность своего компьютера. Так же нужно помнить, что всего хорошего должно быть в меру. Программист, который разрабатывает шейдеры должен четко понимать что, где и как шейдер должен делать. Иначе получится полная ж… иными словами переизбыток шейдеров ничего хорошего не сулит!

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

Эй-эй, это между прочим пример сглаживания!)
Лучшие комментарии
Начинание благое — нести просвещение в массы и рассказать народу про шейдеры.
Но по тексту есть множество принципиальных ошибок и неточностей, которые вводят читателя в заблуждение.
Перечислять их все не буду — это на отдельный пост по размеру выльется.
Скажу лишь, что в статье 50% текста — чистая правда, доступно преподнесённая для рядового геймера. А другие 50% — какая-то бессмыслица, которая не имеет ничего общего с действительностью. Всё-таки видно, что автор сама недостаточно погрузилась в тему.
Не с нуля. Проц всё равно гонит в видюху некоторые входные данные.
Поверхность не может быть «без шейдеров». Её жу в любом случае что-то должно отрисовать. Просто они могут быть так называемыми «fixed-function» или «programmable» шейдерами. В первом случае у разраба есть лишь выбор из некоторых алгоритмов смешения цветов, которые аппаратно зашиты в видюху (как раз то, о чём говорит автор словами «все эффекты были встроены»). А во втором случае — разраб сам пишет любой алгоритм отрисовки, пользуясь уже арсеналом простых арифметических операций и некоторым набором высокоуровневых функций.
И — да — на скриншотах юнити. И, судя по внешнему виду, сравниваются стандартные шейдеры Diffuse и Bumped diffuse. В обоих случаях — это полноценные «programmable» шейдеры, а не fixed-function. Просто один попроще, другой посложнее.
Про виды шейдеров — простите, без обид — вообще ахинея.
В современном графическом пайплайне имеются следующие:
Вертексный
Тесселяционный, hull и domain (эта троица идёт вместе)
Геометрический
пиксельный (именуемый также фрагментным)
Обязательны только первый и последний (vertex + pixel aka fragment), остальные — опциональны и поддерживаются только в последних версиях DX и OGL.
И, да, пиксельный шейдер — не равно геометрический. Вообще ни разу.
Геометрический позволяет плодить новую геометрию прямо в видюхе. Фрагментный — работает попиксельно, вычисляя уже итоговую картинку (цвета писелей на экране).
Про HDR и Motion blur тут вообще ни к месту. Вникать не буду.
Мысль правильная, формулировка — нет. В зависимости от шейдерной модели (DX9 — шейдерные модели с 1.0 по 3.0; DX10 — 4.0, DX11 — 5.0) есть ряд ограничений. На количество инструкций в шейдере, например. И на поддержку этих инструкций в разных шейдеров (например, чтение текстуры в вертексном шейдере — только в ). Кроме того, есть «мягкий лимит» на количество вертексов в сцене. Он ни в каких ограничениях не прописан, но определяется размером памяти в видюхе. И у старых видюх она, естественно, была меньше — так что и геометрии можно было меньше запихнуть.
Кроме того, есть т.н. draw-call’ы. И это отдельная песня
Но такого лимита, как «количество шейдеров в сцене» — его нет.
В целом же — повторюсь, начинание благое. И желание самостоятельно вникнуть в эти дебри — определённо, похвальное. Для человека, который не занимается шейдерным программированием — это титанический труд, поверьте. И он однозначно заслуживает уважения.
Но — сами понимаете — я не мог не обратить внимания на явные и существенные ошибки.

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

Двигателем прогресса в сторону фотореалистичности картинки в компьютерной графике я считаю именно компьютерные игры, поэтому давайте именно в разрезе видео-игр и поговорим о том, что такое “шейдеры”.
До того, как появились первые графические ускорители, всю работу по отрисовке кадров видеоигры выполнял бедняга центральный процессор.
Отрисовка кадра, довольно рутинная работа на самом деле: нужно взять “геометрию” – полигональные модели (мир, персонаж, оружие и т.д.) и растеризовать. Что такое растеризовать? Вся 3d модель состоит из мельчайших треугольников, которые растеризатор превращает в пиксели (то есть “растеризовать” значит превратить в пиксели). После растеризации взять текстурные данные, параметры освещенности, тумана и тп и рассчитать каждый результирующий пиксель игрового кадра, который будет выведен на экран игроку.
Так вот, центральный процессор (CPU – Central Processing Unit) слишком умный парень, чтобы заставлять его заниматься такой рутиной. Вместо этого логично выделить какой-то аппаратный модуль, который разгрузит CPU, чтобы тот смог заниматься более важным интеллектуальным трудом.
Таким аппаратным модулем стал – графический ускоритель или видеокарта (GPU – Graphics Processing Unit). Теперь CPU подготавливает данные и загружает рутинной работой коллегу. Учитывая, что GPU сейчас это не просто один коллега, это толпа миньонов-ядер, то он с такой работой справляется на раз.
Но мы пока не получили ответа на главный вопрос: Что такое шейдеры? Подождите, я подвожу к этому.
Хорошая, интересная и близкая к фото-реализму графика, требовала от разработчиков видеокарт реализовывать многие алгоритмы на аппаратном уровне. Тени, свет, блики и так далее. Такой подход – с реализацией алгоритмов аппаратно называется “Фиксированный пайплайн или конвейер” и там где требуется качественная графика он теперь не встречается. Его место занял “Программируемый пайплайн”.
Запросы игроков “давайте, завозите хороший графоний! удивляйте!”, толкали разработчиков игр (и производителей видеокарт соответственно) все к более и более сложным алгоритмам. Пока в какой-то момент зашитых аппаратных алгоритмов им стало слишком мало.
Наступило время видеокартам стать более интеллектуальными. Было принято решение позволить разработчикам программировать блоки графического процессора в произвольные конвейеры, реализующие разные алгоритмы. То есть разработчики игр, графические программисты отныне смогли писать программы для видеокарточек.
И вот, наконец, мы дошли до ответа на наш главный вопрос.
“Что такое шейдеры?”
Ше́йдер (англ. shader — затеняющая программа) — это программа для видеокарточки, которая используется в трёхмерной графике для определения окончательных параметров объекта или изображения, может включать в себя описание поглощения и рассеяния света, наложения текстуры, отражения и преломление, затенение, смещение поверхности и множество других параметров.

Что такое шейдеры? Например, вот такой эффект можно получить, это шейдер воды примененный к сфере.
Графический пайплайн
Преимущество программируемого конвейера перед его предшественником в том, что теперь программистам можно создавать свои алгоритмы самостоятельно, а не пользоваться зашитым аппаратно набором опций.
Сначала видеокарты оснастили несколькими специализированными процессорами, поддерживающими разные наборы инструкций. Шейдеры делили на три типа в зависимости от того, какой процессор будет их исполнять. Но затем видеокарты стали оснащать универсальными процессорами, поддерживающими наборы инструкций всех трёх типов шейдеров. Деление шейдеров на типы сохранилось для описания назначения шейдера.
Помимо графических задач с такими интеллектуальными видеокартами появилась возможность выполнения на GPU вычислений общего назначения (не связанных с компьютерной графикой).
Впервые полноценная поддержка шейдеров появилась в видеокартах серии GeForce 3, но зачатки были реализованы ещё в GeForce256 (в виде Register Combiners).
Виды шейдеров
В зависимости от стадии конвейера шейдеры делятся на несколько типов: вершинный, фрагментный (пиксельный) и геометрический. А в новейших типах конвейеров есть еще шейдеры тесселяции. Подробно обсуждать графический конвейер мы не будем, я все думаю не написать ли об этом отдельную статью, для тех кто решит заняться изучением шейдеров и программирования графики. Напишите в комментариях если Вам интересно, я буду знать, стоит ли тратить время.
Вершинный шейдер
Вершинными шейдерами делают анимации персонажей, травы, деревьев, создают волны на воде и многие другие штуки. В вершинном шейдере программисту доступны данные, связанные с вершинами например: координаты вершины в пространстве, её текстурные координатами, её цвет и вектор нормали.
Геометрический шейдер
Геометрические шейдеры способны создавать новую геометрию, и могут использоваться для создания частиц, изменения детализации модели «на лету», создание силуэтов и т.п. В отличие от предыдущего вершинного, способны обработать не только одну вершину, но и целый примитив. Примитивом может быть отрезок (две вершины) и треугольник (три вершины), а при наличии информации о смежных вершинах (англ. adjacency) для треугольного примитива может быть обработано до шести вершин.
Пиксельный шейдер
Пиксельными шейдерами выполняют наложение текстур, освещение, и разные текстурные эффекты, такие как отражение, преломление, туман, Bump Mapping и пр. Пиксельные шейдеры также используются для пост-эффектов.
Пиксельный шейдер работает с фрагментами растрового изображения и с текстурами — обрабатывает данные, связанные с пикселями (например, цвет, глубина, текстурные координаты). Пиксельный шейдер используется на последней стадии графического конвейера для формирования фрагмента изображения.
На чем пишут шейдеры?
Изначально шейдеры можно было писать на assembler-like языке, но позже появились шейдерные языки высокого уровня, похожие на язык С, такие как: Cg, GLSL и HLSL.
Такие языки намного проще чем C, ведь задачи решаемые с их помощью, гораздо проще. Система типов в таких языках отражает нужды программистов графики. Поэтому они предоставляют программисту специальные типы данных: матрицы, семплеры, векторы и тп.
RenderMan
Все что мы обсудили выше относится к realtime графике. Но существуют non-realtime графика. В чем разница – realtime – реальное время, тоесть здесь и сейчас – давать 60 кадров в секунду в игре, это процесс реального времени. А вот рендерить комплексный кадр для ультрасовременной анимации по несколько минут это non-realtime. Суть во времени.
Например, графику такого качества как в последних мультипликационных фильмах студии Pixar получить в реальном времени мы сейчас не можем. Очень большие рендер-фермы обсчитывают симуляции света по совсем другим алгоритмам, очень затратным, но дающим почти фотореалистичные картинки.
Супер-реалистичная графика в Sand piper
Например, посмотрите, на вот этот милый мультфильм, песчинки, перышки птички, волны, все выглядит невероятно реальным.
*Видео могут забанить на Youtube, если оно не открывается, погуглите pixar sandpiper – короткометражный мультфильм про храброго песочника очень милый и пушистый. Умилит и продемонстрирует насколько крутой может быть компьютерная графика.
Так вот это RenderMan от фирмы Pixar. Он стал первым языком программирования шейдеров. API RenderMan является фактическим стандартом для профессионального рендеринга, используется во всех работах студии Pixar и не только их.
Полезная информация
Теперь Вы знаете что такое шейдеры, но помимо шейдеров, есть другие очень интересные темы в разработке игр и компьютерной графике, которые наверняка Вас заинтересуют:
-
,- техника создания потрясающих эффектов в современных видео-играх. Обзорная статья и видео с уроками создания эффектов в Unity3d ,- если Вы задумываетесь о разработке видеоигр, в качестве профессиональной карьеры или хобби, эта статья содержит отличный набор рекомендаций “с чего начать”, “какие книги читать” и т.д.
Если остались вопросы
Как обычно, если у Вас остались какие-то вопросы, задавайте их в комментариях, я всегда отвечу. За любое доброе слово или правку ошибок я буду очень признателен.
Шейдеры. Что и как
Сразу оговорюсь, что материал рассчитан на тех, кто никогда не работал с шейдерами или вообще не знаком с разработкой игр, то есть это в некотором смысле научпоп.
Слово «шейдер» в контексте разработки игр очень популярно, слышать его могли и те, кто игры не делает. Само слово изначально появилось от англ. 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 данные.
Что такое шейдеры, зачем они нужны и как разобраться во всем этом. Краткий экскурс по рендерингу в Unity
Всем привет. Сегодня я хотел бы задеть такую тему, как рендеринг и шейдеры в 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 матрицы представляют собой пространственные преобразования, и среди них мы можем найти:
Все они соответствуют матрицам четыре на четыре (4×4), то есть каждая из них имеет четыре строки и четыре столбца числовых значений. Примером матрицы может быть следующий вариант:
Как и говорилось ранее — наши объекты обладают двумя узлами (к примеру в некоторых графических редакторах они называются transform и shape) и оба отвечают за положение наших вершин в пространстве (объектном). Объектное пространство в свою очередь определяет положение вершин относительно центра объекта.
И каждый раз, когда мы будем изменять положение, поворот или масштаб вершин объекта — мы будем умножать каждую вершину на матрицу модели (в случае с Unity — UNITY_MATRIX_M).
Чтобы переводить координаты из одного пространства в другое и работать внутри него — мы будем постоянно работать с различными матрицами.
Свойства полигональных объектов
Продолжая тему работы с полигональными объектами, можно сказать, что в мире 3D графики каждый объект состоит из полигональной сетки. Объекты на нашей сцене обладают свойствами и каждый из них всегда содержит вершины, тагненсы, нормали, UV-координаты и цвет — все это вместе формирует Mesh. Всем этим и управляют такие подпрограммы, как шейдеры.
При помощи шейдеров мы можем получать доступ и изменять каждый из этих параметров. При работе с этими параметрами мы как правило будем использовать вектора (float4). Далее разберем каждый из параметров нашего объекта.
Подробнее про вершины
Вершины объекта, соответствующие набору точек, определяющих площадь поверхности в двухмерном или трехмерном пространстве. В 3D редакторах, как правило, вершины представлены как точки пересечения сетки и объекта.
Вершины характеризуются, как правило двумя моментами:
Они являются дочерними компонентами компонента transform;
Они имеют определенное положение в соответствии с центром общего объекта в локальном пространстве.
Это значит, что у каждой вершины есть свой компонент трансформаций, отвечающий за его размер, поворот и положение, а также атрибуты, которые указывают где эти вершины находятся относительно центра нашего объекта.
Нормали у объектов
Нормали по своей сути помогают определить, где у нас находится лицевая сторона у фрагментов нашего объекта. Нормаль соответствует перпендикулярному вектору на поверхности полигона, который используется для определения направления или ориентации грани или вершины.
Тангенсы
Обратившись к документации 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 мы можем вручную определить несколько свойств и команд, среди них блок 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 добавляет код по умолчанию, чтобы облегчить процесс компиляции. В шейдере мы можем найти блоки кода, структурированные таким образом, чтобы графический процессор мог их интерпретировать.
Если мы откроем наш шейдер, его структура будет выглядит похожим образом:
Скорее всего, посмотрев на этот код, вы не поймете, что происходит в различных его блоках. Однако, чтобы начать наше исследование, мы обратим внимание на его общую структуру.
С текущим примером и его основной структурой становится несколько понятнее. Шейдер начинается с пути в инспекторе редактора Unity (InspectorPath) и имени (shaderName), затем свойства (например.
текстуры, векторы, цвета и т.д.), затем SubShader и в конце необязательный параметр Fallback для поддержки различных вариантов.
Таким образом, мы уже понимаем что, где и зачем начать писать.
Работа с ShaderLab
Большинство наших шейдеров, написанных в коде, начинаются с объявления шейдера и его пути в инспекторе Unity, а также его имени. Оба свойства, такие как SubShader и Fallback, записываются внутри поля «Shader» в декларативном языке ShaderLab.
И путь, и имя шейдера могут быть изменены по мере необходимости в рамках проекта.
Свойства шейдера соответствуют списку параметров, которыми можно манипулировать из инспектора Unity. Существует восемь различных свойств, как по значению, так и по полезности. Мы используем эти свойства относительно шейдера, который мы хотим создать или изменить, динамически или в рантайме. Синтаксис для объявления свойства следующий:
Где «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, соответствующий стандартному шейдеру, чтобы аппаратное обеспечение могло продолжить выполнение своей задачи без графических ошибок.
Прочитать подробнее про параметры и сабшейдеры можно здесь и здесь.
Блендинг
Блендинг нужен нам для процесса смешивания двух пикселей в один. Блендинг поддерживается как в Built-In, так и SRP.
Блендинг происходит на этапе, который объединяет конечный цвет пикселя с его глубиной. Этот этап, который происходит в конце пайплайна рендеринга, после этапа фрагментного (пиксельного) шейдера, при выполнении
stencil-буфера, z-буфера и смешивания цветов.
По умолчанию это свойство не записано в шейдере, так как это необязательная функция и используется в основном при работе с прозрачными объектами, например, когда мы должны нарисовать пиксель с
низким уровнем непрозрачности перед другим (часто такое используется в UI).
Мы можем включить смешивание здесь:
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» после которого следует режим, который мы хотим использовать.
Мы также можем динамически настраивать параметры 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 / OtherSetting, мы можем активировать или деактивировать опции тумана нашего шейдера.
Помимо этого мы можем подключать файлы Cg / HLSL в наш шейдер. Как правило мы делаем это, когда подключаем UnityCG.cginc, который в свою очередь включает в себя координаты тумана, позиции объекта для клиппинга, трансформации текстур, перенос и принятие тумана и многое другое, включая константы UNITY_PI.
Самое важное, что мы можем делать с Cg / HLSL — это написание непосредственных функций обработки вертексных и фрагментных шейдеров, использовать переменные этих языков и различные координаты, вроде текстурных (TEXCOORD0).
Подробнее о 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:
Превратиться в ноду:
Таким образом мы можем упростить себе жизнь и уменьшить время написание шейдеров за счет визуальных графов.
Итоги
Говорить на тему шейдеров можно очень много и очень долго, как и затрагивать сам процесс рендеринга. Здесь я не затронул шейдеры рейтрейсинга и Compute-Shading, капнул по шейдерным языкам поверхностно и описал процессы лишь с верхушки айсберга. Работа с графикой — это целые дисциплины, о которых в интернете можно найти тонны исчерпывающей информации, например:
Интересно было бы послушать о вашем опыте работы с шейдерами и рендерингом в рамках Unity, а так же услышать ваше мнение — что же лучше SRP или Built-In 🙂