You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
thebookofshaders/appendix/04/README-ua.md

450 lines
36 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## Вступ для тих, хто прийшов із JS
автор [Nicolas Barradeau](http://www.barradeau.com/)
Якщо ви JavaScript-розробник, швидше за все, ви будете трохи спантеличені, читаючи книгу.
Дійсно, є багато відмінностей між маніпулюванням високорівневим JS і длубанням у менш високорівневих шейдерах.
Проте, на відміну від більш низькорівневої мови асемблера, GLSL є людино-зрозумілою мовою, і я впевнений, що як тільки ви розберетеся з її особливостями, то швидко запрацюєте.
Я припускаю, що ви маєте хоча б поверхневе знання про JavaScript і API Canvas.
Якщо ні, не хвилюйтеся, ви все одно зможете зрозуміти більшу частину цього розділу.
Крім того, я не буду надто вдаватися в подробиці, і деякі речі можуть бути апівправдою_, тож не розраховувайте на "вичерпний посібник", а радше на ознайомлення.
### Введення для новачків
JavaScript чудово підходить для швидкого прототипування; ви накидуєте купу різноманітних нетипізованих змінних і методів, можете динамічно додавати та видаляти члени класів, оновлювати сторінку і швидко перевіряти чи вона працює.
Потім можна внести нові зміни, оновити сторінку, повторити - легке життя.
Тож ви можете поставити собі питання, у чому різниця між JavaScript і GLSL?
Зрештою, обидва працюють у браузері, обидва використовуються для малювання купи прикольних штук на екрані, і в цьому сенсі JS легше використовувати.
Що ж, головна відмінність полягає в тому, що Javascript є **інтерпретованою** мовою, тоді як GLSL є **компільованою**.
**Скомпільована** програма виконується нативно в ОС, є низькорівневою і загалом швидкою.
**Інтерпретована** програма потребує для свого виконання [віртуальну машину](https://en.wikipedia.org/wiki/Virtual_machine) (VM), є високорівневою і відносно більш повільною.
Коли браузер (_**VM** для JavaScript_) **виконує** або **інтерпретує** фрагмент JS-коду, він ще не має уявлення про те, яка змінна чим являється і яка функція що робить (за винятком **типізованих масивів**).
Тож він не може нічого оптимізувати аперед_.
Потрібен деякий час, щоб прочитати ваш код, щоб **вивести**, на основі використання, типи ваших змінних та методів і, коли це можливо, перетворити ещо_ з вашого коду на код асемблера, який виконуватиметься набагато швидше.
Це повільний, кропіткий і шалено складний процес. Якщо вас цікавлять деталі, я рекомендую подивитися, як [працює рушій V8 у Chrome](https://developers.google.com/v8/).
Найгірше те, що кожен браузер оптимізує JS по-своєму, а процес _прихований_ від вас. В цьому плані ви безсилі.
**Компільована** програма не потребує інтерпретації. Операційна система запускає її і якщо програма валідна, вона виконується.
Це велика відмінність. Якщо ви забули крапку з комою в кінці рядка, ваш код вже не валідний та не скомпілюється, він взагалі не перетвориться на програму.
Це суворо, але це те, чим є **шейдер**: _програмою, що компілюється для виконання на GPU_.
Не бійтеся! **Компілятор** який перевіряє правильність вашого коду, стане вашим найкращим другом.
Приклади цієї книги та [редактор коду](http://editor.thebookofshaders.com/) дуже дружні до користувача.
Вони підкажуть вам де і чому не вдалося скомпілювати вашу програму.
Потім, після потрібних виправлень, коли шейдер буде готовий до компіляції, він миттєво зобразить результат своєї роботи. Це чудовий спосіб навчання, оскільки він дуже наочний і безпечний, бо насправді ви нічого не зможете зламати.
Останнє зауваження: **шейдер** складається з 2 програм: **вершинного шейдера** і **фрагментного шейдера**.
Якщо коротко, то **вершинний шейдер** це перша програма, що отримує на вхід *геометрію* та перетворює її на серію **пікселів** (або *фрагментів*).
Потім вона передає їх до **фрагментного шейдера** - другої програми, яка вирішить, яким кольором пофарбувати ці пікселі.
Дана книга здебільшого зосереджена на **фрагментних шейдерах**. У всіх прикладах геометрія є простим чотирикутником, який охоплює весь екран.
Готові?
Поїхали!
### Сильні типи
![перший результат пошуку для 'strong type' в Google Image на 20.05.2016](strong_type.jpg)
Коли ви приходите з JS або будь-якої іншої нетипізованої мови, **типізація** змінних являється для вас чужорідною концепцією, що стає найважчим кроком до GLSL.
**Типізація**, як випливає з назви, означає, що вам потрібно надавати **тип** усім своїм змінним і функціям.
Це фактично означає, що слів **`var`** або **`let`** більше не існує.
Поліція думок GLSL стерла їх із загальної мови й ви більше не можете їх використовувати, тому що, ну... їх не існує.
Замість використання чарівного слова **`var`** вам доведеться _явно вказати тип кожної змінної_, яку ви використовуєте, тоді компілятор бачитиме лише ті об'єкти та примітиви, з якими він вміє ефективно поводитися.
Мінус, що ви не можете використовувати ключове слово **`var`** і повинні _явно вказувати всі типи_, полягає в тому, що вам доведеться знати типи усіх змінних і знати їх добре.
Будьте спокійні, їх небагато і вони досить прості (GLSL це вам не Java-фреймворк).
Це може здатися страшним, але загалом це не дуже відрізняється від того, що ви робите на JavaScript. Наприклад, якщо ви маєте `boolean`-змінну, то ви очікуєте, що вона зберігатиме лише `true` або `false` і нічого більше.
Якщо змінна називається `var uid = XXX;`, то в ній вірогідно зберігається цілочисельне значення, а `var y = YYY;` оже_ бути посиланням на значення з рухомою крапкою.
Що ще краще, завдяки **сильним типам** ви не витрачатимете час на роздуми про те, чи `X == Y` (чи `typeof X == typeof Y`?, чи `typeof X !== null && Y...`). Ви просто *знаєте* це, а якщо ні, то компілятор знатиме напевно.
Ось **скалярні типи** (**скаляр** описує величину), які можна використовувати в GLSL: `bool` (булів тип), `int` (ціле число), `float` (число з рухомою крапкою).
Є й інші типи, але не будемо поспішати. Наступний фрагмент показує, як оголосити **`vars`** (так, я використав заборонене слово) у GLSL:
```glsl
// логічне значення
JS: var b = true; GLSL: bool b = true;
// цілочисельне значення
JS: var i = 1; GLSL: int i = 1;
// числове значення з рухомою крапкою
JS: var f = 3.14159; GLSL: float f = 3.14159;
```
Не дуже важко, правда? Як згадувалося вище, це навіть полегшує роботу, оскільки ви не витрачаєте час на перевірку типів даних змінних.
Якщо це здається сумнівним, то пам'ятайте, що ви робите це для того, щоб ваша програма виконувалася набагато швидше, ніж на JS.
#### void
Тип `void` приблизно відповідає `null`. Він використовується в якості типу, який має повертати метод, коли він нічого не повертає.
Ви не можете призначати його змінним.
#### boolean
Як вам відомо, логічні значення здебільшого використовуються в умовних перевірках: "`if (myBoolean == true) {...} else {...}`".
Якщо умовне розгалуження є звичайним підходом для CPU, то для [паралельної природи](http://thebookofshaders/01/?lan=ua) GLSL це твердження є менш правдивим.
Використання умовних галужень навіть не рекомендується у більшості випадків і у книзі показано кілька альтернативних методів для вирішення цього обмеження.
#### приведення типів
Як казав [Боромир](https://en.wikipedia.org/wiki/Boromir), "не можна просто взяти та скомбінувати типізовані примітиви". На відміну від JavaScript, GLSL не дозволить вам виконувати операції між змінними різних типів.
Наприклад, цей код:
```glsl
int i = 2;
float f = 3.14159;
// спроба помножити ціле число на значення з рухомою крапкою
float r = i * f;
```
не спрацює, тому що ви намагаєтеся схрестити **_кота_** і **_жирафа_**.
Проблема вирішується за допомогою **приведення типу**, що _змусить компілятор повірити_, що *`i`* має тип `float` без фактичної зміни типу *`i`*.
```glsl
// приведення типу цілочисельної змінної 'i' до float
float r = float(i) * f;
```
Це як вдягти **_кота_** у **шкіру _жирафа_** і працюватиме належним чином (змінна `r` зберігатиме результат множення `i` на `f`).
Можна **привести** будь-який зі згаданих вище типів до будь-якого іншого. Зауважте, що приведення `float` до `int` поводитиметься як `Math.floor()`, оскільки видалятиме значення після рухомої крапки.
Приведення `float` або `int` до `bool` поверне `true`, якщо змінна не дорівнює нулю.
#### конструктор
**Типи** змінних також є **конструкторами класів** для самих себе. Змінну `float` фактично можна розглядати як _`екземпляр`_ класу _`float`_.
Наступні оголошення рівнозначно валідні:
```glsl
int i = 1;
int i = int(1);
int i = int(1.9995);
int i = int(true);
```
Це може здатися не дуже схожим на `скалярні` типи та не дуже відрізняється від **приведення типів**, але у цьому з'явиться більше сенсу, коли дійдемо до розділу *перевантаження*.
Отже, ми познайомилися з трьома `примітивними типами`, без яких ви не зможете жити, але, звісно, GLSL має й інші.
### Вектори
![перший результат пошуку для 'vector villain' у Google Image на 20.05.2016](vector.jpg)
У Javascript, як і в GLSL, вам знадобляться складніші способи маніпуляції даними, ось де **`вектори`** стануть у пригоді.
Я припускаю, що вам вже доводилося писати клас `Point` на JavaScript, для утримання значень `x` і `y`, що виглядав якось так:
```glsl
// визначення класу:
var Point = function(x, y) {
this.x = x || 0;
this.y = y || 0;
}
// створення екземляру:
var p = new Point(100, 100);
```
Як ми щойно побачили, це ДУЖЕ неправильно на ДУЖЕ багатьох рівнях! По-перше, ключове слово **`var`**, далі жахливе **`this`**, потім знову **нетипізовні** значення `x` і `y`...
Ні, це не запрацює в шейдерленді.
Натомість GLSL надає вбудовані структури даних для їх групування, а саме:
* **`bvec2`**: 2D вектор для bool, **`bvec3`**: 3D вектор для bool, **`bvec4`**: 4D вектор для bool
* **`ivec2`**: 2D вектор для int, **`ivec3`**: 3D вектор для int, **`ivec4`**: 4D вектор для int
* **`vec2`**: 2D вектор для float, **`vec3`**: 3D вектор для float, **`vec4`**: 4D вектор для float
Ви відразу помітили, що для кожного примітиву є відповідний **векторний** тип.
З показаного вище, ви можете зробити висновок, що `bvec2` буде містити два значення типу `bool`, а `vec4` — чотири значення `float`.
Також вектори вводять таку річ як **вимірність** або **розмірність**. Це не означає, що 2D-вектор використовується при малюванні 2D-графіки, а 3D-вектор при малюванні 3D-сцен. Ні!
Що в такому разі буде представляти 4D-вектор? (ну насправді це називається тесерактом або гіперкубом)
**Розмірність** представляє кількість і тип **компонентів** або **змінних**, що зберігаються у **векторі**:
```glsl
// створюємо двовимірний булів вектор
bvec2 b2 = bvec2(true, false);
// створюємо тривимірний цілочисельний вектор
ivec3 i3 = ivec3(0, 0, 1);
// створюємо чотиривимірний вектор з рухомою комою
vec4 v4 = vec4(0.0, 1.0, 2.0, 1.0);
```
`b2` зберігає два різних булевих значення, `i3` - 3 різні цілочисельні значення, а `v4` - 4 різні значення з рухомою комою.
Але як звернутися до цих значень?
У випадку `скалярів` відповідь очевидна: для "`float f = 1.2;`", змінна `f` містить значення `1.2`.
З **векторами** це трохи інакше і доволі красиво.
#### аксесори - доступи до елементів
Існують різні способи доступу до значень
```glsl
// створимо чотиривимірний вектор типу float
vec4 v4 = vec4(0.0, 1.0, 2.0, 3.0);
```
отримати кожне з 4-х значень, можна наступним чином:
```glsl
float x = v4.x; // x = 0.0
float y = v4.y; // y = 1.0
float z = v4.z; // z = 2.0
float w = v4.w; // w = 3.0
```
Просто і легко. Наведені нижче приклади показують інші еквівалентні способи доступу до цих даних:
```glsl
float x = v4.x = v4.r = v4.s = v4[0]; // x = 0.0
float y = v4.y = v4.g = v4.t = v4[1]; // y = 1.0
float z = v4.z = v4.b = v4.p = v4[2]; // z = 2.0
float w = v4.w = v4.a = v4.q = v4[3]; // w = 3.0
```
Кмітливий читач міг помітити три речі:
* `X`, `Y`, `Z`, `W` зазвичай використовуються в 3D-програмах для представлення 3D-векторів
* `R`, `G`, `B`, `A` використовуються для кодування кольорів і альфа-каналу
* `[0]`, `[1]`, `[2]`, `[3]` означає, що ми можемо звертатися до значень через індекси
Тож залежно від того, чи працюєте ви з дво- чи тривимірними координатами, кольором з альфа-каналом чи без нього, або просто з деякими довільними значеннями, ви можете вибрати найбільш відповідний тип і розмірність **вектора**.
Зазвичай двовимірні координати та вектори (в геометричному сенсі) зберігаються як `vec2`, `vec3` або `vec4`, кольори як `vec3` або `vec4`, якщо вам потрібна непрозорість. Але, в цілому, обмежень на те як використовувати вектори немає.
Наприклад, якщо ви хочете зберегти лише одне логічне значення в `bvec4`, це можливо, але буде марною витратою пам'яті.
**Примітка**: у шейдері значення кольорів (`R`, `G`, `B`, `A`) нормалізовані, тобто варіюються в діапазоні від 0 до 1, а не від 0 до 0xFF, тому для них краще використовувати тип float `vec4`, ніж цілочисельний тип `ivec4`.
Вже маємо хороший початок, але рушаймо далі!
#### змішування
З вектора можна отримати більше одного значення за раз. Скажімо, із `vec4` вам потрібні лише значення `X` і `Y`. У JavaScript вам довелося б написати щось подібне:
```glsl
var needles = [0, 1]; // розміщення 'x' і 'y' в нашій структурі даних
var a = [0, 1, 2, 3]; // наша структура даних 'vec4'
var b = a.filter(function(val, i, array) {
return needles.indexOf(array.indexOf(val)) != -1;
});
// b = [0, 1]
// або більш буквально:
var needles = [0, 1];
var a = [0, 1, 2, 3]; // структура даних 'vec4'
var b = [a[needles[0]], a[needles[1]]]; // b = [0, 1]
```
Виглядає потворно. У GLSL ви можете отримати ці дані так:
```glsl
// створюємо 4D-вектор типу float
vec4 v4 = vec4(0.0, 1.0, 2.0, 3.0);
// і одночасно отримуємо лише X та Y
vec2 xy = v4.xy; // xy = vec2(0.0, 1.0);
```
Що це щойно сталося?! Коли ви **об'єднуєте аксесори**, GLSL елегантно повертає підмножину значень, які ви запросили, у найбільш відповідному **векторному** форматі.
Тож тут вектор — це структура даних із **довільним доступом**, схожий на масив як у JavaScript.
Таким чином, ви можете не тільки отримати підмножину ваших даних, але і вказати їх **порядок**, у якому вони вам потрібні. Наступний приклад змінює порядок отримання компонентів вектора:
```glsl
// створюємо чотиривимірний вектор типу float: R,G,B,A
vec4 color = vec4(0.2, 0.8, 0.0, 1.0);
// отримуємо компоненти кольору в порядку A,B,G,R
vec4 backwards = color.abgr; // backwards = vec4(1.0, 0.0, 0.8, 0.2);
```
І, звичайно, ви можете повернути той самий компонент кілька разів:
```glsl
// створюємо чотиривимірний вектор типу float: R,G,B,A
vec4 color = vec4(0.2, 0.8, 0.0, 1.0);
// отримуємо vec3 з компонентами GAG на основі каналів G і A з початкового кольору
vec3 GAG = color.gag; // GAG = vec4(0.8, 1.0, 0.8);
```
Це надзвичайно зручно для об'єднання частин вектору, виділення лише rgb-каналів із кольору RGBA тощо.
#### перевантаження
У розділі типів я згадував про **конструктор** і можливість **перевантаження**.
Для тих, хто не знає, **перевантаження** оператора або функції означає _'зміну поведінки зазначеного оператора або функції залежно від операндів/аргументів'_.
В JavaScript немає перевантаження, тому спочатку воно може здатися трохи дивним, але я впевнений, що як тільки ви звикнете до нього, то будете дивуватися, чому це не реалізовано в JS (коротка відповідь - *типізація*).
Найпростіший приклад перевантаженого оператора виглядає так:
```glsl
vec2 a = vec2(1.0, 1.0);
vec2 b = vec2(1.0, 1.0);
// перевантажене додавання
vec2 c = a + b; // c = vec2(2.0, 2.0);
```
ЩО? Отже, можна додавати сутності, які не є числами?!
Саме так. Також це стосується всіх операторів (`+`, `-`, `*` і `/`), але це тільки початок.
Розглянемо наступний фрагмент:
```glsl
vec2 a = vec2(0.0, 0.0);
vec2 b = vec2(1.0, 1.0);
// перевантажений конструктор
vec4 c = vec4(a, b); // c = vec4(0.0, 0.0, 1.0, 1.0);
```
Ми створили `vec4` з двох `vec2`. Таким чином новий `vec4` використав `a.x` і `a.y` як `X`і `Y` компоненти та `b.x` і `b.y` як `Z` і `W` компоненти для вектора `c`.
Ось що відбувається, коли **функція** перевантажується для прийняття різних аргументів, у попередньому випадку це був **конструктор** `vec4`.
Це означає, що в одній програмі може співіснувати багато **версій** одного і того самого методу з різною сигнатурою. Наприклад, усі наступні оголошення є валідними:
```glsl
vec4 a = vec4(1.0, 1.0, 1.0, 1.0);
vec4 a = vec4(1.0); // x, y, z, w - усі дорівнюють 1.0
vec4 a = vec4(v2, float, v4); // vec4(v2.x, v2.y, float, v4.x);
vec4 a = vec4(v3, float); // vec4(v3.x, v3.y, v3.z, float);
тощо
```
Єдине про що ви повинні попіклуватися, так це про передачу достатньої кількості аргументів для заповнення **вектору**.
Нарешті, ви також можете перевантажувати вбудовані функції у вашій програмі, щоб вони могли приймати аргументи, для яких не були розроблені (хоча це не повинно траплятися занадто часто).
#### більше типів
Вектори прикольні і є основою вашого шейдера.
А ще існують інші примітиви, такі як матриці та текстурні семплери, які будуть розглянуті пізніше в книзі.
Ми також можемо використовувати масиви. Звичайно, їх потрібно типізувати й вони мають свої особливості у порівнянні з JS:
* мають фіксований розмір
* ви не можете використовувати методи push(), pop(), splice() тощо, і немає властивості ```length```
* ви не можете відразу ініціалізувати їх значеннями
* Ви повинні встановити значення індивідуально
Це не спрацює:
```glsl
int values[3] = [0, 0, 0];
```
А ось це спрацює:
```glsl
int values[3];
values[0] = 0;
values[1] = 0;
values[2] = 0;
```
Добре, коли ви знаєте свої дані або маєте невеликі масиви значень.
Якщо вам потрібно більше виразності, то можете скористатися типом ```struct```. Це як _об'єкти_ без методів.
Вони дозволяють зберігати та отримувати доступ до кількох змінних всередині одного об'єкта:
```glsl
struct ColorStruct {
vec3 color0;
vec3 color1;
vec3 color2;
}
```
Ви можете створити та отримати значення _colors_ наступним чином:
```glsl
// ініціалізуємо структуру деякими значеннями
ColorStruct sandy = ColorStruct(
vec3(0.92, 0.83, 0.60),
vec3(1., 0.94, 0.69),
vec3(0.95, 0.86, 0.69)
);
// отримуємо доступ до значень із структури
sandy.color0 // vec3(0.92, 0.83, 0.60)
```
Це синтаксичний цукор, але він може допомогти написати чистіший код, принаймні більш звичний для вас.
#### вирази та умови
Структури даних корисні, але нам оже_ знадобитися можливість для повторення дії або виконання умовних перевірок.
На щастя для нас, синтаксис дуже близький до JavaScript.
Умова виглядає так:
```glsl
if (condition) {
// true
} else {
// false
}
```
Звичайни цикл `for`:
```glsl
const int count = 10;
for (int i = 0; i <= count; i++) {
// do something
}
```
Приклад циклу з ітератором типу float:
```glsl
const float count = 10.;
for (float i = 0.0; i <= count; i += 1.0) {
// do something
}
```
Зауважте, що ```count``` потрібно визначити як ```константу```.
Це означає перед змінною потрібно додати **кваліфікатор** ```const```, який ми розглянемо трохи згодом.
Нам також доступні оператори ```break``` і ```continue```:
```glsl
const float count = 10.;
for (float i = 0.0; i <= count; i += 1.0) {
if (i < 5.) continue;
if (i >= 8.) break;
}
```
Зауважте, що на деяких типах пристроїв ```break``` не працює належним чином і завчасно не перериває виконання циклу.
Загалом, кількість ітерацій має бути якомога меншою, та і в цілому бажано уникати використання циклів і умовних галужень.
#### кваліфікатори
Окрім типів змінних, GLSL використовує **кваліфікатори**.
Коротко кажучи, кваліфікатори допомагають повідомити компілятору призначення змінних.
Наприклад, деякі дані для GPU можуть бути надані тільки від CPU і називаються **атрибутами** та **уніформами**.
**Атрибути** використовуються у вершинних шейдерах, а **уніформи** можна використовувати як у вершинних, так і у фрагментних шейдерах.
Існує також кваліфікатор ```variying```, який використовується для передачі змінних від вершинного шейдеру до фрагментного.
Я не буду вдаватися в деталі, оскільки ми зосереджені на **фрагментних шейдерах**, але далі в книзі ви побачите щось на кшталт:
```glsl
uniform vec2 u_resolution;
```
Бачите, що ми тут зробили? Ми додали кваліфікатор ```uniform``` перед типом змінної.
Це означає, що змінна, яка відповідає за роздільну здатність полотна з яким ми працюємо, передається шейдеру з CPU.
Ширина полотна зберігається в x, а висота в y-компоненті даного 2D-вектора.
Коли компілятор бачить змінну, якій передує цей кваліфікатор, він простежить, щоб ви не могли *змінити* такі значення під час рантайму.
Те саме стосується нашої змінної ```count```, яка слугувала обмеженням для циклу ```for```:
```glsl
const float count = 10.;
for ( ... )
```
Коли ми використовуємо кваліфікатор ```const```, компілятор не дає змогу перезаписувати значення такої змінної, інакше вона не була б константою.
У сигнатурах функцій можуть використовуватися 3 додаткові кваліфікатори: ```in```, ```out``` та ```inout```.
У JavaScript передані до функції значення скалярних аргументів доступні лише для читання. Якщо ви змінюєте їхні значення всередині функції, то ці зміни не застосовуються до змінної поза функцією.
```glsl
function banana(a) {
a += 1;
}
var value = 0;
banana(value);
console.log(value); // 0 - значення за межами функції не змінилося
```
За допомогою кваліфікаторів перед аргументами ви можете вказати їх поведінку:
* ```in``` - лише для читання (за замовчуванням)
* ```out``` - лише для запису: можна змінити, але не можна прочитати значення
* ```inout``` - читання і запис: можна і прочитати й встановити нове значення
Переписаний метод banana у GLSL виглядає так:
```glsl
void banana(inout float a) {
a += 1.;
}
float A = 0.;
banana(A); // тепер A = 1.;
```
Це дуже відрізняється від JS і є досить потужною можливістю, але вам не обов'язково вказувати кваліфікатори аргументів. За замовчуванням вони доступні лише для зчитування.
#### простір і координати
Останнє зауваження: у DOM і Canvas 2D ми звикли, що вісь Y спрямована 'вниз'.
Це має сенс у контексті DOM, оскільки відповідає способу розгортання вебсторінки: панель навігації вгорі, а контент прокручується донизу.
У полотні WebGL вісь Y перевернута і вказує 'вгору'.
Це означає, що початок координат, точка (0, 0), знаходиться в нижньому лівому куті контексту WebGL, а не у верхньому лівому куті, як у 2D Canvas.
Координати текстур також дотримуються цього правила, що спочатку може бути контрінтуїтивним.
## Ось і все!
Звісно, ми б могли більше заглибитися в різноманітні концепції, але, як згадувалося раніше, цей розділ написаний як просте введення для новачків.
Тут вже написано достатньо для того, щоб за деякий час переварити нові знання, але з терпінням і практикою ця мова ставатиме для вас все більш природною.
Сподіваюся, цей матеріал був корисним для вас. А тепер як щодо початку вашої подорожі основною частиною книги?