OpenGL - First Person Shooter

Zatím jste se naučili vytvořit scénu a otáčet objekty v ní, nebo otáčet celou scénou. V této kapitole se naučíte, jak se v prostoru pohybovat ve stylu FPS. Tedy jako byste v prostoru chodili. Kromě trochy matematiky se tu vlastně nic nového nenaučíte. Výsledkem bude struktura Camera, kterou budu používat i v dalších tutoriálech pro pohyb.

Popis struktury Camera

Nejdříve vám ukáži, co struktura Camera obsahuje a k čemu jsou její atributy dobré. Ještě více nejdřív ukáži definici výčtového typu Camera_Movement.

typedef enum {
        FORWARD,
        BACKWARD,
        LEFT,
        RIGHT
} Camera_Movement;

Pomocí klávesnice se budete moci v prostoru pohybovat vpřed a vzad a také vlevo a vpravo (úkrokem na jednu nebo druhou stranu, ne otáčení. Otáčet se bude myší).

Takto vypadá struktura Camera:

typedef struct Camera {
    // Camera Attributes
    vec3 Position;
    vec3 Front;
    vec3 Up;
    vec3 Right;
    vec3 WorldUp;
    // Eular Angles
    GLfloat Yaw;
    GLfloat Pitch;
    // Camera options
    GLfloat MovementSpeed;
    GLfloat MouseSensitivity;
    GLfloat Zoom;
    // Camera methods
    void (*GetViewMatrix)(Camera *camera, mat4x4 view);
    void (*ProcessKeyboard)(Camera *camera, Camera_Movement direction, GLfloat deltaTime);
    void (*ProcessMouseMovement)(Camera *camera, GLfloat xoffset, GLfloat yoffset);
    void (*ProcessMouseScroll)(Camera *camera, GLfloat yoffset);
} Camera;

Hlavní práci odvede funkce, vám již známá, lookAt(). Ta potřebuje jako argument pozici kamery Position. Dále pozici místa, na které se chcete dívat. Vektor Front bude určovat směr pohledu a bude se měnit pohybem myši. Přičtením k pozici Position, která se bude měnit pomocí kláves W,S,A a D, se získá bod určující směr pohledu. Vektor Up je poslední vektor, který potřebuje funkce lookAt(). K jeho výpočtu se bude používat jednak vektor Front, ale také vektor Right a vektor WorldUp, který určuje „směr nahoru“ celého světa (zatímco Up určuje „směr nahoru“ pro kameru, který se mění v závislosti na tom, kam se kamera natočí).

Yaw a Pitch jsou úhly otočení dle osy y a x. Otočení podle osy x určuje, jak moc se díváte nahoru nebo dolů, podle y určuje otočení doleva nebo doprava.

MoveSpeed určuje hodnotu, kterou se bude násobit uplynulý čas, čímž se určí vzdálenost, kterou se ujde při stisknuté klávese pro pohyb.

MouseSensitivity bude obdobně ovlivňovat rychlost reakcí myši.

Zoom se bude používat v perspektivě k určění úhlu fov. Jeho velikost určuje, jak se budou zdát věci daleko nebo blízko. Měnit se bude skrolováním kolečka myši (nebo čím na svém počítači skrolujete).

Poslední čtyři argumenty jsou odkazy na funkce. První, GetViewMatrix, se bude používat pro získání transformační matice (k tomu vlastně celá struktura Camera je vytvořená). Zbylé funkce jsou pro obsluhu událostí eventů myši a klávesnice.

Použití

Než popíši jak struktura Camera funguje, ukáži vám, jak se použije.

Celá strukura, včetně funkcí, je definována v souboru camera.h. Prvním krokem je tedy includování tohoto souboru (v main.c).

Struktura se vytvoří a inicializuje funkcí camera3f(), která dostává jako své argumenty úvodní pozici kamery.

Camera camera3f(GLfloat posX, GLfloat posY, GLfloat posZ);

Funkce nastaví nějaké defaultní hodnoty. Můžete třeba změnit rychlost pohybu:

    camera = camera3f(0.0f, 0.0f, 3.0f);
    camera.MovementSpeed = 500.f;

Proměnnou camera jsem definoval jako globální proměnnou v souboru events.h. To, jak víte, není úplně nejlepší způsob programování, ale už se stalo :-).

Dalším krokem je zavolání funkce ProcessKeyboard, pokud je stisknutá nějaká (důležitá) klávesa. To je ale trošku komplikované, protože existují události klávesnice pro stisk a uvolnění klávesnice, ale ne pro „klávesa je stisknuta“. Proto jsem vytvořil v events.h pole static bool keys[1024];, do kterého ukládám true, když uživatel klávesu stiskne a false, když ji zase uvolní.

// events.h
static Camera camera;
static bool keys[1024];

static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
        if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
                glfwSetWindowShouldClose(window, GL_TRUE);
        }
        if (key >= 0 && key < 1024)
        {
                if (action == GLFW_PRESS)
                        keys[key] = true;
                else if (action == GLFW_RELEASE)
                        keys[key] = false;
        }
}

Pak už jen ve funkci animate() zavolám ProcessKeyboard, pokud je příslušná klávesa stisknuta:

    void animate(void) {
        // Set frame time
        GLfloat currentFrame = (GLfloat) glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;
        // Camera controls
        if (keys[GLFW_KEY_W])
                camera.ProcessKeyboard(&camera, FORWARD, deltaTime);
        if (keys[GLFW_KEY_S])
                camera.ProcessKeyboard(&camera, BACKWARD, deltaTime);
        if (keys[GLFW_KEY_A])
                camera.ProcessKeyboard(&camera, LEFT, deltaTime);
        if (keys[GLFW_KEY_D])
                camera.ProcessKeyboard(&camera, RIGHT, deltaTime);
}

Posledním argumentem funkce ProcessKeyboard je deltaTime. To je čas, kterým se násobí MovementSpeed a tím se určí, o kolik se v prostoru pohnete. Pro výpočet deltaTime se používá funkce glfwGetTime(), která vrací počet milisekund od spuštění programu. Hodnota se ukládá do globální proměnné lastFrame pro další použití.

Funkce animate() se spouští od prvního okamžiku stále dokola, takže se deltaTime stále updatuje, bez ohledu na to, zda je nějaká klávesa stisknuta. Proto je deltaTime vždy malá hodnota (zlomek sekundy).

V souboru events.h jsou i funkce pro obsluhu události myší. Začnu tou jednodušší, a to je obsluha scroll eventu:

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
        camera.ProcessMouseScroll(&camera, yoffset);
}

Ta jen zavolá funkci, jejíž odkaz je v atributu ProcessMouseScroll. Předá jí informaci o tom, o kolik jste odscrollovali (může jít o kladné i záporné číslo).

Funkce se musí samozřejmě registrovat (třeba ve funkci main()):

    //...
    glfwSetScrollCallback(window, scroll_callback);
    //...

