将OpenGL渲染的结果保存为图片

概述

所需要做的很简单,就是使用glReadPixels函数来获取OpenGL当前渲染出的像素数据,主要参考了 opengl 保存渲染好的图像_szfhy的博客-CSDN博客_opengl保存图像

保存BMP图片在之前的博客中的第一部分有记录,代码非常简单,也不需要额外的库。

而OpenGL的最基础环境搭建在之前的文章《创建一个最小的OpenGL实例》中有说明,本篇的代码也将以此为起点。

步骤

首先,依照《创建一个最小的OpenGL实例》搭建OpenGL环境,或者直接从GIT上拉下工程的代码

然后,将之前的博客中的第一部分的代码拷贝过来,其中定义了void WriteBMP(const char* FileName, RGBColor* ColorBuffer, int ImageWidth, int ImageHeight)函数可供调用来写bmp文件。而代码中的main函数则可以舍弃。

下面,就是写一个ScreenShot()函数用来获取OpenGL当前渲染的结果并保存为bmp了:

void ScreenShot()
{
    //申请颜色数据内存
    RGBColor* ColorBuffer = new RGBColor[WindowSizeX * WindowSizeY];

    //读取像素(注意这里的格式是 BGR)
    glReadPixels(0, 0, WindowSizeX, WindowSizeY, GL_BGR, GL_UNSIGNED_BYTE, ColorBuffer);

    //将数据写入文件
    WriteBMP("output.bmp", ColorBuffer, WindowSizeX, WindowSizeY);
    
    //清理颜色数据内存
    delete[] ColorBuffer;
}

(注意,这里代码中获取的颜色数据的格式应指定是BGR而不是RGB)

然后,将ScreenShot()放在OpenGL绘制结束后,在交换缓冲之前:(另外,由于我这里只用绘制一帧,所以不需要循环了)
在这里插入图片描述
最后,运行程序就可以看到输出的 output.bmp图片了:
在这里插入图片描述

完整代码

#define GLFW_INCLUDE_NONE
#include <glfw3.h>
#include <glad/glad.h>

#include<iostream>

#define WindowSizeX 640
#define WindowSizeY 480

#pragma pack(2)//影响了“对齐”。可以实验前后 sizeof(BITMAPFILEHEADER) 的差别

typedef unsigned char  BYTE;
typedef unsigned short WORD;
typedef unsigned long  DWORD;
typedef long    LONG;

//BMP文件头:
struct BITMAPFILEHEADER
{
    WORD  bfType;		//文件类型标识,必须为ASCII码“BM”
    DWORD bfSize;		//文件的尺寸,以byte为单位
    WORD  bfReserved1;	//保留字,必须为0
    WORD  bfReserved2;	//保留字,必须为0
    DWORD bfOffBits;	//一个以byte为单位的偏移,从BITMAPFILEHEADER结构体开始到位图数据
};

//BMP信息头:
struct BITMAPINFOHEADER
{
    DWORD biSize;			//这个结构体的尺寸
    LONG  biWidth;			//位图的宽度
    LONG  biHeight;			//位图的长度
    WORD  biPlanes;			//The number of planes for the target device. This value must be set to 1.
    WORD  biBitCount;		//一个像素有几位
    DWORD biCompression;    //0:不压缩,1:RLE8,2:RLE4
    DWORD biSizeImage;      //4字节对齐的图像数据大小
    LONG  biXPelsPerMeter;  //用象素/米表示的水平分辨率
    LONG  biYPelsPerMeter;  //用象素/米表示的垂直分辨率
    DWORD biClrUsed;        //实际使用的调色板索引数,0:使用所有的调色板索引
    DWORD biClrImportant;	//重要的调色板索引数,0:所有的调色板索引都重要
};

//一个像素的颜色信息
struct RGBColor
{
    char B;		//蓝
    char G;		//绿
    char R;		//红
};

//将颜色数据写到一个BMP文件中
//FileName:文件名
//ColorBuffer:颜色数据
//ImageWidth:图像宽度
//ImageHeight:图像长度
void WriteBMP(const char* FileName, RGBColor* ColorBuffer, int ImageWidth, int ImageHeight)
{
    //颜色数据总尺寸:
    const int ColorBufferSize = ImageHeight * ImageWidth * sizeof(RGBColor);

    //文件头
    BITMAPFILEHEADER fileHeader;
    fileHeader.bfType = 0x4D42;	//0x42是'B';0x4D是'M'
    fileHeader.bfReserved1 = 0;
    fileHeader.bfReserved2 = 0;
    fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + ColorBufferSize;
    fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

    //信息头
    BITMAPINFOHEADER bitmapHeader = { 0 };
    bitmapHeader.biSize = sizeof(BITMAPINFOHEADER);
    bitmapHeader.biHeight = ImageHeight;
    bitmapHeader.biWidth = ImageWidth;
    bitmapHeader.biPlanes = 1;
    bitmapHeader.biBitCount = 24;
    bitmapHeader.biSizeImage = ColorBufferSize;
    bitmapHeader.biCompression = 0; //BI_RGB


    FILE* fp;//文件指针

    //打开文件(没有则创建)
    fopen_s(&fp, FileName, "wb");

    //写入文件头和信息头
    fwrite(&fileHeader, sizeof(BITMAPFILEHEADER), 1, fp);
    fwrite(&bitmapHeader, sizeof(BITMAPINFOHEADER), 1, fp);
    //写入颜色数据
    fwrite(ColorBuffer, ColorBufferSize, 1, fp);

    fclose(fp);
}

