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

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

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

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

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

GPU-ядрышки резвятся на фоне серьезного CPU
GPU-ядрышки резвятся на фоне серьезного CPU

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

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

Кубик до и после графичекой карты
Кубик до и после графичекой карты

Процесс отрисовки

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

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

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

Пример буфера глубины
Пример буфера глубины

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

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

Шейдер вершин

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

Трансформации вершин.
Трансформации вершин.

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

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

Тень!
Тень!

Из занимательного — итоговые вершины располагаются на так называемой плоскости Clip Space и находятся в диапазоне от -1.0 до 1.0. Именно с такими координатами потом и работает видеокарта.

Шейдер фрагментов

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

Куб!
Куб!

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

Знакомое лицо
Знакомое лицо

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

Знакомое лицо (20
Знакомое лицо (20

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

color = texture_color(cube_texture, data.uv);

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

Как же он хорош
Как же он хорош

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

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

В общем-то, на этом все :)

Куб свиборга между резвящимися GPU-ядрышками
Куб свиборга между резвящимися GPU-ядрышками
131131
25 комментариев

фрагмент может на самом деле чуть-чуть залезать на соседние пикселиЭта фраза вынесла мозг даже мне.

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

Не смешивайте эти 2 понятия пожалуйста.

шейдер (или группа шейдеров по очереди) занимается переводом координат относительно объекта, в координаты на текстуре.Начал было возмущенно писать, но вы чуть ниже исправились и упомянули что все же Clip Space ;)

Этим занимается графическая библиотека и движок, самостоятельно интерполируя точки из вершин.Стоило вас похвалить - и на тебе. Какая библиотека? Какой движок?  Этим занимается texture sampler прямо на GPU.

пиксельном шейдере можно получить информацию о теняхЛучше бы вы это не писали, а то большая часть народу подумает что есть некая встроенная функция просчета теней на GPU :)

А вообще инфа ну просто капец как поверхностно подана - те кто не в теме все равно ничего не поймут, а те кто в теме будут бугуртить (как это делаю я, ага )) )

26
Ответить

Спасибо за содержательную критику. Я постарался исправить статью, чтобы чуть она была более технически точной. Про интерполяцию UV — я хотел объяснить, что это делается «за шторой», но немного перестарался :)

Те, кто не в теме, надеюсь все-таки что-то поймут 

5
Ответить

Комментарий недоступен

1
Ответить

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

1
Ответить

Милейший, а где Вы изучали вышеизложенную теорию?

Ответить

Комментарий недоступен

11
Ответить

Именно для песков времени пришлось купить Geforce FX с shader model 2.0.

4
Ответить