Funkce pro obsluhu události pohybu myši volá funkci kamery ProcessMouseMovement a předává jí informaci o tom, o kolik se myš pohla směrem v osách x a y od poslední polohy. Poslední poloha se ukládá do globálních proměnných lastX a lastY a při prvním spuštění se inicializují na aktuální polohu, aby nedocházelo k uskočení v okamžiku prvního najetí myši nad okno:

static GLfloat lastX = 400, lastY = 300;
static bool firstMouse = true;
static void cursor_position_callback(GLFWwindow* window, double xpos, double ypos)
{
        if (firstMouse)
        {
                lastX = (GLfloat)xpos;
                lastY = (GLfloat)ypos;
                firstMouse = false;
        }

        GLfloat xoffset = (GLfloat)xpos - lastX;
        // Y souřadnice jde v opačném směru
        GLfloat yoffset = (GLfloat)lastY - ypos;

        lastX = (GLfloat)xpos;
        lastY = (GLfloat)ypos;

        camera.ProcessMouseMovement(&camera, xoffset, yoffset);
}

Posledním krokem je získání transformační matice a použití Zoom v perspektivě. To se udělá ve funkci draw().

        //...
        mat4x4_identity(uMVMatrix);
        mat4x4_identity(uPMatrix);
        mat4x4_perspective(uPMatrix, degToRad(camera.Zoom), ((GLfloat)width) / height, 1.0, 20000.0);

        camera.GetViewMatrix(&camera, view);
        mat4x4_mul(uMVMatrix, uMVMatrix, view);

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

Poslední argument pro perspektivu jsem nastavil na ohromných 20 000. To proto, protože v příkladu použiji cubemap se stranou 1000, ze které budete moci odkráčet. Tak aby ji perspektiva „neodstřihla“ příliš brzo a krychle s tapetou byla vidět i z venčí.

Jak Camera funguje

Teď už víte, jak strukturu Camera používat. To by vám možná mohlo stačit, ale kvůli tomu tady vy určitě nejste. Vy chcete vědět, jak to celé funguje. Připravte se na trochu matematiky.

Funkce camera3f() vrací strukturu Camera inicializovanou nějakými defaultními hodnotami:

// Default camera values
static const GLfloat YAW = -90.0f;
static const GLfloat PITCH = 0.0f;
static const GLfloat SPEED = 3.0f;
static const GLfloat SENSITIVTY = 0.25f;
static const GLfloat ZOOM = 45.0f;

Camera camera3f(GLfloat posX, GLfloat posY, GLfloat posZ) {
    Camera camera;
    camera.Position[0] = posX;
    camera.Position[1] = posY;
    camera.Position[2] = posZ;
    camera.WorldUp[0] = 0.f;
    camera.WorldUp[1] = 1.f;
    camera.WorldUp[2] = 0.f;
    camera.Yaw = YAW;
    camera.Pitch = PITCH;
    camera.Front[0] = 0.f;
    camera.Front[1] = 0.f;
    camera.Front[2] = -1.f;
    camera.MovementSpeed = SPEED;
    camera.MouseSensitivity = SENSITIVTY;
    camera.Zoom = ZOOM;
    updateCameraVectors(&camera);
    camera.GetViewMatrix = GetViewMatrix;
    camera.ProcessKeyboard = ProcessKeyboard;
    camera.ProcessMouseMovement = ProcessMouseMovement;
    camera.ProcessMouseScroll = ProcessMouseScroll;
    return camera;
}

Vektor WorldUp se už nikdy nijak měnit nebude. Přiřazení ostatních hodnot asi není třeba vysvětlovat. Úhly Yaw a Pitch jsou nastaveny tak, že se díváte ve směru osy z.

Úhel Pitch určuje úhel otočení kolem osy x (tj. jako když kývete hlavou nahoru a dolů) a úhel Yaw kolem osy y (jako když kroutíte hlavou z leva do prava).

V této funkci se volá jiná pomocná funkce, updateCameraVectors(). Ta se stará o nastavení atributů Front, Right a Up v závislosti na úhlech Yaw a Pitch. Tato funkce se bude volat pokaždé, když se některý z těchto úhlů změní.

static void updateCameraVectors(Camera *camera)
{
    // Calculate the new Front vector
    vec3 front;
    front[0] = cos(radians(camera->Yaw)) * cos(radians(camera->Pitch)); // x
    front[1] = sin(radians(camera->Pitch));                             // y
    front[2] = sin(radians(camera->Yaw)) * cos(radians(camera->Pitch)); // z
    vec3_norm(camera->Front, front);

    // Also re-calculate the Right and Up vector
    vec3 cross;
    vec3_mul_cross(cross, camera->Front, camera->WorldUp);
    vec3_norm(camera->Right, cross);  // Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
    vec3_mul_cross(cross, camera->Right, camera->Front);
    vec3_norm(camera->Up, cross);
}

Tato funkce se také volá pokaždé, když pohnete myší.

Výpočet Front se provádí na základě úhlů Yaw a Pitch. K tomuto výpočtu se vrátím o kousek níže. Zatím ho považujte za kouzlo.

Right se vypočítá jako cross produkt WorldUp vektoru a (normalizovaného) Front vektoru. Tyto dva vektory nemohou být nikdy rovnoběžné (což by znemožnilo výpočet cross produktu), protože úhel Pitch lze nastavit jen v intervalu <89.0, -89.0> (viz dále).

Obdobně se vypočte Up, viz zdrojový kód.

Euler angles

Výpočet Front vektoru, tedy směru, kterým se kamera dívá, probíhá na základě tzv. Euler angles.

Buď můžete brát výpočet tak, jak je ve zdrojovém kódu (jako kouzlo), nebo se zkuste zamyslet nad následujícími obrázky:

Your Browser doesnt support SVG images.
Your Browser doesnt support SVG images.

První obrázek ukazuje, jak se ovlivňuje hodnota y a z při „kývání hlavou“ nahoru a dolů. Druhý obrázek ovlivnění x a z při „otáčení hlavou“ z leva do prava.

Jde o tyto čtyři rovnice:

  1. x = cos(yaw)
  2. y = sin(pitch)
  3. z = cos(pitch)
  4. z = sin(yaw)

Jak vidíte, hodnota z je ovlivněna jak kýváním, tak otáčením hlavy. Na prvním obrázku máte hlavu otočenou o (-)90 stupňů, tj ve směru osy z. Osu Z takto kýváním hlavy ovlivníte nejvíce. Naopak, pokud bude yaw = 0, (hlava otočená doprava), pak kýváním zetovou souřadnici neovlivníte. Bude 0. (Díváte se ve směru osy x).

Tím se snažím říct, že výpočet z je přímo úměrný na pitch i yaw a vypočítá se vynásobením předchozích dvou rovnic:

  1. x = cos(yaw)
  2. y = sin(pitch)
  3. z = sin(yaw) * cos(pitch)