//shader信息
struct ShaderInfo
{
    GLenum       type;      //shader的种类:顶点着色器?像素着色器?
    const char* filename;   //shader的文件
    GLuint       shader;    //shader对象,在LoadShaders函数中创建
};

//顶点的“布局”:(当前只有一个Position)
enum Attrib_IDs { vPosition = 0 };
//顶点数目
const GLuint  NumVertices = 6;
//顶点数组对象
GLuint  VertexArrayObject;

//读取shader文件到char数组中
static const GLchar* ReadShader(const char* filename)
{
    //读取文件:
    FILE* infile;
    fopen_s(&infile, filename, "rb");

    if (!infile)
        return NULL;

    //将文件指针位置设置为最末以获得char数目
    fseek(infile, 0, SEEK_END);
    int len = ftell(infile);

    //设置文件指针为开始
    fseek(infile, 0, SEEK_SET);

    //申请内存以存储文件内容
    GLchar* source = new GLchar[len + 1];

    //读取文件内容
    fread(source, 1, len, infile);
    source[len] = '\0';//文件末置为'\0'标志

    //关闭文件
    fclose(infile);

    return const_cast<const GLchar*>(source);
}
//编译shader们最后返回一个program对象
GLuint LoadShaders(ShaderInfo* shaders)
{
    if (shaders == NULL)
        return 0;

    //创建一个program对象
    GLuint program = glCreateProgram();

    //遍历所有的ShaderInfo
    for (ShaderInfo* entry = shaders; entry->type != GL_NONE; ++entry)
    {
        //创建一个shader对象并将它放到ShaderInfo中
        GLuint shader = glCreateShader(entry->type);
        entry->shader = shader;

        //读取shader文件到char数组中
        const GLchar* source = ReadShader(entry->filename);
        //失败则删除shader对象并返回
        if (source == NULL)
        {
            for (entry = shaders; entry->type != GL_NONE; ++entry)
            {
                glDeleteShader(entry->shader);
                entry->shader = 0;
            }
            return 0;
        }

        //将shader源代码传递到shader对象中
        glShaderSource(shader, 1, &source, NULL);
        delete[] source;
        //编译shader
        glCompileShader(shader);

        //查询是否编译成功
        GLint compiled;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
        //如果编译失败则输出log
        if (!compiled)
        {
#ifdef _DEBUG
            GLsizei len;
            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
            GLchar* log = new GLchar[len + 1];
            glGetShaderInfoLog(shader, len, &len, log);
            std::cerr << "Shader compilation failed: " << log << std::endl;
            delete[] log;
#endif /* DEBUG */
            return 0;
        }

        //将shader对象连接到program对象上
        glAttachShader(program, shader);
    }

    //将program对象连接
    glLinkProgram(program);

    //查询是否连接失败
    GLint linked;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    //如果连接失败则输出log
    if (!linked)
    {
#ifdef _DEBUG
        GLsizei len;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
        GLchar* log = new GLchar[len + 1];
        glGetProgramInfoLog(program, len, &len, log);
        std::cerr << "Shader linking failed: " << log << std::endl;
        delete[] log;
#endif /* DEBUG */
        //删除所有的shader
        for (ShaderInfo* entry = shaders; entry->type != GL_NONE; ++entry)
        {
            glDeleteShader(entry->shader);
            entry->shader = 0;
        }
        return 0;
    }

    return program;
}

