OpenGL - shadery
V předchozí kapitole jste se naučili používat shadery — programovat je pomocí GLSL. Teď se naučíte, jak zdrojové kódy GLSL použít, to jest, jak GLSL pro vertex a fragment shader přeložit, slinkovat do jednoho programu a spustit.
Shader program
Začnu zase trochu od konce. A to tím, jak vytvořit a použít shader program. Veškerý kód najdete v souboru shader.h.
Shader program se skládá (minimálně) z vertex shaderu a fragment shaderu. Postup vytvoření programu je následující:
- Požádej OpenGL o vytvoření vertex shaderu.
- Nahraj do shaderu GLSL zdrojový kód.
- Požádej OpenGL o kompilaci shaderu
- Předchozí kroky zopakuj pro fragment shader
- Požádej OpenGL o vytvoření shader programu
- Připoj vertex a fragment shader k programu
- Slinkuj program dohormady
- Vytvořené struktury pro vertex a fragment shader už nejsou potřeba, můžeš je smazat
Zdrojový kód GLSL se předává jako třetí argument do následující funkce:
Jde o pole řetězců, ze kterého se zdrojový kód skládá. Já budu ukládat zdrojové
kódy do souboru, ze kterého ho načtu do jednoho řetězce, takže budu používat
pole s jednou položkou (GLchar *shaderCodeArr[1]
).
Druhý argument je počet položek pole, v mém případě tedy 1 a poslední argument určuje délku stringů v poli. Používá se, pokud není pole ukončeno nulovým znakem. Což já budu mít, takže za poslední argument můžu dosadit NULL.
První argument je ID shaderu, které získám tím, že požádám OpenGL
o vytvoření shaderu. A to funkcí glCreateShader()
.
Pátým krokem, „Požádej OpenGL o vytvoření shader programu“,
prozměnu získám ID programu (glCreateProgram()
). Toto ID
je pak jediné, co potřebuji k použití shader programu.
Celý výše popsaný proces jsem zapouzdřil v souboru shader.h.
Obsahuje funkci shader()
, která vrací strukturu Shader
,
která vypadá takto:
Struktura obsahuje ID programu Program
a dále nějaké ukazatele na funkce,
které mají jako první argument odkaz na tutéž strukturu.
Shader programů můžete mít a používat více. Pro každý si tedy můžete vytvořit
tuto strukturu. Zavoláním funkce Use()
řeknete, že chcete používat
zrovna tento shader program. Podívejte se na ukázku použití:
program->Use(&program);
Funkce Use
vypadá takto:
Pouze se zavolá OpenGL funkce glUseProgram()
, která očekává
jako argument ID programu (získané dříve voláním glCreateProgram()
).
Teď si asi říkáte, proč tak složitě? Nestačilo by, aby funkce createProgram()
vracelo jen ID programu a pak se zavolalo glUseProgram(Program)
?
Stačilo. Samozřejmě že stačilo. Ale pokud si vytvořím funkci Use()
,
mohu do ní přidat podle potřeby nějaké další volání (kvůli ladění programu atp.).
OK. Proč ale ukládat na tuto funkci odkaz do nějaké struktury (Shader)?
Protože si pak mohu vytvořit jinou funkci na vytvoření struktury Shader,
která uloží do dokazu Use jinou funkci. Například si vytvořím createDebugProgram()
,
který do atributů Use, getAttribLocation a getUniformLocation
uloží funkce, které mají navíc nějaký ladící tentononc. Nebo se může funkce
createProgram()
rozhodnovat dynamicky za běhu programu,
kterou verzi funkcí do struktury přidá.
Takovýto způsob programování není nijak OpenGL specifický. Je to ale oblíbený
způsob, jak umožnit dynamický výběr funkcí (za běhu programu), nebo jak snadno
vyměnit jednu sadu funkcí za druhou změnou jen na jednom místě v kódu (místo createShader()
se zavolá createDebugShader()
a všechny 3 funkce v celém programu jsou zaměněny).
Teď už jen zbývá dovysvětlit, k čemu jsou funkce getAttribLocation
a getUniformLocation. Obě funkce získávají odkaz (reprezentovaný číslem typu GLuint)
na vstupní proměnné shader programu. První funkce na proměnné označené kavalifikátorem
attribute
(nebo in
v novější verzi), druhá
na proměnné uniform
.
Obě funkce vypadají velmi jednoduše. Jako první argument očekávají odkaz na strukturu Shader, jako druhý argument jméno proměnné z GLSL zdrojového kódu.
První funkci potřebujete jen v případě, že použijete starší verzi
GLSL, kde se používá pro vertex data kvalifikátor attribute
.
V novější verzi můžete použít zápis layout (location = 0) in vec3 aVertexPosition;
,
který říká, že proměnná aVertexPosition bude mít odkaz/lokaci/id (nazvěte to jak chcete) roven 0.
Tím pádem tento identifikátor znáte a nemusíte ho získávat voláním glGetAttribLocation()
.
Použití pak vypadá nějak takto:
program->Use(&program);
...
GLuint uMVmatrix = program->getUniformLocation(&program, "uMVMatrix");
// nahraj data do uMVmatrix pomocí nějaké gl funkce
Zdrojový kód shader.h
Teď je ten správný okamžik ukázat vám kompletní zdrojový kód shader.h. Projdu jej krok za krokem.
Zdroják začíná includováním hlavičkových souborů. Kvůli Visual Studiu
je tam předefinování funkce _getcwd
na getcwd
.
getcwd
se v VS použít nedá.
Includuje se i knihovna <GL/glew.h>, která definuje datové typy
GLuint
atp. a taky includuje hlavičkové soubory pro OpenGL
funkce.
- #ifndef SHADER_H
- #define SHADER_H
- #include <errno.h>
- #include <stdlib.h>
- #ifdef _MSC_VER
- #include <direct.h> //_getcwd
- #define getcwd _getcwd
- #else
- #include <unistd.h> //getcwd
- #endif
- #include <stdio.h>
- #include <string.h>
- #include <GL/glew.h>
Dále následuje pomocná funkce readfile()
, která
načte obsah souboru do paměti a vrátí ukazatel na tuto (dynamicky
alokovanou paměť). Je napsaná tak, že max. velikost souboru je 2048 znaků.
(Můžete ji za domácí úkol přepsat tak, aby byla velikost neomezená.)
V případě neúspěchu vrací NULL.
Zdrojový kód GLSL pro vertex shader ukládám do souboru s příponou .vs, pro fragment shader s příponou .fs. Na jménu souboru ale opravdu nezáleží.
Následuje definice struktury Shader
a funkcí, jejichž odkazy
se do struktury Shader budou ukládat, jak už jem popisoval výše.
- struct Shader;
- GLuint Program;
- } Shader;
- {
- glUseProgram(shader->Program);
- }
- {
- }
- {
- }
A teď konečně funkce createProgram()shader()
,
která načte zdrojové kódy vertex a fragment shaderů ze souborů,
zkompiluje je, vytvoří shader program,
vykonstruuje Shader strukturu a vrátí ji. Provede oněch 8 kroků,
které jsem na začátku popisoval.
Na začátek si vytvořím nějaké pomocné proměnné, inicializuji strukturu Shader
odkazy na funkce a id programu nastavím na 0 (neúspěch funkce shader()
poznáte tak, že Program
zůstane 0.) Pak ještě načtu obsah souborů,
ve kterých je uložen zdrojový kód GLSL.
- {
- GLuint vertex, fragment;
- GLint success;
- GLchar infoLog[512];
- Shader shader;
- shader.Use = Use;
- shader.getAttribLocation = getAttribLocation;
- shader.getUniformLocation = getUniformLocation;
- shader.Program = 0;
- return shader;
- }
Dalším krokem je vytvoření vertex shaderu, nahrání zdrojového kódu a jeho kompilace. Navíc ještě zjistím status kompilace a v případě neúspěchu vypíšu chybové hlášení.
- /* Vertex Shader */
- vertex = glCreateShader(GL_VERTEX_SHADER);
- shaderCodeArr[0] = vShaderCode;
- glShaderSource(vertex, 1, shaderCodeArr, NULL);
- glCompileShader(vertex);
- glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
- glGetShaderInfoLog(vertex, 512, NULL, infoLog);
- infoLog);
- }
Totéž pro fragmen shader:
- /* Fragment Shader */
- fragment = glCreateShader(GL_FRAGMENT_SHADER);
- shaderCodeArr[0] = fShaderCode;
- glShaderSource(fragment, 1, shaderCodeArr, NULL);
- glCompileShader(fragment);
- glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
- glGetShaderInfoLog(fragment, 512, NULL, infoLog);
- infoLog);
- }
Následuje vytvoření shader programu, připojení vertex a fragment shaderů, slinkování a kontrola úspěchu.
- /* Shader Program */
- shader.Program = glCreateProgram();
- glAttachShader(shader.Program, vertex);
- glAttachShader(shader.Program, fragment);
- glLinkProgram(shader.Program);
- glGetProgramiv(shader.Program, GL_LINK_STATUS, &success);
- glGetProgramInfoLog(shader.Program, 512, NULL, infoLog);
- infoLog);
- shader.Program = 0;
- }
Nakonec zbývá jen uklidit paměť, kterou již nepotřebujeme a vrátit vytvořenou strukturu.
A to je vše. Praktické použití struktury Shader
uvidíte v příští kapitole. Soubor shader.h budu používat ve všech
následujících příkladech a už se nebude nijak měnit.
Jediné, co z této kapitoly budete prakticky potřebovat pro zbytek
tutoriálu, je umět zavolat funkci shader()
:-).