⚠️ Обращайте внимание на даты.
Этот блог больше не ведётся с 17 января 2023, и на тот момент с написания этой страницы (13.07.2013) прошло 9 лет.
На сегодня удвоенная доза GLSL! Разберём сразу два шейдера. Новые приёмы, которые мы сегодня рассмотрим, будут достаточно типичными даже для программирования не на видеокарточках, поэтому с их пониманием проблем возникнуть не должно.
На сегодня единственное правило, которое вам надо запомнить: если операция обычно действует с отдельными числами, то на вектор она действует так, как если бы её применили к каждой его компоненте по порядку. А если векторов несколько, то ещё и к одним и тем же компонентам.
Элементарный пример, с которым мы уже сталкивались ранее - умножение. Пусть у нас есть жёлтый цвет, и он смешивается с тёмно-серым (цвета мы мешаем при помощи умножения):
\((1, 1, 0, 1) \cdot (0.5, 0.5, 0.5, 1) = (0.5, 0.5, 0, 1)\)
Первое умножается на первое, второе на второе, и так далее.
Поехали.
Шейдер, который мы сейчас рассмотрим, реализован отвратительно. Он не плох с точки зрения кода, но эффект, который он обеспечивает, сложно реализовать хуже. Идея проста - сделать рисуемое более “запикселенным”, чтобы каждая точка нарисованного была крупной, и представляла много точек исходного изображения. Проблема в этом эффекте всего одна - выбор цвета, которым будет закрашена каждая точка. И в зависимости от того, как будет выбран этот цвет, эффект будет очень разным.
Наш шейдер с этим не парится - он берёт цвет из левого верхнего угла каждой крупной точки. Но припоминаем - каждая точка обрабатывается шейдером отдельно, без взаимодействия с другими. Как она узнаёт, где границы точки? Ответ прост - текстурные координаты. Взгляните на сам код:
Этот шейдер очень быстр, и только поэтому хоть сколько-нибудь полезен. Качество можно повысить так: брать из текстуры несколько точек неподалёку и брать средний цвет. Но имейте в виду, что каждый вызов texture2D - удовольствие достаточно дорогое.
Последний шейдер предельно прост. Он берёт один цвет, и заменяет другим. И сделано это вполне очевидным способом, через if:
Последнее замечание - у видеокарт большие проблемы со скачками в другие места выполняемого кода. А на низком уровне, после компиляции многие конструкции выражаются через скачки. На центральном процессоре, по крайней мере. if - это скачок за следующее дейстие, если условие под ним ложно. Цикл - это как if, только в конце скачок к началу if. Вот любые такие конструкции для видеокарт - высокоэффективные генераторы мощных тормозов. Компиляторы при первой возможности пытаются развернуть такие циклы, и если это не получается - выдают предупреждение или даже ошибку.
nVidia уже работает над исправлением этого неприятного эффекта, и какие-то подвижки в эту сторону делались в их архитектуре Fermi (если я верно помню). Но пока она не получила широкого распространения (пост уже несколько неактуален, примечание от 13.02.2014, Tegra K1 построена аж на Kepler), нам придётся несколько ограничить использование циклов.
Люди не сидят на месте и придумывают разные весёлые костыли, чтобы покрыть обработку больших объёмов данных. К примеру, я видел следующий приём - чтобы усреднить цвет с картинки (512х512) в две строчки (2х512), делается несколько прогонов шейдера, который уменьшает количество строк вдвое, при этом каждый пиксель берёт цвет, средний между своим текселем и стоящим под ним.
По количеству строк: 512 -> 256 -> 128 -> 64 -> 32 -> 16 -> 8 -> 4 -> 2
Но это даёт уж очень много (8) прогонов шейдеров. Впрочем, каждый из них довольно лёгкий. В каких-то случаях это работает, в GM я это не проверял.
Я всю эту войну с шейдерами в GM веду с одной целью - сделать быструю систему освещения. Я прочитал занятную реализацию на HLSL (язык шейдеров в DirectX), но она использует штуки, недоступные в нашем GLSL ES. Но мне кажется, я знаю, как этот момент преодолеть. Несколько потеряв в производительности, но получив взамен потрясный эффект.