OpenGL - osvětlení

Tato kapitola navazuje na kapitolu o osvětelní (phong model). Tak, jako v předchozích kapitolách, nenaučíte se tu nic nového o OpenGL. Naučíte se, jak aplikovat dříve popsané matematické principy popisující phongův model.

Kapitola je rozdělena do několika části, které popisují jednotlivé složky phongova modelu. Výsledkem bude ale jeden program, ve kterém budou použity všechny tři typy osvětlení. Příklad bude vycházet z minulé kaptioly, kde se jehlan otáčí pomocí rotační matice.

Ambient

Ambient světlo je nejjednodušší. Osvětluje všechno na scéně stejnou měrou, bez ohledu na pozici světla. Připomeňme si výpočet:

color'= color*lightColor*intenzita

Intenzitu budu uvažovat rovnou jedné, takže ji do výpočtu zahrnovat nebudu. Stačí předat barvu světla do fragment shaderu (jako uniform proměnnou) a vynásobit s ní výslednou barvu.

Ve fragment shaderu si připravím pomocnou proměnnou vLightIntenzity, do které si budu v další části vkládat barvu i difuzního a specular světla. Fragment shader tak prozatím bude vypadat takto:

#version 330 core

uniform vec3 uAmbientColor;

in vec4 vColor;

out vec4 fragColor;

void main(void) {
        vec3 vLightIntenzity = uAmbientColor;

        fragColor = vec4(vColor.rgb * vLightIntenzity, vColor.a);
}

Vypočtená intenzita osvětlení by neměla mít vliv na průhlednost, proto se násobí jen rgb složka barvy a průhlednost se použije taková, jaká je.

Úpravy v main.c už byste mohli zvládnout sami. Definuji si nějakou barvu osvětlení, kterou použiji pro uniform proměnnou uAmbientColor. Navíc ale přidám možnost světlo zapnout a vypnout. Proto si v events.h definuji strukturu, která ponese informaci o tom, která světla chci mít zapnutá. A přidám do funkce key_callback() možnost vypínat a zapínat ambient světlo. Rovnou si vše připravím i pro directional a specular světlo:

struct {
        GLint ambient;
        GLint directional;
        GLint specular;
} useColor;

static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
        if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
                glfwSetWindowShouldClose(window, GLFW_TRUE);
                return;
        }
        if(action != GLFW_PRESS) {
                return;
        }
        if(key == GLFW_KEY_A) {
                useColor.ambient = !useColor.ambient;
        }
        if(key == GLFW_KEY_D) {
                useColor.directional = !useColor.directional;
        }
        if(key == GLFW_KEY_S) {
                useColor.specular = !useColor.specular;
        }
}

Jak vidíte, klávesami a, d a s budete moci vypínat a zapínat jednotlivé složky světla.

Strukturu useColor inicializuji ve funkci main(), někde před voláním funkce ticking(), která se stará o vykreslování a animaci scény:

        //...
        useColor.ambient = 1;
        useColor.directional = 1;
        useColor.specular = 1;
       
        ticking(&program, window);
        //...

Nejdůležitější na konec — inicializace uAmbientColor.

V globální proměnné, struktuře variables, si udělám nový atribut pro uložení odkazu na proměnnou uAmbientColor:

//...
struct {
        GLuint uPMatrixLoc;
        GLuint uMVMatrixLoc;
        GLuint uAmbientColorLoc;
} variables;
//...

Ve funkci initVariables() tento odkaz získám:

//...
void initVariables(Shader *program)
{
        variables.uMVMatrixLoc = program->getUniformLocation(program, "uMVMatrix");
        variables.uPMatrixLoc = program->getUniformLocation(program, "uPMatrix");
        variables.uAmbientColorLoc = program->getUniformLocation(program, "uAmbientColor");
}
//...

Ve funkci draw() pak použiji jako ambient osvětlení barvu podle toho, zda je nebo není ambient osvětlení zapnuté. Pokud není, použije se černá barva (0,0,0), což způsobí, že jehlan celý zčerná (a díky černému pozadí zmizí ve tmě).

        //...
        if (useColor.ambient) {
                glUniform3f(variables.uAmbientColorLoc, 0.2, 0.2, 0.2);
        }
        else {
                glUniform3f(variables.uAmbientColorLoc, 0.0, 0.0, 0.0);
        }
        //...

