概述
所需要做的很简单,就是使用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;
}