Manuals
Manuals




This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this manual in English

Шейдери

Шейдерні програми лежать в основі рендерингу графіки. Це програми, написані C-подібною мовою GLSL (GL Shading Language), які графічне обладнання виконує для обробки вихідних тривимірних даних (вершин) або пікселів, що зрештою потрапляють на екран («фрагментів»). Шейдери використовують для малювання спрайтів, освітлення тривимірних моделей, створення повноекранних постефектів і багато чого іншого.

Цей посібник описує взаємодію конвеєра рендерингу Defold із шейдерами GPU. Щоб створювати шейдери для свого вмісту, потрібно також розуміти поняття матеріалів і роботу конвеєра рендерингу.

Специфікації OpenGL ES 2.0 (OpenGL for Embedded Systems) і OpenGL ES Shading Language можна знайти в реєстрі OpenGL Khronos.

Зауважте, що на настільних комп’ютерах можна писати шейдери з використанням можливостей, недоступних в OpenGL ES 2.0. Драйвер вашої відеокарти може без проблем скомпілювати й виконати код шейдера, який не працюватиме на мобільних пристроях.

Поняття

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

Вхідними даними вершинного шейдера є дані вершин (у вигляді attributes) і константи (uniforms). Типовими константами є матриці, потрібні для трансформації та проєктування позиції вершини в екранний простір.

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

Фрагментний шейдер
Після завершення роботи вершинного шейдера фрагментний шейдер має визначити колір кожного фрагмента (або пікселя) отриманих примітивів.

Вхідними даними фрагментного шейдера є константи (uniforms), а також усі змінні varying, задані вершинним шейдером.

Результатом роботи фрагментного шейдера є значення кольору конкретного фрагмента (gl_FragColor).

Обчислювальний шейдер
Обчислювальний шейдер — це шейдер загального призначення, який можна використовувати для виконання будь-яких завдань на GPU. Він узагалі не є частиною графічного конвеєра: обчислювальні шейдери працюють в окремому контексті виконання й не залежать від вхідних даних з інших шейдерів.

Вхідними даними обчислювального шейдера є буфери констант (uniforms), зображення текстур (image2D), семплери (sampler2D) і буфери зберігання (buffer).

Результат роботи обчислювального шейдера не визначений явно: на відміну від вершинних і фрагментних шейдерів, він не має обов’язково створювати певний результат. Оскільки обчислювальні шейдери мають загальне призначення, програміст сам визначає тип результату, який має створювати обчислювальний шейдер.

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

Коли модель розміщується в ігровому світі, її локальні координати вершин потрібно перетворити на світові. Це перетворення виконує матриця світової трансформації, яка визначає, які перенесення (переміщення), поворот і масштаб слід застосувати до вершин моделі, щоб правильно розташувати їх у системі координат ігрового світу.

Світова трансформація

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

Проєкція

Атрибути
Значення, пов’язане з окремою вершиною. Рушій передає атрибути шейдеру, і для доступу до атрибута достатньо оголосити його в шейдерній програмі. Різні типи компонентів мають різні набори атрибутів:
  • Спрайт має position і texcoord0.
  • Сітка плиток має position і texcoord0.
  • Вузол GUI має position, textcoord0 і color.
  • Ефект частинок має position, texcoord0 і color.
  • Модель має position, texcoord0 і normal.
  • Шрифт має position, texcoord0, face_color, outline_color і shadow_color.