Kéž by bylo všechno tak jednoduché.

Diffuse

Difůzní světlo už bere v potaz, pod jakým úhlem světlo na plochu dopadá. Čím je úhel větší, tím menší je intenzita světla (a naopak).

První věc, kterou si tedy definuji, bude pozice světla. A aby to nebyla nuda, nechám světlo rotovat kolem jehlanu (v rovině x,y). Druhá věc, kterou budu potřebovat, jsou normály osvětlovaných ploch (trojúhelníků).

Pojďme se nejdřív podívat na výpočet normál.

Výpočet normál

K výpočtu normál se používá cross product (ze dvou vektorů se vypočte třetí, kolmý vektor).

Vertex data jsou v vertex-data.h uloženy dvěma způsoby. Jehlan je uložen jako pole vrcholů trojúhelníků (proměnná vertices), kde každé 3 vrcholy určují jeden trojúhelník. Osy jsou uloženy jako pole bodů (lines), a trojúhelníky jsou určeny indexy (pole lineIndices). Proto jsem napsal dvě funkce, které vypočítávají normály.

Obě funkce vracejí strukturu, která obsahuje pole normál a délku tohoto pole. Pole je alokováno dynamicky, proto jsem do struktury ještě přidal odkaz na funkci, která pole uvolní (vrátí paměť). Takto vypadá hlavičkový soubor normals.h:

#include <GLFW/glfw3.h>

typedef struct ComputedNormals {
        GLfloat * normals;
        size_t length;
        void(*free)(const struct ComputedNormals *cn);
} ComputedNormals;

ComputedNormals createNormals(const GLfloat * vertices, size_t verticesLength, const int * directions);
ComputedNormals createNormalsByIndex(const GLfloat * vertices, size_t verticesLength,
                                const GLuint * indices, size_t indicesLength,
                                const int * directions);

Cross product vypočte kolmý vektor. Jenže je ještě potřeba určit směr tohoto vektoru, aby bylo jasné, zda je strana natočená směrem ke světlu, nebo je od něj odvrácena. Proto mají funkce jako poslední argument pole directions, kde se nulou nebo jedničkou určuje výsledné znaménko vektoru.

Funkce createNormals() vypočítává normály pro první typ dat, funkce createNormalsByIndex() pro druhý. Konec konců, z názvů jejich argumentů je to zřejmé.

Definici těchto funkcí vám ukazovat nebudu. Nejde o nic magického. Inicializuje se struktura ComputeNormals — nastaví se odkaz na funkci free() a získá se potřebné množství paměti pomocí malloc(). Použijí se vrcholy trojúhelníka pro získání dvou vektorů (v každé funkci samozřejmě trohu jinak), a vypočte se cross product (pomocí funkce vec3_mul_cross() z knihovny linmath.h), který se uloží do alokovaného pole pro každý trojúhelník. Předtím se tedy ještě vypočtený vektor normalizuje funkcí vec3_norm(n, n);, aby byla výsledná normála jednotkovým vektorem. Kdyby vás to nějak hlouběji zajímalo, vše najdete v souboru normals.c.

Bohužel, takto vypočítané normály jsou platné jen pro nijak netransformované objekty. Protože se ale můžete jehlan i s osami otáčet, musíte transformovat i příslušné normály, a to tím správným způsobem. K tomu bude potřeba nová transformační matice uNMatrix.

Zprávný způsob výpočtu tranformovaných normál, v závislosti na matici transformace objektu, vypadá zhruba takto:

transformedNormal= mat3transposeinverseuMVMatrix *vertexNormal

Převedeno do jazyka C, ve funkci draw() se vypočítá matice uNMatrix takto:

    //...
    mat4x4 uMVMatrix, uPMatrix;
    mat3x3 uNMatrix;

    //
    // vypocet uMVMatrix
    mat3x3_from_mat4x4(uNMatrix, uMVMatrix);
    mat3x3_invert(uNMatrix, uNMatrix);
    mat3x3_transpose(uNMatrix, uNMatrix);
   
    //...
    glUniformMatrix3fv(variables.uNMatrixLoc, 1, GL_FALSE, (const GLfloat *)uNMatrix);
    //...

