Основи програмування шейдерів мовою GLSL. Частина 2 — Що таке шейдери, графічний конвеєр та растеризація.

Всім привіт, це друга частина серії статей про основи програмування шейдерів за допомогою мови GLSL. У цій частині ми розберемо що таке шейдери, їх призначення, які вони бувають, що таке мова програмування GLSL, та які ще мови програмування шейдерів існують окрім GLSL. Також розглянемо деякі поняття, які стосуються шейдерів та розуміння яких нам знадобиться у майбутньому.
На відміну від попередньої частини, де ми налаштовували інструменти та змогли написати простий тестовий шейдер, ця частина буде винятково теоретичною. У цій частині будуть розглянуті основні поняття для розуміння що таке шейдери, де вони застосовуються і з чим працюють.

Шейдери використовуються в комп’ютерній графіці, тому для початку нам треба визначити, що таке комп’ютерна графіка. Комп’ютерна графіка це технологія формування зображення за допомогою комп’ютера та його функціональних частин, застосовуючи алгоритми та функціонал цих самих частин, яки можуть бути запрограмованими.

Більшість комп’ютерів мають відеокарту, спеціалізований пристрій, який надає певний функціонал і можливості для формування зображень. Відеокарти отримують дані від комп’ютера, обробляють та повертають готове зображення, яке було сформовано за допомогою алгоритмів відеокарти.
Сучасні відеокарти використовують для формування зображення з допомогою тривимірних моделей. Відеокарта отримує тривимірну модель, а також певні параметри та налаштування, обробляє модель та видає зображення. Використовуючи різні параметри та тривимірну модель, за допомогою відеокарти можна отримувати потрібну візуалізацію цієї моделі.

Тому треба визначити що таке тривимірна модель. Тривимірна модель, це модель, складена в тривимірному просторі. Простір називається тривимірним, оскільки він має три однорідні виміри — довжину, ширину і висоту, тобто тривимірний простір описується за допомогою трьох одиничних ортогональних векторів.

Точки у такому просторі називаються вершинами (Vertex). Розташування вершин у просторі описується трьома векторами, які описують положення точки за висотою, шириною та глибиною (довжиною), від центру координат тривимірного простору.

Декілька вершин можуть бути об’єднані в так звані примітиви. Примітиви — це геометричні фігури, які можна одержати з допомогою кількох вершин. Дві вершини можуть утворювати лінію, що їх з’єднує. Три вершини дозволяють описувати трикутник. Одна вершина, дві вершини що формують лінію між ними, і три вершини що формують трикутник — є основними примітивами (точка, лінія, трикутник), з яких складається тривимірна модель.

Художникі тривімірної графикі за допомогою цих трьох примітивів моделюють тривимірні об’єкти, які будуть використані як вхідні дані для відеокарти, а результатом буде отримане зображення цієї моделі.

На зображенні вище видно тривимірну модель, складену з примітивів (точка, лінія, трикутник), яка буде передана відеокарті та її візуалізацію, яка отримана від відеокарти. Іншими словами, відеокарта отримує тривимірну модель та видає зображення її візуалізації.

Наступним основним поняттям яке нам треба розібрати для подальшого розуміння шейдерів, є буфер кадру (Framebuffer). Зображення, отримане за допомогою комп’ютера, складається з точок певного кольору. Набір цих точок називається буфером кадру, а кожна точка — фрагментом або пікселем (Pixel). Зображення в комп’ютерній графіці формуються з дискретного набору пікселів, які розташовані послідовно один за одним, стовпцями і по рядкам.

Власне, будь-яке зображення в комп’ютерній графіці є сіткою пікселів, де кожен піксель має певний колір. Тому пікселі так і називаються, pixel — скорочення від piсtures element. Відеокарти завжди видають зображення які складаються з пікселів, а також можуть обробляти таки зображення.

Але як відеокарта з набору вершин видає зображення, що складається з точок? Цей процес називається растеризацією. Растеризація — це процес перенесення на дискретну площину, яка складається з пікселів, геометричних примітивів.

Відеокарта отримує вершини і примітиви, описані в тривимірному просторі, і проектує їх на двовимірну площину буфера кадру, що складається з пікселів. Так ми отримуємо візуалізацію тривимірної моделі.