Ač to možná není úplně zřejmé, tak velikost x-ové složky je také závislá na pitch. A to stejně, jako z. Představte si, že se nedíváte ve směru osy z (jako na prvním obrázku), ale ve směru osy x. Pak kýváním nahoru a dolů ovlivníte x, stejně jako je na prvním obrázku zobrazeno ovlivňování z. Takže i tuto je potřeba zahrnout do výpočtu a konečný výpočet souřadnic vektoru určující směr pohledu je tedy toto:

  1. x = cos(yaw) * cos(pitch)
  2. y = sin(pitch)
  3. z = sin(yaw) * cos(pitch)

Získání transformační matice

K získání transformační matice se používá funkce GetViewMatrix, která využívá lookAt():

static void GetViewMatrix(Camera *camera, mat4x4 view)
{
    vec3 center;
    vec3_add(center, camera->Position, camera->Front);
    mat4x4_look_at(view, camera->Position, center, camera->Up);
}

Díky tomu, že se pozice místa, na které se kouká, počítá jako součet Position a Front, nikdy se nestane, že by se kamera koukala do stejného místa kde je (Front není nikdy nulový). Což je super, protože, jak víte, v takovém případě by přestala fungovat.

Zpracování událostí

Tak popis toho nejdůležitějšího už máte zasebou.

Posun pozice kamery vpřed nebo vzad funguje tak, že se vezme směr, kterým se kamera dívá (Front), vynásobí se rychlostí (danou uběhnutým časem a hodnotou MovementSpeed) a výsledek se přičte k současné Position.

Posun doleva nebo doprava probíhá obdobně, jen se použije místo vektoru Front vektor Right.

static void ProcessKeyboard(Camera *camera, Camera_Movement direction, GLfloat deltaTime) {
    GLfloat velocity = camera->MovementSpeed * deltaTime;
    vec3 scale;
    if (direction == FORWARD) {
        vec3_scale(scale, camera->Front, velocity);
        vec3_add(camera->Position, camera->Position, scale);
    }
    if (direction == BACKWARD) {
        vec3_scale(scale, camera->Front, velocity);
        vec3_sub(camera->Position, camera->Position, scale);
    }
    if (direction == LEFT) {
        vec3_scale(scale, camera->Right, velocity);
        vec3_sub(camera->Position, camera->Position, scale);
    }
    if (direction == RIGHT) {
        vec3_scale(scale, camera->Right, velocity);
        vec3_add(camera->Position, camera->Position, scale);
    }
}

Otáčení kolem osy y je bez omezení, kývání nahoru a dolů je omezeno v intervalu <-89.0, 89.0>, z důvodů popsaných výše.

static void ProcessMouseMovement(Camera *camera, GLfloat xoffset, GLfloat yoffset) {
    xoffset *= camera->MouseSensitivity;
    yoffset *= camera->MouseSensitivity;

    camera->Yaw += xoffset;
    camera->Pitch += yoffset;

    // Make sure that when pitch is out of bounds, screen doesn't get flipped
    if (camera->Pitch > 89.0f)
        camera->Pitch = 89.0f;
    if (camera->Pitch < -89.0f)
        camera->Pitch = -89.0f;
    // Update Front, Right and Up Vectors using the updated Eular angles
    updateCameraVectors(camera);
}

Vůbec nejjednodušší je proces výpočtu Zoom, tedy úhlu fov z perspektivy:

static void ProcessMouseScroll(Camera *camera, GLfloat yoffset) {
    if (camera->Zoom >= 1.0f && camera->Zoom <= 90.0f)
        camera->Zoom -= yoffset;
    if (camera->Zoom <= 1.0f)
        camera->Zoom = 1.0f;
    if (camera->Zoom >= 90.0f)
        camera->Zoom = 90.0f;
}

A tím je popis camera.h u konce.

Výsledek

Ukázka vychází z příkladu cubemap z minulé kapitoly. Jen jsem vyměnil texturu oblohy za texturu hradního nádrvoří.

Pohyb

Kliknutím na animaci zapínáte/vypínáte události myši (včetně skrolování).

Klávesou escape uvolníte myš.

var Camera_Movement = {
    FORWARD: 0,
    BACKWARD: 1,
    LEFT: 2,
    RIGHT: 3
};

// Default camera values
var YAW = -90.0;
var PITCH = 0.0;
var SPEED = 3.0;
var SENSITIVTY = 0.25;
var ZOOM = 45.0;

var Camera3f = function(posX, posY, posZ) {
    var camera = this;
    camera.Position = vec3.fromValues(posX, posY, posZ);
    camera.WorldUp = vec3.fromValues(0.0, 1.0, 0.0);
    camera.Yaw = YAW;
    camera.Pitch = PITCH;
    camera.Front = vec3.fromValues(0.0, 0.0, -1.0);
    camera.Right = vec3.create();
    camera.Up = vec3.create();
    camera.MovementSpeed = SPEED;
    camera.MouseSensitivity = SENSITIVTY;
    camera.Zoom = ZOOM;
    updateCameraVectors(camera);
    camera.GetViewMatrix = GetViewMatrix;
    camera.ProcessKeyboard = ProcessKeyboard;
    camera.ProcessMouseMovement = ProcessMouseMovement;
    camera.ProcessMouseScroll = ProcessMouseScroll;
};

function updateCameraVectors(camera)
{
    // Calculate the new Front vector
    var front = vec3.create();
    front[0] = Math.cos(WebGLUtils.degToRad(camera.Yaw)) * Math.cos(WebGLUtils.degToRad(camera.Pitch));
    front[1] = Math.sin(WebGLUtils.degToRad(camera.Pitch));
    front[2] = Math.sin(WebGLUtils.degToRad(camera.Yaw)) * Math.cos(WebGLUtils.degToRad(camera.Pitch));
    vec3.normalize(camera.Front, front);

    // Also re-calculate the Right and Up vector
    var cross = vec3.create();
    vec3.cross(cross, camera.Front, camera.WorldUp);
    vec3.normalize(camera.Right, cross);
    vec3.cross(cross, camera.Right, camera.Front);
    vec3.normalize(camera.Up, cross);
}

function GetViewMatrix(view)
{
    var camera = this;
    var center = vec3.create();
    vec3.add(center, camera.Position, camera.Front);
    mat4.lookAt(view, camera.Position, center, camera.Up);
}

/**
* @param Camera_Movement direction
* @param float deltaTime
*/

function ProcessKeyboard(direction, deltaTime)
{
    var camera = this;
    var velocity = camera.MovementSpeed * deltaTime;
    var scale = vec3.create();
    if (direction == Camera_Movement.FORWARD) {
        vec3.scale(scale, camera.Front, velocity);
        vec3.add(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.BACKWARD) {
        vec3.scale(scale, camera.Front, velocity);
        vec3.sub(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.LEFT) {
        vec3.scale(scale, camera.Right, velocity);
        vec3.sub(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.RIGHT) {
        vec3.scale(scale, camera.Right, velocity);
        vec3.add(camera.Position, camera.Position, scale);
    }
}

/**
* @param float xoffset
* @param float yoffset
* @param bool constrainPitch
*/

function ProcessMouseMovement(xoffset, yoffset, constrainPitch /* = true */)
{
    var camera = this;
    xoffset *= camera.MouseSensitivity;
    yoffset *= camera.MouseSensitivity;

    camera.Yaw += xoffset;
    camera.Pitch += yoffset;

    // Make sure that when pitch is out of bounds, screen doesn't get flipped
    if (constrainPitch)
    {
        if (camera.Pitch > 89.0)
            camera.Pitch = 89.0;
        if (camera.Pitch < -89.0)
            camera.Pitch = -89.0;
    }
    updateCameraVectors(camera);
}
function ProcessMouseScroll(yoffset)
{
    var camera = this;
    if (camera.Zoom >= 1.0 && camera.Zoom <= 90.0)
        camera.Zoom -= yoffset;
    if (camera.Zoom <= 1.0)
        camera.Zoom = 1.0;
    if (camera.Zoom >= 90.0)
        camera.Zoom = 90.0;
}
<script id="shader-vs-pohyb" type="x-shader/x-vertex">
    attribute vec3 vertexPosition;
    attribute vec3 textureCoord;
   
    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec3 texCoord;

    void main(void) {
        vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
        texCoord = textureCoord;
        gl_Position = uPMatrix * mvPosition;
    }
</script>

<script id="shader-fs-pohyb" type="x-shader/x-fragment">
    precision mediump float;

    uniform samplerCube texture1;

    varying vec3 texCoord;

    void main(void) {
        gl_FragColor = textureCube(texture1, texCoord);
    }
</script>
var boxVertices = [
 // Positions          
    -1000.0,  1000.0, -1000.0,
    -1000.0, -1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,
     1000.0,  1000.0, -1000.0,
    -1000.0,  1000.0, -1000.0,

    -1000.0, -1000.0,  1000.0,
    -1000.0, -1000.0, -1000.0,
    -1000.0,  1000.0, -1000.0,
    -1000.0,  1000.0, -1000.0,
    -1000.0,  1000.0,  1000.0,
    -1000.0, -1000.0,  1000.0,

     1000.0, -1000.0, -1000.0,
     1000.0, -1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,

    -1000.0, -1000.0,  1000.0,
    -1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0, -1000.0,  1000.0,
    -1000.0, -1000.0,  1000.0,

    -1000.0,  1000.0, -1000.0,
     1000.0,  1000.0, -1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
    -1000.0,  1000.0,  1000.0,
    -1000.0,  1000.0, -1000.0,

    -1000.0, -1000.0, -1000.0,
    -1000.0, -1000.0,  1000.0,
     1000.0, -1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,
    -1000.0, -1000.0,  1000.0,
     1000.0, -1000.0,  1000.0
];

var boxTexCoord = [
        // Positions          
        -1.0,  1.0, -1.0,
        -1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0,  1.0, -1.0,
        -1.0,  1.0, -1.0,
 
        -1.0, -1.0,  1.0,
        -1.0, -1.0, -1.0,
        -1.0,  1.0, -1.0,
        -1.0,  1.0, -1.0,
        -1.0,  1.0,  1.0,
        -1.0, -1.0,  1.0,
 
         1.0, -1.0, -1.0,
         1.0, -1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0, -1.0,
         1.0, -1.0, -1.0,
   
        -1.0, -1.0,  1.0,
        -1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,
 
        -1.0,  1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
        -1.0,  1.0,  1.0,
        -1.0,  1.0, -1.0,
 
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0
];
var variables = {};
var buffers = {};
var useColor = {};
useColor.ambient = 0;
var camera;
var keys = [];

function initVariables(gl, shaderProgram) {
    variables.vertexPositionLoc = gl.getAttribLocation(shaderProgram, "vertexPosition");
    variables.textureCoordLoc = gl.getAttribLocation(shaderProgram, "textureCoord");
    gl.enableVertexAttribArray(variables.vertexPositionLoc);
    gl.enableVertexAttribArray(variables.textureCoordLoc);

    variables.uMVMatrixLoc = gl.getUniformLocation(shaderProgram, "uMVMatrix");
    variables.uPMatrixLoc = gl.getUniformLocation(shaderProgram, "uPMatrix");
    variables.texture1 = gl.getUniformLocation(shaderProgram, "texture1");
}
 
var initBuffers = function(gl) {
    //logo
    buffers.positionBuffer = gl.createBuffer();
    buffers.texCoordBuffer = gl.createBuffer();
    buffers.texture1 = gl.createTexture();

    var faces = [
                "mp_amh/amh_ft.png", //right (4)
                "mp_amh/amh_bk.png", //left (2)
                "mp_amh/amh_up.png", //top (6)
                "mp_amh/amh_dn.png", //bottom (5)
                "mp_amh/amh_rt.png", //back (1)
                "mp_amh/amh_lf.png" //front (3)
    ];
 
    load_cubemap(gl, buffers.texture1, faces);

    //pohyb
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxVertices), gl.STATIC_DRAW);

    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxTexCoord), gl.STATIC_DRAW);
}
    $(function() {
        var lastX, lastY;
        var canvas1 = document.getElementById('pohyb-canvas');
        var lastDownTarget;

        canvas1.requestPointerLock = canvas1.requestPointerLock ||
                            canvas1.mozRequestPointerLock || function() {};
        document.exitPointerLock = document.exitPointerLock ||
                           document.mozExitPointerLock || function() {};

        $(canvas1).on('mousedown', function(event) {
            if(lastDownTarget != event.target) {
                lastDownTarget = event.target;
                canvas1.requestPointerLock();
            }
            else {
                lastDownTarget = null;
                //document.exitPointerLock();
            }
            lastX = event.pageX; lastY = event.pageY;
        });
       
        $(document).on('mousemove', function(event) {
            if(lastDownTarget != canvas1)
                return;

            var xoffset = event.originalEvent.movementX;
            var yoffset = -event.originalEvent.movementY;
            if(event.originalEvent.movementX  === undefined) {
                var xoffset = event.pageX - lastX;
                var yoffset = - event.pageY + lastY;
           
                lastX = event.pageX;
                lastY = event.pageY;
            }
           
            camera.ProcessMouseMovement(xoffset, yoffset, true);

            event.preventDefault();
            return false;
        });

        // IE9+
        $(document).bind('mousewheel DOMMouseScroll wheel', function(event){
            if(lastDownTarget != canvas1)
                return;
            var yoffset = event.originalEvent.wheelDelta || event.originalEvent.detail;
            if(yoffset) {
                camera.ProcessMouseScroll(yoffset);
            }
            if(yoffset > 0 || yoffset < 0) {
            return false;
            }
        });
       
        $(canvas1).keydown(function( event ) { keys[event.which] = 1; });
        $(canvas1).keyup(function( event ) { keys[event.which] = 0; });
    });
    function drawScene(gl, canvas) {
        gl.viewport(0, 0, canvas.width, canvas.height);
       
        gl.clearColor(1.0, 1.0, 1.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        var uPMatrix = mat4.create();
        var uMVMatrix = mat4.create();
        var view = mat4.create();

        mat4.identity(uPMatrix);
        mat4.identity(uMVMatrix);

        /********************************************/
        mat4.perspective(uPMatrix, WebGLUtils.degToRad(camera.Zoom), canvas.width / canvas.height, 1.0, 20000.0);
        camera.GetViewMatrix(view);
        mat4.multiply(uMVMatrix, uMVMatrix, view);
        /********************************************/
        gl.uniformMatrix4fv(variables.uMVMatrixLoc, false, uMVMatrix);
        gl.uniformMatrix4fv(variables.uPMatrixLoc, false, uPMatrix);

        // vykresli pohyb
        gl.bindTexture(gl.TEXTURE_CUBE_MAP, buffers.texture1);
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
        gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
        gl.vertexAttribPointer(variables.textureCoordLoc, 3, gl.FLOAT, false, 0, 0);
        gl.drawArrays(gl.TRIANGLES, 0, boxVertices.length/3);
    }

    var lastFrame = 0;
    var GLFW_KEY_W = 87, GLFW_KEY_S = 83, GLFW_KEY_A = 65, GLFW_KEY_D = 68;
    function animate() {
        var d = new Date();
        var currentFrame = d.getTime();
        if(!lastFrame) lastFrame = currentFrame;
        var deltaTime = (currentFrame - lastFrame)/1000.0;
        lastFrame = currentFrame;
        // Camera controls
        if (keys[GLFW_KEY_W])
                camera.ProcessKeyboard(Camera_Movement.FORWARD, deltaTime);
        if (keys[GLFW_KEY_S])
                camera.ProcessKeyboard(Camera_Movement.BACKWARD, deltaTime);
        if (keys[GLFW_KEY_A])
                camera.ProcessKeyboard(Camera_Movement.LEFT, deltaTime);
        if (keys[GLFW_KEY_D])
                camera.ProcessKeyboard(Camera_Movement.RIGHT, deltaTime);
    }
function tick(gl, canvas) {
    drawScene(gl, canvas);
    animate();
    requestAnimFrame(function() { tick(gl, canvas);});
}

function webGLStart() {
    var canvas = document.getElementById("textura");
    var gl = WebGLUtils.setupWebGL(canvas, { antialias: true });
    if(!gl) return;
    var shaderProgram = WebGLUtils.initShaders(gl, "shader-vs-pohyb","shader-fs-pohyb");
    gl.useProgram(shaderProgram);
   
    camera = new Camera3f(0.0, 0.0, 0.0);
    camera.MovementSpeed = 500.0;
   
    initVariables(gl,shaderProgram);
    initBuffers(gl);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    tick(gl, canvas);
}

webGLStart();

FPS

Struktura Camera je super, ale do pohybu ve stylu FPS jí ještě něco chybí.

Dokud se budete na nádvoří jen rozhlížet kolem sebe, iluze prostoru je dokonalá. Jakmile se ale začnete pohybovat, prostředí se začne deformovat. Nakonec můžete odkráčet mimo krychli s texturou a dívat se na ni i z venku. To není pro FPS ideální.

Proto upravím příklad tak, aby se krychle s cubemap pohybovala spolu s kamerou. Abyste viděli, že se skutečně pohybujete, přidám do scény ještě další malé krychle, které se spolu s kamerou hýbat nebudou.

Dalším vylepšením bude to, že se nebudete moci pohybovat ve směru osy y. Místo toho se bude kamera během změny polohy pohybovat malinko nahoru a dolů, čímž se vytvoří lepší iluze „chůze“.

Krychle na scéně

Pro krychličky na scéně si vytvořím globální proměnnou, do které uložím měřítko a pozici každé krychle.

#define CUBES_COUNT 500
GLfloat cubes[CUBES_COUNT][4];

Toto pole inicializuje funkce initCubes():

void initCubes(void)
{
       int i;
       srand(time(NULL));
 
       for (i = 0; i < CUBES_COUNT; i++) {
               cubes[i][0] = 0.01f; // scale
               cubes[i][1] = 2000.0 * rand()/ RAND_MAX - 1000.0; //x
               cubes[i][2] = -10.0f; // +500.0 * rand() / RAND_MAX; // y
               cubes[i][3] = 2000.0 * rand()/ RAND_MAX - 1000.0; //z
       }
};

Velkou změnou projde funkce draw(). Pro vykreslení jedné krychle jsem vytvořil funkci drawCube(), takže se funkce draw() změní na toto:

void draw(Shader *program, GLFWwindow* window, GLuint *VAO)
{      
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);
        glViewport(0, 0, width, height);
        GLfloat ratio = ((GLfloat)width) / height;
       
        glClearColor(1.f, 1.f, 1.f, 1.f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
       
        program->Use(program);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_CUBE_MAP, TEX[0]);
        vec3 center = { 0.0, 0.0, 0.0 };
        drawCube(ratio, 1.0, 0.0, 0.0, 0.0, &center);
        glClear(GL_DEPTH_BUFFER_BIT);
       
        glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
        int i;
        for (i = 0; i < sizeof(cubes) / sizeof(cubes[0]); i++) {
                drawCube(ratio, cubes[i][0], cubes[i][1], cubes[i][2], cubes[i][3], NULL);
        }
}

První volání funkce drawCube() vykreslí cubemap s oblohou. Funkce dostává jako poslední argument pozici kamery. Ostatní krychličky budou používat aktuální pozici kamery (kam se kamera přesune pomocí kláves), zatímco krychle s oblohou se bude pořád kreslit se středem (0,0,0). Tím se dosáhne toho, že se při pohybu nikdy nedostanete z centra kychle s oblohou.

Důležitá je ještě jedna řádka. A to volání glClear(GL_DEPTH_BUFFER_BIT);. Když odejdete tak daleko, že se malé krychličky dostanou mimo velkou krychli s oblohou, tak je kryhle zastíní a krchličky nebudou vidět. Po smazání depth bufferu OpenGL zapomene, že už se něco vykreslilo. Obloha tak nikdy nezakryje žádný objekt (který se vykresluje až po ní). Ať se od krychliček vzdálíte jak daleko chcete, budou stále vidět (alespoň dokud je neodsekne perspektiva).

Funkce drawCube() nedělá nic jiného, že nastaví transformační matice a zavolá program na vykreslení krychle. Protože jsem ve funkci draw() před kreslením krychliček vypnul texturu (glBindTexture(GL_TEXTURE_CUBE_MAP, 0);), vykreslí se krychličky bez textury, tedy černé.

static void drawCube(GLfloat ratio, GLfloat scale, GLfloat x, GLfloat y, GLfloat z, vec3 * cameraPosition)
{              
        mat4x4 uMVMatrix, uPMatrix, view;
        mat4x4_identity(uMVMatrix);
        mat4x4_identity(uPMatrix);
        mat4x4_perspective(uPMatrix, degToRad(camera.Zoom), ratio, 1.0, 20000.0);
        mat4x4_translate(uMVMatrix, x, y, z);
        mat4x4_scale_aniso(uMVMatrix, uMVMatrix, scale, scale, scale);

        // Create camera transformation
        camera.GetViewMatrix(&camera, view, cameraPosition);
        mat4x4_mul(uMVMatrix, view, uMVMatrix);
        glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *)uMVMatrix);
        glUniformMatrix4fv(variables.uPMatrixLoc, 1, GL_FALSE, (const GLfloat *)uPMatrix);

        glBindVertexArray(VAO[0]);
        glDrawArrays(GL_TRIANGLES, 0, sizeof(boxVertices) / sizeof(boxVertices[0]) / 3);
        glBindVertexArray(0);
}

Úprava struktury Camera

Ve funkci drawCube() jste si mohli všimnout, že GetViewMatrix má, oproti předchozí verzi, jeden argument navíc – cameraPosition. Už víte, že slouží k přeplácnutí aktuální pozice kamery. Nová verze funkce GetViewMatrix vypadá takto:

static void GetViewMatrix(Camera *camera, mat4x4 view, vec3 *position /* = null */)
{
    vec3 center;
        if (!position) {
                position = &(camera->Position);
        }
    vec3_add(center, *position, camera->Front);
    mat4x4_look_at(view, *position, center, camera->Up);
}

Zbývá se jen podívat na změnu v ProcessKeyboard. V této funkci se odehrály dvě změny. Jednak už zmíněné kolísání y-ové souřadnice. Navíc jsem přidal možnost otáčení se. Úkroky stranou jsem přesunul na klávesy Q a E, klávesy A a D budou kamerou otáčet vlevo a vpravo (podobně jako myš). Klávesy W a S stále posouvají kameru vpřed a vzad.

Rozšířil jsem strukturu Camera_Movement.

typedef enum {
        FORWARD,
        BACKWARD,
        LEFT,
        RIGHT,
        TURN_LEFT,
        TURN_RIGHT
} Camera_Movement;

Také jsem musel pozměnit funkci animate() z main.c, která volá camera->ProcessKeyboard(...). Ta změna je ale natolik přímočará, že vám ji ani tady nebudu ukazovat.

Teď k funkci ProcessKeyboard(). Na začátek spočítám posun ve směru osy y. Vytvořil jsem si pro to v struktuře Camera nový atribut stepHeight, kam si ukládám aktuální úhel, jež používám pro výpočet y-ové souřadnice.

  1. static void ProcessKeyboard(Camera *camera, Camera_Movement direction, GLfloat deltaTime) {
  2.     GLfloat velocity = camera->MovementSpeed * deltaTime;
  3.     vec3 scale;
  4.     camera->stepHeight += 15.0 * deltaTime;
  5.     GLfloat y = camera->Position[1] + sin(camera->stepHeight) * .5;
  6.     if (camera->stepHeight > 2 * M_PI) {
  7.         camera->stepHeight = 0.0;
  8.         y = 0.0;
  9.     }
  10.  

Pro výpočet y používám sinus úhlu, čímž dosahuji onoho pohupování nahoru a dolů. Atribut stepHeight vynuluji, pokud přesáhne 360 stupňů, aby nerostl do moc velkých čísel (kde by sinus, kvůli nepřesnostem výpočtů s čísli s desetinnou čárkou, nepočítal moc přesně).

Následuje výpočet posunu pozice kamery na základě Front vektoru, ale bez y-ové složky:

  1. if (direction == FORWARD) {
  2.     vec3_scale(scale, (vec3){ camera->Front[0], 0.0, camera->Front[2] }, velocity);
  3.     vec3_add(camera->Position, camera->Position, scale);
  4. } else if (direction == BACKWARD) {
  5.     vec3_scale(scale, (vec3){ camera->Front[0], 0.0, camera->Front[2] }, velocity);
  6.     vec3_sub(camera->Position, camera->Position, scale);
  7. }          
  8. if (direction == LEFT) {
  9.     vec3_scale(scale, (vec3){ camera->Right[0], 0.0, camera->Right[2] }, velocity);
  10.     vec3_sub(camera->Position, camera->Position, scale);
  11. } else if (direction == RIGHT) {
  12.     vec3_scale(scale, (vec3){ camera->Right[0], 0.0, camera->Right[2] }, velocity);
  13.     vec3_add(camera->Position, camera->Position, scale);
  14. }
  15.  

Novinkou je pak otáčení, které mění úhel Yaw. A protože se tento úhel mění, nesmí se zapomenout zavolat funkce updateCameraVectors().

  1. if (direction == TURN_LEFT) {
  2.         camera->Yaw -= velocity / 2.0;
  3.         updateCameraVectors(camera);
  4.     } else if (direction == TURN_RIGHT) {
  5.         camera->Yaw += velocity / 2.0;
  6.         updateCameraVectors(camera);
  7.     } else {
  8.         camera->Position[1] = y;
  9.     }
  10. }

A je hotovo. Zde je výsledek dnešního snažení:

Fps

Kliknutím na animaci zapínáte/vypínáte události myši (včetně skrolování).

Klávesou escape uvolníte myš.

var Camera_Movement = {
    FORWARD: 0,
    BACKWARD: 1,
    LEFT: 2,
    RIGHT: 3,
    TURN_LEFT: 4,
    TURN_RIGHT: 5
};

// Default camera values
var YAW = -90.0;
var PITCH = 0.0;
var SPEED = 3.0;
var SENSITIVTY = 0.25;
var ZOOM = 45.0;

var Camera3f = function(posX, posY, posZ) {
    var camera = this;
    camera.Position = vec3.fromValues(posX, posY, posZ);
    camera.WorldUp = vec3.fromValues(0.0, 1.0, 0.0);
    camera.Yaw = YAW;
    camera.Pitch = PITCH;
    camera.Front = vec3.fromValues(0.0, 0.0, -1.0);
    camera.Right = vec3.create();
    camera.Up = vec3.create();
    camera.MovementSpeed = SPEED;
    camera.MouseSensitivity = SENSITIVTY;
    camera.Zoom = ZOOM;
    camera.stepHeight = 0.0;
    updateCameraVectors(camera);
    camera.GetViewMatrix = GetViewMatrix;
    camera.ProcessKeyboard = ProcessKeyboard;
    camera.ProcessMouseMovement = ProcessMouseMovement;
    camera.ProcessMouseScroll = ProcessMouseScroll;
};

function updateCameraVectors(camera)
{
    // Calculate the new Front vector
    var front = vec3.create();
    front[0] = Math.cos(WebGLUtils.degToRad(camera.Yaw)) * Math.cos(WebGLUtils.degToRad(camera.Pitch));
    front[1] = Math.sin(WebGLUtils.degToRad(camera.Pitch));
    front[2] = Math.sin(WebGLUtils.degToRad(camera.Yaw)) * Math.cos(WebGLUtils.degToRad(camera.Pitch));
    vec3.normalize(camera.Front, front);

    // Also re-calculate the Right and Up vector
    var cross = vec3.create();
    vec3.cross(cross, camera.Front, camera.WorldUp);
    vec3.normalize(camera.Right, cross);
    vec3.cross(cross, camera.Right, camera.Front);
    vec3.normalize(camera.Up, cross);
}