Funkce mat3x3_from_mat4x4(), mat3x3_invert() i mat3x3_transpose() jsem pro vás napsal do souboru linmath-extend.h. V souboru linmath.h žádné podobné funkce nejsou. Tento výpočet dá snadno provést ve vertex shaderu pomocí předdefinovaných funkci transpose() a inverse(), ale počítat tu samou matici pro každý vertex není úplně ekologické :-).

Jak vidíte, struktura variables obsahuje další atribut, uNMatrixLoc, který obsahuje odkaz na uniform proměnnou z vertex shaderu uNMatrix. Její inicializaci ve funkci initBuffers() už zvládnete.

Poslední důležitá věc, co se normál týče, je nahrání normál do bufferů ve funkci initBuffers(). Budou tedy potřeba další dva array buffery (rozšíří se proměnná BO[5] na BO[7]).

void initBuffers(Shader *program)
{
        //...
        glBindVertexArray(VAO[0]);
        {
            //...
            const int directions[] = { 1, 1, 1, 1, 1, 1 };
            ComputedNormals normals = createNormals(vertices, sizeof(vertices) / sizeof(vertices[0]), directions);
            glBindBuffer(GL_ARRAY_BUFFER, BO[5]);
            glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*normals.length, normals.normals, GL_STATIC_DRAW);
            glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
            glEnableVertexAttribArray(2);
            normals.free(&normals);
        }

        glBindVertexArray(VAO[1]);
        {
            //...
            ComputedNormals normals = createNormalsByIndex(
                lines, sizeof(lines) / sizeof(lines[0]),
                lineIndices, sizeof(lineIndices) / sizeof(lineIndices[0]),
                NULL
            );
            glBindBuffer(GL_ARRAY_BUFFER, BO[6]);
            glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*normals.length, normals.normals, GL_STATIC_DRAW);
            glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
            glEnableVertexAttribArray(2);
            normals.free(&normals);
            //...
        }
        glBindVertexArray(0); // Unbind VAO
}

Ve výpisu funkce výše jsem z generování VAO[0] vynechal nahrávání vertices a colors a z generování VAO[1] nahrávání lines, linesColors a linesIndices. Oboje zůstává stejné, jako v předchozích kapitolách.

Při volání funkce createNormals() jsou všechny směry stejné. Při volání funkce createNormalsByIndex() také. Tam jsem jako argument použil NULL, protože (obě) fukce v takovém případě předpokládají u všech vypočtených normál kladný směr (mohl jsem použít NULL už u volání první funkce). Důvod, proč mi to tak hezky vyšlo je, že výsledný směr normály se dá ovlivnit i tím v jakém pořadí jsou definovány vrcholy trojúhelníků (zda pro nebo proti směru hodinových ručiček). A já si je definoval tak šikovně, že directions ani nepotřebuji.

Průběh nahrání vertex dat už je vám dobře známý. Nejdříve požádám o vygenerování array bufferu, nahraji do něj data, svážu buffer s proměnnou z vertex dat layout (location = 2) in vec3 vertexNormal; a tuto proměnou označím za aktivní. Nakonec uvolním již nepotřebnou paměť.

Výpočet osvětlení bude probíhat v fragment shaderu. Musí se tak do fragment shaderu poslat jak normála, tak pozice vrcholu.

#version 330 core
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec4 vertexColor;
layout (location = 2) in vec3 vertexNormal;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform mat3 uNMatrix;
 
out vec4 mvPosition;
out vec4 vColor;
out vec3 transformedNormal;

void main(void) {
    mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
    gl_Position = uPMatrix * mvPosition;
    vColor = vertexColor;
    transformedNormal = uNMatrix * vertexNormal;
}
 

