添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

本文是 Piasy 原创,发表于 ,请阅读原文支持原创

在中,我主要分析了坐标系、基本绘制流程、绘制三角形、投影变换和相机视觉的参数意义,在本篇中,我将分析绘制矩形、绘制图片纹理、读取显存的内容,以及一些注意事项,完整代码可以在 。

1. 绘制矩形

上篇中有提到,三角形是基本形状,利用三角形我们可以“拼出”其他的任何形状,例如矩形。

根据 ,我们使用 glDrawElements 来绘制矩形。

绘制矩形时,我们除了需要一个数组保存顶点数据之外,还需要一个数组保存顶点的绘制顺序:

// ... private static final float [] VERTEX = { // in counterclockwise order: 1 , 1 , 0 , // top right - 1 , 1 , 0 , // top left - 1 , - 1 , 0 , // bottom left 1 , - 1 , 0 , // bottom right }; private static final short [] VERTEX_INDEX = { 0 , 1 , 2 , 0 , 2 , 3 }; MyRenderer () { mVertexBuffer = ByteBuffer . allocateDirect ( VERTEX . length * 4 ) . order ( ByteOrder . nativeOrder ()) . asFloatBuffer () . put ( VERTEX ); mVertexBuffer . position ( 0 ); mVertexIndexBuffer = ByteBuffer . allocateDirect ( VERTEX_INDEX . length * 2 ) . order ( ByteOrder . nativeOrder ()) . asShortBuffer () . put ( VERTEX_INDEX ); mVertexIndexBuffer . position ( 0 ); } // ...

在上面的代码中,VERTEX 保存了 4 个顶点的坐标,VERTEX_INDEX 保存了顶点的绘制顺序。0 -> 1 -> 2绘制的是 右上 -> 左上 -> 左下 上半个三角形,逆时针方向,而 0 -> 2 -> 3 则绘制的是 右上 -> 左下 -> 右下下半个三角形,也是逆时针方向,这两个三角形则“拼接”成了一个矩形。

顶点的绘制顺序重不重要?由于这里绘制的是纯颜色,看不出区别,在下面绘制图片纹理的时候,我发现,调换顺序似乎并没有影响,绘制的图片没有变化。

shader 代码、投影变换和相机视觉的逻辑都不需要更改,我们只需要改一下绘制时调用的函数即可:

