FBO中多重采样抗锯齿(MSAA:MultiSampling Anti-Aliasing)

时间:2022-06-30 05:27:53

原文

今天在写这样一个程序,就是导入一个OBJ模型然后显示出来的时候,遇到了一个问题。我在程序中开启了多重采样,在屏幕上显示出来的效果确实有抗锯齿。但是当我用FBO离屏渲染,然后保存为BMP图像的时候,发现保存出来的BMP图像并没有抗锯齿效果。

问题产生原因及解决方案:

在默认帧缓冲中启用多重采样并不会导致FBO里也会启用多重采样。因此要在FBO里达到多重采样的效果,必须创建适用多重采样的FBO,而不是普通的FBO。

关于FBO介绍及使用可参考:FBO

下面分别介绍多重采样抗锯齿以及怎样在FBO中使用这一技术。


一、多重采样抗锯齿技术(multisampling anti-aliasing)

1、抗锯齿技术种类分类

全屏反锯齿

全屏反锯齿(full scene Anti-aliasing,简称FSAA),也称全屏抗锯齿,它指的是利用反锯齿技术对输出到显示器的满屏画面信号进行放大与采样分析并重新制作满屏画面信号输出至显示器,而不是对画面某一部分使用反锯齿技术。全屏反锯齿对于3D游戏画面有着很大的影响,它能使整个3D游戏画面变得细腻、清晰与逼真,这是是一种重要的技术应用。全屏抗锯齿反锯齿技术,有以下方法:

超级采样抗锯齿

超级采样抗锯齿(Super-Sampling Anti-aliasing,简称SSAA)此是早期抗锯齿方法,比较消耗资源,但简单直接,先把图像映射到缓存并把它放大,再用超级采样把放大后的图像像素进行采样,一般选取2个或4个邻近像素,把这些采样混合起来后,生成的最终像素,令每个像素拥有邻近像素的特征,像素与像素之间的过渡色彩,就变得近似,令图形的边缘色彩过渡趋于平滑。再把最终像素还原回原来大小的图像,并保存到帧缓存也就是显存中,替代原图像存储起来,最后输出到显示器,显示出一帧画面。这样就等于把一幅模糊的大图,通过细腻化后再缩小成清晰的小图。如果每帧都进行抗锯齿处理,游戏或视频中的所有画面都带有抗锯齿效果。[2]而将图像映射到缓存并把它放大时,放大的倍数被用于分别抗锯齿的效果,如:图1,AA后面的x2、x4、x8就是原图放大的倍数。
超级采样抗锯齿中使用的采样法一般有两种:
顺序栅格超级采样(Ordered Grid Super-Sampling,简称OGSS),采样时选取2个邻近像素。
旋转栅格超级采样(Rotated Grid Super-Sampling,简称RGSS),采样时选取4个邻近像素。

多重采样抗锯齿

多重采样抗锯齿(MultiSampling Anti-Aliasing,简称MSAA)是一种特殊的超级采样抗锯齿(SSAA)。MSAA首先来自于OpenGl。具体是MSAA只对Z缓存(Z-Buffer)和模板缓存(Stencil Buffer)中的数据进行超级采样抗锯齿的处理。可以简单理解为只对多边形的边缘进行抗锯齿处理。这样的话,相比SSAA对画面中所有数据进行处理,MSAA对资源的消耗
需求大大减弱,不过在画质上可能稍有不如SSAA。

覆盖采样抗锯齿

覆盖采样抗锯齿(CoverageSampling Anti-Aliasing,简称CSAA)

可编程过滤抗锯齿

可编程过滤抗锯齿(Custom Filter Anti-Aliasing)


2、多重采样抗锯齿技术详解

首先看下面这幅图,左右对比了采样多重采样抗锯齿技术和不采用的效果对比:

FBO中多重采样抗锯齿(MSAA:MultiSampling Anti-Aliasing)

图一 采样多重采样抗锯齿技术前后对比

可以看出,左图边缘有很明显的锯齿状。导致这一现象的原因是:每个像素的绘制是由它是否完全位于多边形内部所决定的。如果它在多边形内部,则渲染它;否则不渲染。很显然,这是不准确的。一些像素恰好位于边缘上面。如果我们依据一个像素它位于多边形内部的区域的大小来决定它的渲染,那么获得的效果要好很多。最终像素的颜色就是多边形颜色和其外部的颜色的混合。你也许会认为这样会导致性能上的消耗,事实却是如此。但是我们可以针对每个像素使用多个采样来近似估计最终结果。

MSAA技术包括针对每个像素实施多个采样,然后对这些采样的结果进行混合来决定这个像素的最终值。采样点位于像素内部的不同位置。很显然,大多数的采样点会位于多边形内部,但是对于那些位于多边形边缘的像素,一些采样点会位于多边形外部。

如果对每个像素进行4次采样,那么光栅化的频率将是不进行多重采样的4倍。对于每个像素,片断着色器执行一次,输出结果由4个采样点中位于多边形内部的数量决定。


3、在OpenGL中实现MSAA技术

在OpenGL中实施这一技术非常简单,不需要过多的操作。它是通过使用额外的缓冲区来存储子像素样本来实现的。然后这些样本被合成以生成片断的最终颜色。

下面以在Qt中为例来进行说明,它和使用GLUT等API很相似。

(1)在创建OpenGL窗口的时候,需要选择支持MSAA的OpenGL上下文:

QGLFormat format;
format.setVersion(4,0);
format.setProfile(QGLFormat::CoreProfile);
format.setSampleBuffers(true);
format.setSamples(4);
QGLWidget *glView = new QGLWidget(format);
 
 

EGL的配置方法


(2)判断多重采样缓冲区是否存在,以及对每个像素使用几个采样:

GLint bufs, samples;
glGetIntegerv(GL_SAMPLE_BUFFERS, &bufs);
glGetIntegerv(GL_SAMPLES, &samples);
printf("MSAA: buffers = %d samples = %d\n", bufs, samples);
 
 

(3)启用MSAA:

glEnable(GL_MULTISAMPLE);
 
 

(4)如果要禁用MSAA,使用以下代码:

glDisable(GL_MULTISAMPLE);

 
 

如果使用GLUT,则使用以下代码:

//申请一个采用了双重缓存,包含颜色,深度的帧缓存和多重采样。
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_MULTISAMPLE);
glEnable(GL_MULTISAMPLE);//开启多重缓存
glDisable(GL_MULTISAMPLE);//关闭多重缓存

 


 

二、在FBO中使用多重采样抗锯齿技术

首先我们需要创建1个适用于多重采样的FBO:

//创建FBO:multisampling
glGenFramebuffers(1,&m_frameBufferMS);
glBindFramebuffer(GL_FRAMEBUFFER,m_frameBufferMS);

glGenRenderbuffers(1,&m_renderBufferColorMS);
glBindRenderbuffer(GL_RENDERBUFFER,m_renderBufferColorMS);
glRenderbufferStorageMultisample(GL_RENDERBUFFER,4,
GL_RGB,m_subImageWidth,m_subImageHeight);
glBindRenderbuffer(GL_RENDERBUFFER,0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,m_renderBufferColorMS);

glGenRenderbuffers(1,&m_renderBufferDepthMS);
glBindRenderbuffer(GL_RENDERBUFFER,m_renderBufferDepthMS);
glRenderbufferStorageMultisample(GL_RENDERBUFFER,4,
GL_DEPTH_COMPONENT24,m_subImageWidth,m_subImageHeight);
glBindRenderbuffer(GL_RENDERBUFFER,0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER,m_renderBufferDepthMS);

glBindFramebuffer(GL_FRAMEBUFFER,0);

 
 
 

记住需要在应用程序后面进行清除操作:

glDeleteRenderbuffers(1,&m_renderBufferColorMS);
glDeleteRenderbuffers(1,&m_renderBufferDepthMS);
glDeleteFramebuffers(1,&m_frameBufferMS);
 
 
 

然后在绘制物体的时候进行绑定:

glBindFramebuffer(GL_FRAMEBUFFER,m_frameBufferMS);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
{
cout << "The frame buffer status is not complete!" << endl;
return;
}

drawing();//draw something
glBindFramebuffer(GL_FRAMEBUFFER,0);
 
 
 

接下来我要用函数glReadPixels读取数据,然后保存为BMP图像。在这里我使用了PBO(像素缓冲区对象)。在这里需要注意的是:不能用glReadPixels直接读取多重采样缓冲区里面的数据,否则会出现GL_INVALID_OPERATION错误。那么应该怎么做呢?

一个常用的方法是创建另外一个FBO。它是一个普通的FBO,用于进行传图操作(Blit).

创建用于Blit的普通FBO:

//创建普通FBO
glGenFramebuffers(1,&m_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER,m_frameBuffer);

glGenRenderbuffers(1,&m_renderBufferColor);
glBindRenderbuffer(GL_RENDERBUFFER,m_renderBufferColor);
glRenderbufferStorage(GL_RENDERBUFFER,GL_RGB,
m_subImageWidth,m_subImageHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,m_renderBufferColor);

glGenRenderbuffers(1,&m_renderBufferDepth);
glBindRenderbuffer(GL_RENDERBUFFER,m_renderBufferDepth);
glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT,
m_subImageWidth,m_subImageHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER,m_renderBufferDepth);

glBindFramebuffer(GL_FRAMEBUFFER,0); //解除绑定


 
 

记住也要进行清除操作:

glDeleteRenderbuffers(1,&m_renderBufferColor);
glDeleteRenderbuffers(1,&m_renderBufferDepth);
glDeleteFramebuffers(1,&m_frameBuffer);
 
 
 

然后绑定两个FBO,一个用于读,一个进行写入,进行blit操作:

glBindFramebuffer(GL_FRAMEBUFFER,0);
glBindFramebuffer(GL_READ_FRAMEBUFFER,m_frameBufferMS);
status = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
{
cout << "The frame buffer status is not complete!" << endl;
return;
}
glBindFramebuffer(GL_DRAW_FRAMEBUFFER,m_frameBuffer);
status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
{
cout << "The frame buffer status is not complete!" << endl;
return;
}

glBlitFramebuffer(0,0,m_subImageWidth,m_subImageHeight,
0,0,m_subImageWidth,m_subImageHeight,
GL_COLOR_BUFFER_BIT,GL_LINEAR);

glBindFramebuffer(GL_READ_FRAMEBUFFER,0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);
 
 
 

之后,绑定普通的FBO,读取像素:

glBindBuffer(GL_PIXEL_PACK_BUFFER,m_subImageBuffer);
glPixelStorei(GL_PACK_ALIGNMENT,1);
glBindFramebuffer(GL_FRAMEBUFFER,m_frameBuffer);
glReadPixels(0,0,m_subImageWidth,m_subImageHeight,
GL_BGR,GL_UNSIGNED_BYTE,bufferOffset(0));
 
 
 

这样所有的操作就完成了。

参考资料:

*问题1

*问题2

*问题3

stacioverflow问题4

GL_framebuffer_multisample详解


延伸阅读:

多重采样(MultiSample)下的FBO反锯齿

OpenGL 这两种Multisample方式(RenderBuffer/Texture)有什么区别和好处?

关于支持多重采样的FBO和Texture

iOS上FBO的MSAA源码