Ve vertex shaderu tedy přibila proměnná vertexNormal, a dále uNMatrix, jež se počítala viz výše. Výstupem pro fragment shader bude (transformovaná) pozice vrcholu mvPosition, barva a transformovaná normála transformedNormal.

Uff, nebylo toho s těmi normálami málo. Ale když se nad tím zamyslíte, vlastně se jen nahráli do bufferů vypočtené normály, spočítala se transformační matice normál a tyto dvě věci se použily pro výpočet transforedNormal. Navíc se tedy ještě do fragment shaderu posílá transformovaná pozice vrcholu.

Jak se vypočtené hodnoty z vertex shaderu použijí ve fragment shaderu popíši spolu s difuzním světlem.

Difuzní světlo

Pro výpočet difuzního světla budu ještě potřebovat uniform proměnnou uDirectionalColor určující barvu/intenzitu difuzního světla a uUseDirectional typu boolean, která bude určovat, zda se má nebo nemá difuzní sětlo používat. (Jasně, měl sem je pojmenovat ..Diffuse.. a ne ..Directional... Považujte difuzní a directional světlo za synonyma.)

O (ne)použití ambient světla jsem rozhodoval v main.c, o použití difuzního (a i specular) světla budu rozhodovat v fragment shaderu. Aby byl svět pestřejší :-).

Úpravu struktury varialbes a získání odkazů v initVariables() probíhá jako vždy, takže to tentokrát přeskočím.

Nicméně, zapoměl jsem ještě na jednu důležitou proměnnou. A to pozici světla. Za chvilku se k ní vrátím, teď vám jen prozadím, že pozice světla se bude nacházet v uniform proměnné uLightingPosition.

Jistě zvládnete i rozšíření funkce draw() o následující volání:

        //...
        glUniform1i(variables.uUseDirectionalLoc, useColor.directional);
        glUniform3f(variables.uDirectionalColorLoc, 0.8, 0.8, 0.8);
        //...

A teď to nejdůležitější – fragment shader:

#version 330 core

uniform vec3 uAmbientColor;
uniform vec3 uLightingPosition;
uniform vec3 uDirectionalColor;
uniform bool uUseDirectional;
uniform bool uLightBothSides;
 
in vec4 mvPosition;
in vec4 vColor;
in vec3 transformedNormal;

void main(void) {
        vec3 vLightIntenzity = uAmbientColor;
 
        vec3 normal = normalize(transformedNormal);
        vec3 lightDirection = normalize(uLightingPosition - mvPosition.xyz);

        // directional light
        if(uUseDirectional) {
                float lightIntenzity = dot(normal, lightDirection);
                if(uLightBothSides) {
                        lightIntenzity = max(lightIntenzity, -lightIntenzity);
                } else {
                        lightIntenzity = max(lightIntenzity, 0.0);
                }
                vLightIntenzity += uDirectionalColor * lightIntenzity;
        }
 
        gl_FragColor = vec4(vColor.rgb * vLightIntenzity, vColor.a);
}

První důležitá věc je normalizace transformedNormal. Tato hodnota se z vertex shaderu do fragment shaderu interpoluje ze tři vrcholů, proto výsledná hodnota nemusí být jednotkový vektor. Proto se musí normalizovat.

Dále potřebujeme vektor, který určuje směr od zdroje světla k fragmentu. Směr od jednoho bodu k druhému se snadno spočítá odečtením jejich pozic.

Následuje podmínka, zda použít či nepoužít diffuse světlo. Pokud ano, spočítá se intenzita jakožto kosínus úhlu mezi normálou a směrem světla. Čím je úhel menší, tím je intenzita větší. Pro rovnoběžné vektory je rovna 1, pro kolmé 0, při ještě větším úhlu (trojúhelník je ke zdroji světla zády), dokonce záporná. Proto je tam ještě jedna uniform proměnná, uLightBothSides, která určuje, zda chci osvětlovat i odvrácenou stranu. Pokud ano, otočím záporné znaménko na kladné (aby světlo naopak neztmavovalo). Pokud ne, použiji buď hodnotu lightIntenzity v případě, že je nezáporná, jinak nulu. Nakonec nastavím výsledné osvětlení vLightIntenzity na násobek barvy difuzního světla a lightIntenzity.