Константи
Константи шейдера залишаються незмінними протягом виклику малювання. Константи додаються до розділів Constants у файлі матеріалу, а потім оголошуються як uniform у шейдерній програмі. Змінні uniform для семплерів додаються до розділу Samplers матеріалу, а потім оголошуються як uniform у шейдерній програмі. Матриці, потрібні для трансформації вершин у вершинному шейдері, доступні як константи:
  • CONSTANT_TYPE_WORLD — це світова матриця, яка перетворює координати з локального простору об’єкта у світовий простір.
  • CONSTANT_TYPE_VIEW — це матриця виду, яка перетворює координати зі світового простору в простір камери.
  • CONSTANT_TYPE_PROJECTION — це матриця проєкції, яка перетворює координати з простору камери в екранний простір.
  • CONSTANT_TYPE_WORLDVIEW, CONSTANT_TYPE_VIEWPROJ і CONSTANT_TYPE_WORLDVIEWPROJ надають відповідні комбіновані матриці.
  • CONSTANT_TYPE_WORLD_INVERSE, CONSTANT_TYPE_VIEW_INVERSE, CONSTANT_TYPE_PROJECTION_INVERSE, CONSTANT_TYPE_VIEWPROJ_INVERSE, CONSTANT_TYPE_WORLDVIEW_INVERSE і CONSTANT_TYPE_WORLDVIEWPROJ_INVERSE надають обернені матриці, щоб шейдеру не доводилося їх обчислювати.
  • CONSTANT_TYPE_TIME — це vec4, який надає рушій: у .x міститься час, що минув від запуску рушія, у .y — тривалість кадру, а в .z і .w — нулі.
  • CONSTANT_TYPE_USER — це константа типу vec4, яку можна використовувати на власний розсуд.

У посібнику з матеріалів пояснюється, як задавати константи.

Семплери
Шейдери можуть оголошувати змінні uniform типу sampler. Семплери використовуються для зчитування значень із джерела зображення:
  • sampler2D виконує вибірку з текстури двовимірного зображення.
  • sampler2DArray виконує вибірку з текстури масиву двовимірних зображень. Здебільшого використовується для багатосторінкових атласів.
  • samplerCube виконує вибірку з кубічної текстури, яка складається із 6 зображень.
  • image2D завантажує (і потенційно зберігає) дані текстури в об’єкт зображення. Здебільшого використовується для зберігання даних в обчислювальних шейдерах.

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

UV-координати
Двовимірна координата пов’язана з вершиною та відповідає точці на двовимірній текстурі. Завдяки цьому частину текстури або всю текстуру можна нанести на форму, описану набором вершин.

UV-координати

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

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

Інтерполяція varying

Наприклад, якщо в кожній вершині трикутника задати для varying значення кольору RGB типу vec3, кольори будуть інтерпольовані по всій формі. Так само задання координат вибірки з текстурної мапи (або UV-координат) у кожній вершині прямокутника дає фрагментному шейдеру змогу вибирати значення кольорів текстури для всієї площі форми.

Інтерполяція varying

Написання сучасних шейдерів GLSL

Оскільки рушій Defold підтримує багато платформ і графічних API, розробникам має бути просто писати шейдери, які працюють усюди. Конвеєр обробки ресурсів забезпечує це переважно двома способами (далі — конвеєри шейдерів, shader pipelines):

  1. Старий конвеєр, у якому шейдери пишуться мовою GLSL, сумісною з ES2.
  2. Сучасний конвеєр, у якому шейдери пишуться мовою GLSL, сумісною зі SPIR-v.

Починаючи з Defold 1.9.2, рекомендовано писати шейдери, які використовують новий конвеєр. Для цього більшість шейдерів потрібно перевести щонайменше на версію 140 (OpenGL 3.1). Щоб перевести шейдер, переконайтеся, що виконуються наведені нижче вимоги:

Оголошення версії

На початку шейдера вкажіть щонайменше #version 140:

#version 140

Саме так під час збирання вибирається конвеєр шейдерів, тому ви й надалі можете використовувати старі шейдери. Якщо директиву препроцесора з версією не знайдено, Defold використає старий конвеєр.

Атрибути

У вершинних шейдерах замініть ключове слово attribute на in:

// instead of:
// attribute vec4 position;
// do:
in vec4 position;

Примітка: фрагментні шейдери (і обчислювальні шейдери) не приймають вхідних даних вершин.

