OpenGL - 2. Input Callbacks
GLApp - Callbacks
Initialization 에서 생성한 GLApp 베이스 클래스에 콜백용 버추얼 함수(OnMouseDown, ...) 과 콜백 초기화 함수 (InitCallbacks) 를 선언해준다.
class GLApp {
// ...
protected:
virtual void OnMouseDown(int button, double xpos, double ypos) = 0;
virtual void OnMouseUp(int button, double xpos, double ypos) = 0;
virtual void OnMouseMove(double xpos, double ypos) = 0;
protected:
bool InitCallbacks();
};
Callback 설정
InitCallbacks 에서는 다음과 같이 콜백을 배정한다.
- 마우스 버튼 누르기 -> OnMouseDown
- 마우스 버튼 떼기 -> OnMouseUp
- 마우스 버튼 이동하기 -> OnMouseMove
OnMouseDown 과 OnMouseUp 은 버튼을 누르거나 뗀 시점의 프레임에만 호출되고, OnMouseMove 는 마우스를 움직인 모든 프레임에 호출된다.
callback 함수를 배정하기 위해서 다음 함수들을 활용한다.
glfwSetWindowUserPointer | GLFW 윈도우에 유저 포인터를 저장한다. |
glfwGetWindowUserPointer | GLFW 윈도우에 저장된 유저 포인터를 불러온다. |
glfwSetMouseButtonCallback | (GLFWwindow* window, int button, int action, int mods) 시그니쳐의 콜백 함수를 마우스 버튼 액션 (누르기, 떼기, ...) 에 배정한다. |
glfwSetCursorPosCallback | (GLFWwindow* window, double xpos, double ypos) 시그니쳐의 콜백 함수를 마우스 움직임에 배정한다. |
glfwGetCursorPos | 현재 GLFW 윈도우에서의 마우스 위치를 xpos, ypos 에 반환한다. 이 때 반환되는 좌표값은 (0~width, 0~height) 사이의 정수 좌표 값이다. |
OpenGL 에 직접 배정될 콜백 함수는 lambda 함수로 생성하고, 해당 함수 내에서 GLApp 에서 선언한 OnMouseDown, OnMouseUp, OnMouseMove 등의 함수를 호출한다.
bool GLApp::InitCallbacks() {
glfwSetWindowUserPointer(window, this);
// use OnMouseDown and OnMouseUp
glfwSetMouseButtonCallback(window, [](GLFWwindow* window, int button, int action, int mods) {
GLApp* app = static_cast<GLApp*>(glfwGetWindowUserPointer(window));
if (!app)
return;
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
if(GLFW_PRESS == action)
app->OnMouseDown(button, xpos, ypos);
else if(GLFW_RELEASE == action)
app->OnMouseUp(button, xpos, ypos);
});
// use OnMouseMove
glfwSetCursorPosCallback(window, [](GLFWwindow* window, double xpos, double ypos) {
GLApp* app = static_cast<GLApp*>(glfwGetWindowUserPointer(window));
if (!app)
return;
app->OnMouseMove(xpos, ypos);
});
return true;
}
MainApp
GLApp 에서 콜백 함수를 정의하고 OpenGL 에 배정하였다.
이제 GLApp 을 활용하여, 두 가지 기능을 수행하는 간단한 어플리케이션을 작성할 것이다.
- 화면의 점을 클릭하면 점의 좌표값을 출력한다
- 점을 클릭한 채로 드래그하면, 점이 마우스를 따라 움직인다
GLApp 을 상속한 MainApp 에 버추얼 콜백 함수 (OnMouseDown, OnMouseUp, OnMouseMove) 와 보조 함수 (ClosestPoint; NDC 좌표계 기준, 입력받은 좌표값과 가장 가까운 점을 리턴하는 함수) 를 선언한다.
typedef pair<float, float> Point;
class MainApp : GLApp {
public:
~MainApp() {};
public:
virtual bool Initialize() override;
public:
void Run();
protected:
virtual void Update() override {};
virtual void Draw() override;
protected:
virtual void OnMouseDown(int button, double xpos, double ypos) override;
virtual void OnMouseUp(int button, double xpos, double ypos) override;
virtual void OnMouseMove(double xpos, double ypos) override;
protected:
Point* ClosestPoint(float xpos, float ypos);
protected:
vector<Point> points;
Point* selected_point = NULL;
};
콜백 함수를 작성하기에 앞서, Initialize 와 Draw 함수를 수정한다.
화면에 그릴 point 의 좌표를 vector<Point> 로부터 불러오도록 설정한다.
bool MainApp::Initialize() {
// init GLApp functionalities
GLApp::InitOpenGL();
GLApp::InitCallbacks();
points.push_back({0.0f, 0.5f});
points.push_back({-0.5f, -0.5f});
points.push_back({0.5f, -0.5f});
return true;
}
void MainApp::Run() {
GLApp::Run();
}
void MainApp::Draw() {
glBegin(GL_TRIANGLES);
for (auto &[x, y] : points)
glVertex2f(x, y);
glEnd();
}
수정 이후에도, 화면에 동일한 삼각형이 그려지는 것을 확인할 수 있다.
공간 상에서 가장 가까운 점을 선택하는 헬퍼 함수인 ClosestPoint 를 다음과 같이 구현한다.
이 때, 거리가 특정 범위 이내 (코드 내에선 0.05) 인 점만 선택되도록 설정하는 것도 가능하다.
Point* MainApp::ClosestPoint(float x, float y) {
Point cursor = {x, y};
Point* candidate = NULL;
double min_dist = 0.05; // only select the point within r = 0.05
for (Point & p : points) {
double dist = sqrt((p.first - x)*(p.first - x) + (p.second - y)*(p.second - y));
if (dist < min_dist) {
min_dist = dist;
candidate = &p;
}
}
return candidate;
}
- 화면의 점을 클릭하면 점의 좌표값을 출력한다
위에서 언급한 첫번째 기능을 구현하기 위해 OnMouseDown 함수를 작성한다.
"화면의 점을 클릭하면" 이라는 조건은 다음 단계를 통해 확인할 수 있다.
1. 픽셀 좌표계 (0~width, 0~height) 를 Normalized Device Coordinate (-1~1, -1~1) 로 변환한다.
2. ClosestPoint 함수를 호출하여, 가장 가까운 점의 포인터를 획득한다.
3. 만약 포인터가 비어있지 않다면, 해당 점의 좌표를 출력하고 포인터를 저장한다.
코드로 구현하면 다음과 같다.
void MainApp::OnMouseDown(int button, double xpos, double ypos) {
// select the closest point
// convert pixel coordinate to NDC
xpos = (xpos / width) * 2 - 1;
ypos = -(ypos / height) * 2 + 1;
// query for the closest point
Point* closest = ClosestPoint(xpos, ypos);
// if a point has been selected, print out its coordinates & save its pointer
if (!!closest) {
cout << closest->first << " " << closest->second << "\n";
selected_point = closest;
}
}
- 점을 클릭한 채로 드래그하면, 점이 마우스를 따라 움직인다
두 번째 기능을 구현하기 위해서 OnMouseMove 함수를 작성한다.
OnMouseDown 함수와 유사하게, 입력받은 픽셀 좌표를 NDC 좌표계로 변환하고, 선택된 점의 좌표값으로 배정한다.
void MainApp::OnMouseMove(double xpos, double ypos) {
xpos = (xpos / width) * 2 - 1;
ypos = -(ypos / height) * 2 + 1;
if (!!selected_point) {
selected_point->first = xpos;
selected_point->second = ypos;
}
}
마우스를 떼면 점 선택을 해제한다.
void MainApp::OnMouseUp(int button, double xpos, double ypos) {
selected_point = NULL;
}
구현이 완료되면 다음과 같이 화면 내 점을 드래그하여 조작할 수 있게 된다.