Rendering OpenGL animations to video
andrew@paon.wtfProblem statement: I wrote a cool animation using OpenGL and now I want to share a video of it. How do I capture the video?
I could record my screen with OBS or similar, but there are some downsides to this approach:
- Is OBS going to capture at full resolution?
- What if my animation doesn’t render in realtime?
- Can I continue using my computer while the animation is rendering? Does my window need to stay on top?
Let’s avoid these issues by making the application responsible for rendering itself to video.
Here’s a sample from a recent project I worked on:
How does it work?
- The application renders a series of image files, one for each frame. (
frame_00001.bmp
,frame_00002.bmp
, etc.) - We use ffmpeg to stitch the frames together into a video.
BMP Images
We will render frames to BMP. It’s very easy to write a BMP encoder, and we can get pixel data from OpenGL in a convenient format for writing directly to a BMP file.
The magic GL function is
glReadPixels
.
Here is the way I call it in my latest project:
uint8_t bytes_per_pixel = 3;
uint8_t total_pixels = resolution[0] * resolution[1];
uint8_t pixel_buffer_size = total_pixels * bytes_per_pixel
uint8_t pixel_buffer[pixel_buffer_size];
gl.drawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, n);
gl.readPixels(0, 0, rules.resolution[0], rules.resolution[1], GL_BGR, GL_UNSIGNED_BYTE, pixel_buffer);
Note the output format that we’re requesting from OpenGL: GL_BGR
.
This means each pixel uses 3 bytes: one for its blue value, one for its green value, and one for its red value.
The order here is significant; it is the same order that the BMP file format expects.
Here is a simple BMP encoder. I wrote this using Wikipedia as a reference.
void bmp_dump(FILE* f, uint8_t *buffer, size_t n, uint32_t window_width, uint32_t window_height) {
/* Template for the BMP file header. We override it with concrete values before writing */
static uint8_t bmp_header[] = {
/* BMP header */
0x42, 0x4d, /* "BM" */
0x00, 0x00, 0x00, 0x00, /* file size */
0x00, 0x00, 0x00, 0x00, /* unnecessary */
0x00, 0x00, 0x00, 0x00, /* offset where pixel data begins */
/* DIB header */
0x0c, 0x00, 0x00, 0x00, /* this header's size */
0x00, 0x00, /* override the image width */
0x00, 0x00, /* override the image height */
0x01, 0x00, /* color planes (must be 1) */
0x18, 0x00, /* bits per pixel (24) */
};
uint16_t pixel_offset = sizeof(bmp_header);
memcpy(bmp_header + 10, &pixel_offset, 2);
uint32_t file_size = 26 + n;
memcpy(bmp_header + 2, &file_size, 4);
memcpy(bmp_header + 18, &window_width, 2);
memcpy(bmp_header + 20, &window_height, 2);
fwrite(bmp_header, 1, 26, f);
fwrite(buffer, 1, n, f);
}
Here is how I use this function in my project:
uint64_t frame = 0;
while (1) {
HandleEvents()
Update(&w, TIME_PER_FRAME);
gl.clearNamedFramebufferfv(0, GL_COLOR, 0, clear_color);
gl.useProgram(program_id);
BindData(); /* set uniforms, bind VAO, override buffers, etc */
gl.drawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, w.n);
gl.readPixels(0, 0, rules.resolution[0], rules.resolution[1], GL_BGR, GL_UNSIGNED_BYTE, pixel_buffer);
{
static char captureName[256];
sprintf(captureName, "frame_%05lu.bmp", ++frame);
FILE *f = fopen(captureName, "wb");
bmp_dump(f, pixel_buffer, pixel_data_size, rules.resolution[0], rules.resolution[1]);
fclose(f);
}
}
Stitching with FFmpeg
As the program runs, your working directory will fill up with bmp files. Here is the incantation I use to stitch those frames into a video.
FPS=30
ffmpeg -framerate "$FPS" -i 'frame_%05d.bmp' out.mp4
I have been using 30fps, but this seems to work equally well for other FPS values. Feel free to experiment.