Pohyb světla

Aby byla scéna trochu zajímavější, nechám zdroj světla (jeho pozici) kolem jehlanu rotovat. Výpočet pozice světla probíhá v funkci animate():

void animate() {
        float rLight = glfwGetTime() / 1.0;
        x = 2.0*cos(rLight);
        y = 2.0*sin(rLight);
        z = -9.5;
}

Proměnné x,y a z jsou globální, typu GLfloat a ve funkci draw() se použijí takto:

    //...
    glUniform3f(variables.uLightingPositionLoc, x, y, z);
    //...

Funkce animate() se volá před draw(), takže ani není nutné tyto proměnné nikde inicializovat :-).

Specular

Výpočet specular světla je o malinko komplikovanější než výpočet difuzního světla. Ovšem díky tomu, že už jsem popsal všechny kroky spojené s normálami nebo rotací světla v předchozí části, smrskne se popis výpočtu specluar světla jen na kousek kódu v fragment shaderu (kompletní kód):

  1. #version 330 core
  2.  
  3. uniform vec3 uAmbientColor;
  4. uniform vec3 uLightingPosition;
  5. uniform vec3 uDirectionalColor;
  6. uniform bool uLightBothSides;
  7. uniform bool uUseSpecular;
  8. uniform bool uUseDirectional;
  9.  
  10. in vec4 mvPosition;
  11. in vec4 vColor;
  12. in vec3 transformedNormal;
  13.  
  14. void main(void) {
  15.         vec3 vLightIntenzity = uAmbientColor;
  16.  
  17.         vec3 normal = normalize(transformedNormal);
  18.         vec3 lightDirection = normalize(uLightingPosition - mvPosition.xyz);
  19.  
  20.         // directional light
  21.         if(uUseDirectional) {
  22.                 float lightIntenzity = dot(normal, lightDirection);
  23.                 if(uLightBothSides) {
  24.                         lightIntenzity = max(lightIntenzity, -lightIntenzity);
  25.                 } else {
  26.                         lightIntenzity = max(lightIntenzity, 0.0);
  27.                 }
  28.                 vLightIntenzity += uDirectionalColor * lightIntenzity;
  29.         }
  30.  
  31.         // specular light
  32.         if(uUseSpecular) {
  33.                 vec3 eyeDirection = normalize(-mvPosition.xyz);
  34.                 vec3 reflectionDirection = reflect(-lightDirection, normal);
  35.                 float specularLightIntenzity = max(dot(reflectionDirection, eyeDirection),0.0);
  36.                 specularLightIntenzity = pow(specularLightIntenzity,10.0);
  37.                 vLightIntenzity += uDirectionalColor * specularLightIntenzity;
  38.         }
  39.  
  40.         gl_FragColor = vec4(vColor.rgb * vLightIntenzity, vColor.a);
  41. }
  42.  

Díky tomu, že je kamera vždy umístěna v bodě (0,0,0), tak směr od oka k fragmentu se vypočítá jednoduše jako (0,0,0) - mvPosition.xyz a výsledek se znormalizuje.

K výpočtu směru odraženého světla se použije vestavěná funkce reflect().

Intenzita světla se zase spočítá jak kosínus, tentokrát mezi vektorem odraženého svěla a směrem kam se kouká kamera.

Pak se ještě intenzita mocní na desátou, aby intenzita světla se vzrůstajícím kosínem rychle klesala. Čím větší bude mocnina, tím rychleji intenzita klesne a tím ostřeji bude specular světlo působit.

A to je vše. Pokud jste se náhodou někde ztratili, ukáži vám ještě některé důležité části programu komplet.

Kompetní fragment i vertex shader už jsem ukazoval. Funkci initBuffer() více méně také (alespoň to rozšíření o nahrávání normal bufferů).

Includování hlavičkových souborů a globální proměnné v main.c vypadají takto:

