Základy programování shaderů v OpenGL část 1 program Petr Felkel, Jaroslav Sloup Katedra počítačové grafiky a interakce, ČVUT FEL místnost KN:E-413 (Karlovo náměstí, budova E) E-mail: felkel@fel.cvut.cz S použitím materiálů Vlastimila Havrana Poslední změna: 14.10.2015
Obsah přednášky Dnes Tok dat v OpenGL se shadery Překlad a sestavení programu Struktura programu typu shader Příště Předávání dat a parametrů do programu typu shader Příklady zdrojových kódů pro shadery
Hrubé schéma OpenGL s programovatelným zobrazovacím řetězcem Vertex Data Pixel Data Vertex Shader Texture Store Primitive Assembly and Rasterization Fragment Shader Fragment operations OpenGL verze 3.1 Každá aplikace definuje dva programy (tzv. shadery): vertex shader (VS) program zapsaný v jazyku GLSL, který zpracovává vrcholy fragment shader (FS) program zapsaný v GLSL zpracovává fragmenty
Shadery a OpenGL Shadery jsou programy, které plně běží na grafické kartě GPU OpenGL na CPU jen organizuje (data a shadery) Shadery pracují paralelně SIMD (single instruction multiple data) VS po vrcholech FS po fragmentech Fragment = kandidát na zobrazený pixel Zda bude nakonec zobrazen rozhodne další část zobrazovacího řetězce Obecně nese více informací, než jen zobrazenou barvu (hloubku, průhlednost, normálu, směr ke světlu, ) 4
Princip zpracování primitiv na pixely VS FS [http://www.lighthouse3d.com/tutorials/glsl-tutorial/pipeline-overview/] 5
Zjednodušený model proudového zpracování Aplikace GPU Data Flow Framebuffer Vrcholy Vrcholy Fragmenty Pixely Vertex Processing Rasterizer Fragment Processing Vertex Shader Fragment Shader
Tok dat v OpenGL pipeline Primitive assembly 7
Tok dat v OpenGL po částech 1/3 OpenGL okopíruje blok dat na GPU (blok vrcholů) [Gortler] Pro každý vrchol se spustí jeden Vertex shader Primitive assembly Příkaz draw vykreslí trojice vrcholů jako trojúhelníky (souřadnice a atributy) VS obdrží souřadnice a atributy vrcholu + Uniformní proměnné (společné více primitivům) VS vypočítá normalizovanou 2D pozici vrcholu v okně (-1,-1) (1,1) + další proměnné měnící se v ploše trojúhelníka (out,varying) Blok primitive assembly počká na tři vrcholy a poskládá z vrcholů zase trojúhelník 8
Tok dat v OpenGL po částech 2/3 [Gortler] Primitive assembly Blok primitive assembly předá trojúhelníky (trojice vrcholů) rasterizátoru Rasterizer Vygeneruje fragmenty trojúhelníka Dále odešle pozici fragmentu a Interpolované hodnoty proměnných (varying) ve vrcholech 9
Tok dat v OpenGL po částech 3/3 [Gortler] FS se spustí pro každý fragment Vypočítá barvu a hloubku fragmentu Odešle je do grafické paměti Fragment se stane pixelem na obrazovce, pokud projde testy (výstřižek, šablona, alfa, hloubka) 10
Shadery Shader je program, který běží na grafické kartě (GPU) Definuje funkci jednoho bloku zobrazovacího řetězce OpenGL používá jazyk podobný jazyku C, který se jmenuje OpenGL Shading Language (GLSL) vstupní atributy / proměnné shader zapsán v GLSL výstupní proměnné 11
Shadery (typické úlohy) vertex shader (VS) pracuje na úrovni vrcholů transformace vrcholu transformace a normalizace normály transformace souřadnic pro textury změna atributů asociovaných s vrcholy (např. některé složky barvy) fragment shader (fs) pracuje na úrovni fragmentů přístup k textuře a další zpracování textury mlha, míchání barev, stínování operace nad interpolovanými hodnotami 12
Moderní programování v OpenGL Moderní programy v OpenGL při vykreslování obrázku v zásadě provádějí pouze tyto čtyři kroky: 1. Přelož všechny shadery a nahraj je na GPU 2. Rezervuj místo pro data ( buffer objects ) a nahraj do nich data 3. Propoj - data v bufferech se vstupními proměnnými (VS) a - výstupní proměnné (VS) se vstupy dalšího bloku (FS) 4. Spusť vykreslování, obrázek je vypočítán na GPU a zobrazen. 13
Každý shader se překládá zvlášť Vytvoř shader Nahraj zdrojový kód pro shader Zkompiluj shader I. glcreateshader() glshadersource() glcompileshader() Tato část se opakuje pro každý shader 14
Programy s použitím shaderu v OpenGL Před použitím musí být shadery přeloženy a sestaveny Překládá se až za běhu aplikace na konkrétním HW OpenGL má proto příkazy pro kompilátor a linker programu Každý program musí obsahovat: vertex shader fragment shader Vytvoř program Vytvoř shader Nahraj zdrojový kód pro shader Zkompiluj shader Připoj shader k programu II. I. glcreateprogram() glcreateshader() glshadersource() glcompileshader() glattachshader() Tato část se opakuje pro každý shader Sestav program gllinkprogram() Použij program gluseprogram() 15
Zápis programu typu shader Program pro shader je text typicky se ukládá ve zvláštních souborech vertex shader přípona.vert,.vs, fragment shader.frag,.fs, lze jej uložit i přímo v kódu jazyka C do textových řetězců 16
Příklady zdrojových kódů Příklad textu programu pro vertex shader Příklad textu programu pro fragment shader #version 400 in vec3 VertexPosition; in vec3 VertexColor; out vec3 Color; void main() { Color = VertexColor; gl_position = vec4(vertexposition, 1.0); } #version 400 in vec3 Color; out vec4 FragColor; void main() { FragColor = vec4(color, 1.0); } 17
Práce se shadery v kódu aplikace I. Kroky při překladu shaderů: 1. Vytvoř objekt typu shader (shader object) 2. Předej mu zdrojový kód programu 3. Přelož zdrojový kód shaderu uvnitř objektu typu shader (compile) 4. Zkontroluj výsledek (status) překladu II. Kroky při sestavení programu (vertex a fragment shader) 5. Vytvoř objekt programu 6. Připoj objekty shaderů k objektu programu 7. Sestav program (link) 8. Zkontroluj status sestaveného programu III. Použití programu v aplikaci 18
I. Překlad programu typu shader I. Kroky při překladu shaderů: 1. Vytvoř objekt typu shader (shader object) 2. Předej mu zdrojový kód programu (jeden textový řetězec nebo pole řetězců) 3. Přelož zdrojový kód shaderu uvnitř objektu typu shader 4. Zkontroluj výsledek (status) překladu shader source shader source shader object glshadersource compile check status 19
1. Vytvoření objektu typu shader Jméno shaderu = kladná hodnota odkazující na vytvořený shader objekt Typ vytvářeného shaderu GL_VERTEX_SHADER nebo GL_FRAGMENT_SHADER GLuint vertshader = glcreateshader( GL_VERTEX_SHADER ); if (vertshader == 0) { // check for error fprintf(stderr, "Error creating vertex shader.\n"); exit(1); } Objekt shaderu udržuje zdrojový kód a přeložený shader. Po sestavení programu (link) lze objekt shaderu smazat Odkazujeme na něj jménem (kladná celočíselná hodnota) 20
2. Nahrání zdrojového kódu z pole řetězců Funkce sloužící k nahrání textů zdrojového kódu shaderu z externího souboru const GLchar * shadercode1 = loadshaderasstring("basic.vert"); const GLchar* codearray[] = {shadercode1, shadercode2}; glshadersource( vertshader, 2, codearray, NULL ); Jméno objektu shader Počet řetězců zdrojových kódů Pole ukazatelů na řetězce zdrojový text shaderu lze poskládat z více částí Pole hodnot Glint délky jednotlivých řetězců zdrojových kódů (NULL indikuje, že řetězce jsou ukončeny nulovým znakem 0x00) 21
2. Nahrání zdroj. kódu z jednoho řetězce Funkce sloužící k nahrání textů zdrojového kódu shaderu z externího souboru const GLchar * shadercode = loadshaderasstring("basic.vert"); glshadersource( vertshader, 1, &shadercode, NULL ); Jméno objektu shader Počet řetězců zdrojových kódů Reference na odkaz na řetězec (celý program je v jediném řetězci) NULL indikuje, že jediný řetězec je ukončen nulovým znakem 0x00 22
3. 4. Překlad a kontrola správnosti glcompileshader(vertshader); GLint result; glgetshaderiv(vertshader, GL_COMPILE_STATUS, &result); if (result == GL_FALSE) { Dotaz na výsledek fprintf( stderr, "Vertex shader compilation failed!\n" ); překladu GLint loglen; glgetshaderiv(vertshader, GL_INFO_LOG_LENGTH, &loglen ); } if ( loglen > 0 ) { char * log = (char *)malloc(loglen); GLsizei written; } glgetshaderinfolog(vertshader, loglen, &written, log); fprintf(stderr, "Shader log:\n%s", log); free(log); Překlad zdrojového kódu v objektu shaderu Dotaz na délku chybové zprávy o překladu Získání textové zprávy o chybě 23
Shader pomocí knihovny pgr Jméno shaderu = kladná hodnota odkazující na vytvořený shader objekt Typ vytvářeného shaderu GL_VERTEX_SHADER nebo GL_FRAGMENT_SHADER GLuint vertshader = pgr::createshaderfromfile ( GL_VERTEX_SHADER, "basic.vert"); if (vertshader == 0) { // check for error fprintf(stderr, "Error creating vertex shader.\n"); exit(1); } Soubor s textem shaderu 24
II. Sestavení objektu typu program se shadery Přeložené shadery musí být sestaveny do shader programu (vertex + fragment shader) Kroky k sestavení shader programu: 5. Vytvoř objekt programu 6. Připoj shadery k objektu typu program (attach) 7. Sestav program (link) 8. Zkontroluj výsledek sestavování programu shader object shader object attach attach shader program object link check status 25
5. Vytvoř objekt typu program Nenulová hodnota k identifikaci vytvořeného program objektu Vytvoř prázdný objekt programu GLuint programhandle = glcreateprogram(); if (programhandle == 0) {// check for error fprintf(stderr, "Error creating program object.\n"); exit(1); } Objekt typu program v OpenGL obsahuje kód všech shaderů použitých v programu (alespoň vertex + fragment shader). 26
6-7. Připoj shader k objektu typu program Připoj vertex shader k programu Připoj fragment shader k programu glattachshader(programhandle, vertshader); glattachshader(programhandle, fragshader); gllinkprogram(programhandle); Sestav spustitelný program typu.gpuexe, který běží na dvou komponentách GPU: programovatelný vertex processor (je-li shader objekt typu GL_VERTEX_SHADER připojen k programu) programovatelný fragment processor (je-li shader objekt typu GL_FRAGMENT_SHADER připojen k programu) 27
8. Zkontroluj stav sestaveného programu Příklad části programu v C++ GLint status; glgetprogramiv(programhandle, GL_LINK_STATUS, &status); if (status == GL_FALSE) { fprintf( stderr, "Failed to link shader program!\n" ); GLint loglen; } glgetprogramiv(programhandle, GL_INFO_LOG_LENGTH, &loglen); if ( loglen > 0 ) { char * log = (char *)malloc(loglen); GLsizei written; glgetprograminfolog(programhandle, loglen, &written, log); fprintf(stderr, "Program log: \n%s", log); free(log); } Zkontroluj zda sestavení proběhlo v pořádku Dotaz na délku chyb. zprávy o sestavení Získej textové zprávy o chybě sestavení programu a vypiš je 28
Program pomocí knihovny pgr Jméno programu = kladná hodnota odkazující na vytvořený program objekt Seznam objektů shaderů const GLuint shaderidarray[] = {vertshader, fragshader, 0}; GLuint programhandle = pgr::createprogram (shaderidarray); if (programhandle == 0) { // check for error fprintf(stderr, "Error creating vertex shader.\n"); exit(1); } 29
III. Nahraj program do zobrazovacího řetězce OpenGL if (status == GL_FALSE) { } else { gluseprogram(programhandle); } Instaluj program s připojenými shadery do zobrazovacího řetězce na GPU Příklad části programu v C++ Jméno objektu programu, který se nyní bude provádět v zobrazovacím řetězci program je připraven pro spuštění a zobrazovaní obrazu na výstup Objektů typu program lze vytvořit několik a poté mezi nimi přepínat příkazem gluseprogram() v rámci běhu jedné aplikace. Programy přitom mohou mít připojené stejné shadery. 30
Struktura zdrojového kódu shaderu Verze GLSL (zde 4.0) vstupní proměnné shaderu (pro VS atributy) výstupní proměnné shaderu Funkce main(), která je spouštěna pro každý vrchol (VS) nebo fragment (FS) Příklad zdrojového programu pro vertex shader: #version 400 in vec3 VertexPosition; in vec3 VertexColor; out vec3 Color; void main() { Color = VertexColor; gl_position = vec4(vertexposition, 1.0); } Vestavěná výstupní proměnná (prefix gl_) 31
Kvalifikátory proměnných Kvalifikátor uložení v paměti in out uniform Význam Vstup atributu (VS), nebo propojení na výstup předchozího bloku (FS - výstup rasterizátoru) Výstupní hodnota shaderu propojí se s dalším blokem (VS s rasterizátorem, FS s blokem testů) Hodnota společná více primitivům. Je přístupná ve všech shaderech (VS i FS). Nastavuje ji OpenGL (v C++) před příkazem draw. Způsob interpolace hodnot v rasterizátoru se nastavuje interpolačním kvalifikátorem (pro out ve VS a pro in FS): Interpolační kvalifikátor smooth flat noperspective význam perspektivně správná interpolace hodnot bez interpolace lineární interpolace 32
Propojení shaderů navzájem Propojení VS a FS (FS dostávají interpolované hodnoty) vertex shader Rasterizace a interpolace fragment shader Příklad zdroj. kódu vertex shader #version 400 in vec3 VertexPosition; in vec3 VertexColor; out vec3 Color; void main() { Jméno a datový typ Color = VertexColor; musí být stejný gl_position = vec4(vertexposition, 1.0); } Příklad zdroj. kódu fragment shader #version 400 in vec3 Color; out vec4 FragColor; void main() { FragColor = vec4(color, 1.0); } 33
Připojení atributů = vstupů vertex shaderu Každá vstupní proměnná vertex shaderu má vlastní celočíselný index (location). Ten lze nastavit: V OpenGL aplikaci před sestavením shader programu příkazem glbindattriblocation() glbindattriblocation(programhandle, 0, VertexPosition ); Index (location) atributu Jméno atributu V shaderu pomocí kvalifikátoru layout: layout (location = 0) in vec3 VertexPosition; Pokud hodnotu indexu (location) explicitně nenastavíme, je index vygenerován při sestavení programu automaticky 34
Připojení atributů = vstupů vertex shaderu Pokud je index atributu přiřazen automaticky linkerem při sestavení programu, lze se na jeho hodnotu dotázat (po zkompilování a slinkování!) příkazem: glgetattriblocation(programhandle, VertexPosition ); vrací index atributu (=location, typ GLint) jméno atributu Index atributu (location) nabývá hodnot 0,1,2,,, pokud byl atribut ve VS aktivně použit 1 pokud nebyl v textu VS nalezen, nebo v nebyl v shaderu použit a byl proto vypuštěn 35
Atributy vrcholu a proměnné uniform UNIFORM (společné proměnné) ATRIBUTY vrcholu 0 1 n vertex shader 0 1 n fragment shader GPU attriblocation (index vstupu VS) uniformlocation (index společné proměnné uniform) 36
Předání dat do shaderu pomocí proměnných s kvalifikátorem uniform uniform kvalifikátor pro proměnné Vhodný pro hodnoty, které se mění málo (např. jsou stejné pro celý objekt, třeba transformační matice) Hodnoty se nastavují v OpenGL API, v C++ Vždy jsou vždy použity jako vstupní proměnné V kódu shaderu je nelze měnit (read-only) Deklarace proměnné s kvalifikátorem uniform a jménem RotationMatrix (matice k transformaci vrcholů) Příklad zdrojového kódu pro Vertex shader #version 400 in vec3 VertexPosition; in vec3 VertexColor; out vec3 Color; uniform mat4 RotationMatrix; void main() { Color = VertexColor; gl_position = RotationMatrix* vec4(vertexposition, 1.0); } 37
Předání dat do shaderu pomocí proměnných s kvalifikátorem uniform (pokr.) Příklad části programu v C++ // clear the color buffer glclear(gl_color_buffer_bit); // use glm (part of -framework) function to create a transformation matrix mat4 rotationmatrix = glm::rotate(mat4(1.0f), angle, vec3(0.0f,0.0f,1.0f)); // find location of a uniform variable GLuint location = glgetuniformlocation(programhandle, "RotationMatrix"); if (location >= 0) { // location == -1 if uniform is not active } // assign a value to the uniform variable gluniformmatrix4fv(location, 1, GL_FALSE, &rotationmatrix[0][0]); // bind to the vertex array object and call gldrawarrays to initiate rendering glbindvertexarray(vaohandle); gldrawarrays(gl_triangles, 0, 3 ); 38
Varianty příkazu gluniform gluniform{1 2 3 4}{f i ui} 1 pro float, int, unsigned int, bool; 2 pro vec2, ivec2, uvec2, bvec2, atd. f pro float (float, vec2, vec3, vec4, nebo jejich pole) i pro int (int, ivec2, ivec3, ivec4, nebo jejich pole) ui pro unsigned int, uvec2, uvec3, uvec4, nebo jejich pole) i, ui or f pro bool (bool, bvec2, bvec3, bvec4, nebo jejich pole) false pro 0 a 0.0f a true pro nenulové hodnoty 39
Příklady příkazu gluniform Jednotlivé hodnoty 1 až 4x float gluniform1f(location, 7.5f); gluniform4f(location, 0.5f, 0.5f, 0.2f, 1.0f); Pole hodnot int pole[count] pole o délce count gluniform1iv(location, count, pole); Matice či pole matic gluniformmatrix4fv(location, 1, GL_FALSE, &rotationmatrix[0][0]); 1 matice Nebude se transponovat 40
Typ datových proměnných základní datové typy: float, double, bool, int, uint stejně jako v jazyce C vektory s 2, 3 nebo 4 složkami: vec{2,3,4} {2,3,4}-složkový vektor s jednoduchou přesností (float) dvec{2,3,4} {2,3,4}-složkový vektor s dvojitou přesností (double) bvec{2,3,4} {2,3,4}-složkový vektor s hodnotami bool ivec{2,3,4} {2,3,4}-složkový vektor s celočíselnými hodnotami čtvercové matrice: mat2, dmat2 matice 2 2 s jednoduchou či dvojnásobnou přesností mat3, dmat3 matice 3 3 dtto v pohyblivé řádové čárce mat4, dmat4 matice 4 4 dtto 41
Deklarace a inicializace proměnných int a = 2; // a is initialized with 2 bool d = true; // d is true float b = 2; // incorrect, no automatic type casting supported float c = float(a); // correct. c is 2.0 vec3 f; // declaring f as a vec3 vec3 g = vec3(1.0, 2.0, 3.0); mat4 m = mat4(1.0) // initializing the diagonal of the matrix with 1.0 mat2 k = mat2(1.0,0.0,1.0,0.0); // all elements are specified float frequencies[3] = float[](3.4, 4.2, 5.0); // initialize array of floats struct dirlight { // type definition vec3 direction; vec3 color; }; dirlight d1; dirlight d2 = dirlight(vec3(1.0,1.0,0.0),vec3(0.8,0.8,0.4)); // structure initialization 42
Přístup ke složkám vektorových proměnných vec3 pos = vec3(1.0, 2.0, 3.0); float f = 1.2; double c = 2.0LF; // float and double constants pos.x // is legal, the same as pos.r pos.xy // is legal, the same as pos.rg pos.w // is illegal f.x // is legal, the same as f.r or f.s f.y // is illegal const int L = pos.length(); // number of components in vector // the order of the components can be different to swizzle them, or replicated vec3 swiz= pos.zyx; // swizzled = (3.0, 2.0, 1.0) vec4 dup = pos.xxyy; // duplicated = (1.0, 1.0, 2.0, 2.0) vec4 dup = f.xxxx; // duplicated = (1.2, 1.2, 1.2, 1.2) // accessing matrix components mat4 m; m[1] = vec4(2.0); // sets the second column to all 2.0 m[2][3] = 2.0; // sets the 4th element of the third column to 2.0 43
Vestavěné funkce v GLSL trigonometrické funkce radians(), degrees(), sin(), cos(), atan(), exponenciální funkce pow(), exp(), log(), sqrt(), geometrické funkce pro vektory dot(), cross(), length(), normalize(), reflect() maticové funkce transpose(), determinant(), inverse() funkce pracující s vektory relačně po složkách: bvec equal(vec x, vec y), bvec greaterthan(vec x, vec y) běžné algebraické funkce abs(), floor(), min(), max(), mix(), 44
Uživatelské funkce v rámci shaderu Definice funkce může být přetížena pokud jsou parametry funkce rozdílné Chování rekurzivních funkcí není definováno in vec3 VertexPosition; in float VertexStartTime; Uživatelsky definovaná funkce s identifikátorem funkce12 Vestavěná výstupní proměnná (prefix gl_) out float Transp; uniform float ParticleLifetime; Uniform mat4 MVP; void funkce12(float Time) { float age = Time - VertexStartTime; Transp = 0.0; if(time >= VertexStartTime) Transp = 1.0 - age / ParticleLifetime; } gl_position = MVP * vec4(vertexposition, 1.0); 45
Příště Předávání dat z aplikace a mezi shadery 46
Zajímavé odkazy David Wolff: OpenGL 4.0 Shading Language Cookbook. Packt Publishing, 2011, ISBN 978-1-849514-76-7. Richard S. Wright, Nicholas Haemel, Graham Sellers, Benjamin Lipchak: OpenGL SuperBible: Comprehensive Tutorial and Reference. 5th ed., Addison-Wesley Professional, 2010, ISBN 0-321-71261-7. Ed Angel, Dave Shreiner: An Introduction to Modern OpenGL Programming, SIGGRAPH 2011 tutorial, http://www.daveshreiner.com/siggraph/s11/modern- OpenGL.pptx Joe Groff. An intro to modern OpenGL. Updated July 14, 2010 http://duriansoftware.com/joe/an-intro-to-modern-opengl.-table-of-contents.html Vertex Array Object na OpenGL Wiki: http://www.opengl.org/wiki/vao Vertex Specification na OpenGL Wiki: http://www.opengl.org/wiki/vertex_specification 47