Змінні varying

У вершинних шейдерах перед змінними varying слід ставити out. У фрагментних шейдерах змінні varying стають in:

// In a vertex shader, instead of:
// varying vec4 var_color;
// do:
out vec4 var_color;

// In a fragment shader, instead of:
// varying vec4 var_color;
// do:
in vec4 var_color;

Змінні uniform (у Defold називаються константами)

Змінні uniform непрозорих типів (семплери, зображення, атомарні змінні, SSBO) не потребують жодних змін — їх можна використовувати так само, як і раніше:

uniform sampler2D my_texture;
uniform image2D my_image;

Змінні uniform інших типів потрібно помістити в блок uniform (uniform block). Блок uniform — це просто набір змінних uniform, який оголошується ключовим словом uniform:

uniform vertex_inputs
{
    mat4 mtx_world;
    mat4 mtx_proj;
    mat4 mtx_view;
    mat4 mtx_normal;
    ...
};

void main()
{
    // Individual members of the uniform block can be used as-is
    gl_Position = mtx_proj * mtx_view * mtx_world * vec4(position, 1.0);
}

Усі члени блоку uniform доступні матеріалам і компонентам як окремі константи. Для використання буферів констант рендерингу, а також go.set і go.get жодних змін не потрібно.

Вбудовані змінні

У фрагментних шейдерах gl_FragColor вважається застарілою починаючи з версії 140. Натомість використовуйте out:

// instead of:
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
// do:
out vec4 color_out;

void main()
{
    color_out = vec4(1.0, 0.0, 0.0, 1.0);
}

Функції роботи з текстурами

Окремих функцій вибірки з текстур, як-от texture2D і texture2DArray, більше немає. Натомість просто використовуйте функцію texture:

uniform sampler2D my_texture;
uniform sampler2DArray my_texture_array;

// instead of:
// vec4 sampler_2d = texture2D(my_texture, uv);
// vec4 sampler_2d_array = texture2DArray(my_texture_array, vec3(uv, slice));
// do:
vec4 sampler_2d = texture(my_texture, uv);
vec4 sampler_2d_array = texture(my_texture_array, vec3(uv, slice));

Точність

Під час кроскомпіляції шейдерів для GLSL ES Defold генерує глобальні кваліфікатори точності за замовчуванням. За замовчуванням для значень із рухомою комою використовується mediump, а для цілих чисел — highp. Їх можна змінити за допомогою налаштувань проєкту GLSL ES Default Precision Float (shader.glsl_es_default_precision_float) і GLSL ES Default Precision Int (shader.glsl_es_default_precision_int); обидва приймають mediump або highp.

Явний кваліфікатор змінної, входу або виходу має пріоритет над згенерованим глобальним значенням за замовчуванням. У фрагментних шейдерах OpenGL ES 2.0 і WebGL 1.0 точність highp підтримується не на всіх пристроях. Якщо highp вибрано як глобальне значення за замовчуванням, Defold захищає його за допомогою GL_FRAGMENT_PRECISION_HIGH і використовує mediump на пристроях, які його не підтримують.

Усе разом

Як підсумковий приклад застосування всіх цих правил наведемо вбудовані шейдери спрайта, переведені в новий формат:

#version 140

uniform vx_uniforms
{
    mat4 view_proj;
};

// positions are in world space
in vec4 position;
in vec2 texcoord0;

out vec2 var_texcoord0;

void main()
{
    gl_Position = view_proj * vec4(position.xyz, 1.0);
    var_texcoord0 = texcoord0;
}
#version 140

in vec2 var_texcoord0;

out vec4 color_out;

uniform sampler2D texture_sampler;

uniform fs_uniforms
{
    vec4 tint;
};

void main()
{
    // Premultiply alpha since all runtime textures already are
    vec4 tint_pm = vec4(tint.xyz * tint.w, tint.w);
    color_out = texture(texture_sampler, var_texcoord0.xy) * tint_pm;
}

