glReadPixels
可运行的 PBO 示例
以下示例生成以下内容之一:
- 每帧以200 FPS产生一个ppm图像,无需额外的依赖;
- 每帧以600 FPS使用libpng生成一个png图像;
- 以1200 FPS使用FFmpeg生成所有帧的一个mpg视频。
这些操作均在ramfs上进行。压缩得越好,FPS就越大,所以我们必须受到内存I/O限制。
在我的60 FPS屏幕上,FPS大于200,并且所有图像都是不同的,所以我确定它不仅仅限制于屏幕的FPS。
本答案中的GIF动画是根据视频生成的,方法请参考:https://askubuntu.com/questions/648603/how-to-create-an-animated-gif-from-mp4-video-via-command-line/837574#837574
glReadPixels
是关键的OpenGL函数,用于从屏幕读取像素。还请查看init()
下的设置。
glReadPixels
函数首先读取底部像素行,这与大多数图像格式不同,因此通常需要进行转换。
offscreen.c
#ifndef PPM
#define PPM 1
#endif
#ifndef LIBPNG
#define LIBPNG 1
#endif
#ifndef FFMPEG
#define FFMPEG 1
#endif
#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 int offscreen = 1;
static unsigned int max_nframes = 128;
static unsigned int nframes = 0;
static unsigned int time0;
static unsigned int height = 128;
static unsigned int width = 128;
#define PPM_BIT (1 << 0)
#define LIBPNG_BIT (1 << 1)
#define FFMPEG_BIT (1 << 2)
static unsigned int output_formats = PPM_BIT | LIBPNG_BIT | FFMPEG_BIT;
static double angle;
static double delta_angle;
#if PPM
static void screenshot_ppm(const char *filename, unsigned int width,
unsigned int height, GLubyte **pixels) {
size_t i, j, 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
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
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));
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 void 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) {
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glGenRenderbuffers(1, &rbo_color);
glBindRenderbuffer(GL_RENDERBUFFER, rbo_color);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB565, width, height);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo_color);
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);
assert(glCheckFramebufferStatus(GL_FRAMEBUFFER));
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &glget);
assert(width < (unsigned int)glget);
assert(height < (unsigned int)glget);
} else {
glReadBuffer(GL_BACK);
}
glClearColor(0.0, 0.0, 0.0, 0.0);
glEnable(GL_DEPTH_TEST);
glPixelStorei(GL_PACK_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
if (output_formats & LIBPNG_BIT) {
free(png_bytes);
free(png_rows);
}
#endif
#if FFMPEG
if (output_formats & FFMPEG_BIT) {
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 filename[SCREENSHOT_MAX_FILENAME];
draw_scene();
if (offscreen) {
glFlush();
} else {
glutSwapBuffers();
}
#if PPM
if (output_formats & PPM_BIT) {
snprintf(filename, SCREENSHOT_MAX_FILENAME, "tmp.%d.ppm", nframes);
screenshot_ppm(filename, width, height, &pixels);
}
#endif
#if LIBPNG
if (output_formats & LIBPNG_BIT) {
snprintf(filename, SCREENSHOT_MAX_FILENAME, "tmp.%d.png", nframes);
screenshot_png(filename, width, height, &pixels, &png_bytes, &png_rows);
}
#endif
# if FFMPEG
if (output_formats & FFMPEG_BIT) {
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) {
int arg;
GLint glut_display;
glutInit(&argc, argv);
arg = 1;
if (argc > arg) {
offscreen = (argv[arg][0] == '1');
} else {
offscreen = 1;
}
arg++;
if (argc > arg) {
max_nframes = strtoumax(argv[arg], NULL, 10);
}
arg++;
if (argc > arg) {
width = strtoumax(argv[arg], NULL, 10);
}
arg++;
if (argc > arg) {
height = strtoumax(argv[arg], NULL, 10);
}
arg++;
if (argc > arg) {
output_formats = strtoumax(argv[arg], NULL, 10);
}
if (offscreen) {
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) {
}
init();
glutDisplayFunc(display);
glutIdleFunc(idle);
atexit(deinit);
glutMainLoop();
return EXIT_SUCCESS;
}
在 GitHub 上。
使用以下命令进行编译:
sudo apt-get install libpng-dev libavcodec-dev libavutil-dev
gcc -DPPM=1 -DLIBPNG=1 -DFFMPEG=1 -ggdb3 -std=c99 -O0 -Wall -Wextra \
-o offscreen offscreen.c -lGL -lGLU -lglut -lpng -lavcodec -lswscale -lavutil
运行10个帧的“离屏”(主要是待办事项,可以工作但没有优势),尺寸为200 x 100,并支持所有输出格式:
./offscreen 1 10 200 100 7
CLI 格式为:
./offscreen [offscreen [nframes [width [height [output_formats]]]]]
而 output_formats
是一个位掩码:
ppm >> 0 | png >> 1 | mpeg >> 2
在屏幕上运行(不限制我的FPS):
./offscreen 0
在Ubuntu 15.10,OpenGL 4.4.0 NVIDIA 352.63,Lenovo Thinkpad T430上进行了基准测试。
还在Ubuntu 18.04,OpenGL 4.6.0 NVIDIA 390.77,Lenovo Thinkpad P51上进行了测试。
待办事项:找到一种在没有GUI(例如X11)的机器上进行操作的方法。似乎OpenGL不适用于离屏渲染,并且将像素读回GPU的操作是通过窗口系统接口(例如
GLX)实现的。参见:
Linux中无X.org的OpenGL。
TODO:使用1x1窗口,使其不可调整大小,并隐藏它以使事情更加健壮。如果我执行其中任何一个操作,则渲染将失败,请参见代码注释。防止调整大小在Glut中似乎不可能,但GLFW支持它。无论如何,这些都不重要,因为即使offscreen
关闭时,我的FPS也不受屏幕刷新频率的限制。
PBO之外的其他选项
- 渲染到后台缓冲区(默认渲染位置)
- 渲染到纹理
- 渲染到
Pixelbuffer
对象(PBO)
Framebuffer
和Pixelbuffer
比后台缓冲区和纹理更好,因为它们是为数据读取到CPU而设计的,而后台缓冲区和纹理则是为保留在GPU上并显示在屏幕上而设计的。
PBO 用于异步传输,所以我认为我们不需要它,参见:OpenGL 中帧缓冲对象和像素缓冲对象之间的区别是什么?
也许离屏 Mesa 值得一看:http://www.mesa3d.org/osmesa.html
Vulkan
似乎 Vulkan 的设计比 OpenGL 更好地支持离屏渲染。
这在 NVIDIA 的概述中提到:https://developer.nvidia.com/transitioning-opengl-vulkan
这是一个可运行的示例,我刚刚在本地成功运行: https://github.com/SaschaWillems/Vulkan/tree/b9f0ac91d2adccc3055a904d3a8f6553b10ff6cd/examples/renderheadless/renderheadless.cpp
安装驱动程序并确保 GPU 正常工作后,我可以进行以下操作:
git clone https://github.com/SaschaWillems/Vulkan
cd Vulkan
git checkout b9f0ac91d2adccc3055a904d3a8f6553b10ff6cd
python download_assets.py
mkdir build
cd build
cmake ..
make -j`nproc`
cd bin
./renderheadless
这将立即生成一个图像headless.ppm
,而不需要打开任何窗口:
![enter image description here](https://istack.dev59.com/R401x.webp)
我还成功地在Ubuntu Ctrl + Alt + F3非图形TTY上运行了这个程序,这进一步表明它确实不需要屏幕。
其他可能感兴趣的示例:
相关:Vulkan中是否可以进行无Surface的离屏渲染?
在Ubuntu 20.04、NVIDIA驱动程序435.21和NVIDIA Quadro M1200 GPU上测试通过。
apiretrace
https://github.com/apitrace/apitrace
只需要简单操作,无需修改您的代码即可正常运行:
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 trace /path/to/opengl_executable.out
./apitrace dump-images opengl_executable.out.trace
同样适用于Ubuntu 18.10:
sudo apt-get install apitrace
现在你有一堆以以下名称命名的屏幕截图:
animation.out.<n>.png
待办事项:工作原理。
文档还建议在视频中使用此方法:
apitrace dump-images -o - application.trace \
| ffmpeg -r 30 -f image2pipe -vcodec ppm -i pipe: -vcodec mpeg4 -y output.mp4
另请参阅:
WebGL + 画布图像保存
由于性能问题,这主要是一种玩具,但它在某些基本用例中还是有点作用的:
参考文献
FBO 大于窗口大小:
无窗口 / X11: