如何使用GLUT / OpenGL渲染到文件?

我有一个程序模拟随时间变化的物理系统。 我希望以预定的时间间隔(比方说每10秒钟)将一个模拟状态的可视化输出到一个文件中。 我想这样做,很容易“closures可视化”,而不是输出可视化。

我正在研究OpenGL和GLUT作为graphics工具来进行可视化。 然而,问题似乎是,首先,它看起来只是输出到一个窗口,不能输出到文件。 其次,为了生成可视化,您必须调用GLUTMainLoop并停止主函数的执行 – 从此开始调用的唯一函数是来自GUI的调用。 但是我不希望这是一个基于GUI的应用程序 – 我希望它只是一个从命令行运行的应用程序,它会生成一系列图像。 有没有办法在GLUT / OpenGL中做到这一点? 或者是OpenGL完全是错误的工具,我应该使用别的东西

无论如何,你几乎肯定不想要GLUT。 你的要求不符合它打算做的事情(甚至当你的要求符合其预期的目的时,你通常不希望它)。

你可以使用OpenGL。 要在文件中生成输出,您基本上将OpenGL设置为渲染到纹理,然后将生成的纹理读入主内存并将其保存到文件中。 至less在某些系统上(例如Windows),我敢肯定你仍然需要创build一个窗口并将渲染上下文与窗口关联起来,尽pipe如果窗口总是隐藏的话也许会好起来的。

可运行的PBO示例

下面的例子生成:

  • 在200 FPS下每帧一个ppm,不需要额外的依赖性,
  • 每帧一帧,每秒600帧,libpng
  • 每帧1200帧,FFmpeg一帧

在一个ramfs上。 压缩越好,FPS就越大,所以我们必须把内存IO绑定起来。

我的60 FPS屏幕上的FPS大于200,所有图像都不一样,所以我相信它不仅限于屏幕的FPS。

glReadPixels是从屏幕读取像素的关键OpenGL函数。 也看看在init()下的设置。

glReadPixels读取像素的底线,不像大多数图像格式,所以通常需要进行转换。

TODO:在没有GUI的机器上find一种方法(例如X11)。 看起来OpenGL只是不是用于屏外渲染,并且将像素读回GPU是在与窗口系统(例如GLX )的接口上实现的。 请参阅: Linux中没有X.org的OpenGL

待办事项:使用1×1窗口,使其不可resize,并隐藏它,使事情更加健壮。 如果我这样做,渲染失败,看代码的意见。 在Glut中防止resize似乎是不可能的 ,但是GLFW支持它 。 在任何情况下,这些都不重要,因为我的FPS不受屏幕刷新频率的限制,即使offscreen是closures的。

 /* Turn output methods on and off. */ #define PPM 1 #define LIBPNG 1 #define FFMPEG 1 #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define GL_GLEXT_PROTOTYPES 1 #include <GL/gl.h> #include <GL/glu.h> #include <GL/glut.h> #include <GL/glext.h> #if LIBPNG #include <png.h> #endif #if FFMPEG #include <libavcodec/avcodec.h> #include <libavutil/imgutils.h> #include <libavutil/opt.h> #include <libswscale/swscale.h> #endif enum Constants { SCREENSHOT_MAX_FILENAME = 256 }; static GLubyte *pixels = NULL; static GLuint fbo; static GLuint rbo_color; static GLuint rbo_depth; static const unsigned int HEIGHT = 100; static const unsigned int WIDTH = 100; static int offscreen = 1; static unsigned int max_nframes = 100; static unsigned int nframes = 0; static unsigned int time0; /* Model. */ static double angle; static double delta_angle; #if PPM /* Take screenshot with glReadPixels and save to a file in PPM format. - filename: file path to save to, without extension - width: screen width in pixels - height: screen height in pixels - pixels: intermediate buffer to avoid repeated mallocs across multiple calls. Contents of this buffer do not matter. May be NULL, in which case it is initialized. You must `free` it when you won't be calling this function anymore. */ static void screenshot_ppm(const char *filename, unsigned int width, unsigned int height, GLubyte **pixels) { size_t i, j, k, cur; const size_t format_nchannels = 3; FILE *f = fopen(filename, "w"); fprintf(f, "P3\n%d %d\n%d\n", width, height, 255); *pixels = realloc(*pixels, format_nchannels * sizeof(GLubyte) * width * height); glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, *pixels); for (i = 0; i < height; i++) { for (j = 0; j < width; j++) { cur = format_nchannels * ((height - i - 1) * width + j); fprintf(f, "%3d %3d %3d ", (*pixels)[cur], (*pixels)[cur + 1], (*pixels)[cur + 2]); } fprintf(f, "\n"); } fclose(f); } #endif #if LIBPNG /* Adapted from https://github.com/cirosantilli/cpp-cheat/blob/19044698f91fefa9cb75328c44f7a487d336b541/png/open_manipulate_write.c */ static png_byte *png_bytes = NULL; static png_byte **png_rows = NULL; static void screenshot_png(const char *filename, unsigned int width, unsigned int height, GLubyte **pixels, png_byte **png_bytes, png_byte ***png_rows) { size_t i, nvals; const size_t format_nchannels = 4; FILE *f = fopen(filename, "wb"); nvals = format_nchannels * width * height; *pixels = realloc(*pixels, nvals * sizeof(GLubyte)); *png_bytes = realloc(*png_bytes, nvals * sizeof(png_byte)); *png_rows = realloc(*png_rows, height * sizeof(png_byte*)); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, *pixels); for (i = 0; i < nvals; i++) (*png_bytes)[i] = (*pixels)[i]; for (i = 0; i < height; i++) (*png_rows)[height - i - 1] = &(*png_bytes)[i * width * format_nchannels]; png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png) abort(); png_infop info = png_create_info_struct(png); if (!info) abort(); if (setjmp(png_jmpbuf(png))) abort(); png_init_io(png, f); png_set_IHDR( png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT ); png_write_info(png, info); png_write_image(png, *png_rows); png_write_end(png, NULL); png_destroy_write_struct(&png, &info); fclose(f); } #endif #if FFMPEG /* Adapted from: https://github.com/cirosantilli/cpp-cheat/blob/19044698f91fefa9cb75328c44f7a487d336b541/ffmpeg/encode.c */ static AVCodecContext *c = NULL; static AVFrame *frame; static AVPacket pkt; static FILE *file; static struct SwsContext *sws_context = NULL; static uint8_t *rgb = NULL; static void ffmpeg_encoder_set_frame_yuv_from_rgb(uint8_t *rgb) { const int in_linesize[1] = { 4 * c->width }; sws_context = sws_getCachedContext(sws_context, c->width, c->height, AV_PIX_FMT_RGB32, c->width, c->height, AV_PIX_FMT_YUV420P, 0, NULL, NULL, NULL); sws_scale(sws_context, (const uint8_t * const *)&rgb, in_linesize, 0, c->height, frame->data, frame->linesize); } void ffmpeg_encoder_start(const char *filename, int codec_id, int fps, int width, int height) { AVCodec *codec; int ret; avcodec_register_all(); codec = avcodec_find_encoder(codec_id); if (!codec) { fprintf(stderr, "Codec not found\n"); exit(1); } c = avcodec_alloc_context3(codec); if (!c) { fprintf(stderr, "Could not allocate video codec context\n"); exit(1); } c->bit_rate = 400000; c->width = width; c->height = height; c->time_base.num = 1; c->time_base.den = fps; c->gop_size = 10; c->max_b_frames = 1; c->pix_fmt = AV_PIX_FMT_YUV420P; if (codec_id == AV_CODEC_ID_H264) av_opt_set(c->priv_data, "preset", "slow", 0); if (avcodec_open2(c, codec, NULL) < 0) { fprintf(stderr, "Could not open codec\n"); exit(1); } file = fopen(filename, "wb"); if (!file) { fprintf(stderr, "Could not open %s\n", filename); exit(1); } frame = av_frame_alloc(); if (!frame) { fprintf(stderr, "Could not allocate video frame\n"); exit(1); } frame->format = c->pix_fmt; frame->width = c->width; frame->height = c->height; ret = av_image_alloc(frame->data, frame->linesize, c->width, c->height, c->pix_fmt, 32); if (ret < 0) { fprintf(stderr, "Could not allocate raw picture buffer\n"); exit(1); } } void ffmpeg_encoder_finish(void) { uint8_t endcode[] = { 0, 0, 1, 0xb7 }; int got_output, ret; do { fflush(stdout); ret = avcodec_encode_video2(c, &pkt, NULL, &got_output); if (ret < 0) { fprintf(stderr, "Error encoding frame\n"); exit(1); } if (got_output) { fwrite(pkt.data, 1, pkt.size, file); av_packet_unref(&pkt); } } while (got_output); fwrite(endcode, 1, sizeof(endcode), file); fclose(file); avcodec_close(c); av_free(c); av_freep(&frame->data[0]); av_frame_free(&frame); } void ffmpeg_encoder_encode_frame(uint8_t *rgb) { int ret, got_output; ffmpeg_encoder_set_frame_yuv_from_rgb(rgb); av_init_packet(&pkt); pkt.data = NULL; pkt.size = 0; ret = avcodec_encode_video2(c, &pkt, frame, &got_output); if (ret < 0) { fprintf(stderr, "Error encoding frame\n"); exit(1); } if (got_output) { fwrite(pkt.data, 1, pkt.size, file); av_packet_unref(&pkt); } } void ffmpeg_encoder_glread_rgb(uint8_t **rgb, GLubyte **pixels, unsigned int width, unsigned int height) { size_t i, j, k, cur_gl, cur_rgb, nvals; const size_t format_nchannels = 4; nvals = format_nchannels * width * height; *pixels = realloc(*pixels, nvals * sizeof(GLubyte)); *rgb = realloc(*rgb, nvals * sizeof(uint8_t)); /* Get RGBA to align to 32 bits instead of just 24 for RGB. May be faster for FFmpeg. */ glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, *pixels); for (i = 0; i < height; i++) { for (j = 0; j < width; j++) { cur_gl = format_nchannels * (width * (height - i - 1) + j); cur_rgb = format_nchannels * (width * i + j); for (k = 0; k < format_nchannels; k++) (*rgb)[cur_rgb + k] = (*pixels)[cur_gl + k]; } } } #endif static int model_init(void) { angle = 0; delta_angle = 1; } static int model_update(void) { angle += delta_angle; return 0; } static int model_finished(void) { return nframes >= max_nframes; } static void init(void) { int glget; if (offscreen) { /* Framebuffer */ glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); /* Color renderbuffer. */ glGenRenderbuffers(1, &rbo_color); glBindRenderbuffer(GL_RENDERBUFFER, rbo_color); /* Storage must be one of: */ /* GL_RGBA4, GL_RGB565, GL_RGB5_A1, GL_DEPTH_COMPONENT16, GL_STENCIL_INDEX8. */ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB565, WIDTH, HEIGHT); glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo_color); /* Depth renderbuffer. */ glGenRenderbuffers(1, &rbo_depth); glBindRenderbuffer(GL_RENDERBUFFER, rbo_depth); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, WIDTH, HEIGHT); glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo_depth); glReadBuffer(GL_COLOR_ATTACHMENT0); /* Sanity check. */ assert(glCheckFramebufferStatus(GL_FRAMEBUFFER)); glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &glget); assert(WIDTH * HEIGHT < (unsigned int)glget); } else { glReadBuffer(GL_BACK); } glClearColor(0.0, 0.0, 0.0, 0.0); glEnable(GL_DEPTH_TEST); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glViewport(0, 0, WIDTH, HEIGHT); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); time0 = glutGet(GLUT_ELAPSED_TIME); model_init(); #if FFMPEG ffmpeg_encoder_start("tmp.mpg", AV_CODEC_ID_MPEG1VIDEO, 25, WIDTH, HEIGHT); #endif } static void deinit(void) { printf("FPS = %f\n", 1000.0 * nframes / (double)(glutGet(GLUT_ELAPSED_TIME) - time0)); free(pixels); #if LIBPNG free(png_bytes); free(png_rows); #endif #if FFMPEG ffmpeg_encoder_finish(); free(rgb); #endif if (offscreen) { glDeleteFramebuffers(1, &fbo); glDeleteRenderbuffers(1, &rbo_color); glDeleteRenderbuffers(1, &rbo_depth); } } static void draw_scene(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glRotatef(angle, 0.0f, 0.0f, -1.0f); glBegin(GL_TRIANGLES); glColor3f(1.0f, 0.0f, 0.0f); glVertex3f( 0.0f, 0.5f, 0.0f); glColor3f(0.0f, 1.0f, 0.0f); glVertex3f(-0.5f, -0.5f, 0.0f); glColor3f(0.0f, 0.0f, 1.0f); glVertex3f( 0.5f, -0.5f, 0.0f); glEnd(); } static void display(void) { char extension[SCREENSHOT_MAX_FILENAME]; char filename[SCREENSHOT_MAX_FILENAME]; draw_scene(); if (offscreen) { glFlush(); } else { glutSwapBuffers(); } #if PPM snprintf(filename, SCREENSHOT_MAX_FILENAME, "tmp%d.ppm", nframes); screenshot_ppm(filename, WIDTH, HEIGHT, &pixels); #endif #if LIBPNG snprintf(filename, SCREENSHOT_MAX_FILENAME, "tmp%d.png", nframes); screenshot_png(filename, WIDTH, HEIGHT, &pixels, &png_bytes, &png_rows); #endif # if FFMPEG frame->pts = nframes; ffmpeg_encoder_glread_rgb(&rgb, &pixels, WIDTH, HEIGHT); ffmpeg_encoder_encode_frame(rgb); #endif nframes++; if (model_finished()) exit(EXIT_SUCCESS); } static void idle(void) { while (model_update()); glutPostRedisplay(); } int main(int argc, char **argv) { GLint glut_display; glutInit(&argc, argv); if (argc > 1) offscreen = 0; if (offscreen) { /* TODO: if we use anything smaller than the window, it only renders a smaller version of things. */ /*glutInitWindowSize(50, 50);*/ glutInitWindowSize(WIDTH, HEIGHT); glut_display = GLUT_SINGLE; } else { glutInitWindowSize(WIDTH, HEIGHT); glutInitWindowPosition(100, 100); glut_display = GLUT_DOUBLE; } glutInitDisplayMode(glut_display | GLUT_RGBA | GLUT_DEPTH); glutCreateWindow(argv[0]); if (offscreen) { /* TODO: if we hide the window the program blocks. */ /*glutHideWindow();*/ } init(); glutDisplayFunc(display); glutIdleFunc(idle); atexit(deinit); glutMainLoop(); return EXIT_SUCCESS; } 

在GitHub上 。

编译:

 gcc main.c -lGL -lGLU -lglut #-lpng -lavcodec -lswscale -lavutil 

运行“屏幕外”(主要是TODO,但没有任何优势):

 ./a.out 

在屏幕上运行(不限制我的FPS):

 ./a.out 1 

testing了Ubuntu 15.10,OpenGL 4.4.0 NVIDIA 352.63,联想Thinkpad T430。

除PBO之外的其他选项

  • 渲染到backbuffer(默认渲染地点)
  • 渲染到纹理
  • 渲染到Pixelbuffer对象(PBO)

FramebufferPixelbuffer比backbuffer和texture更好,因为它们是为了将数据读回到CPU而制作的,backbuffer和纹理会留在GPU上并显示在屏幕上。

PBO用于asynchronous传输,所以我认为我们不需要它,请参阅: OpenGL中的帧缓冲区对象和像素缓冲区对象之间的区别是什么? ,

也许屏幕以外的Mesa值得研究: http : //www.mesa3d.org/osmesa.html

apiretrace

只是工作,并不需要你修改你的代码:

 git clone https://github.com/apitrace/apitrace cd apitrace git checkout 7.0 mkdir build cd build cmake .. make # Creates opengl_executable.out.trace ./apitrace /path/to/opengl_executable.out ./apitrace dump-images opengl_executable.out.trace 