Включення фрагментів коду до шейдерів

Шейдери в Defold підтримують включення вихідного коду з файлів проєкту, що мають розширення .glsl. Щоб включити файл glsl із шейдера, використовуйте директиву #include з подвійними лапками або дужками. Шляхи включення мають бути відносними до проєкту або до файлу, який виконує включення:

// In file /main/my-shader.fp

// Absolute path
#include "/main/my-snippet.glsl"
// The file is in the same folder
#include "my-snippet.glsl"
// The file is in a sub-folder on the same level as 'my-shader'
#include "sub-folder/my-snippet.glsl"
// The file is in a sub-folder on the parent directory, i.e /some-other-folder/my-snippet.glsl
#include "../some-other-folder/my-snippet.glsl"
// The file is on the parent directory, i.e /root-level-snippet.glsl
#include "../root-level-snippet.glsl"

Обробка включень має певні особливості:

  • Файли мають бути відносними до проєкту, тобто включати можна лише файли, розташовані всередині проєкту. Будь-який абсолютний шлях має починатися з /
  • Код можна включати в будь-якому місці файлу, але не можна включити файл усередині інструкції. Наприклад, const float #include "my-float-name.glsl" = 1.0 не працюватиме

Захист від повторного включення

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

// In my-shader.vs
#include "math-functions.glsl"
#include "pi.glsl"

// In math-functions.glsl
#include "pi.glsl"

// In pi.glsl
const float PI = 3.14159265359;

У цьому прикладі константа PI буде визначена двічі, що спричинить помилки компілятора під час запуску проєкту. Натомість слід захистити вміст від повторного включення:

// In pi.glsl
#ifndef PI_GLSL_H
#define PI_GLSL_H

const float PI = 3.14159265359;

#endif // PI_GLSL_H

Код із pi.glsl буде розгорнуто у my-shader.vs двічі, але завдяки захисту від повторного включення символ PI буде визначено лише один раз, і шейдер успішно скомпілюється.

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

// In red-color.glsl
vec3 my_red_color = vec3(1.0, 0.0, 0.0);

// In my-shader.fp
vec3 get_red_color()
{
  #include "red-color.glsl"
  return my_red_color;
}

vec3 get_red_color_inverted()
{
  #include "red-color.glsl"
  return 1.0 - my_red_color;
}

Код шейдера для редактора

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

Це особливо корисно для:

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

Використовуйте директиву препроцесора #ifdef EDITOR для умовної компіляції коду, який має виконуватися лише в редакторі:

#ifdef EDITOR
    // This code will only execute when the shader is rendered in the Defold Editor
    color_out = vec4(1.0, 0.0, 1.0, 1.0); // Magenta color for editor preview
#else
    // This code will execute when running in the game
    color_out = texture(texture_sampler, var_texcoord0) * tint_pm;
#endif

Процес рендерингу

Перш ніж потрапити на екран, дані, які ви створюєте для гри, проходять низку етапів:

Конвеєр рендерингу

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

Вершинний шейдер

Вхідними даними програми є позиція вершини (та інші дані атрибутів, пов’язані з вершиною), а результатом — нова позиція вершини (gl_Position) і всі змінні varying, які потрібно інтерполювати для кожного фрагмента.

Найпростіша програма вершинного шейдера просто задає нульові координати вихідної вершини (що не надто корисно):

void main()
{
    gl_Position = vec4(0.0,0.0,0.0,1.0);
}

Повніший приклад — вбудований вершинний шейдер спрайта:

-- sprite.vp
uniform mediump mat4 view_proj;             // [1]

attribute mediump vec4 position;            // [2]
attribute mediump vec2 texcoord0;

varying mediump vec2 var_texcoord0;         // [3]

