【Android 音视频开发打怪升级:OpenGL渲染视频画面篇
【声 明】
首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。
码字不易,转载请注明出处!
教程代码:【Github传送门】 |
---|
目录
一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染视频画面
- 3,OpenGL渲染多视频,实现画中画
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO数据缓冲区
- 6,Android音视频硬编码:生成一个MP4
三、Android FFmpeg音视频解码篇
- 1,FFmpeg so库编译
- 2,Android 引入FFmpeg
- 3,Android FFmpeg视频解码播放
- 4,Android FFmpeg+OpenSL ES音频解码播放
- 5,Android FFmpeg+OpenGL ES播放视频
- 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
- 7,Android FFmpeg视频编码
本文你可以了解到
本文主要介绍OpenGL相关的基础知识,包括坐标系、着色器、基本渲染流程等。
一 简介
提到OpenGL,想必很多人都会说,我知道这个东西,可以用来渲染2D画面和3D模型,同时又会说,OpenGL很难、很高级,不知道怎么用。
1、为什么OpenGL“感觉很难”?
- 函数多且杂,渲染流程复杂
- GLSL着色器语言不好理解
- 面向过程的编程思维,和Java等面向对象的编程思维不同
2、OpenGL ES是什么?
为了解决以上问题,让OpenGL“学起来不是很难”,需要把其分解成一些简单的步骤,然后简单的东西串联起来,一切就水到渠成了。
首先,来看看什么是OpenGL。
- CPU和GPU
在手机上,有两大元件,一个是CPU,一个是GPU。而手机上显示图形界面也有两种方式,一个是使用CPU来渲染,一个是使用GPU来渲染,可以说,GPU渲染其实是一种硬件加速。
为什么GPU可以大大提高渲染速度,因为GPU最擅长的是并行浮点运算,可以用来对许许多多的像素做并行运算。
OpenGL(Open Graphics Library)则是间接操作GPU的工具,是一组定义好的跨平台和跨语言的图形API,是可用于2D和3D画面渲染的底层图形库,是由各个硬件厂家具体实现的编程接口。
- OpenGL 与 OpenGL ES
OpenGL ES 全称:OpenGL for Embedded Systems,是OpenGL 的子集,是针对手机 PAD等小型设备设计的,删减了不必须的方法、数据类型、功能,减少了体积,优化了效率。
3、 OpenGL ES版本
目前主要版本有1.0/1.1/2.0/3.0/3.1
- 1.0:Android 1.0和更高的版本支持这个API规范
- 2.0:不兼容 OpenGL ES 1.x。Android 2.2(API 8)和更高的版本支持这个API规范
- 3.0:向下兼容 OpenGL ES 2.x。Android 4.3(API 18)及更高的版本支持这个API规范
- 3.1:向下兼容 OpenGL ES3.0/2.0。Android 5.0(API 21)和更高的版本支持这个API规范
2.0 版本是 Android 目前支持最广泛的版本,后续主要以该版本为主,进行介绍和代码编写。
二、OpenGL ES坐标系
在音视频开发中,涉及到的坐标系主要有两个:世界坐标和纹理坐标。
由于基本不涉及3D贴图,所以只看x/y轴坐标,忽略z轴坐标,涉及到3D相关知识可自行Google,不在讨论范围内。
首先来看两个图:
世界坐标 纹理坐标- OpenGL ES世界坐标
通过名字就可以知道,这是OpenGL自己世界的坐标,是一个标准化坐标系,范围是 -1 ~ 1,原点在中间。
- OpenGL ES纹理坐标
纹理坐标,其实就是屏幕坐标,标准的纹理坐标原点是在屏幕的左下方,而Android系统坐标系的原点是在左上方的。这是Android使用OpenGL需要注意的一个地方。
纹理坐标的范围是 0 ~ 1。
注:坐标系的xy轴方向很重要,决定了如何做顶点坐标和纹理坐标映射。
那么,这两个坐标系究竟有什么关系呢?
世界坐标,是用于显示的坐标,即像素点应该显示在哪个位置由世界坐标决定。
纹理坐标,表示世界坐标指定的位置点想要显示的颜色,应该在纹理上的哪个位置获取。即颜色所在的位置由纹理坐标决定。
两者之间需要做正确的映射,才能正常的显示一张画面。
三、OpenGL 着色器语言 GLSL
在OpenGL 2.0以后,加入了新的可编程渲染管线,可以更加灵活的控制渲染。但也因此需要学习多一门针对GPU的编程语言,语法与C语言类似,名为GLSL。
- 顶点着色器 & 片元着色器
在介绍GLSL之前,先来看两个比较陌生的名词:顶点着色器和片元着色器。
着色器,是一种可运行在GPU上的小程序,用GLSL语言编写。从命名上,顶点着色器是用于操控顶点的程序,而片元着色器是用于操控像素颜色属性的程序。
简单理解:其实就是对应了以上两个坐标系:顶点着色器对应世界坐标,片元着色器对应纹理坐标。
画面上的每个点,都会执行一次顶点和片元着色器中的程序片段,并且是并行执行,最后渲染到屏幕上。
- GLSL编程
下面,通过一个最简单的顶点着色器和片元着色器来简单介绍一下GLSL语言
#顶点着色器
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
}
#片元着色器
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
}
首先可以看到,GLSL语言是一种类C语言,着色器的框架基本和C语言一样,在最上面声明变量,接着是main函数。在着色器中,有几个内建的变量,可以直接使用(这里只列出音视频开发常用的,还有其他的一些3D开发会用到的):
-
顶点着色器的内建输入变量
gl_Position:顶点坐标
gl_PointSize:点的大小,没有赋值则为默认值1
-
片元着色器内建输出变量
gl_FragColor:当前片元颜色
看回上面的着色器代码。
1)在顶点着色器中,传入了一个vec4的顶点坐标xyzw,然后直接传递给内建变量gl_Position,即直接根据顶点坐标渲染,不再做位置变换。
注:顶点坐标是在Java代码中传入的,后面会讲到,另外w是齐次坐标,2D渲染没有作用
2)在片元着色器中,直接给gl_FragColor赋值,依然是一个vec4类型的数据,这里表示rgba颜色值,为红色。
可以看到vec4是一个4维向量,可用于表示坐标xyzw,也可用表示rgba,当然还有vec3,vec2等,可以参考这篇文章:着色器语言GLSL,讲的非常详细,建议看看。
这样,两个简单的着色器串联起来后,每一个顶点(像素)都会显示一个红点,最后屏幕会显示一个红色的画面。
具体GLSL关于数据类型和语法不再展开介绍,后面涉及到的GLSL代码会做更深入的讲解。更详细的可以参考这位作者的文章【着色器语言GLSL】,非常详尽。
四、Android OpenGL ES渲染流程
OpenGL的渲染流程说实话是比较繁琐的,也是让很多人望而生畏的地方,但是,如果归结起来,其实整个渲染流程基本是固定的,只要把它按照固定的流程封装好,其实并没有那么复杂。
接下来,就进入实战,一层一层扒开OpengGL的神秘面纱。
1、初始化
在Android中,OpenGL通常配合GLSurfaceView使用,在GLSurfraceView中,Google已经封装好了渲染的基础流程。
这里需要单独强调一下,OpenGL是基于线程的一个状态机,有关OpenGL的操作,比如创建纹理ID,初始化,渲染等,都必须要在同一个线程中完成,否则会造成异常。
通常开发者在刚刚接触OpenGL的时候并不能深刻体会到这种机制,原因是Google在GLSurfaceView中已经帮开发者做了这部分的内容。这是OpenGL非常重要的一个方面,在后续的有关EGL的文章中会继续深入了解到。
- 新建页面
class SimpleRenderActivity : AppCompatActivity() {
//自定义的OpenGL渲染器,详情请继续往下看
lateinit var drawer: IDrawer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simpler_render)
drawer = if (intent.getIntExtra("type", 0) == 0) {
TriangleDrawer()
} else {
BitmapDrawer(BitmapFactory.decodeResource(CONTEXT!!.resources, R.drawable.cover))
}
initRender(drawer)
}
private fun initRender(drawer: IDrawer) {
gl_surface.setEGLContextClientVersion(2)
gl_surface.setRenderer(SimpleRender(drawer))
}
override fun onDestroy() {
drawer.release()
super.onDestroy()
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.opengl.GLSurfaceView
android:id="@+id/gl_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
页面非常简单,放置了一个满屏的GLSurfaceView,初始化的时候,设置了OpenGL使用的版本为2.0,然后配置了渲染器SimpleRender,继承自GLSurfaceView.Renderer
IDrawer将在绘制三角形的时候具体讲解,定义该接口类只是为了方便拓展,也可以直接将渲染代码写在SimpleRender中。
- 实现渲染接口
class SimpleRender(private val mDrawer: IDrawer): GLSurfaceView.Renderer {
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
mDrawer.setTextureID(OpenGLTools.createTextureIds(1)[0])
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
mDrawer.draw()
}
}
注意到,实现了三个回调接口,这三个接口就是Google封装好的流程中,暴露出来的接口,留给给开发者实现初始化和渲染,并且这三个接口的回调都在同一个线程中。
- 在onSurfaceCreated中,调用了两句OpenGL ES的代码实现清屏,清屏颜色为黑色。
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
同时,创建了一个纹理ID,并设置给Drawer,如下:
fun createTextureIds(count: Int): IntArray {
val texture = IntArray(count)
GLES20.glGenTextures(count, texture, 0) //生成纹理
return texture
}
- 在onSurfaceChanged中,调用glViewport,设置了OpenGL绘制的区域宽高和位置
这里所说的绘制区域,是指OpenGL在GLSurfaceView中的绘制区域,一般都是全部铺满。
GLES20.glViewport(0, 0, width, height)
- 在onDrawFrame中,就是真正实现绘制的地方了。该接口会不停的回调,刷新绘制区域。这里使用一个简单的三角形绘制来说明整个绘制流程。
2、渲染一个简单的三角形
先定义一个渲染接口类:
interface IDrawer {
fun draw()
fun setTextureID(id: Int)
fun release()
}
class TriangleDrawer(private val mTextureId: Int = -1): IDrawer {
//顶点坐标
private val mVertexCoors = floatArrayOf(
-1f, -1f,
1f, -1f,
0f, 1f
)
//纹理坐标
private val mTextureCoors = floatArrayOf(
0f, 1f,
1f, 1f,
0.5f, 0f
)
//纹理ID
private var mTextureId: Int = -1
//OpenGL程序ID
private var mProgram: Int = -1
// 顶点坐标接收者
private var mVertexPosHandler: Int = -1
// 纹理坐标接收者
private var mTexturePosHandler: Int = -1
private lateinit var mVertexBuffer: FloatBuffer
private lateinit var mTextureBuffer: FloatBuffer
init {
//【步骤1: 初始化顶点坐标】
initPos()
}
private fun initPos() {
val bb = ByteBuffer.allocateDirect(mVertexCoors.size * 4)
bb.order(ByteOrder.nativeOrder())
//将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
mVertexBuffer = bb.asFloatBuffer()
mVertexBuffer.put(mVertexCoors)
mVertexBuffer.position(0)
val cc = ByteBuffer.allocateDirect(mTextureCoors.size * 4)
cc.order(ByteOrder.nativeOrder())
mTextureBuffer = cc.asFloatBuffer()
mTextureBuffer.put(mTextureCoors)
mTextureBuffer.position(0)
}
override fun setTextureID(id: Int) {
mTextureId = id
}
override fun draw() {
if (mTextureId != -1) {
//【步骤2: 创建、编译并启动OpenGL着色器】
createGLPrg()
//【步骤3: 开始渲染绘制】
doDraw()
}
}
private fun createGLPrg() {
if (mProgram == -1) {
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())
//创建OpenGL ES程序,注意:需要在OpenGL渲染线程中创建,否则无法渲染
mProgram = GLES20.glCreateProgram()
//将顶点着色器加入到程序
GLES20.glAttachShader(mProgram, vertexShader)
//将片元着色器加入到程序中
GLES20.glAttachShader(mProgram, fragmentShader)
//连接到着色器程序
GLES20.glLinkProgram(mProgram)
mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
}
//使用OpenGL程序
GLES20.glUseProgram(mProgram)
}
private fun doDraw() {
//启用顶点的句柄
GLES20.glEnableVertexAttribArray(mVertexPosHandler)
GLES20.glEnableVertexAttribArray(mTexturePosHandler)
//设置着色器参数
GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer)
GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer)
//开始绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}
override fun release() {
GLES20.glDisableVertexAttribArray(mVertexPosHandler)
GLES20.glDisableVertexAttribArray(mTexturePosHandler)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
GLES20.glDeleteTextures(1, intArrayOf(mTextureId), 0)
GLES20.glDeleteProgram(mProgram)
}
private fun getVertexShader(): String {
return "attribute vec4 aPosition;" +
"void main() {" +
" gl_Position = aPosition;" +
"}"
}
private fun getFragmentShader(): String {
return "precision mediump float;" +
"void main() {" +
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
"}"
}
private fun loadShader(type: Int, shaderCode: String): Int {
//根据type创建顶点着色器或者片元着色器
val shader = GLES20.glCreateShader(type)
//将资源加入到着色器中,并编译
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
}
虽然只是画一个简单的三角形,代码依然看起来很复杂。这里把它拆解为三个步骤,就比较清晰明了了。
1) 初始化顶点坐标
前面我们讲到OpenGL的世界坐标和纹理坐标,在绘制前就需要先把这两个坐标确定好。
【重要提示】
有一点还没说的是,OpenGL ES所有的画面都是由三角形构成的,比如一个四边形由两个三角形构成,其他更复杂的图形也都可以分割为大大小小的三角形。
因此,顶点坐标也是根据三角形的连接来设置的。其绘制方式有三种:
- GL_TRIANGLES:独立顶点的构成三角形
- GL_TRIANGLE_STRIP:复用顶点构成三角形
- GL_TRIANGLE_FAN:复用第一个顶点构成三角形
通常情况下,一般使用GL_TRIANGLE_STRIP绘制模式。那么一个四边形的顶点顺序看起来是这样子的(v1-v2-v3)(v2-v3-v4)
顶点坐标顺序对应的纹理坐标也要和顶点坐标顺序一致,否则会出现颠倒,变形等异常
纹理坐标顺序由于绘制的是三角形,所以两个坐标如下(这里只设置xy轴坐标,忽略z轴坐标,每两个数据构成一个坐标点):
//顶点坐标
private val mVertexCoors = floatArrayOf(
-1f, -1f,
1f, -1f,
0f, 1f
)
//纹理坐标
private val mTextureCoors = floatArrayOf(
0f, 1f,
1f, 1f,
0.5f, 0f
)
在initPos方法中,由于底层不能直接接收数组,所以将数组转换为ByteBuffer
2) 创建、编译并启动OpenGL着色器
private fun createGLPrg() {
if (mProgram == -1) {
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())
//创建OpenGL ES程序,注意:需要在OpenGL渲染线程中创建,否则无法渲染
mProgram = GLES20.glCreateProgram()
//将顶点着色器加入到程序
GLES20.glAttachShader(mProgram, vertexShader)
//将片元着色器加入到程序中
GLES20.glAttachShader(mProgram, fragmentShader)
//连接到着色器程序
GLES20.glLinkProgram(mProgram)
mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
}
//使用OpenGL程序
GLES20.glUseProgram(mProgram)
}
private fun getVertexShader(): String {
return "attribute vec4 aPosition;" +
"void main() {" +
" gl_Position = aPosition;" +
"}"
}
private fun getFragmentShader(): String {
return "precision mediump float;" +
"void main() {" +
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
"}"
}
private fun loadShader(type: Int, shaderCode: String): Int {
//根据type创建顶点着色器或者片元着色器
val shader = GLES20.glCreateShader(type)
//将资源加入到着色器中,并编译
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
上面已经说过,GLSL是针对GPU的编程语言,而着色器就是一段小程序,为了能够运行这段小程序,需要先对其进行编译和绑定,才能使用。
本例中的着色器就是上文提到的最简单的着色器。
可以看到,着色器其实就是一段字符串
进入loadShader中,通过GLES20.glCreateShader,根据不同类型,获取顶点着色器和片元着色器。
然后调用以下方法,编译着色器
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
编译好着色器以后,就是绑定,连接,启用程序即可。
还记得上面说过,着色器中的坐标是由Java传递给GLSL吗?
细心的你可能发现了这两句代码
mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
没错,这就是Java和GLSL交互的通道,通过属性可以给GLSL设置相关的值。
3) 开始渲染绘制
private fun doDraw() {
//启用顶点的句柄
GLES20.glEnableVertexAttribArray(mVertexPosHandler)
GLES20.glEnableVertexAttribArray(mTexturePosHandler)
//设置着色器参数, 第二个参数表示一个顶点包含的数据数量,这里为xy,所以为2
GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer)
GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer)
//开始绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3)
}
首先激活着色器的顶点坐标和纹理坐标属性,然后把初始化好的坐标传递给着色器,最后启动绘制:
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3)
绘制有两种方式:glDrawArrays和glDrawElements,两者区别在于glDrawArrays是直接使用定义好的顶点顺序进行绘制;而glDrawElements则是需要定义另外的索引数组,来确认顶点的组合和绘制顺序。
通过以上步骤,就可以在屏幕上看到一个红色的三角形了。
三角形可能有人就有疑问了:绘制三角形的时候只是直接设置了像素点的颜色值,并没有用到纹理,纹理到底有什么用呢?
接下来,就用纹理来显示一张图片,看看纹理到底怎么使用。
建议先看清楚绘制三角形的流程,绘制图片就是基于以上流程,重复代码就不再贴出。
3、纹理贴图,显示一张图片
以下只贴出和绘制三角形不一样的部分代码,详细代码请看源码。
class BitmapDrawer(private val mTextureId: Int, private val mBitmap: Bitmap): IDrawer {
//-------【注1:坐标变更了,由四个点组成一个四边形】-------
// 顶点坐标
private val mVertexCoors = floatArrayOf(
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
)
// 纹理坐标
private val mTextureCoors = floatArrayOf(
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
)
//-------【注2:新增纹理接收者】-------
// 纹理接收者
private var mTextureHandler: Int = -1
fun draw() {
if (mTextureId != -1) {
//【步骤2: 创建、编译并启动OpenGL着色器】
createGLPrg()
//-------【注4:新增两个步骤】-------
//【步骤3: 激活并绑定纹理单元】
activateTexture()
//【步骤4: 绑定图片到纹理单元】
bindBitmapToTexture()
//----------------------------------
//【步骤5: 开始渲染绘制】
doDraw()
}
}
private fun createGLPrg() {
if (mProgram == -1) {
//省略与绘制三角形一致的部分
//......
mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
//【注3:新增获取纹理接收者】
mTextureHandler = GLES20.glGetUniformLocation(mProgram, "uTexture")
}
//使用OpenGL程序
GLES20.glUseProgram(mProgram)
}
private fun activateTexture() {
//激活指定纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
//绑定纹理ID到纹理单元
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId)
//将激活的纹理单元传递到着色器里面
GLES20.glUniform1i(mTextureHandler, 0)
//配置边缘过渡参数
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}
private fun bindBitmapToTexture() {
if (!mBitmap.isRecycled) {
//绑定图片到被激活的纹理单元
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0)
}
}
private fun doDraw() {
//省略与绘制三角形一致的部分
//......
//【注5:绘制顶点加1,变为4】
//开始绘制:最后一个参数,将顶点数量改为4
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}
private fun getVertexShader(): String {
return "attribute vec4 aPosition;" +
"attribute vec2 aCoordinate;" +
"varying vec2 vCoordinate;" +
"void main() {" +
" gl_Position = aPosition;" +
" vCoordinate = aCoordinate;" +
"}"
}
private fun getFragmentShader(): String {
return "precision mediump float;" +
"uniform sampler2D uTexture;" +
"varying vec2 vCoordinate;" +
"void main() {" +
" vec4 color = texture2D(uTexture, vCoordinate);" +
" gl_FragColor = color;" +
"}"
}
//省略和绘制三角形内容一致的部分
//......
}
不一致的地方,代码中已经做了标识(见代码中的【注:x】)。逐个来看看:
1)顶点坐标
顶点坐标和纹理坐标由3个变成4个,组成一个长方形,组合方式也是GL_TRIANGLE_STRIP。
2)着色器
首先介绍一下GLSL中的限定符
- attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。
- uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。
- varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。
const:常量。
各行代码解析如下:
private fun getVertexShader(): String {
return //顶点坐标
"attribute vec2 aPosition;" +
//纹理坐标
"attribute vec2 aCoordinate;" +
//用于传递纹理坐标给片元着色器,命名和片元着色器中的一致
"varying vec2 vCoordinate;" +
"void main() {" +
" gl_Position = aPosition;" +
" vCoordinate = aCoordinate;" +
"}"
}
private fun getFragmentShader(): String {
return //配置float精度,使用了float数据一定要配置:lowp(低)/mediump(中)/highp(高)
"precision mediump float;" +
//从Java传递进入来的纹理单元
"uniform sampler2D uTexture;" +
//从顶点着色器传递进来的纹理坐标
"varying vec2 vCoordinate;" +
"void main() {" +
//根据纹理坐标,从纹理单元中取色
" vec4 color = texture2D(uTexture, vCoordinate);" +
" gl_FragColor = color;" +
"}"
}
绘制过程新增了两个步骤:
3)激活并绑定纹理单元
private fun activateTexture() {
//激活指定纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
//绑定纹理ID到纹理单元
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId)
//将激活的纹理单元传递到着色器里面
GLES20.glUniform1i(mTextureHandler, 0)
//配置纹理过滤模式
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
//配置纹理环绕方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}
由于显示图片需要用到纹理单元来传递整张图片的内容,所以首先需要激活一个纹理单元。
为什么说是一个纹理单元?
因为OpenGL ES中内置了很多个纹理单元,并且是连续,比如GLES20.GL_TEXTURE0,GLES20.GL_TEXTURE1,GLES20.GL_TEXTURE3...可以选择其中一个,一般默认选第一个GLES20.GL_TEXTURE0,并且OpenGL默认激活的就是第一个纹理单元。
另外,纹理单元GLES20.GL_TEXTURE1 = GLES20.GL_TEXTURE0 + 1,以此类推。
激活指定的纹理单元后,需要把它和纹理ID做绑定,并且在传递到着色器中的时候:GLES20.glUniform1i(mTextureHandler, 0),第二个参数索引需要和纹理单元索引保持一致。
到这里,可以发现,OpenGL方法的命名都是比较规律的,比如GLES20.glUniform1i对应的是GLSL中的uniform限定符变量;ES20.glGetAttribLocation对应GLSL中的attribute限定符变量等等
最后四行代码,用于配置纹理过滤模式和纹理环绕方式(对于这两个模式的介绍引用自【LearnOpenGL-CN】)
- 纹理过滤模式
纹理坐标不依赖于分辨率,它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素映射到纹理坐标。
一般使用这两个模式:GL_NEAREST(邻近过滤)、GL_LINEAR(线性过滤)
当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
当设置为GL_LINEAR的时候,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。
来源LearnOpenGL-CN- 纹理环绕方式
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
4)绑定图片到纹理单元
激活了纹理单元以后,调用texImage2D方法,就可以把bmp绑定到指定的纹理单元上面了。
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0)
5)绘制
绘制的时候,最后一句的最后一个参数由三角形的3个顶点变成为长方形的4个顶点。如果还是填入3,你会发现会显示图片的一半,即三角形(对角线分割开)。
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
至此,一张图片就通过纹理贴图显示出来了。
纹理贴图当然,你会发现,这张图片是变形的,铺满整个GLSurfaceView窗口了。这里就涉及到了顶点坐标变换的问题了,将在下一篇文章中具体讲解。
五、总结
经过上面简单的绘制三角形和纹理贴图,可以总结出Android中OpenGL ES的2D绘制流程:
- 通过GLSurfaceView配置OpenGL ES版本,指定Render
- 实现GLSurfaceView.Renderer,复写暴露的方法,并配置OpenGL显示窗口,清屏
- 创建纹理ID
- 配置好顶点坐标和纹理坐标
- 初始化坐标变换矩阵
- 初始化OpenGL程序,并编译、链接顶点着色和片段着色器,获取GLSL中的变量属性
- 激活纹理单元,绑定纹理ID,配置纹理过滤模式和环绕方式
- 绑定纹理(如将bitmap绑定给纹理)
- 启动绘制
以上基本是一个通用的流程,当然渲染图片和渲染视频稍有不同,以及第5点,都将在下一篇说到。