안녕 창
안녕 창(Window)! 되게 오랜만에 보는 것 같아. 내년부터는 더 자주 보고 지내자. - 2025. 12. 24.
우리가 GLFW를 작동시킬수 있는지 한번 확인해봅시다. 먼저 .cpp 파일을 하나 만들고, 맨 위에 include 문을 쓰겠습니다.
#include <glad/glad.h>
#include <GLFW/glfw3.h>
꼭 순서를 지켜주세요. GLAD 다음에 GLFW를 포함(include)해야합니다. GLAD의 헤더 파일(.h 파일)은 내부적으로 OpenGL 헤더 파일(GL/gl.h 등)을 포함하기 때문에 OpenGL을 필요로 하는 라이브러리를 포함하기 전에 GLAD를 먼저 포함해야 합니다.
다음으로 GLFW 창을 인스턴스화할 main 함수를 만들겠습니다.
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
return 0;
}
main 함수 안에서는 처음으로 glfwInit 함수를 사용하여 GLFW를 초기화 합니다. 그리고 glfwWindowHint 함수를 사용해서 GLFW를 설정합니다. glfwWindowHint 함수의 첫번째 인자는 구성하려는 옵션을 나타냅니다. 여기서 GLFW_ 로 시작하는 다양한 옵션 열거형(Enum) 중에서 하나를 선택할 수 있습니다. 두 번째 인수는 옵션 값을 설정하는 정수입니다. 가능한 모든 옵션과 그에 해당하는 값 목록은 GLFW의 윈도우 힌트 문서에서 확인할 수 있습니다. 지금 애플리케이션을 실행했을 때 정의되지 않은 참조 오류(undefined reference errors)가 많이 발생한다면 GLFW 라이브러리가 제대로 링크 되지 않은 것입니다.
이 책에서는 OpenGL 3.3을 사용하므로, GLFW에게 우리가 3.3 버전을 사용한다는 것을 알려주고 싶습니다. 이렇게 하면 GLFW는 OpenGL 컨텍스트를 생성할 때 버전에 맞는 적절한 준비를 할 수 있게 됩니다. 또한 이렇게 했을때 사용자가 적절한 OpenGL 버전을 가지고 있지 않을 경우 GLFW가 실행되지 않도록 할 수도 있습니다. 우리는 메이저 버전과 마이너 버전을 모두 3으로 설정했습니다. 또한 GLFW에 core-profile을 명시적으로 사용하겠다고 알려줍니다. core-profile을 사용하겠다고 GLFW에 알리면 더 이상 필요하지 않은 하위 호환 기능을 제외하고 OpenGL 기능의 더 작은 하위 집합에 액세스할 수 있게 됩니다. 참고로 Mac OS X에서는 제대로 작동하려면 초기화 코드에 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);를 추가해야 합니다.
시스템/하드웨어에 OpenGL 버전 3.3 이상이 설치되어 있는지 확인하세요. 그렇지 않으면 애플리케이션이 충돌하거나 알 수 없는 동작이 발생할 수 있습니다. 시스템의 OpenGL 버전을 확인하려면 Linux에서는 glxinfo 명령어를, Windows에서는 OpenGL Extension Viewer와 같은 유틸리티를 사용하세요. 지원되는 버전이 3.3 미만인 경우, 그래픽 카드가 OpenGL 3.3 이상을 지원하는지 확인하거나(지원하지 않는 경우 매우 오래된 카드입니다) 드라이버를 업데이트하세요.
glxinfo | grep "OpenGL version" 를 사용하면 됩니다.
다음으로 윈도우 객체를 생성해야 합니다. 이 윈도우 객체는 모든 윈도우 데이터를 저장하며 GLFW의 다른 대부분의 함수에서 필요합니다.
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwCreateWindow 함수는 첫 번째와 두 번째 인수로 각각 창의 너비와 높이를 필요로 합니다. 세 번째 인수는 창의 이름을 지정하는 데 사용됩니다. 현재는 "LearnOpenGL"이라고 지정했지만 원하는 이름으로 지정할 수 있습니다. 마지막 두 매개변수는 무시해도 됩니다. 이 함수는 나중에 다른 GLFW 작업에 필요한 GLFWwindow 객체를 반환합니다. 그 후, GLFW에게 현재 스레드에서 해당 창의 컨텍스트를 메인 컨텍스트로 설정하도록 지시합니다.
마지막 두 인자가 진짜 궁굼하다면 알려드리겠습니다. 4번째 인자는 어떤 모니터에 띄울지 결정합니다. 이걸로 창 모드와 전채 화면 모드를 설정할 수 있습니다. 5번째 인자는 OpenGL 컨텍스트를 다른 윈도우와 공유할 수 있게 해주는 옵션입니다. 이해하지 못해도 괜찮습니다. 평범한 경우에서는 쓸 일이 없습니다.
GLAD
이전 장에서 GLAD가 OpenGL의 함수 포인터를 관리한다고 말했습니다. 그렇기 때문에 OpenGL 함수를 호출하기 전에 GLAD를 초기화해야 합니다.
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
우리는 GLAD에게 GLFW의 함수인 glfwGetProcAddress를 GLADloadproc라는 함수 포인터 타입으로 형변환하여 전달합니다. glfwGetProcAddress는 OpenGL 함수 포인터의 주소를 운영체제에 맞게 반환하는 함수입니다.
뷰포트
우리는 렌더링을 시작하기 전 마지막 단계에 왔습니다. 바로 OpenGL이 데이터를 어떻게 창의 어디를 기준으로 렌더링할지 알 수 있도록 렌더링 창의 크기를 OpenGL에 알려주는 일입니다. glViewport 함수를 통해 해당 크기를 설정할 수 있습니다.
glViewport(0, 0, 800, 600);
glViewport의 첫 번째와 두 번째 매개변수는 창의 왼쪽 아래 모서리 위치를 설정합니다. 세 번째와 네 번째 매개변수는 렌더링 창의 너비와 높이를 픽셀 단위로 설정하며, 이는 GLFW의 창 크기와 동일하게 설정합니다.
실제로 뷰포트 크기를 GLFW의 크기보다 작게 설정할 수 있습니다. 그러면 모든 OpenGL 렌더링이 더 작은 창에 표시되고, 예를 들어 OpenGL 뷰포트 외부에 다른 요소를 표시할 수 있습니다.
내부적으로 OpenGL은 glViewport를 통해 지정된 데이터를 사용하여 처리된 2D 좌표를 화면 좌표로 변환합니다. 예를 들어, 처리된 위치 (-0.5, 0.5)는 최종 변환 결과로 화면 좌표 (200, 450)에 매핑됩니다. OpenGL에서 처리된 좌표는 -1에서 1 사이의 값이므로, 실질적으로 (-1에서 1) 범위를 (0, 800)과 (0, 600)으로 매핑하는 것입니다.
하지만 사용자가 창 크기를 조정하는 순간 뷰포트도 함께 조정되어야 합니다. 창 크기가 조정될 때마다 호출되는 콜백 함수를 창에 등록할 수 있습니다. 이 크기 조정 콜백 함수의 선언(prototype)은 다음과 같습니다.
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
프레임버퍼 크기 함수는 첫 번째 인수로 GLFWwindow 객체를 받고, 두 번째 인수로 새 창 크기를 나타내는 두 개의 정수를 받습니다. 창 크기가 변경될 때마다 GLFW는 이 함수를 호출하고 사용자가 처리할 수 있도록 적절한 인수를 채워 넣습니다.
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
창 크기가 조정될 때마다 이 함수를 호출하려면 GLFW에 해당 함수를 등록하여 알려야 합니다.
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
창이 처음 표시될 때 framebuffer_size_callback 함수가 호출되며, 이때 창의 크기가 결정됩니다. 레티나 디스플레이의 경우, 너비와 높이가 원래 입력값보다 훨씬 크게 결정됩니다.
콜백 함수를 설정하여 사용자 정의 함수를 등록할 수 있습니다. 예를 들어, 조이스틱 입력 변경을 처리하거나 오류 메시지를 처리하는 등의 콜백 함수를 만들 수 있습니다. 콜백 함수는 창을 생성한 후 렌더링 루프가 시작되기 전에 등록합니다.
엔진 준비하기
우리는 애플리케이션이 이미지를 하나 그린 다음 바로 종료되고 창이 닫히는 것을 원하지 않습니다. 프로그램이 명시적으로 중지하라는 명령을 받을 때까지 이미지를 계속 그리고 사용자 입력을 처리하도록 해야 합니다. 이를 위해 GLFW에 중지 명령을 내릴 때까지 계속 실행되는 while 루프(렌더링 루프**{:.g}라고 부름)를 만들어야 합니다. 다음 코드는 매우 간단한 렌더링 루프를 보여줍니다.
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwWindowShouldClose 함수는 각 루프 반복 시작 시 GLFW에 창을 닫으라는 명령이 내려졌는지 확인합니다. 명령이 내려졌다면 함수는 true를 반환하고 렌더링 루프가 중지되며, 그 후 애플리케이션을 종료할 수 있습니다.
glfwPollEvents 함수는 키보드 입력이나 마우스 움직임 이벤트와 같은 이벤트가 발생했는지 확인하고, 창 상태를 업데이트하며, 해당 함수들을 호출합니다(이러한 함수들은 콜백 메서드를 통해 등록할 수 있습니다). glfwSwapBuffers 함수는 이번 렌더링 반복에서 사용되는 컬러 버퍼(GLFW 창의 각 픽셀에 대한 색상 값을 포함하는 큰 2D 버퍼)를 교체하고, 교체된 버퍼를 화면에 출력합니다.
이중 버퍼
애플리케이션이 단일 버퍼에 이미지를 그릴 때, 결과 이미지에 깜빡임 현상이 발생할 수 있습니다. 이는 최종 출력 이미지가 즉시 그려지는 것이 아니라 픽셀 단위로, 일반적으로 왼쪽에서 오른쪽, 위에서 아래로 순차적으로 그려지기 때문입니다. 렌더링이 진행되는 동안 이미지가 사용자에게 실시간으로 표시되지 않으므로, 결과물에 화면 깜박임 같은 문제(아티팩트)가 발생할 수 있습니다. 이러한 문제를 해결하기 위해 윈도우 기반 애플리케이션은 이중 버퍼 렌더링 방식을 사용합니다. 프런트 버퍼에는 화면에 표시되는 최종 출력 이미지가 저장되고, 모든 렌더링 명령은 백 버퍼에 그려집니다. 모든 렌더링 명령이 완료되면 백 버퍼의 이미지를 프런트 버퍼로 전환하여 이미지가 렌더링되는 동안에도 화면에 표시될 수 있도록 함으로써 앞서 언급한 아티팩트를 제거합니다.
마지막 하나
렌더링 루프가 끝나면 GLFW에 할당된 모든 리소스를 제대로 정리/삭제해야 합니다. 이는 메인 함수 끝에서 호출하는 glfwTerminate 함수를 통해 수행할 수 있습니다.
glfwTerminate();
return 0;
이렇게 하면 모든 리소스가 정리되고 애플리케이션이 정상적으로 종료됩니다. 이제 애플리케이션을 컴파일해 보세요. 모든 것이 잘 진행되었다면 다음과 같은 출력이 표시될 것입니다.

