本文是 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
想了解更多信息,关注后反馈给我吧!
返回搜狐,查看更多
责任编辑:
声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。