你现在有一堆截图命名为:

 animation.out.<n>.png 

TODO:工作原理。

文件也build议这个video:

 apitrace dump-images -o - application.trace \ | ffmpeg -r 30 -f image2pipe -vcodec ppm -i pipe: -vcodec mpeg4 -y output.mp4 

福尔康

Vulkan似乎被devise成比OpenGL支持离屏渲染。

这个在NVIDIA概述中提到: https : //developer.nvidia.com/transitioning-opengl-vulkan

有一个可运行的例子: https : //github.com/SaschaWillems/Vulkan/blob/0616eeff4e697e4cd23cb9c97f5dd83afb79d908/offscreen/offscreen.cpp,但我还没有设法让Vulkan运行。 1 kloc 🙂

相关: 在Vulkan中可以在没有Surface的情况下进行离屏渲染吗?

参考书目

比窗口大的FBO:

  • OpenGL如何创build和渲染比窗口大的帧缓冲区?
  • FBO lwjgl大于屏幕大小 – 我做错了什么?
  • 渲染缓冲区大于窗口大小 – OpenGL
  • 保存比窗口大的openGL FBO的问题

无窗/ X11:

  • 在Linux中没有X.org的OpenGL
  • 你可以在不打开窗口的情况下创buildOpenGL上下文吗?
  • 使用OpenGL而不使用X-Window系统

不确定OpenGL是最好的解决scheme。
但是你总是可以渲染到一个屏幕外的缓冲区。

将OpenGL输出写入文件的典型方法是使用readPixels将生成的场景像素像素复制到图像文件

不要脱离其他优秀的答案,但是如果你想要一个现有的例子,我们已经在OpenSCAD做了几年Offscreen GL渲染,作为testing框架的一部分,从命令行渲染成.png文件。 相关文件位于Offscreen * .cc下的https://github.com/openscad/openscad/tree/master/src

它运行在OSX(CGL),Linux X11(GLX),BSD(GLX)和Windows(WGL)上,由于驱动程序的差异而出现一些怪癖。 基本的诀窍是忘记打开一个窗口(就像道格拉斯·亚当斯说飞行的伎俩就是忘记打地面一样)。 如果你有一个像Xvfb或Xvnc一样运行的虚拟X11服务器,它甚至可以在'无头'linux / bsd上运行。 在运行程序之前,还可以通过设置环境variablesLIBGL_ALWAYS_SOFTWARE = 1来在Linux / BSD上使用软件渲染,这在某些情况下可能有所帮助。

这不是唯一的系统做到这一点,我相信VTK成像系统做类似的事情。

这个代码在方法上有点旧了(我把Brian的GLX代码中的GLX代码撕掉了),特别是当新的系统出现时,OSMesa,Mir,Wayland,EGL,Android,Vulkan等,但注意到了OffscreenXXX.cc其中XXX是GL上下文子系统的文件名,它理论上可以移植到不同的上下文生成器。