이 지루하고 멍청하고 아무딴에도 쓸모없게 생긴 검은화면이 나왔다면 잘 하신겁니다!(재미있고, 할일을 만들어주는 오류가 나왔다면... 응원합니다.) 원하는 화면을 얻지 못했거나 모든 요소가 어떻게 연결되는지 이해가 안 된다면 여기에서 전체 소스 코드를 확인하세요 (그리고 소스 코드가 여러 가지 색깔로 보이기 시작했다면 계속 읽어보세요).
입력
GLFW에서 입력 제어 기능도 구현하고 싶다면, GLFW의 여러 입력 함수를 이용하면 가능합니다. 창 이름과 키를 인자로 받는 GLFW의 glfwGetKey 함수를 사용할 겁니다. 이 함수는 현재 해당 키가 눌려 있는지 여부를 반환합니다. 모든 입력 관련 코드를 체계적으로 관리하기 위해 processInput 함수를 만들겠습니다.
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
여기서는 사용자가 Esc 키를 눌렀는지 확인합니다(누르지 않았다면 glfwGetKey는 GLFW_RELEASE를 반환합니다). 사용자가 Esc 키를 눌렀다면 glfwSetwindowShouldClose를 사용하여 GLFW의 WindowShouldClose 속성을 true로 설정하여 GLFW 창을 닫습니다. 그러면 메인 while 루프의 다음 조건 검사가 실패하고 애플리케이션이 종료됩니다.
그런 다음 렌더링 루프의 각 반복마다 processInput을 호출합니다.
while (!glfwWindowShouldClose(window))
{
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
이를 통해 특정 키 입력을 쉽게 확인하고 매 프레임마다 그에 따라 반응할 수 있습니다. 렌더링 루프의 한 반복을 일반적으로 프레임이라고 합니다.
렌더링
렌더링 명령은 모두 렌더링 루프 안에 넣어야 합니다. 루프의 각 반복 또는 프레임마다 모든 렌더링 명령을 실행해야 하기 때문입니다. 코드는 대략 다음과 같습니다.
// render loop
while(!glfwWindowShouldClose(window))
{
// input
processInput(window);
// rendering commands here
...
// check and call events and swap the buffers
glfwPollEvents();
glfwSwapBuffers(window);
}
제대로 작동하는지 테스트하기 위해 원하는 색상으로 화면을 지우려고 합니다. 각 프레임 시작 시 화면을 지워야 합니다. 그렇지 않으면 이전 프레임의 결과가 계속 표시될 수 있습니다(잔상이라고 부릅니다. 이것이 원하는 효과일 수도 있지만, 일반적으로는 그렇지 않겠죠.). glClear 함수를 사용하여 화면의 색상 버퍼를 지울 수 있으며, 이때 버퍼 비트를 전달하여 지울 버퍼를 지정합니다. 설정 가능한 비트는 GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT입니다. 현재는 색상 값만 필요하므로 색상 버퍼만 지웁니다.
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glClearColor 함수를 사용하여 화면을 지울 색상을 지정한다는 점에 유의하십시오. glClear를 호출하여 색상 버퍼를 지우면 전체 색상 버퍼가 glClearColor에 설정된 색상으로 채워집니다. 결과적으로 어두운 녹청색이 나타납니다.
OpenGL 챕터에서 기억하시겠지만, glClearColor 함수는 상태를 설정하는 함수이고 glClear는 현재 상태를 사용하여 지울 색상을 가져오는 상태 사용 함수입니다.

애플리케이션의 전체 소스 코드는 여기에서 확인할 수 있습니다.
자, 지금 우리는 렌더링 루프를 수많은 렌더링 호출로 채울 준비를 모두 마쳤지만, 그건 다음 장에서 다루도록 하겠습니다. 이야기가 너무 길어진 것 같네요.