3D Engine/OpenGL

OpenGL - 2. Input Callbacks

Polariche 2024. 12. 26. 22:00

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;
}

 

 

구현이 완료되면 다음과 같이 화면 내 점을 드래그하여 조작할 수 있게 된다.