Головним розумінням вершин є те, що по суті це просто деякі обʼєкти які мають свої атрибути. Основним та ключовим атрибутом кожної вершини є позиція у тривимірному просторі. Позиція є основним атрибутом вершин, але не єдиним.
У вершин можуть бути різні атрибути, наприклад, текстурні координати, за допомогою яких примітив буде текстурований. Або колір вершини, за допомогою цього атрибуту можна прикрасити тривимірну модель. У вершини також може бути атрибут, що описує її напрямок (нормаль) для розрахунку освітлення.

Атрибутів у вершин може бути дуже багато, але головним атрибутом є положення, оскільки за допомогою її можна сформувати примітив. У наступних частинах ми дуже детально зупинимося на атрибутах вершин, тому що вони часто використовуються при програмуванні шейдерів. Зараз вам головне зрозуміти що таке вершини та її атрибути. По суті, вершини — це набір атрибутів, а один із головних атрибутів — це позиція.

Для того, щоб нам зрозуміти що таке шейдери, спочатку нам треба визначити таке поняття як графічний конвеєр (Graphics Pipeline). Графічний конвеєр — це система, яка описує вхідні дані та кожен етап їх обробки, з можливістю налаштування або перепрограмування певного етапу обробки даних. У більш простому сенсі графічний конвеєр визначає всі етапи обробки тривимірної моделі, після якої вона буде візуалізована.

На зображені вище за допомогою схеми описано стандартний графічний конвеєр. Ця схема застосовна для більшості графічних конвеєрів у сучасних графічних API, таких як Vulkan, DirectX, Metal тощо. Давайте пройдемося по кожному етапу цього конвеєра та детальніше розберемо кожен етап.

Першим етапом іде етап під назвою Input Assembler. На цьому етапі конвеєр отримує вершини тривимірної моделі та інформацію про те, яку геометричну топологію вони описують (лінію, трикутник, точку). Також на цьому етапі йде отримання всіх атрибутів вершин, щоб графічний конвеєр знав, як з ними працювати на наступних етапах.

Далі слідує етап вершинного шейдера — Vertex Shader. На цьому етапі кожна з раніше отриманих вершин обробляється за допомогою алгоритму, який називається “вершинний шейдер” (Vertex Shader). Це перший етап конвеєра, який можна перепрограмувати. Якщо не заданий вершинний шейдер, цей етап пропускається, оскільки за умовчанням немає причин обробляти чи модифікувати вершини тривимірної моделі.
Результатом вершинного шейдера є модифікація позиції вершини та опис додаткових атрибутів вершин, які можуть бути доступні для наступних етапів конвеєра.

Наступним етапом йде шейдер тесселяції — Tesselation Shader. Шейдери тесселяції дозволяють підрозділяти геометрію з урахуванням певних правил підвищення якості сітки. Це часто використовується для того, щоб такі поверхні, як цегляні стіни та сходи, виглядали менш плоскими та більш деталізованими. Просто кажучи, цей етап дозволяє збільшувати кількість примітивів, поділяючи вхідний примітив на додаткові примітиви.

Четвертим етапом йде шейдер геометрії — Geometry Shader. Шейдер геометрії запускається для кожного примітиву (трикутника, лінії, точки) і може відкидати його чи виводити більше примітивів, ніж надійшло. Це схоже на шейдер тесселяції, але набагато гнучкіше. Однак у сучасних програмах він використовується нечасто, оскільки не всі відеокарти дозволяють використовувати цей етап.

Далі йде етап растеризації — Rasterization. На етапі растеризації, примітив з математичної моделі, дискретизується в набір фрагментів, які є нічим іншим як пікселями буфера кадру (Framebuffer). Іншими словами, примітив (точка, лінія, трикутник) стають пікселями (фрагментами) на екрані.

Після етапу растеризації йде етап фрагментного шейдера — Fragment Shader. За допомогою фрагментного шейдера кожному пікселю (фрагменту) задається свій колір. Фрагментний шейдер буквально займається визначенням кольору пікселя і результатом його роботи є обчислення кольору пікселя, використовуючи його позиції та значення атрибутів вершин примітиву.

Останнім етапом графічного конвеєра є змішування вже наявних у буфері кадру кольорів пікселів із кольорами пікселя розтеризованого примітиву. Наприклад, в буфері кадру є вже готове зображення і поверх нього ми малюємо якийсь примітив. Унас є можливість змішати кольори пікселів наявні в буфері кадру з кольорами пікселів примітиву, отриманих після роботи фрагментного шейдера (Fragment Shader). Цей етап дозволяє реалізувати різні ефекти накладання одного зображення на інше.

