Работа пиксельшейдера

Sovka Ivanov
5 min readJul 2, 2018

--

Эта статья для совсем гуманитариев. Если ты немного шаришь в каком-нибудь матане или программировании — переходи по следующей ссылке, и успехов тебе.

http://guglite.ru/?q=%D0%BF%D0%B8%D0%BA%D1%81%D0%B5%D0%BB%D1%8C+%D1%88%D0%B5%D0%B9%D0%B4%D0%B5%D1%80

Погнали.

Ниже есть скрин экрана. Можно подумать, что там шар, но это не так. Там кое-что получше — рисунок шара. То есть это просто набор пикселей, каждый из которых принимает определённый цвет, и в комплекте этот массив ̶в̶ы̶г̶л̶е̶д̶и̶т̶ ̶к̶а̶к̶ ̶х̶у̶й̶ выдаёт осмысленное изображение. Главный вопрос — откуда каждый пиксель знает, какой цвет ему принять. Я говорю о рилтайм-рендоре конечно же.

рисунок шара

Расчётом цвета заведует видеокарта. В ней много ядер (слабых процессоров), каждый из которых берёт на себя порцию пикселей, чтоб всем поровну, и занимается по очереди вычислением цвета каждого. И вот этот этап рендера, где происходит вышеописанное, представляет из себя функцию (часто называют программой) и называется пиксельным шейдером (pixel shader). Как и любая функция, он получает какие-то данные и использует их для расчётов, в результате получая выходные данные. Кароч пиксельшейдер выполняется для каждого пикселя, вычисляет его цвет на основе некоторых данных. Ща по порядку.

Входные данные — обязательно позиция геометрии в пространстве экрана. Если в этом пикселе геометрии нет, то он вообще не обрабатывается. Необязательно на усмотрение программиста — что угодно: нормаль, бинормаль, тангенциальная, UV-координата, пространственная координата, и даже любой кустомный левак, который программист посчитает нужным сюда впиздюрить.

Алгоритм — основная часть функции, обрабатывает то, что в неё напихали, выпихивает из себя выходные данные.

Выходные данные — обязательно искомый цвет (RGBA). Необязательно на усмотрение колдуна — глубина. Кустомного левака здесь быть не может.

Это был интродукшн, он кончился.

Алгоритм, маковка.

И вот у тебя есть пиксель. Ты можешь закрасить его любым цветом. Прям так и написать: “return float4(0.0, 1.0, 0.0, 1.0);”, и каждый пиксель, в котором есть геометрия, будет закрашен зелёным цветом. (скрин ниже) Это ̶ч̶о̶ ̶з̶а̶ ̶х̶у̶й̶н̶я̶ похоже на круг, но не похоже на шар, потому что не хватает освещения. И вот тут надобно отвлечься и порассуждать, чо такое освещение, чтобы понять как его добавить.

похоже на круг

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

так бля ёпта, тут у нас нормаль, хуё-моё блядь, источник света нахуй, значит направление луча туда-сюда ебать

тааааак падажжи ёбана

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

тут ещё переотражения должны быть, но сейчас можно пренебречь

Эту херню можно учесть. Насколько сильно свет рассеивается — полностью зависит от его угла падения на поверхность, например если пучок лучей прилетает под прямым углом (нулевым к нормали), то он попадает почти на все микроповерхности, а если под очень острым (близким к прямому к нормали), то только под те, которые находятся к нему лицом (микронормалью). Снова поясняющий рисунок.

Значит интенсивность рассеивания пропорциональна углу между падающим лучом и нормалью (не микронормалью). Чем больше угол — тем сильнее свет рассеивается, хуль тут непонятного. И чем сильнее он рассеется, тем больше его канет в хер и не попадёт тебе в глаз. И тем темнее будет поверхность в этом пикселе. Расчитать этот угол — как в тапочки насрать, через косинус, который можно получить через скалярное произведение луча и нормали, операция dot().

Закон Ламберта. Сложна, нипанятна нихрена

Кстати предыдущий абзац — довольно грубая интерпретация закона Ламберта, в которой я опустил много ненужного для интуитивного понимания техношлака. Можешь погуглить его на досуге, этот закон — великий отец всея графония, про него найдутся понятные научпопные статейки (надеюсь). Так вот теперь я возьму и напрямик напишу: освещённость = косинусУгла(луч, нормаль);

Ньютон велик, слава Ньютону.

Чтобы сделать его более глосси — можно возвести в степень. Чем ближе значение к нулю — тем оно охотнее ещё приблизится к нулю после возведения. Ещё можно на цвет умножить. Или добавить текстуру, но это я ща не буду делать. Вот так, один алгоритм на все пиксели.

Всего лишь вонючий косинус

если чо пиши мне сюда
https://t.me/MrHuempolbu

--

--

Sovka Ivanov

Папкин математик, мамкин программист, дизайнер папиной подруги.