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.