Rendering OpenGL animations to video

andrew@paon.wtf

Problem 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:

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?

  1. The application renders a series of image files, one for each frame. (frame_00001.bmp, frame_00002.bmp, etc.)
  2. 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.