目录
1. 概述
2. 疑难点剖析
2.1 SetupRC函数分析
2.2 multibuffer.vs分析
2.3 RenderScene分析
3. 其它
1. 概述
《OpenGL超级宝典(第五版)》如下:
该书第8版的 fbo_drawbuffers工程展示了如下技术点:
- 什么是帧缓冲区对象(FBO)。
- 如何将绘制输出到非默认的帧缓冲区对象上,即如何实现离屏渲染(即不绘制在窗体)。
- 如何使用FBO,包括FBO的创建、销毁、映射、完整性检查、在帧缓冲区中复制数据。
- 什么是渲染缓冲区对象(RBO),包括RBO的创建、设置RBO大小、销毁。
- 如何将FBO、RBO、纹理缓冲区(TBO)结合在一起。
- glDrawBuffers、glReadBuffers、glBlitFramebuffer、glBindFragDataLocation函数的作用。
说明:请先把OpenGL超级宝典(第五版)第8章中有关上述的知识点,自己研究一遍,扫清基本的概念后再读本博文。
2. 疑难点剖析
2.1 SetupRC函数分析
在该函数中有如下代码:
gltMakeTorus(torusBatch, 0.4f, 0.15f, 35, 35);
gltMakeSphere(sphereBatch, 0.1f, 26, 13);
这两句代码,没啥用,可以注释掉,估计编者将前几章例子中的代码直接复制过来改成本例子时,忘记删除这两句了。
该函数通过如下代码加载忍者(受过日本传统打斗和轻功训练的人):
ninja.LoadFromSBM("../../../Src/Models/Ninja/ninja.sbm",
GLT_ATTRIBUTE_VERTEX,
GLT_ATTRIBUTE_NORMAL,
GLT_ATTRIBUTE_TEXTURE0);
转到LoadFromSBM函数:
bool SBObject::LoadFromSBM(const char * filename, int vertexIndex, int normalIndex, int texCoord0Index)
{
FILE * f = NULL;
f = fopen(filename, "rb");
fseek(f, 0, SEEK_END);
size_t filesize = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char * data = new unsigned char [filesize];
unsigned char * raw_data;
fread(data, filesize, 1, f);
fclose(f);
SBM_HEADER * header = (SBM_HEADER *)data;
raw_data = data + sizeof(SBM_HEADER) + header->num_attribs * sizeof(SBM_ATTRIB_HEADER) + header->num_frames * sizeof(SBM_FRAME_HEADER);
SBM_ATTRIB_HEADER * attrib_header = (SBM_ATTRIB_HEADER *)(data + sizeof(SBM_HEADER));
SBM_FRAME_HEADER * frame_header = (SBM_FRAME_HEADER *)(data + sizeof(SBM_HEADER) + header->num_attribs * sizeof(SBM_ATTRIB_HEADER));
unsigned int total_data_size = 0;
memcpy(&m_header, header, sizeof(SBM_HEADER));
m_attrib = new SBM_ATTRIB_HEADER[header->num_attribs];
memcpy(m_attrib, attrib_header, header->num_attribs * sizeof(SBM_ATTRIB_HEADER));
m_frame = new SBM_FRAME_HEADER[header->num_frames];
memcpy(m_frame, frame_header, header->num_frames * sizeof(SBM_FRAME_HEADER));
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_attribute_buffer);
glBindBuffer(GL_ARRAY_BUFFER, m_attribute_buffer);
unsigned int i;
for (i = 0; i < header->num_attribs; i++) {
int attribIndex = i;
if(attribIndex == 0)
attribIndex = vertexIndex;
else if(attribIndex == 1)
attribIndex = normalIndex;
else if(attribIndex == 2)
attribIndex = texCoord0Index;
glVertexAttribPointer(attribIndex, m_attrib[i].components, m_attrib[i].type, GL_FALSE, 0, (GLvoid *)total_data_size);
glEnableVertexAttribArray(attribIndex);
total_data_size += m_attrib[i].components * sizeof(GLfloat) * header->num_vertices;
}
glBufferData(GL_ARRAY_BUFFER, total_data_size, raw_data, GL_STATIC_DRAW);
if (header->num_indices) {
glGenBuffers(1, &m_index_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_index_buffer);
unsigned int element_size;
switch (header->index_type) {
case GL_UNSIGNED_SHORT:
element_size = sizeof(GLushort);
break;
default:
element_size = sizeof(GLuint);
break;
}
glBufferData(GL_ELEMENT_ARRAY_BUFFER, header->num_indices * element_size, data + total_data_size, GL_STATIC_DRAW);
}
glBindVertexArray(0);
delete [] data;
return true;
}
该函数前面部分根据ninja.sbm文件的内定格式,解析出相应格式的数据并存放到对应的结构体中用作后续绘图。该函数的后半部是关注的重点,其流程为:
- 创建一个VAO(顶点数组对象)并绑定这个VAO。
- 创建一个VBO(顶点缓冲区对象)并绑定到GL_ARRAY_BUFFER。
- 通过glVertexAttribPointer函数定义ninja.sbm文件中包含的通用顶点的数据在VBO中的排列格式,以便绘制时,GPU能识别并解析该数据。调用glEnableVertexAttribArray使相应的顶点数组变为有效状态,以便GPU能读取数据。
- 通过glBufferData函数将ninja.sbm文件中包含的通用顶点数据填充到步骤2中创建的VBO对象。
glVertexAttribPointer、glEnableVertexAttribArray、VAO、VBO之间的关系,请参见理解glVertexAttribPointer、glEnableVertexAttribArray、VAO、VBO的关系 博文。
接下来是如下代码:
......................... // 其它代码略
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBufferName);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferNames[0]);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_RENDERBUFFER, renderBufferNames[1]);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_RENDERBUFFER, renderBufferNames[2]);
// See bind frag location in Chapter 9
processProg = gltLoadShaderPairWithAttributes("multibuffer.vs", "multibuffer_frag_location.fs", 3,
GLT_ATTRIBUTE_VERTEX, "vVertex",
GLT_ATTRIBUTE_NORMAL, "vNormal",
GLT_ATTRIBUTE_TEXTURE0, "texCoord0");
glBindFragDataLocation(processProg, 0, "oStraightColor");
glBindFragDataLocation(processProg, 1, "oGreyscale");
glBindFragDataLocation(processProg, 2, "oLumAdjColor");
......................... // 其它代码略
其中glBindFragDataLocation函数的具体用法参见:glBindFragDataLocation函数说明
该函数定义如下:
void glBindFragDataLocation( GLuint program,
GLuint colorNumber,
const char * name);
该函数功能为:绑定一个用户自定义的varying类型的输出变量到片段着色器的颜色号上。参数 program表示着色器程序的句柄;colorNumber表示绑定到用户自定义的varying类型的输出变量上的颜色号,对于本例来说就是GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1、GL_COLOR_ATTACHMENT2这三个枚举值后面的数字0、1、2;name表示片段着色器中用户自定义的varying类型的输出变量的名称,该变量会绑定到GL_COLOR_ATTACHMENTN(N为0到15)。
说白了,上面三句glBindFragDataLocation代码功能就是把片段着色器multibuffer_frag_location.fs中的如下三个输出变量表示的颜色值:
.................. // 其它代码略,具体参见multibuffer_frag_location.fs
out vec4 oStraightColor;
out vec4 oGreyscale;
out vec4 oLumAdjColor;
.................. // 其它代码略,具体参见multibuffer_frag_location.fs
传递给GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1、GL_COLOR_ATTACHMENT2连接的渲染缓冲区,而渲染缓冲区是在帧缓冲区中的,帧缓冲区只是一个容器,经过这三句代码后,GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1、GL_COLOR_ATTACHMENT2表示的渲染缓冲区也即帧缓冲区的颜色就分别由片段着色器oStraightColor、oGreyscale、oLumAdjColor的值来决定,于是通过改变片段着色器这三个颜色值,就可以相应地改变GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1、GL_COLOR_ATTACHMENT2表示的渲染缓冲区也即帧缓冲区颜色。
如下代码:
......................// 其它代码略
// Load the Tan ramp first
glBindBuffer(GL_TEXTURE_BUFFER_ARB, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_BUFFER_ARB, texBOTexture);
glTexBufferARB(GL_TEXTURE_BUFFER_ARB, GL_R32F, texBO[0]);
glActiveTexture(GL_TEXTURE0);
......................// 其它代码略
这段代码需要注意的是:调用glActiveTexture(GL_TEXTURE1);将纹理单元1 设置为当前活动纹理,接下来设置纹理单元1的纹理数据为texBO[0]缓冲区存放的数据,即LumTan.data文件中的表示的纹理数据。注意,这里用到了多重纹理,GL_TEXTURE0为大理石表示的纹理,而GL_TEXTURE1表示LumTan.data文件中的表示的纹理数据,这样就在大理石纹理上加上了一层类似光强度的纹理,使大理石纹理和光强度纹理糅合在一起,形成视觉上的效果。
2.2 multibuffer.vs分析
multibuffer.vs可以参考本书的第6章的6.14、6.15例子。
2.3 RenderScene分析
该函数有如下代码:
......................... // 其它代码略
if(bUseFBO)
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboName);
glDrawBuffers(3, fboBuffs);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Need light position relative to the Camera
M3DVector4f vLightTransformed;
m3dTransformVector4(vLightTransformed, vLightPos, mCamera);
UseProcessProgram(vLightTransformed, vFloorColor, 0);
}
else
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDrawBuffers(1, windowBuff);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderManager.UseStockShader(GLT_SHADER_TEXTURE_MODULATE,
transformPipeline.GetModelViewProjectionMatrix(), vFloorColor, 0);
}
......................... // 其它代码略
if(bUseFBO)
{
// Direct drawing to the window
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDrawBuffers(1, windowBuff);
glViewport(0, 0, screenWidth, screenHeight);
// Source buffer reads from the framebuffer object
glBindFramebuffer(GL_READ_FRAMEBUFFER, fboName);
// Copy greyscale output to the left half of the screen
glReadBuffer(GL_COLOR_ATTACHMENT1);
glBlitFramebuffer(0, 0, screenWidth/2, screenHeight,
0, 0, screenWidth/2, screenHeight,
GL_COLOR_BUFFER_BIT, GL_NEAREST );
// Copy the luminance adjusted color to the right half of the screen
glReadBuffer(GL_COLOR_ATTACHMENT2);
glBlitFramebuffer(screenWidth/2, 0, screenWidth, screenHeight,
screenWidth/2, 0, screenWidth, screenHeight,
GL_COLOR_BUFFER_BIT, GL_NEAREST );
// Scale the unaltered image to the upper right of the screen
glReadBuffer(GL_COLOR_ATTACHMENT0);
glBlitFramebuffer(0, 0, screenWidth, screenHeight,
(int)(screenWidth *(0.8)), (int)(screenHeight*(0.8)),
screenWidth, screenHeight,
GL_COLOR_BUFFER_BIT, GL_LINEAR );
glBindTexture(GL_TEXTURE_2D, 0);
}
......................... // 其它代码略
本段代码块说明了如果采用和不采用FBO时,如何绘制,其基本逻辑是:
- 在第4行调用glBindFramebuffer函数将绘制的目标设置为帧缓冲区。 即当前绘制的所有对象都绘制到帧缓冲区上,而不是绘制到窗体,即离屏渲染。根据SetupRC函数有关帧缓冲区和渲染缓冲区的挂接关系,可以知道,离屏渲染对象为:GL_DEPTH_ATTACHMENT、GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1、GL_COLOR_ATTACHMENT2
- 通过glDrawBuffers函数进一步限定步骤1中要绘制到哪个颜色缓冲区。关于该函数的具体用法,参见:glDrawBuffers用法。
- 第13行到20行表示如果不启用帧缓冲区绘制,则直接绘制到窗体上。第15行调用glBindFramebuffer函数将绘制的目标设置为窗体。注意:帧缓冲区默认绘制目标是窗体,当将glBindFramebuffer函数的第2个参数设置为0时,表示解除以前绑定的绘制目标且恢复到默认目标上,0就是表示默认目标即窗体。程序在创建窗体时,OPenGL会自动将默认帧缓冲区挂接到该窗体上。类似地,通过glDrawBuffers函数指出绘制到窗体的哪个缓冲区。
- 步骤1到步骤3说明了要绘制到哪里,第22行省略的代码则是具体绘制代码。
- 第27到29行代码则是将绘制目标切换到窗体,并设置窗体视口范围。
- 第32行,通过设置glBindFramebuffer函数的第1个参数为GL_READ_FRAMEBUFFER,从而设置从帧缓冲区读取像素,注意:glBindFramebuffer函数的第1个参数为GL_READ_FRAMEBUFFER对glBindFramebuffer函数的第1个参数为GL_DRAW_FRAMEBUFFER的功能不影响,即绘制写入目标依然是窗体。第35行设置从帧缓冲区的GL_COLOR_ATTACHMENT1表示的颜色缓冲区读像素,接下来调用glBlitFramebuffer函数将从GL_COLOR_ATTACHMENT1表示的颜色缓冲区中的一块矩形范围中的像素读取到窗体指定的矩形范围中。注意,该函数是在GPU中各个不同缓冲区传输数据,不经过CPU,故效率很高。关于glBlitFramebuffer函数用法,参见:glBlitFramebuffer函数用法。这样就把GL_COLOR_ATTACHMENT1表示的颜色缓冲区中的数据绘制到窗体上了,根据前面对SetupRC函数的讲解,GL_COLOR_ATTACHMENT1是和片段着色器中的oGreyscale输出颜色对应的,即oGreyscale表示的数据就绘制到了窗体左半窗体上了。GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT2逻辑相同,不再分析。
MoveCamera()句代码没啥用,删除掉,否则链接时会报找不到该函数。估计编者将前几章例子中的代码直接复制过来改成本例子时,忘记删除这两句了。
3. 其它
其它代码很简单,请参见书本中对该例子的分析,不再赘述。