Save the OpenGL rendering to an image file

Technology ,

Recent homework of the graphic course requires saving the OpenGL rendering to an image file. Although there are a lot of online postings related to this topic, here I record what I have done in case it may help someone or myself in the future.

Quick answer

First, we get the size of the window through glfwGetFramebufferSize and allocate a buffer buffer. Then we read image data using glReadPixels and write the data to file using stbi_write_png.

void saveImage(char* filepath, GLFWwindow* w) {
 int width, height;
 glfwGetFramebufferSize(w, &width, &height);
 GLsizei nrChannels = 3;
 GLsizei stride = nrChannels * width;
 stride += (stride % 4) ? (4 - stride % 4) : 0;
 GLsizei bufferSize = stride * height;
 std::vector<char> buffer(bufferSize);
 glPixelStorei(GL_PACK_ALIGNMENT, 4);
 glReadBuffer(GL_FRONT);
 glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, buffer.data());
 stbi_flip_vertically_on_write(true);
 stbi_write_png(filepath, width, height, nrChannels, buffer.data(), stride);
}

I used GLFW to create windows and stb to write image data to png files.

Some remarks

Obtain window size

I see several options online. The first one is

GLint pView[4];
glGetIntegerv(GL_VIEWPORT, pView);

and we expect pView[2] and pView[3] to be the window width and height. This works if the window size is not adjusted by the user after the window is created. From what I have observed, if the window size is changed, on macOS, pView is updated to the latest window size automatically. However, pView remains unchanged on Windows 10. I have no idea why this is the case.

The second option is

glfwGetWindowSize(w, &width, &height);

this works on Windows but it does not go well with retina displays on macOS. The logical size (screen coordinate) of a window and the actual corresponding number of pixels may be different on macOS. glfwGetWindowSize gives the logical size but what we need is the size in pixels. For example, an 800x600 window in a retina display usually has 1800*1200 pixels.

Although the instructor only requires the homework run on Windows, I eventually find the solution from OpenGL’s doc: use glfwGetFramebufferSize, which gives the window size in pixels.

Alignment of image data

It seems that most image file formats require the number of bytes used to store a single row of an image to be a multiple of 4, such bmp. As a default, OpenG L also “likes” packing image data in this way since the default value of GL_PACK_ALIGNMENT is 4. I assume that this may enable OpenGL to do parallel computations more easily.

Anyway let me just follow this practice: stride += (stride % 4) ? (4 - stride % 4) : 0; makes sure that stride is a multiple of 4.

On the other hand, if I just do not want to follow this, then the following code also works:

GLsizei stride = nrChannels * width;
//stride += (stride % 4) ? (4 - stride % 4) : 0;
GLsizei bufferSize = stride * height;
std::vector<char> buffer(bufferSize);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadBuffer(GL_FRONT);
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, buffer.data());
//...

Direction of the y-axis

The y-axis of the OpenGL screen coordinate goes upward but usually, the y-axis of an image file goes downward. Therefore, without stbi_flip_vertically_on_write(true);, the saved image file is flipped upside-down.