24 KiB
Wprowadzenie dla osób przychodzących z JS
przez Nicolas Barradeau
Jeśli jesteś programistą JavaScript, prawdopodobnie nieco zaskoczy Cię treść tej książki. Rzeczywiście, istnieje wiele różnic pomiędzy manipulowaniem wysokopoziomowym kodem JS a zagłębianiem się w świat shaderów. Jednak w przeciwieństwie do niskopoziomowego języka asemblerowego, GLSL jest czytelny dla człowieka i jestem pewien, że po zrozumieniu jego specyfiki szybko zaczniesz go używać.
Zakładam, że masz podstawową (choć może płytką) wiedzę o JavaScript, ale także o Canvas API. Jeśli nie, nie martw się – większość tej sekcji będzie dla Ciebie zrozumiała.
Nie będę zagłębiać się zbytnio w szczegóły, a niektóre kwestie mogą być półprawdziwe; nie oczekuj "wyczerpującego przewodnika", ale raczej
WIELKI UŚCISK
JavaScript świetnie nadaje się do szybkiego prototypowania; wrzucasz garść losowych, nieotypowanych zmiennych i metod, możesz dynamicznie dodawać i usuwać członków klas, odświeżać stronę i sprawdzać, czy wszystko działa, wprowadzać zmiany, odświeżać stronę, powtarzać – życie jest proste. Możesz się więc zastanawiać, jaka jest różnica między JavaScriptem a GLSL. W końcu oba działają w przeglądarce, oba służą do rysowania różnych efektownych rzeczy na ekranie i pod tym względem JS jest łatwiejszy w użyciu.
Główna różnica polega jednak na tym, że JavaScript jest językiem interpretowanym, podczas gdy GLSL jest językiem kompilowanym. Kompilowany program jest wykonywany natywnie przez system operacyjny, jest niskopoziomowy i zazwyczaj szybki. Interpretowany program wymaga wirtualnej maszyny (VM) do wykonania, jest wysokopoziomowy i zazwyczaj wolniejszy.
Gdy przeglądarka (czyli JavaScriptowa VM) wykonuje lub interpretuje fragment kodu JS, nie wie, jaka zmienna czym jest i jaka funkcja co robi (z wyjątkiem oczywistych przypadków, takich jak TypedArrays). Dlatego nie może nic zoptymalizować z góry, więc potrzebuje trochę czasu, aby przeczytać Twój kod, wywnioskować (dedukować na podstawie użycia) typy Twoich zmiennych i metod, a kiedy to możliwe, przekształci część Twojego kodu w kod asemblerowy, który wykona się znacznie szybciej.
To powolny, mozolny i niesamowicie skomplikowany proces – jeśli interesują Cię szczegóły, polecam przyjrzeć się działaniu silnika V8 w Chrome. Najgorsze jest to, że każda przeglądarka optymalizuje JS na swój sposób, a cały proces jest ukryty przed użytkownikiem; jesteś bezsilny.
Kompilowany program nie jest interpretowany; system operacyjny go uruchamia, a jeśli program jest poprawny, zostaje wykonany. To duża zmiana; jeśli zapomnisz o średniku na końcu linii, Twój kod jest niepoprawny i nie skompiluje się: Twój kod w ogóle nie przekształci się w program.
To surowe, ale tak właśnie działa shader: skompilowany program wykonywany na GPU. Nie bój się! Kompilator, czyli część programu, która dba o to, aby Twój kod był poprawny, stanie się Twoim najlepszym przyjacielem. Przykłady z tej książki oraz edytor towarzyszący są bardzo przyjazne użytkownikowi. Powiedzą Ci, gdzie i dlaczego Twój program nie skompilował się, a następnie będziesz musiał wprowadzić poprawki – i za każdym razem, gdy shader będzie gotowy do kompilacji, zostanie on natychmiast wyświetlony. To świetny sposób na naukę, ponieważ jest bardzo wizualny i tak naprawdę niczego nie możesz zepsuć.
Ostatnia uwaga: shader składa się z dwóch programów, vertex shader i fragment shader. W skrócie, vertex shader, pierwszy program, otrzymuje geometrię jako dane wejściowe i przekształca ją w serię pikseli (lub fragmentów), które następnie przekazuje do fragment shader, drugiego programu, który decyduje, jaki kolor nadać pikselom. Ta książka koncentruje się głównie na tym drugim – we wszystkich przykładach geometria to prosty czworobok pokrywający cały ekran.
Więc! Gotowy?
No to ruszamy!
Silne typowanie
Dla osób przychodzących z JS lub innego języka bez typów, typowanie zmiennych jest obcym konceptem, co sprawia, że typowanie stanowi najtrudniejszy krok w kierunku nauki GLSL.
Typowanie, jak sama nazwa wskazuje, oznacza, że musisz przypisać typ każdej zmiennej (oraz funkcjom, oczywiście).
To zasadniczo oznacza, że słowo var przestaje istnieć.
Polityka myślenia GLSL wymazała je z powszechnego języka i nie jesteś w stanie go używać, ponieważ... po prostu nie istnieje.
Zamiast używać magicznego słowa var, będziesz musiał jawnie określić typ każdej używanej zmiennej, dzięki czemu kompilator będzie widział tylko obiekty i prymitywy, które wie, jak efektywnie obsługiwać.
Minusem braku możliwości użycia słowa var i konieczności jawnego określania wszystkiego jest to, że musisz znać typ wszystkich zmiennych i dobrze je rozumieć.
Spokojnie – jest ich niewiele, a do tego są dość proste (GLSL nie jest frameworkiem Java).
Może to brzmieć strasznie, ale ogólnie nie różni się to bardzo od tego, co robisz w JavaScript; jeśli zmienna jest typu boolean, oczekujesz, że będzie przechowywać true lub false i nic więcej.
Jeśli zmienna jest zadeklarowana jako var uid = XXX;, prawdopodobnie przechowasz w niej wartość całkowitą, a var y = YYY; może odnosić się do wartości zmiennoprzecinkowej.
Co więcej, dzięki silnemu typowaniu nie będziesz tracił czasu na zastanawianie się, czy X == Y (albo czy typeof X == typeof Y? ... albo typeof X !== null && Y...... w każdym razie) – po prostu będziesz tego wiedział, a jeśli nie, kompilator Cię o tym poinformuje.
Oto typy skalarne (skalar określa ilość), których możesz używać w GLSL: bool (Boolean), int (liczba całkowita), float (liczba zmiennoprzecinkowa).
Istnieją też inne typy, ale nie ma co się przejmować – poniższy fragment pokazuje, jak deklarować zmienne (vars) (tak, użyłem zakazanego słowa) w GLSL:
// wartość typu Boolean:
JS: var b = true; GLSL: bool b = true;
// wartość typu Integer
JS: var i = 1; GLSL: int i = 1;
// wartość typu Float (liczba)
JS: var f = 3.14159; GLSL: float f = 3.14159;
Nie jest to trudne, prawda? Jak wspomniałem, ułatwia to kodowanie, ponieważ nie tracisz czasu na sprawdzanie typu danej zmiennej. W razie wątpliwości pamiętaj, że robisz to po to, aby Twój program działał o wiele szybciej niż w JS.
void
Istnieje typ void, który w przybliżeniu odpowiada null; jest on używany jako typ zwracany przez funkcję, która nic nie zwraca.
Nie możesz przypisać go do zmiennej.
boolean
Jak wiesz, wartości logiczne (Boolean) są najczęściej używane w testach warunkowych, np. if( myBoolean == true ){}else{}.
Choć rozgałęzianie warunkowe jest możliwe na CPU, równoległa natura GLSL czyni to mniej sensownym.
Używanie instrukcji warunkowych jest często wręcz zniechęcane – książka przedstawia kilka alternatywnych technik rozwiązania tego problemu.
rzutowanie typów
Jak powiedział Boromir: "Nie łączy się po prostu otypowanych prymitywów". W przeciwieństwie do JavaScript, GLSL nie pozwala na wykonywanie operacji pomiędzy zmiennymi o różnych typach.
This for instance:
int i = 2;
float f = 3.14159;
// próba pomnożenia liczby całkowitej przez wartość zmiennoprzecinkową:
float r = i * f;
nie zadziała, ponieważ próbujesz połączyć kota z żyrafą.
Rozwiązaniem jest użycie rzutowania typów; to sprawi, że kompilator uwierzy, że i jest typu float, nie zmieniając faktycznie typu i.
// rzutowanie typu zmiennej całkowitej 'i' na float:
float r = float( i ) * f;
Co jest ściśle równoważne przebraniu kota w żyrafi strój i zadziała zgodnie z oczekiwaniami (r będzie wynikiem i x f).
Można rzutować dowolny z wymienionych typów na inny; zauważ, że rzutowanie float na int działa jak Math.floor(), ponieważ usuwa część dziesiętną.
Rzutowanie float lub int na bool zwróci true, jeśli wartość zmiennej nie wynosi zero.
konstruktor
Typy zmiennych są również swoimi własnymi konstruktorami klas; w rzeczywistości zmienna typu float może być traktowana jako instancja klasy Float.
Następujące deklaracje są równie poprawne:
int i = 1;
int i = int( 1 );
int i = int( 1.9995 );
int i = int( true );
Może się to nie wydawać znaczące dla typów skalarnych, ale nie różni się to zbytnio od rzutowania – nabierze sensu, gdy przejdziemy do sekcji dotyczącej przeciążania.
Ok, więc te trzy to typy prymitywne, rzeczy, bez których nie możesz żyć – ale oczywiście GLSL ma do zaoferowania więcej.
Wektory
W JavaScript, podobnie jak w GLSL, potrzebujesz bardziej zaawansowanych sposobów obsługi danych, dlatego przydają się wektory.
Przypuszczam, że już napisałeś klasę Point w JavaScript, która przechowuje razem wartość x i y, kod wyglądałby mniej więcej tak:
// definicja 'klasy':
var Point = function( x, y ){
this.x = x || 0;
this.y = y || 0;
}
// i instancjonowałoby się ją tak:
var p = new Point( 100,100 );
Jak właśnie widzieliśmy, to jest TAK niepoprawne na TYLU poziomach! Po pierwsze, użycie słowa kluczowego var, potem okropne this, a następnie znowu nieotypowane wartości x i y...
Nie, to nie zadziała w świecie shaderów.
Zamiast tego GLSL udostępnia wbudowane struktury danych, które służą do przechowywania danych razem, mianowicie:
bvec2: 2-wymiarowy wektor Boolean,bvec3: 3-wymiarowy wektor Boolean,bvec4: 4-wymiarowy wektor Booleanivec2: 2-wymiarowy wektor Integer,ivec3: 3-wymiarowy wektor Integer,ivec4: 4-wymiarowy wektor Integervec2: 2-wymiarowy wektor Float,vec3: 3-wymiarowy wektor Float,vec4: 4-wymiarowy wektor Float
Natychmiast zauważyłeś, że dla każdego typu prymitywnego istnieje odpowiedni wektor, spryciarzu.
Z tego, co właśnie widzieliśmy, można wywnioskować, że bvec2 przechowa dwie wartości typu bool, a vec4 cztery wartości typu float.
Inną rzeczą wprowadzoną przez wektory jest liczba wymiarów; nie oznacza to, że do renderowania grafiki 2D używasz 2-wymiarowego wektora, a do 3D – 3-wymiarowego. Co by wtedy reprezentował 4-wymiarowy wektor? (właściwie nazywa się tesseraktem lub hiperkostką)
Nie, wymiary oznaczają liczbę oraz typ składowych lub zmiennych przechowywanych w wektorze:
// stwórzmy 2-wymiarowy wektor Boolean
bvec2 b2 = bvec2 ( true, false );
// stwórzmy 3-wymiarowy wektor Integer
ivec3 i3 = ivec3( 0,0,1 );
// stwórzmy 4-wymiarowy wektor Float
vec4 v4 = vec4( 0.0, 1.0, 2.0, 1. );
b2 przechowuje dwie różne wartości logiczne, i3 przechowuje trzy różne wartości całkowite, a v4 cztery różne wartości zmiennoprzecinkowe.
Ale jak odczytać te wartości?
w przypadku skalarów odpowiedź jest oczywista – przy float f = 1.2; zmienna f przechowuje wartość 1.2.
W przypadku wektorów jest to nieco inne i całkiem piękne.
akcesory
Istnieją różne sposoby dostępu do wartości
// stwórzmy 4-wymiarowy wektor Float
vec4 v4 = vec4( 0.0, 1.0, 2.0, 3.0 );
Aby odczytać te 4 wartości, możesz zrobić następująco:
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
bułka z masłem; ale poniższe sposoby są równie poprawne w dostępie do Twoich danych:
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
I spryciarzu, już zauważyłeś trzy rzeczy:
X,Y,ZiWsą używane w programach 3D do reprezentacji wektorów 3DR,G,BiAsłużą do kodowania kolorów oraz kanału alfa[0],[1],[2]i[3]oznaczają, że mamy dostęp do wartości w sposób losowy (tablica indeksowana)
W zależności od tego, czy manipulujesz współrzędnymi 2D czy 3D, kolorem z lub bez kanału alfa, czy też po prostu różnymi zmiennymi, możesz wybrać najbardziej odpowiedni typ i rozmiar wektora.
Zazwyczaj współrzędne 2D i wektory (w sensie geometrycznym) przechowywane są jako vec2, vec3 lub vec4, kolory jako vec3 lub vec4 (jeśli potrzebujesz kanału alfa), ale nie ma ograniczeń co do sposobu użycia wektorów.
Na przykład, jeśli chcesz przechować tylko jedną wartość logiczną w bvec4, jest to możliwe, ale to marnotrawstwo pamięci.
uwaga: w shaderze wartości kolorów (R, G, B i A) są normalizowane, mieszczą się w przedziale od 0 do 1, a nie od 0 do 0xFF, dlatego lepiej użyć Float vec4 niż Integer ivec4 do ich przechowywania.
Już nieźle, ale to nie wszystko!
swizzle
Można zwrócić więcej niż jedną wartość jednocześnie; powiedzmy, że potrzebujesz tylko składowych X i Y z vec4, w JavaScript musiałbyś napisać coś takiego:
var needles = [0, 1]; // pozycja 'x' i 'y' w naszej strukturze danych
var a = [ 0,1,2,3 ]; // nasza struktura danych 'vec4'
var b = a.filter( function( val, i, array ) {
return needles.indexOf( array.indexOf( val ) ) != -1;
});
// b = [ 0, 1 ]
// albo bardziej bezpośrednio:
var needles = [0, 1];
var a = [ 0,1,2,3 ]; // our 'vec4' data structure
var b = [ a[ needles[ 0 ] ], a[ needles[ 1 ] ] ]; // b = [ 0, 1 ]
Brzydko. W GLSL możesz je uzyskać w ten sposób:
// stwrórz 4-wymiarowy wektor Float
vec4 v4 = vec4( 0.0, 1.0, 2.0, 3.0 );
// i odczytaj tylko 'x' i 'y'
vec2 xy = v4.xy; // xy = vec2( 0.0, 1.0 );
Co tu się stało?! Gdy skonkatenujesz akcesory, GLSL elegancko zwraca podzbiór wartości, o które prosiłeś, w najbardziej odpowiednim formacie wektora. Rzeczywiście, wektor to struktura danych o losowym dostępie, podobna do tablicy w JavaScript. Tak więc, nie tylko możesz pobrać podzbiór swoich danych, ale także określić kolejność, w jakiej mają być zwrócone – może to odwrócić kolejność składowych wektora:
// stwórz 4-wymiarowy wektor: R,G,B,A
vec4 color = vec4( 0.2, 0.8, 0.0, 1.0 );
// i odczytaj go w kolejności: A,B,G,R
vec4 backwards = color.abgr; // backwards = vec4( 1.0, 0.0, 0.8, 0.2 );
I oczywiście, możesz zapytać o tę samą składową wielokrotnie:
// stwórz 4-wymiarowy wektor: R,G,B,A
vec4 color = vec4( 0.2, 0.8, 0.0, 1.0 );
// i odczytaj GAG (Green, Alpha, Green) vec3 z kanałów G i A
vec3 GAG = color.gag; // GAG = vec4( 0.8, 1.0, 0.8 );
Jest to niezwykle przydatne, aby łączyć części wektorów, wyodrębniać tylko kanały rgb z koloru RGBA itp.
przeciążaj wszystko!
W sekcji o typach wspomniałem o konstruktorze i tu mamy kolejną świetną cechę GLSL – przeciążanie. Dla tych, którzy nie wiedzą, przeciążanie operatora lub funkcji oznacza mniej więcej: "zmianę zachowania danego operatora lub funkcji w zależności od operandów/argumentów". Przeciążanie nie jest dozwolone w JavaScript, więc na początku może to wydawać się dziwne, ale jestem pewien, że gdy się do tego przyzwyczaisz, zastanowisz się, dlaczego nie zostało to zaimplementowane w JS (krótka odpowiedź, typowanie).
Najprostszy przykład przeciążania operatorów wygląda następująco:
vec2 a = vec2( 1.0, 1.0 );
vec2 b = vec2( 1.0, 1.0 );
// przeciążone dodawanie
vec2 c = a + b; // c = vec2( 2.0, 2.0 );
CO? Czyli można dodawać rzeczy, które nie są liczbami?!
Tak, dokładnie. Oczywiście dotyczy to wszystkich operatorów (+, -, * oraz /), ale to dopiero początek.
Rozważ poniższy fragment:
vec2 a = vec2( 0.0, 0.0 );
vec2 b = vec2( 1.0, 1.0 );
// przeciążony konstruktor
vec4 c = vec4( a , b ); // c = vec4( 0.0, 0.0, 1.0, 1.0 );
Zbudowaliśmy vec4 z dwóch vec2, przy czym nowy vec4 użył a.x i a.y jako składowych X i Y wektora c.
Następnie wziął b.x i b.y i użył ich jako składowych Z i W.
Tak właśnie działa przeciążony konstruktor vec4, który akceptuje różne argumenty. Oznacza to, że wiele wersji tej samej funkcji o różnych sygnaturach może współistnieć w jednym programie, na przykład następujące deklaracje są wszystkie poprawne:
vec4 a = vec4(1.0, 1.0, 1.0, 1.0);
vec4 a = vec4(1.0);// x, y, z, w wszystkie są równe 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 );
etc.
Jedyne, na co musisz zwrócić uwagę, to dostarczenie wystarczającej liczby argumentów, aby wypełnić Twój wektor.
Ostatnia rzecz, możesz przeciążać wbudowane funkcje w swoim programie, aby przyjmowały argumenty, dla których nie zostały zaprojektowane (choć nie powinno się to zdarzać zbyt często).
inne typy
Wektory są fajne, to sedno Twojego shadera. Istnieją inne prymitywy, takie jak macierze (Matrices) i próbki tekstur (Texture samplers), które zostaną omówione później w książce.
Możemy także używać tablic (Arrays). Oczywiście muszą być typowane, a przy tym występują pewne pułapki:
- mają ustalony rozmiar
- nie możesz używać metod push(), pop(), splice() itp., a właściwość
lengthnie istnieje - nie możesz od razu zainicjalizować ich wartościami
- musisz przypisywać wartości pojedynczo
to nie zadziała:
int values[3] = [0,0,0];
ale to zadziała:
int values[3];
values[0] = 0;
values[1] = 0;
values[2] = 0;
To jest w porządku, gdy znasz swoje dane lub masz małe tablice wartości.
Jeśli chcesz bardziej ekspresyjnego sposobu deklaracji zmiennej,
możesz użyć również typu struct. Są one jak obiekty bez metod;
pozwalają przechowywać i uzyskiwać dostęp do wielu zmiennych w jednym obiekcie.
struct ColorStruct {
vec3 color0;
vec3 color1;
vec3 color2;
}
następnie możesz ustawiać i odczytywać wartości kolorów w następujący sposób:
// zainicjuj struct z jakimiś wartościami
ColorStruct sandy = ColorStruct( vec3(0.92,0.83,0.60),
vec3(1.,0.94,0.69),
vec3(0.95,0.86,0.69) );
// odczytaj wartość ze struct
sandy.color0 // vec3(0.92,0.83,0.60)
To lukier składniowy, ale może pomóc w pisaniu bardziej przejrzystego kodu, przynajmniej takiego, do którego jesteś przyzwyczajony.
instrukcje i warunki
Struktury danych są przydatne, ale może będziesz musiał iterować lub wykonywać testy warunkowe w pewnym momencie. Na szczęście składnia jest bardzo zbliżona do tej w JavaScript. Warunek wygląda tak:
if( warunek ){
// prawda
}else{
// fałsz
}
Pętla for zazwyczaj wygląda tak:
const int count = 10;
for( int i = 0; i <= count; i++){
// zrób coś
}
lub z iteratorem typu float:
const float count = 10.;
for( float i = 0.0; i <= count; i+= 1.0 ){
// zrób coś
}
Zauważ, że count musi być zdefiniowane jako stała.
Oznacza to, że poprzedzasz typ kwalifikatorem const, o czym opowiem za chwilę.
Mamy również instrukcje break i continue:
const float count = 10.;
for( float i = 0.0; i <= count; i+= 1.0 ){
if( i < 5. )continue;
if( i >= 8. )break;
}
Miej na uwadze, że na niektórych urządzeniach instrukcja break może nie działać zgodnie z oczekiwaniami i pętla nie przerwie iteracji wcześniej.
Ogólnie rzecz biorąc, powinieneś utrzymywać liczbę iteracji na możliwie najniższym poziomie i unikać pętli oraz instrukcji warunkowych tak często, jak to możliwe.
kwalifikatory
Oprócz typów zmiennych, GLSL używa kwalifikatorów.
Krótko mówiąc, kwalifikatory pomagają kompilatorowi zrozumieć, jaka jest rola danej zmiennej.
Na przykład, niektóre dane mogą być dostarczane tylko przez CPU do GPU, nazywamy je atrybutami i uniformami.
Atrybuty są zarezerwowane dla vertex shaderów, a uniformy mogą być używane zarówno w vertex, jak i fragment shaderach.
Jest też kwalifikator varying, służący do przekazywania zmiennych między vertex a fragment shaderem.
Nie będę zagłębiać się tutaj w szczegóły, ponieważ skupiamy się głównie na fragment shaderze, ale później w książce zobaczysz coś takiego:
uniform vec2 u_resolution;
Widzisz, co zrobiliśmy? Dodaliśmy kwalifikator uniform przed typem zmiennej.
Oznacza to, że rozdzielczość kanwy, nad którą pracujemy, jest przekazywana do shadera z CPU.
Szerokość kanwy zapisana jest w komponencie x, a wysokość w komponencie y 2-wymiarowego wektora.
Gdy kompilator napotka zmienną poprzedzoną tym kwalifikatorem, upewni się, że nie możesz zmieniać tych wartości w czasie wykonywania programu.
To samo dotyczy naszej zmiennej count, która była limitem w pętli for:
const float count = 10.;
for( ... )
Kiedy używamy kwalifikatora const, kompilator upewni się, że wartość zmiennej zostanie ustawiona tylko raz, w przeciwnym razie nie jest to stała.
Istnieją trzy dodatkowe kwalifikatory używane w sygnaturach funkcji: in, out oraz inout. W JavaScript, gdy przekazujesz prymitywne argumenty do funkcji, ich wartość jest tylko do odczytu, a jeśli zmienisz ich wartość wewnątrz funkcji, zmiany nie mają wpływu na zmienną poza funkcją.
function banana( a ){
a += 1;
}
var value = 0;
banana( value );
console.log( value );// > 0 ; zmiany nie są brane pod uwagę poza funkcją
With arguments qualifiers, you can specify the behaviour of the arguments:
inwill be read-only ( default )outwrite-only: you can't read the value of this argument but you can set itinoutread-write: you can both get and set the value of this variable
Przepisanie funkcji banana do GLSL wyglądałoby tak:
void banana( inout float a ){
a += 1.;
}
float A = 0.;
banana( A ); // teraz A = 1.;
To bardzo różni się od JS i jest również potężne, ale nie musisz jawnie określać kwalifikatorów w sygnaturze (domyślnie są one tylko do odczytu).
przestrzeń i współrzędne
Ostatnia uwaga: w DOM oraz w 2D Canvas jesteśmy przyzwyczajeni, że oś Y wskazuje w dół. Ma to sens w kontekście DOM, ponieważ odpowiada sposobowi, w jaki rozwija się strona internetowa – pasek nawigacyjny na górze, zawartość rozciągająca się ku dołowi. W kanwie WebGL oś Y jest odwrócona: Y wskazuje w górę.
Oznacza to, że punkt początkowy, czyli (0,0), znajduje się w lewym dolnym rogu kontekstu WebGL, a nie w lewym górnym, jak ma to miejsce w 2D Canvas. Współrzędne tekstur podlegają tej zasadzie, co na początku może być nieintuicyjne.
I to wszystko!
Oczywiście moglibyśmy zagłębić się w różne koncepcje, ale jak wspomniano wcześniej, chodziło o to, aby dać WIELKI UŚCISK nowoprzybyłym. To sporo materiału do przyswojenia, ale z cierpliwością i praktyką stanie się to coraz bardziej naturalne.
Mam nadzieję, że część z tego okaże się przydatna. A teraz, co powiesz na rozpoczęcie swojej podróży przez tę książkę?