На представленій схемі графічного конвеєра, етапи пофарбовані жовтим кольором є етапами які можуть бути перепрограмовані, а пофарбовані зеленим кольором можуть мати різні налаштування, але не можуть бути перепрограмованими. Також практично всі етапи жовтого кольору мають у своїй назві слово Shader, крім етапу тесселяції (Tesselation). Пов’язано це з тим, що на багатьох відеокартах цей етап не може бути перепрограмований і надається з певними налаштуваннями, або взагалі відсутній. На більш сучасних відеокарт цей етап може бути перепрограмований за допомогою шейдера тесселяції.

Ось ми з вами і підібралися до поняття Шейдер (Shader). У буквальному значенні це програма, яку можна використовувати на певному етапі конвеєра, замінивши стандартну програму цього етапу в графічному конвеєрі. У попередній частині ми перепрограмували етап фрагментного шейдера, за допомогою якого забарвили кожен піксель (фрагмент) у червоний колір. Саме перепрограмовані етапи графічного конвеєра та програми для них і називаються шейдерами.

Чому, власне, шейдери називаються шейдерами, а не програмами, якщо формально це і є програми? Спочатку, шейдерами називалися фрагментні шейдери, слово shader з англійської перекладається як “затіняє”, і цим словом описували частину коду, який затіняв пікселі, надаючи їм ефекту глибини і освітлення. Згодом цей код був перенесений на апаратний рівень — на відеокарту, яка виконувала його в рази швидше. Розробники ігор могли використовувати алгоритми, що реалізуються відеокартою, але не могли змусити відеокарту виконувати свої власні алгоритми для створення більш складних ефектів.
Для вирішення проблеми відеокарти стали апаратно-додавати алгоритми, затребувані розробниками. Незабаром стало ясно, що так реалізувати всі алгоритми неможливо та недоцільно. Подальшим етапом було дати розробникам можливість перепрограмувати алгоритм затінення, а згодом інші етапи графічного конвеєра. Ось так слово шейдер і стало означати певний етап графічного конвеєра, який можна перепрограмувати під потреби розробника.
Шейдери це спеціалізовані блоки коду, призначення яких перепрограмувати певний етап графічного конвеєра. Різні типи шейдерів не можуть замінити інші типи шейдерів. Це означає, що фрагментний шейдер (Fragment Shader) не може бути використаний як вершинний шейдер (Vertex Shader) або замість будь-якого іншого не фрагментного шейдера.
Але, гнучкість мови програмування GLSL і певні розширення Vulkan дозволяють створювати більш гнучкі шейдери, які можуть описувати кілька етапів графічного конвеєра. Але про це буде в наступних статтях цієї серії.
Зараз нам важливо розуміти, що таке шейдери в розрізі етапів графічного конвеєра, а також з боку програми, яку можна перепрограмувати. Так як шейдери це етап, що перепрограмується, то має бути і мова програмування, за допомогою якого і розробляються шейдери. Цією мовою є мова програмування шейдерів GLSL.

GLSL (OpenGL Shading Language, Graphics Library Shader Language) — мова високого рівня для програмування шейдерів. Розроблена для перепрограмування етапів графічного конвеєра OpenGL. Синтаксис мови базується на мові програмування ANSI C, однак, через його специфічну спрямованість, з нього було виключено багато можливостей для спрощення мови та підвищення продуктивності. GLSL є строго типізованим. Також до мови включені додаткові функції та типи даних, наприклад для роботи з векторами та матрицями, які відсутні в ANSI C.
Спочатку мова GLSL розроблялася для графічного API OpenGL, це можна зрозуміти за назвою мови, але вона також використовується в графічному API Vulkan, оскільки Vulkan є продовженням розвитку ідей OpenGL. Це дозволяє без особливих проблем переносити шейдери з OpenGL на Vulkan і назад.

У багатьох графічних API є своя мова програмування шейдерів. Наприклад, у графічному API від Microsoft — DirectX, використовується мова програмування HLSL (High-Level Shading Language) та Cg, для Metal API від Apple — MSL (Metal Shading Language). Для графічного API WebGPU, що працює у браузерах, використовується мова WGSL (WebGPU Shading Language). Всі вони дуже схожі між собою, особливо в плані синтаксису, так як більша частина заснована на мові програмування ANSI C, яка була обрана через свою простоту і добру продуктивність.
Ця серія статей присвячена основам програмування мовою GLSL, інші мови для програмування шейдерів у наступних частинах цієї серії не будуть згадуватись або розглядатися. Але, вивчення GLSL дасть вам розуміння того, як працюють шейдерні мови, і розуміння багатьох синтаксичних елементів інших шейдерних мов, які є спільними з GLSL.
Крім описаних вище етапів графічного конвеєра у вигляді шейдерів, є ще кілька шейдерів, які виконуються на відеокарті і які можна перепрограмувати.

