Shadertoy - medzi matematikou a umením (úvod)
Umenie často nie je len o výsledku, ale aj o spôsobe realizácie a obmedzeniach, ktoré sa museli brať do úvahy. Shadertoy je nástroj, ktorý umožňuje vytvárať obraz a zvuk pomocou malých programov bežiacich na GPU s dosť prísnymi obmedzeniami.
Článok začínam popisom fungovania GPU a ukážkou OpenGL. Pre tých, ktorí poznajú základy OpenGL, alebo ich OpenGL nezaujíma odporúčam rovno preskočiť na kapitolu čo je shadertoy.
OpenGL
Prvá verzia knižnice OpenGL vyvinutej v spoločnosti SGI bola vydaná v roku 1992. API rozhranie je navrhnuté ako stavový automat. Preto sa pred vykreslením najskôr nastavujú atribúty (ako napríklad farba), ktoré budú aplikované na nasledujúce príkazy vykreslenia.
Ahoj obdĺžnik
Začnem najskôr Makefile
súborom. Ten stačí umiestniť do adresára s príkladmi a na kompiláciu príkladov slúži príkaz make
spustený v adresári s príkladmi. Súbor vyzerá nasledovne (pozor, riadky musia byť odsadené tabulátorom).
.PHONY: all CC=gcc CFLAGS=-Wall -Wextra -Werror --std=c99 LDFLAGS=-lGL -lglut -lGLEW SRCS := $(shell find . -name "*.c") OBJS := $(addsuffix .o,$(basename $(SRCS))) BINS := $(addsuffix .bin,$(basename $(SRCS))) %.o: %.c $(CC) -c -o $@ $< $(CFLAGS) %.bin: %.o $(CC) -o $@ $< $(CFLAGS) $(LDFLAGS) all: $(BINS)
Vykreslenie obdĺžnika s rozdielnou farbou v každom rohu je v OpenGL (na rozdiel od Vulkan API) je veľmi jednoduché:
#include <GL/glut.h> // Zobrazenie obdĺžnika void display() { // Vyčistenie pozadia glClear(GL_COLOR_BUFFER_BIT); // Vykreslenie jednotlivých bodov. GPU vie vykresľovať iba trojuholníky, // preto sa obdĺžnik vykreslí ako 2 trojuholníky. OpenGL je implementované ako // stavový automat, prto sa určuje farba vertexu pred odoslaním do GPU. glBegin(GL_TRIANGLE_STRIP); glColor3f(1, 0, 0); glVertex3f(-0.9, -0.9, 0); glColor3f(0, 1, 0); glVertex3f(-0.9, 0.9, 0); glColor3f(0, 0, 1); glVertex3f( 0.9, -0.9, 0); glColor3f(1, 1, 1); glVertex3f( 0.9, 0.9, 0); glEnd(); // Ukončenie definície objektov a vykreslenie na obrazovku glFlush(); } // Inicializácia OpenGL a hlavnej slučky int main(int argc, char** argv) { // Režim RGB bez double bufferu glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); // Vytvorenie okna glutInitWindowSize(400, 300); glutCreateWindow("Okno"); // Zaregistrovanie funkcie, ktorá vykreslí obsah okna glutDisplayFunc(display); // Hlavná slučka, ktorá bude bežať až kým užívateľ nezatvorí okno glutMainLoop(); }
Zdrojový kód jednoduchy_obdlznik.c stačí stiahnuť, umiestniť do adresára s Makefile, spustiť make
a nakoniec spustiť ./jednoduchy_obdlznik.bin
.
Do pozornosti dávam funkciu display
, v ktorej prebieha vykresľovanie. Grafická karta vie zvyčajne zobraziť pixel, čiaru, alebo trojuholník. Akýkoľvek polygón s väčším počtom vrcholov sa dá poskladať z trojuholníkov, preto nie je dôvod, aby grafické karty vedeli vykresľovať zložitejšie polygóny.
Trojuholníky sú vykreslené pomocou 4 vertexov (bod v priestore definovaný súradnicami). Dva trojuholníky sa skladajú zo 6 vertexov, ale pretože majú spoločnú hranu, dajú sa do GPU nahrať ako triangle strip.
Farby sú definované trojicou hodnôt s plávajúcou desatinnou čiarkou v rozsahu 0 (čierna) až 1 (maximálna hodnota danej farebnej zložky).
Nie je žiadnym prekvapením, že koordináty vertexov sú definované 3 súradnicami: x
, y
, a z
. Knižnica OpenGL vykreslí trojuholníky nachádzajúce sa v obdĺžniku so súradnicami od -1 po +1 na x
aj y
osi. Tretia súradnica (hĺbka) sa používa na určenie viditeľnosti. Obdĺžnik v mojom príklade je zobrazený od súradnice -0.9 po 0.9 (teda 5% vzdialenosť od každého okraja).
Prvé grafické akcelerátory dokázali prakticky len transformovať 3D scénu skladajúcu sa z trojuholníkov do 2D (čo je podstatne jednoduchšia operácia, než si väčšina ľudí predstavuje) a vykresliť trojuholníky. Moderné grafické akcelerátory sa prakticky vrátili k pôvodnej funkcii - teda vykresľovaniu trojuholníkov. Medzitým bolo samozrejme divoké obdobie, kedy sa výrobcovia snažili naučiť svoje GPU všetko možné (a nemyslím len šialenosti ako NV_path_rendering
).
Osvetlenie
V prvom príklade som sa vôbec nestaral o osvetlenie. Jednoducho som vykreslil obdĺžnik pevne danou farbou ako za starých čias s prvými grafickými akcelerátormi. Prvé grafické akcelerátory fungovali presne takto a preto sa osvetlenie buď muselo počítať na CPU, alebo sa pripravili priamo textúry s určitým osvetlením. Výpočet osvetlenia bola tak užitočná funkcia, že výrobcovia GPU začali pridávať hardvérovú podporu výpočtu.
Nasledujúci príklad zobrazí rovnaký obdĺžnik, ale so svetlom umiestneným v pravom hornom rohu. Na výpočet osvetlenia je použitý Phongov osvetľovací model.
#include <GL/glut.h> // Zobrazenie obdĺžnika void display() { // Vyčistenie pozadia glClear(GL_COLOR_BUFFER_BIT); // Zapnutie výpočtu osvetlenia glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); // Nastavenie ambientného osvetlenia intenzity 0.1, difúzneho 1.0 // a umiestnenie svetla do pravého horného rohu GLfloat light_ambient[] = {0.1, 0.1, 0.1, 1.0}; GLfloat light_diffuse[] = {1.0, 1.0, 1.0, 1.0}; GLfloat light_specular[] = {0.0, 0.0, 0.0, 1.0}; GLfloat light_position[] = {0.9, 0.9, 0.5, 1.0}; glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); glLightfv(GL_LIGHT0, GL_POSITION, light_position); // Útlm počítaný z prevrátených štvorcov glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 2.0); glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 1.0); glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.5); GLfloat material_specular[] = {0.0, 0.0, 0.0, 1.0}; GLfloat material_shininess[] = {50.0}; glMaterialfv(GL_FRONT, GL_SPECULAR, material_specular); glMaterialfv(GL_FRONT, GL_SHININESS, material_shininess); // Vykreslenie trojuholníkov s nastavením farby pre odraz ambientného // a difúzneho svetla glBegin(GL_TRIANGLE_STRIP); GLfloat color1[] = {1.0, 0.0, 0.0, 1.0}; glMaterialfv(GL_FRONT, GL_AMBIENT, color1); glMaterialfv(GL_FRONT, GL_DIFFUSE, color1); glVertex3f(-0.9, -0.9, 0); GLfloat color2[] = {0.0, 1.0, 0.0, 1.0}; glMaterialfv(GL_FRONT, GL_AMBIENT, color2); glMaterialfv(GL_FRONT, GL_DIFFUSE, color2); glVertex3f(-0.9, 0.9, 0); GLfloat color3[] = {0.0, 0.0, 1.0, 1.0}; glMaterialfv(GL_FRONT, GL_AMBIENT, color3); glMaterialfv(GL_FRONT, GL_DIFFUSE, color3); glVertex3f(0.9, -0.9, 0); GLfloat color4[] = {1.0, 1.0, 1.0, 1.0}; glMaterialfv(GL_FRONT, GL_AMBIENT, color4); glMaterialfv(GL_FRONT, GL_DIFFUSE, color4); glVertex3f(0.9, 0.9, 0); glEnd(); // Ukončenie definície objektov a vykreslenie na obrazovku glFlush(); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(400, 300); glutCreateWindow("Okno"); glutDisplayFunc(display); glutMainLoop(); }
Súbor phong.c sa dá skompilovať a spustiť rovnakým spôsobom, ako predchádzajúci príklad.
V modeli je definované jediné svetlo LIGHT0
. OpenGL podporuje „až“ 8 rôznych zdrojov svetla. Pre svetlo sa definujú parametre ambientnej zložky (svetlo, ktorým sú osvetlené všetky časti modelu rovnomerne bez ohľadu na smer a vzdialenosť svetla), difúznej zložky (svetlo, ktoré sa rozptýli na povrchu) odrazov (specular). Obdobné vlastnosti sa definujú aj pre materiál objektu. Celkové osvetlenie sa vypočíta ako súčet ambientnej zložky, difúznej zložky a odlesku.
Objekt osvetlený Phongovým osvetľovacím modelom má zvyčajne plastický vzhľad. Pomerne dlhú dobu to bol jediný rozumný model podporovaný GPU. Požiadavky na vyššiu realistickosť donútili výrobcov vymyslieť niečo ďalšie.
Grafické karty boli rozšírené o technológie ako shadow mapping, bump mapping a rôzne rozšírenia špecifické pre konkrétneho výrobcu.
Na zobrazenie realistických materiálov to aj tak nestačilo. Skutočné materiály reagujú na svetlo mnohými spôsobmi. Odporúčam si len tak zbežne pozrieť tento dokument s popisom fyzikálne realistického renderingu. Bolo by ťažko predstaviteľné integrovať do GPU všetky možné spôsoby interakcie objektu so svetlom (napríklad metalické odlesky, odlesky od CD/DVD, šírenie svetla tesne pod povrchom atď). Čo tak dať programátorovi možnosť napísať si vlastný vzorec namiesto tých preddefinovaných?
Shader
Okolo roku 2000 začali prichádzať na trh grafické karty s možnosťou definovať vstupné dáta a operácie nad nimi. Najskôr to boli proprietárne rozšírenia, napríklad NV_register_combiners
(GeForce 256), NV_texture_shader
(GeForce 3), NV_texture_shader3
(GeForce 4), NV_fragment_program
(GeForce FX).
Nie len, že každý výrobca si implementoval programovateľnú časť sám, ale dokonca aj každá generácia GPU sa programovala vlastným nekompatibilným assemblerom až kým v roku 2002 nebol prijatý štandard ARB_vertex_program
a ARB_fragment_program
. Jazyk bol podobný assembleru, aj keď so zásadnými obmedzeniami. Neobsahoval napríklad vetvenie, ani slučky.
Zastavím sa trochu pri slove vertex a fragment. Vertex shader je program, ktorý sa spustí pre každý bod v priestore (vertex). Čo je ale fragment? V DirectX existujú vertex a pixel shadery. V OpenGL sú ekvivalentom vertex a fragment shadery. Fragment shader je malý program, ktorý sa vykoná pri vykreslení každého pixelu. Prečo teda nie je pomenovaný jednoducho pixel shader? Shadery v DirectX sú pomenované skôr podľa ich výstupu, zatiaľ čo v OpenGL sú pomenované podľa vstupu (vertex shader mení pozíciu vertexov, fragment shader má na vstupe atribúty postačujúce na vykreslenie - tzv. fragmenty a výstupom je pixel).
Ťažko môžme hovoriť o skutočnom programovaní, keď jazyk nepodporuje cykly a vetvenie. To priniesol až v roku 2004 jazyk OpenGL Shading Language (GLSL). Je to vysokoúrovňový jazyk silne inšpirovaný jazykom C. Práve v ňom sa programujú shadery pre shadertoy.
Programovanie v GLSL má určité obmedzenia a vlastnosti vyplývajúce zo spôsobu vykonávania programu. Každý beh (spustenie) nad jedným elementom je nezávislé od všetkých ostatných elementov a nemôže ich ovplyvniť (jeden pixel nemôže ovplyvniť susedný). Pamäť alokovaná pri vykonávaní je samostatná pre každý element. Spustenie nemôže mať žiaden vedľajší efekt. V tomto sa podobá striktne funkcionálnym jazykom.
Striktné požiadavky sú nutné pre to, aby sa dali shadery spustiť masívne paralelne. Také 4k rozlíšenie má 8 294 400 individuálnych pixelov. Pri obnovovacej frekvencii 60 Hz je potrebné vykresliť 497 664 000 pixelov za sekundu. Iná cesta, než masívny paralelizmus tu nie je.
Na úplne paralelné spustenie obrovského množstva programov by GPU potrebovala desaťtisíce univerzálnych jadier. V skutočnosti sa pre dosiahnutie podobného výsledku používa jeden trik. Namiesto jadra pre každý pixel (alebo ľubovoľný element) tu jedno jadro obsluhuje blok o veľkosti najčastejšie 8x8 alebo 8x16 elementov. Program sa vykonáva inštrukciu za inštrukciou pre celý blok pixelov. Čo sa však stane ak program obsahuje podmienku, ktorá sa má vykonať pre polovicu pixelov? V tom prípade sa najskôr nastaví maska pixelov pre jednu vetvu, vykonajú sa všetky inštrukcie vetvy a následne sa zmení maska na druhú vetvu a vykonajú sa zvyšné inštrukcie. Vykonávanie vetiev je operácia, ktorá dokáže v najhoršom prípade pri veľkosti bloku 8x8 pixelov spomaliť výpočet na 1/64 pôvodnej rýchlosti (ak sa výpočet každého elementu musí vykonať v samostatnej vetve).
Obdĺžnik s „moderným“ OpenGL
Predchádzajúci príklad používal API z OpenGL 1.0. Nahrávanie modelu po jednotlivých vertexoch je možno akceptovateľné pri softvérovom renderingu, alebo pri prvých GPU, ktoré dokázali vykresliť pár trojuholníkov. V modernom OpenGL je lepšie minimalizovať volania, ktoré nahrávajú dáta do GPU. Nasledujúci príklad moderny_obdlznik.c vykreslí rovnaký obdĺžnik, ako v prvom príklade, ale tentoraz pomocou shaderov.
#include <GL/glew.h> #include <GL/freeglut.h> #include <GL/glut.h> #include <stdio.h> // Súradnice vertexov a farby static const GLfloat vertexBufferData[] = { -0.9f, -0.9f, 1, 0, 0, -0.9f, 0.9f, 0, 1, 0, 0.9f, -0.9f, 0, 0, 1, 0.9f, 0.9f, 1, 1, 1, }; GLuint vertexBuffer; // Kód vertex a fragment shadera static const char vertexShader[] = "#version 330 core\n\ \n\ layout(location = 0) in vec2 position;\n\ layout(location = 1) in vec3 colorAttribute;\n\ \n\ out vec3 fragmentColor;\n\ \n\ void main(){\n\ gl_Position = vec4(position, 0.0, 1.0);\n\ fragmentColor = colorAttribute;\n\ }\n\ "; static const char fragmentShader[] = "#version 330 core\n\ in vec3 fragmentColor;\n\ \n\ out vec4 color;\n\ \n\ void main()\n\ {\n\ color = vec4(fragmentColor, 1.0);\n\ }\n\ "; GLuint shaderProgram; // Výpis chyby kompilácie ak shader nebol skompilovaný void printShaderStatus(GLuint shader) { GLint res = GL_FALSE; int infoLength; glGetShaderiv(shader, GL_COMPILE_STATUS, &res); glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength); if (res != GL_TRUE) { char *msg = malloc(infoLength + 1); glGetShaderInfoLog(shader, infoLength, NULL, msg); printf("%s", msg); free(msg); } } // Výpis chyby linkovania ak sa program nepodarilo zlinkovať void printProgramStatus(GLuint program) { GLint res = GL_FALSE; int infoLength; glGetProgramiv(program, GL_LINK_STATUS, &res); glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLength); if (res != GL_TRUE) { char *msg = malloc(infoLength + 1); glGetProgramInfoLog(program, infoLength, NULL, msg); printf("%s", msg); free(msg); } } // Kompilácia ľubovoľného druhu shadera void compileShader(GLuint shader, const char *code) { glShaderSource(shader, 1, &code, NULL); glCompileShader(shader); printShaderStatus(shader); } // Kompilácia shaderov a zlinkovanie do programu GLuint getShaderProgram(const char *vertexShaderSource, const char *fragmentShaderSource) { // Vytvorenie programu GLuint program = glCreateProgram(); // Vytvorenie inštancií shaderov GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); // Kompilácia oboch zdrojových kódov compileShader(vertexShader, vertexShaderSource); compileShader(fragmentShader, fragmentShaderSource); // Priradenie shaderov k jednému programu glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); // Zlinkovanie programu a výpis chyb ak nebolo linkovanie úspešné glLinkProgram(program); printProgramStatus(program); // Ďalej shader nnemusí byť priradený k programu glDetachShader(program, vertexShader); glDetachShader(program, fragmentShader); return program; } // Zobrazenie obdĺžnika void display() { // Vyčistenie pozadia glClear(GL_COLOR_BUFFER_BIT); // Pri vykresľovaní sa použije shader glUseProgram(shaderProgram); // Priradenie poľa k atribútu s location(0) (position) glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glVertexAttribPointer( 0, // Index atribútu (zodpovedá hodnote v location) 2, // Počet komponentov (zadávame pre každý bod iba 2 súradnice) GL_FLOAT, // V poli sú hodnoty s pohyblivou desatinnou čiarkou GL_FALSE, // Nemá význam pre typ float sizeof(GLfloat)*5, // Rozostup medzi prvkami (void *)0 // Posun voči začiatku poľa ); // Priradenie poľa k atribútu s location(1) (colorAttribute) glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (void *)(sizeof(GLfloat)*2)); // Vykreslenie objektu triangle strip zo 4 vertexov glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glDisableVertexAttribArray(0); glDisableVertexAttribArray(1); // Zobrazenie na obrazovke glFlush(); } // Inicializácia OpenGL a hlavnej slučky int main(int argc, char** argv) { glutInit(&argc, argv); // Explicitne vyžadujem OpenGL 3.3 core profile glutInitContextVersion(3, 3); glutInitContextProfile(GLUT_CORE_PROFILE); // Režim RGBA s double bufferom glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA); // Vytvorenie okna glutInitWindowSize(400, 300); glutCreateWindow("Okno"); // Inicializácia GLEW musí nasledovať po otvorení okna glewInit(); // Aktivovaný vertex array objekt GLuint vertexArray; glGenVertexArrays(1, &vertexArray); glBindVertexArray(vertexArray); // Kompilácia shaderProgram = getShaderProgram(vertexShader, fragmentShader); // Upload vertexov a farieb do GPU glGenBuffers(1, &vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(vertexBufferData), vertexBufferData, GL_STATIC_DRAW); // Zaregistrovanie funkcie, ktorá vykreslí obsah okna glutDisplayFunc(display); // Hlavná slučka glutMainLoop(); }
Neplánujem tu písať tutoriál k OpenGL, takže z kódu vyberiem len pár zaujímavostí. Prvým je pole súradníc a farieb:
static const GLfloat vertexBufferData[] = { -0.9f, -0.9f, 1, 0, 0, -0.9f, 0.9f, 0, 1, 0, 0.9f, -0.9f, 0, 0, 1, 0.9f, 0.9f, 1, 1, 1, };
Všetky dáta nahrávam do GPU jediným volaním glBufferData
. Namiesto 3 priestorových súradníc sa posielajú do GPU len 2 súradnice, pretože sa vykresľuje 2D objekt. Chýbajúcu súradnicu sa vygenerujú priamo na GPU pomocou vertex shadera.
Vertex shader je jednoduchý program, ktorý na základe jedného, alebo viacerých vstupov vygeneruje polohu vertxu (4-rozmerný vektor), prípadne ďalšie atribúty s interpolovanou hodnotou medzi bodmi. Pre tento príklad stačí jednoduchý vertex shader:
#version 330 core layout(location = 0) in vec2 position; layout(location = 1) in vec3 colorAttribute; out vec3 fragmentColor; void main(){ gl_Position = vec4(position, 0.0, 1.0); fragmentColor = colorAttribute; }
Tento krátky kód hovorí, že má 2 vstupné atribúty. Sú nimi dvojzložkový vektor position
na pozícii 0 a trojzložkový colorAttribute
na pozícii 1. Výstupnú polohu vertexu reprezentuje vstavaná premenná gl_Position
. Kód vec4(position, 0.0, 1.0)
pridáva k dvojzložkovému vektoru position
nulovú zložku pre os z
a štvrtú zložku 1.0 (tá je užitočná pri operáciách s maticami). Okrem povinného gl_Position
má shader aj ďalší výstupný atribút fragmentColor
s interpolovanou farbou.
Fragment shader je ešte jednoduchší:
#version 330 core in vec3 fragmentColor; out vec4 color; void main() { color = vec4(fragmentColor, 1.0); }
Vstupná hodnota atribútu fragmentColor
sa jednoducho prekopíruje do výstupnej farby color
. Dodatočná štvrtá zložka je alfa kanál (priehľadnosť).
Výsledok je rovnaký, ako v prvom príklade aj keď s podstatne zložitejším kódom.
Čo je shadertoy
Shadertoy je webová stránka, ktorá:
- vľavo hore zobrazuje obdĺžnik
- vpravo hore zobrazuje editor s fragment shaderom obdĺžnika
- umožňuje zdieľať svoje výtvory s ostatnými užívateľmi
V predchádzajúcom texte som vytvoril jednoduchú OpenGL aplikáciu, ktorá zobrazí obdĺžnik s vlastným vertex / fragment shaderom. Súbor shader.c
v príkladoch je triviálnou úpravou predchádzajúceho príkladu, akurát obdĺžnik je zobrazený na celej ploche okna a fragment shader sa načítava zo súboru. Program sa dá spustiť príkazom ./shader.bin cesta_k_shaderu
.
Na začiatok skúsim jednoduchý shader, ktorý vykreslí jednoliatu červenú plochu:
#version 330 core out vec4 color; void main() { color = vec4(1.0, 0.0, 0.0, 1.0); }
Po uložení ako red.frag
a spustení ./shader.bin red.frag
sa zobrazí červené okno. Direktíva #version
slúži na určenie verzie verzie GLSL jazyka. Hodnota 330
znamená verziu 3.30
.
Shadertoy vkladá pred užívateľský kód vlastné premenné a funkcie. Preto sa pri zápise shaderov pre shadertoy nepoužíva ako hlavná funkcia main
, ale mainImage
. Nasledujúci kód by vykreslí rovnaký červený obdĺžnik, ale je predelený na hlavičku, ktorú by automaticky vložil shadertoy a užívateľský kód. Po tejto úprave bude užívateľský kód fungovať ako v shadertoyi, tak aj v narýchlo pripravenom C-kóde.
#version 330 core const vec2 iResolution = vec2(800.0, 480.0); out vec4 outColor; void mainImage(out vec4 fragColor, in vec2 fragCoord); void main() { mainImage(outColor, gl_FragCoord.xy); } // Užívateľský kód void mainImage(out vec4 fragColor, in vec2 fragCoord) { fragColor = vec4(1.0, 0.0, 0.0, 1.0); }
Farebný obdĺžnik
Renderer shader.c vie pridať hlavičku ak v súbore chýba. Nasledujúce príklady budú preto bez hlavičiek a budú sa dať spustiť priamo bez úprav vo webovom rozhraní na shadertoy.com. Nasledujúci kód napríklad vykreslí bielu plochu:
void mainImage(out vec4 fragColor, in vec2 fragCoord) { fragColor = vec4(1.0, 1.0, 1.0, 1.0); }
Funkcia mainImage
má ako vstupný parameter súradnice pixelu - fragCoord
. Súradnicový systém začína vľavo dole v bode x, y = 0, 0
a končí v x, y = šírka - 1, výška - 1
. Parameter nadobúda rovnakú hodnotu, ako globálna premenná gl_FragCoord.
Shadertoy sprístupňuje programátorovi aj rôzne parametre. Medzi ne patrí iResolution
s aktuálnym rozlíšením. Je to trojzložkový vektor, obsahujúci šírku (v pixeloch), výšku (v pixeloch) a konštantu 1. Súradnice majú 3 rozmery, pretože výstupom môže byť aj volumetrická (trojrozmerná) textúra, alebo 360° textúra pre VR okuliare.
Pre tento príklad by boli omnoho vhodnejšie súradnice v rozsahu [0, 1]
namiesto pixelov. Na to stačí predeliť súradnice rozlíšením okna. Nasledujúci príklad prevedie súradnice na rozsah [0, 1]
, vykreslí x
-ovú súradnicu ako červenú farbu a y
-ovú ako modrú.
void mainImage(out vec4 fragColor, in vec2 fragCoord) { // Dvojzložkový vektor so súradnicami v rozsahu [0, 1] vec2 uv = vec2(fragCoord.x / iResolution.x, fragCoord.y / iResolution.y); fragColor = vec4(uv.x, 0.0, uv.y, 1.0); }
Na obrázku môžme vidieť, že úroveň červenej zložky narastá v smere zľava doprava a modrej zdola nahor (pretože počiatok súradnicovej sústavy je vľavo dole).
Do pozornosti dávam spôsob prístupu k prvkom vektoru. Vektory môžu mať maximálne 4 zložky a jednotlivé zložky sa indexujú písmenami xyzw
, rgba
alebo stpq
. Medzi jednotlivými zápismi je len sémantický rozdiel, takže keď sa pracuje s farbami, je lepšie použiť rgba
, ale program sa bude rovnako správať aj keby boli použité písmená xyzw
.
Z viaczložkového vektora sa dá zápisom s bodkou vytiahnuť vektor s menším počtom zložiek, alebo so zložkami v inom poradí. Ak by som chcel získať z trojzložkového vektora iResolution
dvojzložkový stačí napísať iResolution.xy
.
Matematické operácie s vektormi sa vykonávajú pre každú zložku vektora, takže ak a
a b
sú dvojzložkové vektory, potom zápis a / b
znamená vec2(a.x / b.x, a.y / b.y)
. Predchádzajúci kód sa dá zjednodušene zapísať ako:
void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord.xy / iResolution.xy; fragColor = vec4(uv.x, 0.0, uv.y, 1.0); }
V pôvodnom príklade bola zobrazená plocha so súradnicami [-1, 1]
. Body obdĺžnika boli vždy vzdialené o 0,1 jednotiek od okraja. Percentuálne je to 0,1 / (1 - (-1)) = 5 % od okraja.
Najjednoduchšie je upraviť súradnice tak, aby plocha obdĺžnika bola v intervale [0, 1]
a body mimo obdĺžnika môžu byť pokojne mimo interval. Aby bola lepšie viditeľná transformácia súradníc, bola zvolená šírka okraja 2 * 49 %.
const float border = 0.49; // 49 % void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord.xy / iResolution.xy - border) / (1.0 - border * 2.0); fragColor = vec4(uv.x, 0.0, uv.y, 1.0); }
Po transformácii sú súradnice nepatriace obdĺžniku mimo interval. Na pridanie čiernych okrajov stačí malý if
so 4 podmienkami pre pretečenie na ľavom, pravom, dolnom a hornom okraji:
const float border = 0.05; void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord.xy / iResolution.xy - border) / (1.0 - border * 2.0); // Zatemnenie okrajov pomocou podmienky if if (uv.x < 0.0 || uv.y < 0.0 || uv.x > 1.0 || uv.y > 1.0) { fragColor = vec4(0.0); return; } fragColor = vec4(uv.x, 0.0, uv.y, 1.0); }
Grafické karty nemajú moc v láske podmienky. Dal by sa kód prepísať tak, aby neobsahoval podmienky?
Keby len tak existovala funkcia, ktorá vráti 1 ak vstupná hodnota prekročí nejakú hranicu a 0 v opačnom prípade. Keby tak potom existovala matematická operácia zodpovedajúca logickému súčinu (AND) …
Funkcia, ktorá vráti 1.0 ak hodnota prekročí hranicu samozrejme existuje a volá sa step(edge, x).
Teraz stačí zavolať 4x funkciu step tak, aby vrátila 0 pre každý okraj a 1 pre výplň, skombinovať ich operáciou násobenia a nakoniec výsledkom (nazvime ho útlm - attentuation) vynásobiť farbu, takže pre hodnotu 1 bude mať pôvodnú farbu a pre hodnotu 0 bude výsledkom čierna.
const float border = 0.05; void mainImage(out vec4 fragColor, in vec2 fragCoord) { // Výpočet súradníc vec2 uv = (fragCoord.xy / iResolution.xy - border) / (1.0 - border * 2.0); // Čierny rámček na každej strane float borderAttenuation = step(0.0, uv.x) * // 0 pre ľavý okraj step(0.0, uv.y) * // 0 pre dolný okraj step(0.0, 1.0 - uv.x) * // 0 pre pravý okraj step(0.0, 1.0 - uv.y); // 0 pre horný okraj // Výpočet farby vec3 color = vec3(uv.x, 0.0, uv.y); // Vynásobenie útlmom a pridanie alfa kanálu (1.0) fragColor = vec4(color * borderAttenuation, 1.0); }
Zostáva nastaviť farby - červenú v ľavom dolnom rohu, modrú v pravom dolnom, zelené v ľavom hornom a nakoniec modrú v pravom hornom rohu.
Výsledná farba sa vypočíta ako mix jednotlivých zložiek. Najskôr sa mixujú farby pre jednu os (napríklad pre x
sa mixujú farby na hornej a dolnej hrane) a potom sa zmixujú výsledky.
const float border = 0.05; // Farby rohov const vec3 colors[4] = vec3[4]( vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 1.0) ); void mainImage(out vec4 fragColor, in vec2 fragCoord) { // Výpočet súradníc vec2 uv = (fragCoord.xy / iResolution.xy - border) / (1.0 - border * 2.0); // Čierny rámček na každej strane float borderAttenuation = step(0.0, uv.x) * // 0 pre ľavý okraj step(0.0, uv.y) * // 0 pre dolný okraj step(0.0, 1.0 - uv.x) * // 0 pre pravý okraj step(0.0, 1.0 - uv.y); // 0 pre horný okraj // Výpočet farby vec3 color = mix( mix(colors[0], colors[1], uv.x), // Dolná hrana mix(colors[2], colors[3], uv.x), // Horná hrana uv.y); // Vynásobenie útlmom a pridanie alfa kanálu (1.0) fragColor = vec4(color * borderAttenuation, 1.0); }
Výsledný súbor obdlznik.frag sa dá použiť priamo na webe shadertoy.com.
Nasledujúca časť seriálu bude o 2D grafike. Na záver pridávam pár príkladov pokročilých shaderov.
Prílohy
- phong.c (2.2 kB)
- obdlznik.frag (795 Bytes)
- Makefile (346 Bytes)
- shader.c (5.4 kB)
- jednoduchy_obdlznik.c (1.1 kB)
- red.frag (87 Bytes)
Pre pridávanie komentárov sa musíte prihlásiť.
K renderu "Ladybug" je 1. komentár:
ollj, 2022-05-05 This shader challenges the best pc grapic cards, good.
Tož, dosiahol som 0,7 fps na svojej podtaktovanej šunke.
U mňa 7fps s integrovanou GPU, ale mám display so škálovaním 2x takže renderuje v 4x vyššom rozlíšení než bežne.