키보드/마우스를 통해 카메라를 조작하며 물체를 볼 수 있는, 일종의 뷰어 프로그램을 만들 것이다.
카메라를 다루기 전에, GLFW가 받은 인풋을 먼저 처리하는 것은 Window가 될 예정이다. 따라서 Window 클래스에 대해 먼저 살펴보고자 한다.
현재 Window 클래스는 다음과 같다. (이론적인 내용 없는 단순 코딩이므로 접어둠)
Window.h:
#pragma once
#include <stdio.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
class Window
{
public:
/* (생략) */
bool* getsKeys(){return keys;} // 키보드 입력 현황
GLfloat getXChange(); // 마우스 위치의 x축 변화
GLfloat getYChange(); // 마우스 위치의 y축 변화
/* (생략) */
private:
/* (생략) */
// 배열의 각 요소는 각 키에 해당한다. GLFW에서는 키마다 숫자가 할당되어있기 때문.
bool keys[1024];
GLfloat lastX; // 이전 마우스 x좌표
GLfloat lastY; // 이전 마우스 y좌표
GLfloat xChange; // 마우스 x좌표 변화
GLfloat yChange; // 마우스 y좌표 변화
void CreateCallbacks(); // 콜백을 등록하는 함수
// 키보드 콜백으로 등록할 함수. 매개변수 형식을 맞춰주어야 한다.
static void handleKeys(GLFWwindow* window, int key, int code, int action, int mode);
// 마우스 콜백으로 등록할 함수. 매개변수 형식을 맞춰주어야 한다.
static void handleMouse(GLFWwindow* window, double xPos, double yPos);
};
Window.cpp:
#include "Window.h"
Window::Window()
{
/* (생략) */
// keys 배열 false로 초기화
for (size_t i = 0; i < 1024; i++)
keys[i] = 0;
}
GLfloat Window::getXChange()
{
GLfloat theChange = xChange;
xChange = 0.f;
return theChange;
}
GLfloat Window::getYChange()
{
GLfloat theChange = yChange;
yChange = 0.f;
return theChange;
}
void Window::CreateCallbacks()
{
glfwSetKeyCallback(mainWindow, handleKeys); // glfw 키보드 콜백 함수 등록
glfwSetCursorPosCallback(mainWindow, handleMouse); // glfw 마우스 콜백 함수 등록
}
void Window::handleKeys(GLFWwindow* window, int key, int code, int action, int mode)
{
/* (생략) */
if (key >= 0 && key < 1024)
{
if (action == GLFW_PRESS)
{
theWindow->keys[key] = true;
}
else if (action == GLFW_RELEASE)
{
theWindow->keys[key] = false;
}
}
}
void Window::handleMouse(GLFWwindow* window, double xPos, double yPos)
{
/* (생략) */
theWindow->xChange = xPos - theWindow->lastX;
theWindow->yChange = -(yPos - theWindow->lastY);
theWindow->lastX = xPos;
theWindow->lastY = yPos;
}
인풋과 관련있는 부분만 남겨두었다.
키보드 인풋은 bool 배열을 통해 관리할 것이다.
예를 들면 GLFW_KEY_A는 65라는 숫자로 #define되어있다. 즉 A를 누르면 GLFW는 65번 키가 눌렸다고 감지할 것이다.
따라서 이 때 코드 상에서 65번 요소를 false에서 true로 바꿔주도록 한 것이다.
마우스 조작은 xChange, yChange를 통해 관리할 것이다.
이전 마우스 위치를 기억해놓고, 현재 마우스 위치가 그에 비해 얼마나 이동했는지 계속하여 계산하는 것이다.
이제 카메라 클래스를 살펴볼 차례이다.
우선 Camera.h는 다음과 같다.
#pragma once
#include <GL/glew.h>
#include <GLM/glm.hpp>
#include <GLM/gtc/matrix_transform.hpp>
#include <GLFW/glfw3.h>
class Camera
{
public:
Camera();
Camera(glm::vec3 startPosition, glm::vec3 startUp, GLfloat startYaw, GLfloat startPitch, GLfloat startMoveSpeed, GLfloat startTurnSpeed);
void keyControl(bool* keys, GLfloat deltaTime);
void mouseControl(GLfloat xChange, GLfloat yChange);
glm::mat4 calculateViewMatrix();
private:
glm::vec3 position; // 월드 상의 카메라 위치
glm::vec3 front; // 카메라 좌표계 기준 -z 방향
glm::vec3 up; // 카메라 좌표계 기준 +y 방향
glm::vec3 right; // 카메라 좌표계 기준 +x 방향
glm::vec3 worldUp; // 월드 좌표계 기준 +y 방향 (0, 1, 0)
GLfloat yaw;
GLfloat pitch;
GLfloat moveSpeed;
GLfloat turnSpeed;
// 카메라 좌표계를 업데이트한다. (front, up, right)
void update();
};
핵심은 keyControl, mouseControl 함수인데, main.cpp에서 다음과 같은 방식으로 호출할 것이다.
camera.keyControl(mainWindow.getsKeys(), deltaTime);
camera.mouseControl(mainWindow.getXChange(), mainWindow.getYChange());
keyControl에서는 front, right, up을 이용해 카메라의 포지션을 바꿔주기만 하면 된다.
void Camera::keyControl(bool* keys, GLfloat deltaTime)
{
GLfloat velocity = moveSpeed * deltaTime;
if (keys[GLFW_KEY_W])
position += front * velocity;
if (keys[GLFW_KEY_A])
position -= right * velocity;
if (keys[GLFW_KEY_S])
position -= front * velocity;
if (keys[GLFW_KEY_D])
position += right * velocity;
if (keys[GLFW_KEY_Q])
position -= up * velocity;
if (keys[GLFW_KEY_E])
position += up * velocity;
}
WASD 체계로 조작되도록 하였다.
mouseControl 함수는 다음과 같다.
void Camera::mouseControl(GLfloat xChange, GLfloat yChange)
{
xChange *= turnSpeed;
yChange *= turnSpeed;
yaw += xChange;
pitch += yChange;
if (pitch > 89.f)
pitch = 89.f;
if (pitch < -89.f)
pitch = -89.f;
update();
}
마우스의 x축 조작이 yaw를 결정하고, y축 조작이 pitch를 결정한다.
pitch가 90도 이상 돌아가면 부자연스러우므로, 범위를 -89 ~ 89 사이로 정해주었다.
그런데, 마우스를 통해 카메라가 회전했다면 front, right, up을 새로 업데이트해주어야 한다.
예를 들어, 마우스를 조작해 카메라가 위를 보게 하고, W를 누른다면 카메라는 위로 움직여야 하기 때문이다.
이를 위해 호출하는 것이 update 함수이다.
다시 정리해보자면, update 함수 안에서는 front, right, up을 업데이트할 것이다.
이때 front 벡터를 정확하게 구하는 것이 중요하다.
front 벡터를 잘 구했다면 정확한 -z축을 구한 것과 같고, 어설픈 y축 (0, 1, 0) 은 이미 알고 있다. (worldUp)
벡터의 외적을 이용하여,
worldUp X -front을 통해 right를 얻어낼 수 있다.
그리고 다시 -front X right을 한다면? up까지 얻을 수 있다.
이에 대해 자세한 내용은 ModelView Matrix 포스팅 후반부를 참고하면 좋을 것 같다.
그렇다면 front를 어떻게 정확하게 구할 수 있을까?
카메라가 보던 곳이 A에서 B로 바뀌었다고 하자.
그러면 front 벡터의 y값은 0에서 sin(pitch)가 된다. (직각삼각형의 빗변의 길이가 1이라고 가정)
y값을 구했으므로 x, z값도 구해야 하는데, 일단 xz평면 위에서 원점 -> A로 가는 선을 그어보자.
x = cos(pitch) * cos(yaw),
z = cos(pitch) * sin(yaw) 로 나타내짐을 볼 수 있다.
다음은 이를 코드로 짠 update 함수이다.
void Camera::update()
{
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front = glm::normalize(front);
right = glm::normalize(glm::cross(worldUp, -front)); // 어설픈 y와 올바른 z로 올바른 x를 결정
up = glm::normalize(glm::cross(-front, right)); // 올바른 z와 올바른 x로 올바른 y를 결정
}
그런데, 바로 위의 이미지를 보면 마치 yaw가 0일 때 카메라는 +x 방향을 보고있는 것 같다.
위의 식에 yaw, pitch를 0으로 해서 넣어봐도, front 벡터가 (1, 0, 0)이 되는 것을 볼 수 있다.
OpenGL에서 카메라는 기본적으로 -z 방향을 보고있어야 하기 때문에, 이를 고쳐주고 싶다.
그러기 위해서는 카메라를 시계방향으로 90도 돌려주면 된다.
오른손 법칙에 의해 시계 방향은 음의 방향이기에, 초기 yaw값을 다음과 같이 결정해주면 된다.
yaw = -90.f;
이렇게 카메라 조작까지 구현된 프로그램이 완성되었다.
main.cpp 전문:
#include <stdio.h>
#include <string.h>
#include <vector>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <GLM/glm.hpp>
#include <GLM/gtc/matrix_transform.hpp>
#include <GLM/gtc/type_ptr.hpp>
#include "Window.h"
#include "Mesh.h"
#include "Shader.h"
#include "Camera.h"
// 창 크기
Window mainWindow;
std::vector<Mesh*> meshList;
std::vector<Shader> shaderList;
Camera camera;
GLfloat deltaTime = 0.f;
GLfloat lastTime = 0.f;
// Vertex Shader
static const char* vShaderPath = "Shaders/VertexShader.glsl";
// Fragment Shader
static const char* fShaderPath = "Shaders/FragmentShader.glsl";
void CreateObjects()
{
GLfloat vertices[] =
{
-1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, -1.0f, 1.0f,
0.0f, -1.0f, 1.0f,
0.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
0.0f, -1.0f, 1.0f,
1.0f, -1.0f, 0.0f
};
Mesh* obj1 = new Mesh();
obj1->CreateMesh(vertices, 12);
meshList.push_back(obj1);
}
void CreateShaders()
{
Shader* shader1 = new Shader();
shader1->CreateFromFiles(vShaderPath, fShaderPath);
shaderList.push_back(*shader1);
}
int main()
{
mainWindow.Initialize();
CreateObjects();
CreateShaders();
camera = Camera(glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, 1.f, 0.f), -90.f, 0.f, 5.f, 0.5f);
GLuint uniformPVM = 0;
glEnable(GL_DEPTH_TEST);
GLfloat rotationOffset = 0.f;
// 창이 닫힐 때까지 반복
while (!mainWindow.getShouldClose())
{
GLfloat now = glfwGetTime();
deltaTime = now - lastTime;
lastTime = now;
// 사용자 입력 이벤트 가져오고 처리
glfwPollEvents();
camera.keyControl(mainWindow.getsKeys(), deltaTime);
camera.mouseControl(mainWindow.getXChange(), mainWindow.getYChange());
// 창 지우기
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderList[0].UseShader();
uniformPVM = shaderList[0].GetPVMLocation();
glm::mat4 identityMatrix(1.f);
glm::mat4 T = glm::translate(identityMatrix, glm::vec3(0.f, 0.f, -1.f));
glm::mat4 R = glm::rotate(identityMatrix, rotationOffset, glm::vec3(0.f, 1.f, 0.f));
glm::mat4 S = glm::scale(identityMatrix, glm::vec3(2.f));
glm::mat4 model = T * R * S;
glm::mat4 view = camera.calculateViewMatrix(); // glm::lookAt 함수의 리턴값을 받아온다.
glm::mat4 projection = glm::perspective(45.f, mainWindow.getBufferWidth() / mainWindow.getBufferHeight(), 0.1f, 100.f);
glm::mat4 PVM = projection * view * model;
glUniformMatrix4fv(uniformPVM, 1, GL_FALSE, glm::value_ptr(PVM));
meshList[0]->RenderMesh();
glUseProgram(0);
mainWindow.SwapBuffers();
rotationOffset += 0.0001f;
}
return 0;
};
실행 결과:
'OpenGL > 공부' 카테고리의 다른 글
[OpenGL] Textures (2) - Mipmap (0) | 2024.06.06 |
---|---|
[OpenGL] Textures (1) - 개요, Texture Filtering (Nearest, Linear) (0) | 2024.06.06 |
[OpenGL] Projection Matrix (glm::perspective, glm::ortho) (0) | 2024.06.01 |
[OpenGL] ModelView Matrix (glm::lookAt) (0) | 2024.06.01 |
[OpenGL] Polygon Rasterization (Bilinear Interpolation) (0) | 2024.05.28 |