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í:

  1. Požádej OpenGL o vytvoření vertex shaderu.
  2. Nahraj do shaderu GLSL zdrojový kód.
  3. Požádej OpenGL o kompilaci shaderu
  4. Předchozí kroky zopakuj pro fragment shader
  5. Požádej OpenGL o vytvoření shader programu
  6. Připoj vertex a fragment shader k programu
  7. Slinkuj program dohormady
  8. 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:

void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length);

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:

typedef struct Shader {
    GLuint Program;
    void (*Use) (const struct Shader *shader);
    GLuint (*getAttribLocation) (const struct Shader *shader, const char * variableName);
    GLuint (*getUniformLocation) (const struct Shader *shader, const char * variableName);
} Shader;

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í:

Shader program  = createProgram("soubor/s/verexShader.glsl", "soubor/s/fragmentShader.glsl");
program->Use(&program);

Funkce Use vypadá takto:

void Use(const Shader *shader)
{
    glUseProgram(shader->Program);
}

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.

static GLuint getAttribLocation(const Shader *shader, const char * variableName)
{
    return glGetAttribLocation(shader->Program, variableName);
}

static GLuint getUniformLocation(const Shader *shader, const char * variableName)
{
    return glGetUniformLocation(shader->Program, variableName);
}

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:

Shader program  = createProgram("soubor/s/verexShader.glsl", "soubor/s/fragmentShader.glsl");
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.

  1. #ifndef SHADER_H
  2. #define SHADER_H
  3.  
  4. #include <errno.h>
  5. #include <stdlib.h>
  6. #ifdef _MSC_VER
  7. #include <direct.h> //_getcwd
  8. #define getcwd _getcwd
  9. #else
  10. #include <unistd.h> //getcwd
  11. #endif
  12. #include <stdio.h>
  13. #include <string.h>
  14. #include <GL/glew.h>
  15.  

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ží.

  1. #define SHADER_MAXLEN (sizeof(char)*2048)
  2.  
  3. static GLchar *readfile(const GLchar * path)
  4. {
  5.     FILE *soubor;
  6.     GLchar *text = malloc(SHADER_MAXLEN);
  7.     if ((soubor = fopen(path, "rt")) == NULL) {
  8.         char cCurrentPath[FILENAME_MAX];
  9.         getcwd(cCurrentPath, sizeof(cCurrentPath));
  10.         fprintf(stderr, "%s %i %s: ", __FILE__, __LINE__, cCurrentPath);
  11.         perror(NULL);
  12.         free(text);
  13.         return NULL;
  14.     }
  15.     size_t nacteno;
  16.     if ((nacteno =
  17.          fread(text, 1, SHADER_MAXLEN - 1, soubor)) == SHADER_MAXLEN - 1) {
  18.         fprintf(stderr, "SHADER_MAXLEN rached\n");
  19.     }
  20.     if (!nacteno) {
  21.         perror(NULL);
  22.         fprintf(stderr, "No data readed\n");
  23.         free(text);
  24.         return NULL;
  25.     }
  26.     fclose(soubor);
  27.     text[nacteno] = '\0';
  28.     return text;
  29. }
  30.  

Následuje definice struktury Shader a funkcí, jejichž odkazy se do struktury Shader budou ukládat, jak už jem popisoval výše.

  1. struct Shader;
  2.  
  3. typedef struct Shader {
  4.     GLuint Program;
  5.     void (*Use) (const struct Shader *shader);
  6.     GLuint (*getAttribLocation) (const struct Shader *shader, const char * variableName);
  7.     GLuint (*getUniformLocation) (const struct Shader *shader, const char * variableName);
  8. } Shader;
  9.  
  10. static void Use(const Shader *shader)
  11. {
  12.     glUseProgram(shader->Program);
  13. }
  14.  
  15. static GLuint getAttribLocation(const Shader *shader, const char * variableName)
  16. {
  17.     return glGetAttribLocation(shader->Program, variableName);
  18. }
  19.  
  20. static GLuint getUniformLocation(const Shader *shader, const char * variableName)
  21. {
  22.     return glGetUniformLocation(shader->Program, variableName);
  23. }
  24.  

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.

  1. Shader shader(const GLchar * vertexPath, const GLchar * fragmentPath)
  2. {
  3.     GLuint vertex, fragment;
  4.     GLint success;
  5.     GLchar infoLog[512];
  6.     const GLchar *shaderCodeArr[1];
  7.     Shader shader;
  8.     shader.Use = Use;
  9.     shader.getAttribLocation = getAttribLocation;
  10.     shader.getUniformLocation = getUniformLocation;
  11.     shader.Program = 0;
  12.  
  13.     GLchar * const vShaderCode = readfile(vertexPath);
  14.     GLchar * const fShaderCode = readfile(fragmentPath);
  15.     if (!vShaderCode || !fShaderCode) {
  16.         return shader;
  17.     }

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í.

  1.     /*  Vertex Shader */
  2.     vertex = glCreateShader(GL_VERTEX_SHADER);
  3.     shaderCodeArr[0] = vShaderCode;
  4.     glShaderSource(vertex, 1, shaderCodeArr, NULL);
  5.     glCompileShader(vertex);
  6.     glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
  7.     if (!success) {
  8.         glGetShaderInfoLog(vertex, 512, NULL, infoLog);
  9.         fprintf(stderr, "Chyba kompilace Vertext Shaderu\n%s\n",
  10.                 infoLog);
  11.     }
  12.  

Totéž pro fragmen shader:

  1.     /* Fragment Shader */
  2.     fragment = glCreateShader(GL_FRAGMENT_SHADER);
  3.     shaderCodeArr[0] = fShaderCode;
  4.     glShaderSource(fragment, 1, shaderCodeArr, NULL);
  5.     glCompileShader(fragment);
  6.     glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
  7.     if (!success) {
  8.         glGetShaderInfoLog(fragment, 512, NULL, infoLog);
  9.         fprintf(stderr, "Chyba kompilace fragment shaderu\n%s\n",
  10.                 infoLog);
  11.     }
  12.  

Následuje vytvoření shader programu, připojení vertex a fragment shaderů, slinkování a kontrola úspěchu.

  1.     /* Shader Program */
  2.     shader.Program = glCreateProgram();
  3.     glAttachShader(shader.Program, vertex);
  4.     glAttachShader(shader.Program, fragment);
  5.     glLinkProgram(shader.Program);
  6.     glGetProgramiv(shader.Program, GL_LINK_STATUS, &success);
  7.     if (!success) {
  8.         glGetProgramInfoLog(shader.Program, 512, NULL, infoLog);
  9.         fprintf(stderr, "Chyba linkovani shader programu\n%s\n",
  10.                 infoLog);
  11.         shader.Program = 0;
  12.     }
  13.  

Nakonec zbývá jen uklidit paměť, kterou již nepotřebujeme a vrátit vytvořenou strukturu.

  1.     glDeleteShader(vertex);
  2.     glDeleteShader(fragment);
  3.     free(fShaderCode);
  4.     free(vShaderCode);
  5.     return shader;
  6. }
  7.  
  8. #endif

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() :-).

Komentář Hlášení chyby
Created: 24.12.2016
Last updated: 24.11.2017
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..