@Override public void onDrawFrame ( GL10 unused ) { GLES20 . glClear ( GLES20 . GL_COLOR_BUFFER_BIT | GLES20 . GL_DEPTH_BUFFER_BIT ); GLES20 . glUseProgram ( mProgram ); GLES20 . glEnableVertexAttribArray ( mPositionHandle ); GLES20 . glVertexAttribPointer ( mPositionHandle , 3 , GLES20 . GL_FLOAT , false , 0 , mVertexBuffer ); GLES20 . glUniformMatrix4fv ( mMatrixHandle , 1 , false , mMVPMatrix , 0 ); // 用 glDrawElements 来绘制,mVertexIndexBuffer 指定了顶点绘制顺序 GLES20 . glDrawElements ( GLES20 . GL_TRIANGLES , VERTEX_INDEX . length , GLES20 . GL_UNSIGNED_SHORT , mVertexIndexBuffer ); GLES20 . glDisableVertexAttribArray ( mPositionHandle ); }

绘制效果图:

2. 绘制图片纹理

在绘制了矩形的基础上,我们更进一步,不再满足于绘制纯色纹理,而是绘制图片纹理。

2.1. 加载图片

首先我们需要加载图片并且保存在 OpenGL 纹理系统中:

@Override public void onSurfaceChanged ( GL10 unused , int width , int height ) { // ... mTexNames = new int [ 1 ]; GLES20 . glGenTextures ( 1 , mTexNames , 0 ); Bitmap bitmap = BitmapFactory . decodeResource ( mResources , R . drawable . p_300px ); GLES20 . glActiveTexture ( GLES20 . GL_TEXTURE0 ); GLES20 . glBindTexture ( GLES20 . GL_TEXTURE_2D , mTexNames [ 0 ]); GLES20 . glTexParameteri ( GLES20 . GL_TEXTURE_2D , GLES20 . GL_TEXTURE_MIN_FILTER , GLES20 . GL_LINEAR ); GLES20 . glTexParameteri ( GLES20 . GL_TEXTURE_2D , GLES20 . GL_TEXTURE_MAG_FILTER , GLES20 . GL_LINEAR ); GLES20 . glTexParameteri ( GLES20 . GL_TEXTURE_2D , GLES20 . GL_TEXTURE_WRAP_S , GLES20 . GL_REPEAT ); GLES20 . glTexParameteri ( GLES20 . GL_TEXTURE_2D , GLES20 . GL_TEXTURE_WRAP_T , GLES20 . GL_REPEAT ); GLUtils . texImage2D ( GLES20 . GL_TEXTURE_2D , 0 , bitmap , 0 ); bitmap . recycle (); // ... }

我们需要先通过 glGenTextures 创建纹理,再通过 glActiveTexture 激活指定编号的纹理,再通过glBindTexture 将新建的纹理和编号绑定起来。我们可以对图片纹理设置一系列参数,例如裁剪策略、缩放策略,这部分更详细的介绍,建议看看这本书,里面有很详细的讲解。最后,我们通过 texImage2D 把图片数据拷贝到纹理中。

2.2. shader 代码

此时,我们的 shader 代码当然也需要进行更改了:

private static final String VERTEX_SHADER = "uniform mat4 uMVPMatrix;" + "attribute vec4 vPosition;" + "attribute vec2 a_texCoord;" + "varying vec2 v_texCoord;" + "void main() {" + " gl_Position = uMVPMatrix * vPosition;" + " v_texCoord = a_texCoord;" + "}" ; private static final String FRAGMENT_SHADER = "precision mediump float;" + "varying vec2 v_texCoord;" + "uniform sampler2D s_texture;" + "void main() {" + " gl_FragColor = texture2D( s_texture, v_texCoord );" + "}" ;

这里出现了更多的关键字,uniform,attribute,varying,GLSL 并不是我关注的重点,不过这三者的区别可以看看,讲的非常清晰易懂:

uniform 由外部程序传递给 shader,就像是C语言里面的常量,shader 只能用,不能改;attribute 是只能在 vertex shader 中使用的变量;varying 变量是 vertex 和 fragment shader 之间做数据传递用的。

2.3. 绘制

首先我们需要指定截取纹理的哪一部分绘制到图形上:

private static final float [] UV_TEX_VERTEX = { // in clockwise order: 1 , 0 , // bottom right 0 , 0 , // bottom left 0 , 1 , // top left 1 , 1 , // top right }; MyRenderer ( Resources resources ) { // ... mUvTexVertexBuffer = ByteBuffer . allocateDirect ( UV_TEX_VERTEX . length * 4 ) . order ( ByteOrder . nativeOrder ()) . asFloatBuffer () . put ( UV_TEX_VERTEX ); mUvTexVertexBuffer . position ( 0 ); }

接着我们需要修改初始化和绘制的代码:

@Override public void onSurfaceChanged ( GL10 unused , int width , int height ) { mProgram = GLES20 . glCreateProgram (); int vertexShader = loadShader ( GLES20 . GL_VERTEX_SHADER , VERTEX_SHADER ); int fragmentShader = loadShader ( GLES20 . GL_FRAGMENT_SHADER , FRAGMENT_SHADER ); GLES20 . glAttachShader ( mProgram , vertexShader ); GLES20 . glAttachShader ( mProgram , fragmentShader ); GLES20 . glLinkProgram ( mProgram ); mPositionHandle = GLES20 . glGetAttribLocation ( mProgram , "vPosition" ); mTexCoordHandle = GLES20 . glGetAttribLocation ( mProgram , "a_texCoord" ); mMatrixHandle = GLES20 . glGetUniformLocation ( mProgram , "uMVPMatrix" ); mTexSamplerHandle = GLES20 . glGetUniformLocation ( mProgram , "s_texture" ); // ... } @Override public void onDrawFrame ( GL10 unused ) { GLES20 . glClear ( GLES20 . GL_COLOR_BUFFER_BIT | GLES20 . GL_DEPTH_BUFFER_BIT ); GLES20 . glUseProgram ( mProgram ); GLES20 . glEnableVertexAttribArray ( mPositionHandle ); GLES20 . glVertexAttribPointer ( mPositionHandle , 3 , GLES20 . GL_FLOAT , false , 0 , mVertexBuffer ); GLES20 . glEnableVertexAttribArray ( mTexCoordHandle ); GLES20 . glVertexAttribPointer ( mTexCoordHandle , 2 , GLES20 . GL_FLOAT , false , 0 , mUvTexVertexBuffer ); GLES20 . glUniformMatrix4fv ( mMatrixHandle , 1 , false , mMVPMatrix , 0 ); GLES20 . glUniform1i ( mTexSamplerHandle , 0 ); GLES20 . glDrawElements ( GLES20 . GL_TRIANGLES , VERTEX_INDEX . length , GLES20 . GL_UNSIGNED_SHORT , mVertexIndexBuffer ); GLES20 . glDisableVertexAttribArray ( mPositionHandle ); GLES20 . glDisableVertexAttribArray ( mTexCoordHandle ); }

绘制效果如下:

2.4. 纹理坐标系

在上篇中,我们首先了解了各种坐标系,其中就包括纹理坐标系。

二维坐标系,原点在左下角,s(x)轴向右,t(y)轴向上,x y 取值范围都是 [0, 1]:

我们在绘制时,UV_TEX_VERTEX 指定了截取纹理区域的坐标,上面的代码是使用完整的区域。如果我们把它改成这样:

private static final float [] UV_TEX_VERTEX = { // in clockwise order: 0.5f , 0 , // bottom right 0 , 0 , // bottom left 0 , 0.5f , // top left 0.5f , 0.5f , // top right };

这时绘制效果就成了这样子:

为什么截取的是左上角而不是左下角?这和上篇中提到的纹理坐标系不符呀!

在《OpenGL ES 2 for Android A Quick - Start Guide (2013)》这本书中,有这样一幅图:

看完之后我大概懂了,即便规定的是“原点在左下角,s(x)轴向右,t(y)轴向上”,但由于计算机中图片都是 y 轴向下,所以实际上依然是 原点在左上角,s(x)轴向右,t(y)轴向下 。这也就和实测效果一致了。

3. 读取显存

在 onDrawFrame 方法执行完毕之后(实际上是 glDrawElements 执行完毕之后),我们就可以从显存中读取帧数据了。这里我们利用 glReadPixels 方法读取数据:

static void sendImage ( int width , int height ) { ByteBuffer rgbaBuf = ByteBuffer . allocateDirect ( width * height * 4 ); rgbaBuf . position ( 0 ); long start = System . nanoTime (); GLES20 . glReadPixels ( 0 , 0 , width , height , GLES20 . GL_RGBA , GLES20 . GL_UNSIGNED_BYTE , rgbaBuf ); long end = System . nanoTime (); Log . d ( "TryOpenGL" , "glReadPixels: " + ( end - start )); saveRgb2Bitmap ( rgbaBuf , Environment . getExternalStorageDirectory (). getAbsolutePath () + "/gl_dump_" + width + "_" + height + ".png" , width , height ); } static void saveRgb2Bitmap ( Buffer buf , String filename , int width , int height ) { Log . d ( "TryOpenGL" , "Creating " + filename ); BufferedOutputStream bos = null ; try { bos = new BufferedOutputStream ( new FileOutputStream ( filename )); Bitmap bmp = Bitmap . createBitmap ( width , height , Bitmap . Config . ARGB_8888 ); bmp . copyPixelsFromBuffer ( buf ); bmp . compress ( Bitmap . CompressFormat . PNG , 90 , bos ); bmp . recycle (); } catch ( IOException e ) { e . printStackTrace (); } finally { if ( bos != null ) { try { bos . close (); } catch ( IOException e ) { e . printStackTrace (); } } } }

我们把保存的图片导出查看:

结果却是倒的!

这里又涉及到上篇中所说的坐标系的问题了,OpenGL 的坐标系和安卓手机的坐标系的 y 轴是相反的,所以即便我们在屏幕上看起来是正常的,一旦导出帧数据保存为图片,它看起来还是倒的!所以我们在拿到帧数据之后,需要进行处理,而且不是简单的旋转操作,因为这个颠倒,是由于图像沿着 x 轴旋转了 180° 而不是沿着 z 轴旋转了 180° !

还有一点值得一提,glReadPixels 函数非常耗时,上面的例子中,读取 996*1500 的数据,平均需要 33ms。iOS 系统有一个 CVOpenGLESTextureCacheCreateTextureFromImage 方法,可以更高效地实现显存和内存数据的共享(传输),性能比 glReadPixels 高很多。安卓平台就很无奈啦 =_=

4. 注意事项

为了避免 activity pause 之后进行不必要的渲染,我们可以在 activity 的回调中调用 GLSurfaceView 的相应方法进行控制,而在 activity 销毁时,我们需要销毁 OpenGL 纹理:

private boolean mRendererSet ; private GLSurfaceView mGlSurfaceView ; private MyRenderer mRenderer ; @Override protected void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ); setContentView ( R . layout . activity_main ); mGlSurfaceView = ( GLSurfaceView ) findViewById ( R . id . mGLSurfaceView ); mGlSurfaceView . setEGLContextClientVersion ( 2 ); mRenderer = new MyRenderer ( getResources ()); mGlSurfaceView . setRenderer ( mRenderer ); mGlSurfaceView . setRenderMode ( GLSurfaceView . RENDERMODE_CONTINUOUSLY ); mRendererSet = true ; } @Override protected void onPause () { super . onPause (); if ( mRendererSet ) { mGlSurfaceView . onPause (); } } @Override protected void onResume () { super . onResume (); if ( mRendererSet ) { mGlSurfaceView . onResume (); } } @Override protected void onDestroy () { super . onDestroy (); mRenderer . destroy (); } static class MyRenderer implements GLSurfaceView . Renderer { // ... void destroy () { GLES20 . glDeleteTextures ( 1 , mTexNames , 0 ); } // ... } 5. 小结

在本篇中,我分析了绘制矩形、绘制图片纹理、读取显存的内容,以及一些注意事项。关于 GLSL 基本还是没有涉及,纹理参数的内容也没有展开,这些内容在《OpenGL ES 2 for Android A Quick - Start Guide (2013)》这本书都有详细的讲解,感兴趣的朋友可以继续深入。另外,这两篇文章的内容也受到了 系列文章的启发。

来源:Android程序员公众号

Imagination中文社区

权威发布有关Imagination公司CPU,GPU以及连接IP、无线IP最新资讯,提供有关物联网、可穿戴、通信、汽车电子、医疗电子等应用信息,每日更新大量信息,让你紧跟技术发展,欢迎免费注册。网址:imgtec.eetrend.com

想了解更多信息,关注后反馈给我吧! 返回搜狐,查看更多

责任编辑:

声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。