为了开启一个Demo的抗锯齿,OpenGL基础薄弱的我研究了半个多月。终于在同事的帮助指导下实现了。以下对三种情况的抗锯齿做个简单的介绍。他们分别是最简单的纯 Java 环境下的抗锯齿,Jni 中的抗锯齿,以及离屏渲染中的抗锯齿。
忽然想起来,在学习如何开启抗锯齿时,在网上找到了不少资料,可能是他们更偏重于 OpenGL 而非 OpenGLES 的原因,一直提到的一个概念“用 10 以上的 gl_PointSize 绘制几个 GL_POINTS, 他们将呈现为小矩形色块,而开启抗锯齿后,将成为圆形色块”。这个概念我还没来得及在 OpenGL + Windows 的环境下测试,但是经过试验,在 Android + OpenGLES 的环境下是 不能 把矩形 GL_POINTS 抗锯齿优化成圆形的。而绘制简单的 2D 图形,得益于高分屏,在不开抗锯齿的情况下,也很难看到严重的锯齿,因此,拿普通的 2D 图形来测试抗锯齿也不理想。因此,在屏幕中央绘制一个 3D Cube 或许会更好的看到锯齿和抗锯齿的效果。
由于我刚开始接触 OpenGL,技术渣,就不提供测试 Demo 了。以下文中用到的是公司里的一个 Demo ,在 Mentor 的指导下,我修修补补才从最原始的工程里移植到以下两个情况中的,所以那个图形看起来很糟糕,但是,可以看到锯齿和抗锯齿的效果,这对于这篇文章而言也算是足够了。
这种情况是最简单的了,只需要在 GLSurfaceView
类前添加一个 EGLConfigChooser
类,并在初始化阶段调用一下就好。EGLConfigChooser
类代码如下:
class MyConfigChooser implements GLSurfaceView.EGLConfigChooser {
@Override
public EGLConfig chooseConfig(EGL10 egl,
javax.microedition.khronos.egl.EGLDisplay display) {
int attribs[] = {
EGL10.EGL_LEVEL, 0,
EGL10.EGL_RENDERABLE_TYPE, 4, // EGL_OPENGL_ES2_BIT
EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RGB_BUFFER,
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_DEPTH_SIZE, 16,
EGL10.EGL_SAMPLE_BUFFERS, 1,
EGL10.EGL_SAMPLES, 4, // 在这里修改MSAA的倍数,4就是4xMSAA,再往上开程序可能会崩
EGL10.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] configCounts = new int[1];
egl.eglChooseConfig(display, attribs, configs, 1, configCounts);
if (configCounts[0] == 0) {
// Failed! Error handling.
return null;
} else {
return configs[0];
}
}
}
其中,前三个属性可以被注释掉,不影响效果。
最重要的两句就是
EGL10.EGL_SAMPLE_BUFFERS, 1,
用来打开多采样抗锯齿,1更标准的,应该写成 GL_TRUE
;
EGL10.EGL_SAMPLES, 4,
用来指定采样器的个数,一般来说,移动设备开到 4 基本上是极限了,也有极少数开到 2 或者 8 是极限的。这里其实可以用 GLint maxSamples
变量来代替数字4,而 GLES30.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples)
可以获取设备支持的最大抗锯齿数值。
然后,在GLSurfaceView初始化中调用setEGLConfigChooser()
代码来实现上面类的调用。
public class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context) {
super(context);
// Create an OpenGL ES 2.0 context. CHANGED to 3.0 JW.
setEGLContextClientVersion(3);
setEGLConfigChooser(new MyConfigChooser()); // 注意在 setRenderer 之前调用
mRenderer = new MyGLRenderer();
setRenderer(mRenderer);
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
可以通过将 EGL10.EGL_SAMPLE_BUFFERS, 1, EGL10.EGL_SAMPLES, 4,
两行注释掉来对比开启抗锯齿和关闭抗锯齿的效果。以下两张图是运行在我的小米4C上的Demo截屏,在Windows上用美图看看放大到165%倍看起来的效果。
未开启抗锯齿
开启了4x MSAA 抗锯齿
其实这种抗锯齿实现起来也很方便,甚至比第一种情况还要方便。因为 Native EGL 中总要自己来初始化 GL Context,调用一堆初始化的 gl 函数,例如 eglGetDisplay()
,eglInitialize()
, eglGetConfigs()
等等,因此,必然也会有一个 attribs 数组变量,只需在这个数组变量中添加 上面提到的两句属性即可。
const EGLint attribsMSAA[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_SAMPLE_BUFFERS, 1,
EGL_SAMPLES, 4,
EGL_NONE
};
const EGLint attribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_NONE
};
然后在初始化过程中适当地传入不同的数组变量即可实现抗锯齿的开启与关闭。
if (OPENMSAA) {
//Get max number of configs choosed
if (!eglChooseConfig(_display, attribsMSAA, NULL, 0, &numConfigs)) {
LOG_ERROR("eglChooseConfig() returned error %d", eglGetError());
destroy();
return false;
} else {
LOG_INFO("eglChooseConfig get config max number: %d", numConfigs);
}
//Just use the first one as our config
if (!eglChooseConfig(_display, attribsMSAA, &config, 1, &numConfigs)) {
LOG_ERROR("eglChooseConfig() returned error %d", eglGetError());
destroy();
return false;
} else {
LOG_INFO("eglChooseConfig get config number: %d", numConfigs);
}
} else {
//Get max number of configs choosed
if (!eglChooseConfig(_display, attribs, NULL, 0, &numConfigs)) {
LOG_ERROR("eglChooseConfig() returned error %d", eglGetError());
destroy();
return false;
} else {
LOG_INFO("eglChooseConfig get config max number: %d", numConfigs);
}
//Just use the first one as our config
if (!eglChooseConfig(_display, attribs, &config, 1, &numConfigs)) {
LOG_ERROR("eglChooseConfig() returned error %d", eglGetError());
destroy();
return false;
} else {
LOG_INFO("eglChooseConfig get config number: %d", numConfigs);
}
}
这样,只要改变 bool 变量 OPENMSAA 的值即可实现抗锯齿的开闭。
开启了 4x MSAA
未开启MSAA
以上两种情况其实大同小异,均为 GLContext 的抗锯齿,只适用于通过 glDraw()
渲染到 FrameBuffer,然后通过 eglSwapBuffer()
上屏的直接画屏的情况。而实际应用中,更多的是先渲染到一张 Texture2D
上,再把这张 Texture2D
绘制到 Framebuffer0 上进行上屏操作。这样一来,简单的 Context 抗锯齿就失效了。因此,这时将需要一种比较复杂的抗锯齿方法。其实 khronos.org 已经将这种方法进行了很好的描述。主要包括 IMG 和 EXT 扩展两种。这里,我采用了 IMG 扩展的方案。
采用这种方法最主要的是添加 IMG 或者 EXT 扩展,其代码示意如下:
// xx.h
#include
extern PFNGLRENDERBUFFERSTORAGEMULTISAMPLEIMG glRenderbufferStorageMultisampleIMG_;
extern PFNGLFRAMEBUFFERTEXTURE2DMULTISAMPLEIMG glFramebufferTexture2DMultisampleIMG_;
// xx.cpp
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEIMG glRenderbufferStorageMultisampleIMG_;
PFNGLFRAMEBUFFERTEXTURE2DMULTISAMPLEIMG glFramebufferTexture2DMultisampleIMG_;
if ( GL_ExtensionStringPresent( "GL_IMG_multisampled_render_to_texture", extensions ) )
{
IMG_multisampled_render_to_texture = true;
glRenderbufferStorageMultisampleIMG_ = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEIMG)GetExtensionProc ( "glRenderbufferStorageMultisampleIMG" );
glFramebufferTexture2DMultisampleIMG_ = (PFNGLFRAMEBUFFERTEXTURE2DMULTISAMPLEIMG)GetExtensionProc ( "glFramebufferTexture2DMultisampleIMG" );
}
else if ( GL_ExtensionStringPresent( "GL_EXT_multisampled_render_to_texture", extensions ) )
{
// assign to the same function pointers as the IMG extension
IMG_multisampled_render_to_texture = true;
glRenderbufferStorageMultisampleIMG_ = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEIMG)GetExtensionProc ( "glRenderbufferStorageMultisampleEXT" );
glFramebufferTexture2DMultisampleIMG_ = (PFNGLFRAMEBUFFERTEXTURE2DMULTISAMPLEIMG)GetExtensionProc ( "glFramebufferTexture2DMultisampleEXT" );
}
添加完对扩展函数的调用之后,在 OpenGL 创建 FrameBuffer 时,代码如下
#include "xx.h"
#define CONST_SAMPLES 4
.....
void Renderer::CreateBuffers(GLuint * tex, GLuint * fb, GLuint * rb)
{
#ifdef GL_MSAA
glGenRenderbuffers(1, rb);
glBindRenderbuffer(GL_RENDERBUFFER, *rb);
glRenderbufferStorageMultisampleIMG_(GL_RENDERBUFFER, CONST_SAMPLES,
GL_DEPTH_COMPONENT16, w, h); // w width, h height.
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glGenTextures(1, tex);
glBindTexture(GL_TEXTURE_2D, *tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA,
GL_UNSIGNED_BYTE, NULL);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, fb);
glBindFramebuffer(GL_FRAMEBUFFER, *fb);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, *rb);
glFramebufferTexture2DMultisampleIMG_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, *tex, 0, CONST_SAMPLES);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE)
{
LOG("framebuffer not complete");
}
else
{
LOG("Framebuffer complete.");
}
#else
glGenTextures( 1, tex );
glBindTexture( GL_TEXTURE_2D, *tex );
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0 );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glGenFramebuffers( 1, fb );
glGenRenderbuffers ( 1, rb );
glBindRenderbuffer ( GL_RENDERBUFFER, *rb );
glRenderbufferStorage ( GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, w, h );
glBindFramebuffer( GL_FRAMEBUFFER, *fb );
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, *tex, 0 );
glFramebufferRenderbuffer ( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, *rb);
glBindTexture( GL_TEXTURE_2D, 0 );
glBindFramebuffer( GL_FRAMEBUFFER, 0 );
#endif
}
其实与非抗锯齿的代码相比,就只有少数的几句不同。
创建好 Buffer 之后,在绘图之前,绑定 Texture 时需要修改一下调用的函数:
glBindFramebuffer( GL_FRAMEBUFFER, fb );
// glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
// GL_TEXTURE_2D, tex, 0 );
glFramebufferTexture2DMultisampleIMG_( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, tex, 0, CONST_SAMPLES );
glDraw(.....);
glBindFramebuffer( GL_FRAMEBUFFER, 0 );
在调用 glDraw 之前,绑定Texture时用 glFramebufferTexture2DMultisampleIMG_
代替原有的 glFramebufferTexture2D
即可,这里的 ONST_SAMPLES
是 samples ,上面有宏定义。
这样一来,就把需要绘制的图像以 CONST_SAMPLES
倍抗锯齿的形式绘制到了这张带有 多采样的 Texture2D 上了。
最后,再把这张 texture 绘制到屏上即可。
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glBindTexture(GL_TEXTURE_2D, tex);
至此,离屏渲染下的抗锯齿即算成功。
开启了抗锯齿
未开启抗锯齿
当然,虽然写这些内容只用了一晚上的时间,但是整个探索过程充满了坎坷。例如 jni 中如何调用C++ 11 以及 STL,例如很多 gl 函数在普通 OpenGL 里有,但在 OpenGLES 里却没有, 同一个项目,放到了不同的位置就编译不过或者编译过了运行报错。简直是各种妖魔鬼怪啊。。。
好在总算是把这件事搞定了,在这个过程中,也对 OpenGLES 有了更深的了解,对 ndk,Log 等程序调试方法也更加熟悉了。最后。。。。
欢迎关注我的个人公众号 VR_Tech。
刚刚起步。