void init(void)
{
    //----------------------------------------------------------------------------------------------------
    //Shader
    //----------------------------------------------------------------------------------------------------

    //shader信息:
    ShaderInfo  shaders[] =
    {
        { GL_VERTEX_SHADER, "triangles.vert" },
        { GL_FRAGMENT_SHADER, "triangles.frag" },
        { GL_NONE, NULL }   //GL_NONE表示结尾
    };
    //读取shader:
    GLuint program = LoadShaders(shaders);
    //绑定shader:
    glUseProgram(program);


    //----------------------------------------------------------------------------------------------------
    //顶点缓冲
    //----------------------------------------------------------------------------------------------------

    //顶点数据:
    GLfloat  vertices[NumVertices][2] =
    {
        { -0.90f, -0.90f }, {  0.85f, -0.90f }, { -0.90f,  0.85f },  // 第一个三角形
        {  0.90f, -0.85f }, {  0.90f,  0.90f }, { -0.85f,  0.90f }   // 第二个三角形
    };

    //创建顶点数组对象
    glGenVertexArrays(1, &VertexArrayObject);
    //设定顶点数组对象为“当前”
    glBindVertexArray(VertexArrayObject);

    //顶点的缓冲
    GLuint  VertexBuffer;
    //创建缓冲
    glCreateBuffers(1, &VertexBuffer);
    //设定缓冲为“当前”
    glBindBuffer(GL_ARRAY_BUFFER, VertexBuffer);
    //指定缓冲的顶点数据
    glBufferStorage(GL_ARRAY_BUFFER, sizeof(vertices), vertices, 0);

    //指定的顶点的“布局”:(当前只有一个Position)
    glVertexAttribPointer(Attrib_IDs::vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(Attrib_IDs::vPosition);
}

void display(void)
{
    //清理颜色
    static const float black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
    glClearBufferfv(GL_COLOR, 0, black);

    //绑定顶点数据
    glBindVertexArray(VertexArrayObject);

    //Draw Call:
    glDrawArrays(GL_TRIANGLES, 0, NumVertices);
}

void ScreenShot()
{
    //申请颜色数据内存
    RGBColor* ColorBuffer = new RGBColor[WindowSizeX * WindowSizeY];

    //读取像素(注意这里的格式是 BGR)
    glReadPixels(0, 0, WindowSizeX, WindowSizeY, GL_BGR, GL_UNSIGNED_BYTE, ColorBuffer);

    //将数据写入文件
    WriteBMP("output.bmp", ColorBuffer, WindowSizeX, WindowSizeY);
    
    //清理颜色数据内存
    delete[] ColorBuffer;
}

int main(void)
{
    //必须先初始化该库,然后才能使用大多数GLFW函数。成功初始化后,GLFW_TRUE将返回。如果发生错误,GLFW_FALSE则返回。
    if (!glfwInit())
        return -1;

    //创建窗口(OpenGL上下文似乎也一并创建了)
    GLFWwindow* window = glfwCreateWindow(WindowSizeX, WindowSizeY, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    //使用GLAD来加载OpenGL的函数地址
    gladLoadGL();

    //初始化(顶点缓冲和shader)
    init();

    //循环直到用户关闭窗口
    //while (!glfwWindowShouldClose(window))
    //{
        //显示(调用 draw call)
        display();

        //将当前渲染结果保存为图片
        ScreenShot();

        //交换前后缓冲
        glfwSwapBuffers(window);

        //轮询并处理事件
        glfwPollEvents();
   // }

    //使用GLFW完成操作后,通常是在应用程序退出之前,需要终止GLFW
    glfwTerminate();

    return 0;
}
OpenGL保存渲染结果图片文件通常需要结合其他库或者API,因为OpenGL本身并不直接支持图像输出功能。以下是使用GLUT(OpenGL Utility Toolkit)的一个简单示例步骤,它是一个常见的用于创建窗口并处理事件的工具: 1. 引入必要的库: ```c++ #include <GL/glut.h> ``` 2. 在渲染循环里,先开启双缓冲技术(Double Buffering),这样可以避免闪烁: ```c++ glutSetBufferFunc(GLUT_FRONT, RenderToImage, NULL); ``` 3. 定义RenderToImage函数,这个函数会在两个缓冲之间切换,并将当前帧缓冲的内容复制到指定的目标位图格式(例如PNG、BMP等): ```c++ static void RenderToImage(int width, int height, GLenum format) { glReadPixels(0, 0, width, height, format, GL_UNSIGNED_BYTE, image_data); // 获取帧缓冲内容 write_to_file("output.png", image_data, width, height, format); // 将数据写入文件 } ``` 4. 渲染结束后,关闭双缓冲并显示图片: ```c++ void display() { if (glutGetWindowStatus() == GLUT_WINDOW_CLOSED) exit(0); glClear(GL_COLOR_BUFFER_BIT); RenderToImage(width, height, GL_RGBA); glutSwapBuffers(); } // 然后像平常一样调用glutMainLoop()开始主循环 ``` 5. `write_to_file`函数是自定义的,根据选定的格式(如png、bmp等)将像素数据写入文件。这里假设你已经实现了这个函数。 注意:这只是一个基本示例,实际项目可能需要更复杂的错误处理和文件I/O操作。此外,如果你正在使用现代的图形库或框架,如Vulkan或DirectX,处理方法可能会有所不同。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值