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
.
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
:
// 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.
Funkce nastaví nějaké defaultní hodnoty. Můžete třeba změnit rychlost pohybu:
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í.
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:
// 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:
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 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:
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í.
{
// 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:
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:
- x = cos(yaw)
- y = sin(pitch)
- z = cos(pitch)
- 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:
- x = cos(yaw)
- y = sin(pitch)
- 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:
- x = cos(yaw) * cos(pitch)
- y = sin(pitch)
- 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():
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.
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.
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:
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ří.
Kliknutím na animaci zapínáte/vypínáte události myši (včetně skrolování).
Klávesou escape uvolníte myš.
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;
}
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>
// 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 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");
}
//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);
}
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; });
});
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);
}
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.
GLfloat cubes[CUBES_COUNT][4];
Toto pole inicializuje funkce initCubes()
:
Velkou změnou projde funkce draw()
. Pro vykreslení
jedné krychle jsem vytvořil funkci drawCube()
,
takže se funkce draw()
změní na toto:
{
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, ¢er);
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é.
{
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:
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
.
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.
- GLfloat velocity = camera->MovementSpeed * deltaTime;
- vec3 scale;
- camera->stepHeight += 15.0 * deltaTime;
- camera->stepHeight = 0.0;
- y = 0.0;
- }
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:
- vec3_scale(scale, (vec3){ camera->Front[0], 0.0, camera->Front[2] }, velocity);
- vec3_add(camera->Position, camera->Position, scale);
- vec3_scale(scale, (vec3){ camera->Front[0], 0.0, camera->Front[2] }, velocity);
- vec3_sub(camera->Position, camera->Position, scale);
- }
- vec3_scale(scale, (vec3){ camera->Right[0], 0.0, camera->Right[2] }, velocity);
- vec3_sub(camera->Position, camera->Position, scale);
- vec3_scale(scale, (vec3){ camera->Right[0], 0.0, camera->Right[2] }, velocity);
- vec3_add(camera->Position, camera->Position, scale);
- }
Novinkou je pak otáčení, které mění úhel Yaw. A protože se tento úhel
mění, nesmí se zapomenout zavolat funkce updateCameraVectors()
.
A je hotovo. Zde je výsledek dnešního snažení:
Kliknutím na animaci zapínáte/vypínáte události myši (včetně skrolování).
Klávesou escape uvolníte myš.
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;
}
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>
// 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 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
}
};
//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);
}
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; });
});
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);
}
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.