/**
* @param mat4 view
* @param vec3 position = null
*
*/

function GetViewMatrix(view, position)
{
    var camera = this;
    var center = vec3.create();
    if(!position) {
        position = camera.Position;
    }
    vec3.add(center, position, camera.Front);
    mat4.lookAt(view, position, center, camera.Up);
}

/**
* @param Camera_Movement direction
* @param float deltaTime
*/

function ProcessKeyboard(direction, deltaTime)
{
    var camera = this;
    var velocity = camera.MovementSpeed * deltaTime;
    var scale = vec3.create();
    camera.stepHeight += 15.0 * deltaTime;
   
    var y = camera.Position[1] + Math.sin(camera.stepHeight) * 0.5;
    if (camera.stepHeight > 2 * Math.PI) {
        camera.stepHeight = 0.0;
        y = 0.0;
    }
   
    if (direction == Camera_Movement.FORWARD) {
        vec3.scale(scale, camera.Front, velocity);
        vec3.add(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.BACKWARD) {
        vec3.scale(scale, camera.Front, velocity);
        vec3.sub(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.LEFT) {
        vec3.scale(scale, camera.Right, velocity);
        vec3.sub(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.RIGHT) {
        vec3.scale(scale, camera.Right, velocity);
        vec3.add(camera.Position, camera.Position, scale);
    }
    if (direction == Camera_Movement.TURN_LEFT) {
        camera.Yaw -= velocity/2.0;
        updateCameraVectors(camera);
    } else if (direction == Camera_Movement.TURN_RIGHT) {
        camera.Yaw += velocity/2.0;
        updateCameraVectors(camera);
    } else {
        camera.Position[1] = y;
    }
}

/**
* @param float xoffset
* @param float yoffset
* @param bool constrainPitch
*/

function ProcessMouseMovement(xoffset, yoffset, constrainPitch /* = true */)
{
    var camera = this;
    xoffset *= camera.MouseSensitivity;
    yoffset *= camera.MouseSensitivity;

    camera.Yaw += xoffset;
    camera.Pitch += yoffset;

    // Make sure that when pitch is out of bounds, screen doesn't get flipped
    if (constrainPitch)
    {
        if (camera.Pitch > 89.0)
            camera.Pitch = 89.0;
        if (camera.Pitch < -89.0)
            camera.Pitch = -89.0;
    }
    updateCameraVectors(camera);
}
function ProcessMouseScroll(yoffset)
{
    var camera = this;
    if (camera.Zoom >= 1.0 && camera.Zoom <= 90.0)
        camera.Zoom -= yoffset;
    if (camera.Zoom <= 1.0)
        camera.Zoom = 1.0;
    if (camera.Zoom >= 90.0)
        camera.Zoom = 90.0;
}
<script id="shader-vs-fps" type="x-shader/x-vertex">
    attribute vec3 vertexPosition;
    attribute vec3 textureCoord;
   
    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec3 texCoord;

    void main(void) {
        vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
        texCoord = textureCoord;
        gl_Position = uPMatrix * mvPosition;
    }
</script>

<script id="shader-fs-fps" type="x-shader/x-fragment">
    precision mediump float;

    uniform samplerCube texture1;

    varying vec3 texCoord;

    void main(void) {
        gl_FragColor = textureCube(texture1, texCoord);
    }
</script>
var boxVertices = [
 // Positions          
    -1000.0,  1000.0, -1000.0,
    -1000.0, -1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,
     1000.0,  1000.0, -1000.0,
    -1000.0,  1000.0, -1000.0,

    -1000.0, -1000.0,  1000.0,
    -1000.0, -1000.0, -1000.0,
    -1000.0,  1000.0, -1000.0,
    -1000.0,  1000.0, -1000.0,
    -1000.0,  1000.0,  1000.0,
    -1000.0, -1000.0,  1000.0,

     1000.0, -1000.0, -1000.0,
     1000.0, -1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,

    -1000.0, -1000.0,  1000.0,
    -1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0, -1000.0,  1000.0,
    -1000.0, -1000.0,  1000.0,

    -1000.0,  1000.0, -1000.0,
     1000.0,  1000.0, -1000.0,
     1000.0,  1000.0,  1000.0,
     1000.0,  1000.0,  1000.0,
    -1000.0,  1000.0,  1000.0,
    -1000.0,  1000.0, -1000.0,

    -1000.0, -1000.0, -1000.0,
    -1000.0, -1000.0,  1000.0,
     1000.0, -1000.0, -1000.0,
     1000.0, -1000.0, -1000.0,
    -1000.0, -1000.0,  1000.0,
     1000.0, -1000.0,  1000.0
];

var boxTexCoord = [
        // Positions          
        -1.0,  1.0, -1.0,
        -1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0,  1.0, -1.0,
        -1.0,  1.0, -1.0,
 
        -1.0, -1.0,  1.0,
        -1.0, -1.0, -1.0,
        -1.0,  1.0, -1.0,
        -1.0,  1.0, -1.0,
        -1.0,  1.0,  1.0,
        -1.0, -1.0,  1.0,
 
         1.0, -1.0, -1.0,
         1.0, -1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0, -1.0,
         1.0, -1.0, -1.0,
   
        -1.0, -1.0,  1.0,
        -1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,
 
        -1.0,  1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
        -1.0,  1.0,  1.0,
        -1.0,  1.0, -1.0,
 
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0
];
var variables = {};
var buffers = {};
var useColor = {};
useColor.ambient = 0;
var camera;
var keys = [];

var CUBES_COUNT = 500;
var cubes = []; // cubes[CUBES_COUNT][4]

function initVariables(gl, shaderProgram) {
    variables.vertexPositionLoc = gl.getAttribLocation(shaderProgram, "vertexPosition");
    variables.textureCoordLoc = gl.getAttribLocation(shaderProgram, "textureCoord");
    gl.enableVertexAttribArray(variables.vertexPositionLoc);
    gl.enableVertexAttribArray(variables.textureCoordLoc);

    variables.uMVMatrixLoc = gl.getUniformLocation(shaderProgram, "uMVMatrix");
    variables.uPMatrixLoc = gl.getUniformLocation(shaderProgram, "uPMatrix");
    variables.texture1 = gl.getUniformLocation(shaderProgram, "texture1");
}

function initCubes()
{
        for (var i = 0; i < CUBES_COUNT; i++) {
            cubes[i] = [];
            cubes[i][0] = 0.01; // scale
            cubes[i][1] = 2000.0 * Math.random() - 1000.0; //x
            cubes[i][2] = -10.0; // +500.0 * Math.random(); // y
            cubes[i][3] = 2000.0 * Math.random() - 1000.0; //z
        }
};
 
var initBuffers = function(gl) {
    //logo
    buffers.positionBuffer = gl.createBuffer();
    buffers.texCoordBuffer = gl.createBuffer();
    buffers.texture1 = gl.createTexture();

    var faces = [
                "miramar_large2/right.jpg",
                "miramar_large2/left.jpg",
                "miramar_large2/top.jpg",
                "miramar_large2/bottom.jpg",
                "miramar_large2/back.jpg",
                "miramar_large2/front.jpg"
    ];
 
    load_cubemap(gl, buffers.texture1, faces);

    //fps
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxVertices), gl.STATIC_DRAW);

    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxTexCoord), gl.STATIC_DRAW);
}
    $(function() {
        var lastX, lastY;
        var canvas1 = document.getElementById('fps-canvas');
        var lastDownTarget;
       
        canvas1.requestPointerLock = canvas1.requestPointerLock ||
                            canvas1.mozRequestPointerLock || function() {};
        document.exitPointerLock = document.exitPointerLock ||
                           document.mozExitPointerLock || function() {};

        $(canvas1).on('mousedown', function(event) {
            if(lastDownTarget != event.target) {
                lastDownTarget = event.target;
                canvas1.requestPointerLock();
            }
            else {
                lastDownTarget = null;
                //document.exitPointerLock();
            }
            lastX = event.pageX; lastY = event.pageY;
        });
       
        $(document).on('mousemove', function(event) {
            if(lastDownTarget != canvas1)
                return;

            var xoffset = event.originalEvent.movementX;
            var yoffset = -event.originalEvent.movementY;
            if(event.originalEvent.movementX  === undefined) {
                var xoffset = event.pageX - lastX;
                var yoffset = - event.pageY + lastY;
           
                lastX = event.pageX;
                lastY = event.pageY;
            }
           
            camera.ProcessMouseMovement(xoffset, yoffset, true);

            event.preventDefault();
            return false;
        });

        // IE9+
        $(document).bind('mousewheel DOMMouseScroll wheel', function(event){
            if(lastDownTarget != canvas1)
                return;
            var yoffset = event.originalEvent.wheelDelta || event.originalEvent.detail;
            if(yoffset) {
                camera.ProcessMouseScroll(yoffset);
            }
            if(yoffset > 0 || yoffset < 0) {
            return false;
            }
        });
       
        $(canvas1).keydown(function( event ) { keys[event.which] = 1; });
        $(canvas1).keyup(function( event ) { keys[event.which] = 0; });
    });
    function drawScene(gl, canvas, shaderProgram) {
        gl.viewport(0, 0, canvas.width, canvas.height);
        var ratio = canvas.width/canvas.height;
       
        gl.clearColor(1.0, 1.0, 1.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
       
        gl.useProgram(shaderProgram);
       
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_CUBE_MAP, buffers.texture1);
        var center =  vec3.create();
        drawCube(gl, ratio, 1.0, 0.0, 0.0, 0.0, center);
        gl.clear(gl.DEPTH_BUFFER_BIT);

        gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
        for (var i = 0; i < cubes.length; i++) {
            drawCube(gl, ratio, cubes[i][0], cubes[i][1], cubes[i][2], cubes[i][3], null);
        }
    }

    function drawCube(gl, ratio, scale, x, y, z, cameraPosition) {
        var uPMatrix = mat4.create();
        var uMVMatrix = mat4.create();
        var view = mat4.create();

        mat4.identity(uPMatrix);
        mat4.identity(uMVMatrix);

        /********************************************/
        mat4.perspective(uPMatrix, WebGLUtils.degToRad(camera.Zoom), ratio, 1.0, 20000.0);
        mat4.translate(uMVMatrix, uMVMatrix, mat3.fromValues(x, y, z));
        mat4.scale(uMVMatrix, uMVMatrix, mat3.fromValues(scale, scale, scale));
        mat4.multiply(uMVMatrix, uMVMatrix, view);
        camera.GetViewMatrix(view, cameraPosition);
        mat4.multiply(uMVMatrix, view, uMVMatrix);
        /********************************************/
        gl.uniformMatrix4fv(variables.uMVMatrixLoc, false, uMVMatrix);
        gl.uniformMatrix4fv(variables.uPMatrixLoc, false, uPMatrix);

        // vykresli
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
        gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
        gl.vertexAttribPointer(variables.textureCoordLoc, 3, gl.FLOAT, false, 0, 0);
        gl.drawArrays(gl.TRIANGLES, 0, boxVertices.length/3);
    }

    var lastFrame = 0;
    var GLFW_KEY_W = 87, GLFW_KEY_S = 83, GLFW_KEY_A = 65, GLFW_KEY_D = 68,
        GLFW_KEY_Q = 81, GLFW_KEY_E = 69;
    function animate() {
        var d = new Date();
        var currentFrame = d.getTime();
        if(!lastFrame) lastFrame = currentFrame;
        var deltaTime = (currentFrame - lastFrame)/1000.0;
        lastFrame = currentFrame;
        // Camera controls
        if (keys[GLFW_KEY_W])
                camera.ProcessKeyboard(Camera_Movement.FORWARD, deltaTime);
        if (keys[GLFW_KEY_S])
                camera.ProcessKeyboard(Camera_Movement.BACKWARD, deltaTime);
        if (keys[GLFW_KEY_Q])
                camera.ProcessKeyboard(Camera_Movement.LEFT, deltaTime);
        if (keys[GLFW_KEY_E])
                camera.ProcessKeyboard(Camera_Movement.RIGHT, deltaTime);
        if (keys[GLFW_KEY_A])
                camera.ProcessKeyboard(Camera_Movement.TURN_LEFT, deltaTime);
        if (keys[GLFW_KEY_D])
                camera.ProcessKeyboard(Camera_Movement.TURN_RIGHT, deltaTime);
    }