//...
#include "linmath-extended.h"
#include "vertex-data.h"
#include "shader.h"
#include "normals.h"
#include "update-angles.h"
#include "events.h"
//...
GLuint VAO[2], BO[7];
struct {
        GLuint uPMatrixLoc;
        GLuint uMVMatrixLoc;
        GLuint uNMatrixLoc;
        GLuint uAmbientColorLoc;
        GLuint uUseDirectionalLoc;
        GLuint uDirectionalColorLoc;
        GLuint uLightingPositionLoc;
        GLuint uUseSpecularLoc;
} variables;
mat4x4 rotationMatrix;
float x, y, z; // light position
//...

Funkce initVariables():

void initVariables(Shader *program)
{
        variables.uMVMatrixLoc = program->getUniformLocation(program, "uMVMatrix");
        variables.uPMatrixLoc = program->getUniformLocation(program, "uPMatrix");
        variables.uNMatrixLoc = program->getUniformLocation(program, "uNMatrix");
        variables.uAmbientColorLoc = program->getUniformLocation(program, "uAmbientColor");
        variables.uUseDirectionalLoc = program->getUniformLocation(program, "uUseDirectional");
        variables.uDirectionalColorLoc = program->getUniformLocation(program, "uDirectionalColor");
        variables.uLightingPositionLoc = program->getUniformLocation(program, "uLightingPosition");

        variables.uUseSpecularLoc = program->getUniformLocation(program, "uUseSpecular");
}

A zde kompletní funkce draw:

void draw(Shader *program, GLFWwindow* window, GLuint *VAO)
{
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);
        glViewport(0, 0, width, height);

        glClearColor(0.f, 0.f, 0.f, 1.f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        program->Use(program);

        mat4x4 uMVMatrix, uPMatrix;
        mat3x3 uNMatrix;

        mat4x4_perspective(uPMatrix, degToRad(45.0), ((GLfloat)width) / height, 1.0, 100.0);
        mat4x4_translate(uMVMatrix, 0.0, 0.0, -10.0);
        mat4x4_mul(uMVMatrix, uMVMatrix, rotationMatrix);

        /*
         * http://learningwebgl.com/blog/?p=684
         * Normal = mat3(transpose(inverse(model))) * normal;
         */
        mat3x3_from_mat4x4(uNMatrix, uMVMatrix);
        mat3x3_invert(uNMatrix, uNMatrix);
        mat3x3_transpose(uNMatrix, uNMatrix);

        glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *)uMVMatrix);
        glUniformMatrix4fv(variables.uPMatrixLoc, 1, GL_FALSE, (const GLfloat *)uPMatrix);
        glUniformMatrix3fv(variables.uNMatrixLoc, 1, GL_FALSE, (const GLfloat *)uNMatrix);

        if (useColor.ambient) {
                glUniform3f(variables.uAmbientColorLoc, 0.2, 0.2, 0.2);
        }
        else {
                glUniform3f(variables.uAmbientColorLoc, 0.0, 0.0, 0.0);
        }
        glUniform1i(variables.uUseDirectionalLoc, useColor.directional);
        glUniform3f(variables.uDirectionalColorLoc, 0.8, 0.8, 0.8);
        glUniform3f(variables.uLightingPositionLoc, x, y, z);
        glUniform1i(variables.uUseSpecularLoc, useColor.specular);

        // Vykresli osy
        glBindVertexArray(VAO[1]);
        glDrawElements(GL_TRIANGLES, sizeof(lineIndices) / sizeof(lineIndices[0]), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        // Vykresli jehlan
        glBindVertexArray(VAO[0]);
        glDrawArrays(GL_TRIANGLES, 0, sizeof(vertices) / sizeof(vertices[0]) / 3);
        glBindVertexArray(0);
}

Toto je výsledek (který uvidíte, pokud máte zapnuté WebGL):



Závěr

Nedivil bych se, kdyby se vám na první přečtení tato kapitola zdála dlouhá a komplikovaná. Ale jediné, o co vní vlastně šlo, bylo, jak spočítat normály, nahrát do bufferů vertex data (souřadnice vrcholů, barvy a normály), spočítat potřebné matice a další proměnné nutné k výpočtu osvětlení a poslat je do uniform proměnných. A pak vše správně použít v shaderech.

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