Першим подібним типом шейдерів є обчислювальні шейдери (Compute Shader). Ці шейдери не беруть участь у графічному конвеєрі, тому що їх призначення — робити обчислення на відеокарті. За допомогою обчислювальних шейдерів можна проводити складні обчислення, використовуючи потужності відеокарти. Відеокарти (GPU) мають більше обчислювальної потужності, ніж центральні процесори (CPU), тому розрахунок складних обчислень на відеокарті має більше сенсу. Наприклад, на відеокарті за допомогою обчислювальних шейдерів можна обчислювати симуляції складних систем частинок.

Окремо стоять шейдери для конвеєра на основі трасування променів (Ray Tracing Pipeline). Цей тип шейдерів, як і шейдера для графічного конвеєра надають можливості перепрограмування конвеєра трасування променів (Ray Tracing Pipeline).
Відображення тривимірних моделей за допомогою трасування променів це відносно новий метод у рендерингу та ґрунтується на обчисленні траєкторії променів світла та взаємодії з поверхнями, що дозволяє створювати більш реалістичне відображення тривимірних моделей. У цій серії статей метод рендерингу за допомогою трасування променів ми не застосовуватимемо, а використовуватимемо класичний графічний конвеєр, заснований на растеризації.

У цій серії нічого не буде про шейдера для конвеєра трасування променів, але вам буде корисно знати, що такі шейдери теж існують, і як у випадку з графічним конвеєром також призначені для перепрограмування різних етапів конвеєра трасування променів.

Тепер у вас є розуміння, що таке шейдери. Сприймайте їх як етапи графічного конвеєра, які можна перепрограмувати, роблячи їх гнучкішими для вирішення певних проблем, пов’язаних із візуалізацією примітивів. І давайте в кінці швидко пройдемося по шейдерах для графічного конвеєра та закріпимо отриману інформацію.

Вершинний шейдер (Vertex Shader) використовується для того, щоб модифікувати вершини примітивів. Наприклад, за допомогою вершинного шейдера можна гнучкіше змінювати позиції вершин для моделі трави або листя, переміщуючи їх та створюючи ефект вітру. Використовуйте вершинний шейдер у разі, коли потрібно модифікувати положеня вершин.

Шейдер тесселяції використовується у випадках, коли потрібно підрозділити примітив. Наприклад, об’єкти ближче до камери можуть бути більш деталізованими, ніж об’єкти, які знаходяться набагато далі від неї. За допомогою шейдера тесселяції можна керувати деталізацією тривимірної моделі.

Шейдер геометрії можна вважати як комплексний шейдер тесселяції. За допомогою шейдера геометрії можна збільшувати або зменшувати кількість вершин та примітивів, змінюючи початкову геометрію.

Фрагментний шейдер використовується для обчислення кольору фрагмента при растеризації примітиву. За допомогою фрагментного шейдера можна задавати колір примітиву, затінювати його використовуючи різні алгоритми, накладати на примітив текстуру використовуючи різноманітні методи текстурування. Більшість часу цієї серії статей ми будемо використовувати саме фрагментні шейдери, тому що вони дозволяють більш просто візуалізувати основні типи даних і функції в мові GLSL.

У цій частині серії статей з основ програмування шейдерів за допомогою мови GLSL ми розібрали що таке тривимірна модель, що таке вершини, атрибути вершин і як за допомогою вершин описувати примітиви. Дізналися про растеризацію та що таке графічний конвеєр, які у нього бувають етапи, та як він працює. І найголовніше — дізналися що таке шейдери, яких типів вони бувають і де застосовуються, а також за допомогою якої мови програмуються.
У наступній частині ми перейдемо безпосередньо до вивчення синтаксису мови програмування GLSL та програмування на неї. Розглянемо основні типи даних, функціонал та вимоги GLSL до коду шейдера.
На цьому все. Бажаю вам удачі у вивченні мови GLSL і зустрінемося в наступній частині.