function tick(gl, canvas, shaderProgram) {
    drawScene(gl, canvas, shaderProgram);
    animate();
    requestAnimFrame(function() { tick(gl, canvas, shaderProgram);});
}

function webGLStart() {
    var canvas = document.getElementById("textura");
    var gl = WebGLUtils.setupWebGL(canvas, { antialias: true });
    if(!gl) return;
    var shaderProgram = WebGLUtils.initShaders(gl, "shader-vs-fps","shader-fs-fps");

    camera = new Camera3f(0.0, 0.0, 0.0);
    camera.MovementSpeed = 150.0;
   
    initVariables(gl,shaderProgram);
    initBuffers(gl);
    initCubes();

    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    tick(gl, canvas, shaderProgram);
}

webGLStart();

Dokonalé to úplně není. Pokud budete mít stisknuté dvě klávesnice, bude se rychlost poskakování dvojnásobná. Opravu tohoto malého detailu už nechám na vás :-).

Závěr

Úkolem této kapitoly bylo vám představit strukturu Camera. Jde o strukturu, která zapouzdřuje vše, co je potřeba pro pohyb uvnitř prostoru. Tedy zpracování událostí myši a klávesnice a výpočet ViewMatrix.

Snažil jsem se popisovat jen ty části, jejichž význam by nemusel být naprvní pohled patrný. Pokud se vám zdá tento popis složitý nebo zmatený, prostudujte si nejdříve zrdojové kódy příkladů. Zjistíte, že jsou mnohem jednodušší, než by se z této kapitoly mohlo zdát.

Komentář Hlášení chyby
Created: 24.12.2016
Last updated: 25.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..