void main()
{
  gl_Position = view_proj * vec4(position.xyz, 1.0);    // [4]
  var_texcoord0 = texcoord0;                            // [5]
}
  1. Змінна uniform (константа), яка містить добуток матриць виду та проєкції.
  2. Атрибути вершини спрайта. position уже перетворено у світовий простір. texcoord0 містить UV-координату вершини.
  3. Оголосіть вихідну змінну varying. Для кожного фрагмента значення цієї змінної буде інтерпольовано між значеннями, заданими для кожної вершини, і передано фрагментному шейдеру.
  4. Для gl_Position задається вихідна позиція поточної вершини в просторі проєкції. Це значення має 4 компоненти: x, y, z і w. Компонент w використовується для обчислення інтерполяції з урахуванням перспективи. Зазвичай до застосування будь-якої матриці трансформації це значення дорівнює 1.0 для кожної вершини.
  5. Задайте UV-координату varying для цієї позиції вершини. Після растеризації її буде інтерпольовано для кожного фрагмента й передано фрагментному шейдеру.

Після обробки вершин шейдером визначається форма компонента на екрані: генеруються та растеризуються примітиви, тобто графічне обладнання розбиває кожну форму на фрагменти, або пікселі. Потім воно виконує програму фрагментного шейдера по одному разу для кожного фрагмента. Для зображення на екрані розміром 16x24 пікселі програма виконується 384 рази, усі виконання — паралельні.

Фрагментний шейдер

Вхідними даними програми є все, що передають конвеєр рендерингу та вершинний шейдер: зазвичай це uv-координати фрагмента, кольори тонування тощо. Результатом є остаточний колір пікселя (gl_FragColor).

Найпростіша програма фрагментного шейдера просто задає чорний колір кожного пікселя (знову ж таки, не надто корисна програма):

void main()
{
    gl_FragColor = vec4(0.0,0.0,0.0,1.0);
}

І знову повнішим прикладом є вбудований фрагментний шейдер спрайта:

// sprite.fp
varying mediump vec2 var_texcoord0;             // [1]

uniform lowp sampler2D DIFFUSE_TEXTURE;         // [2]
uniform lowp vec4 tint;                         // [3]

void main()
{
  lowp vec4 tint_pm = vec4(tint.xyz * tint.w, tint.w);          // [4]
  lowp vec4 diff = texture2D(DIFFUSE_TEXTURE, var_texcoord0.xy);// [5]
  gl_FragColor = diff * tint_pm;                                // [6]
}
  1. Оголошується змінна varying для текстурних координат. Для кожного фрагмента значення цієї змінної буде інтерпольовано між значеннями, заданими для кожної вершини форми.
  2. Оголошується змінна uniform типу sampler2D. Семплер разом з інтерпольованими текстурними координатами використовується для вибірки з текстури, щоб правильно накласти текстуру на спрайт. Оскільки це спрайт, рушій прив’яже цей семплер до зображення, заданого у властивості Image спрайта.
  3. У матеріалі визначається константа типу CONSTANT_TYPE_USER, яка оголошується як uniform. Її значення дає змогу тонувати спрайт. За замовчуванням це чистий білий колір.
  4. Значення кольору тонування попередньо множиться на його альфа-значення, оскільки в усіх текстурах середовища виконання значення RGB вже попередньо помножені на альфа-значення.
  5. Виконайте вибірку з текстури за інтерпольованою координатою та поверніть отримане значення.
  6. Для gl_FragColor задається вихідний колір фрагмента: дифузний колір із текстури, помножений на значення тонування.

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

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

Подальше вивчення

  • Shadertoy містить величезну кількість шейдерів, створених користувачами. Це чудове джерело натхнення, де можна дізнатися про різні техніки роботи з шейдерами. Багато шейдерів, представлених на сайті, можна перенести до Defold із мінімальними зусиллями. В уроці з Shadertoy покроково розглядається перетворення наявного шейдера для Defold.

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

  • Книга шейдерів навчить вас використовувати шейдери та інтегрувати їх у свої проєкти, покращуючи продуктивність і якість графіки.