首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。php
码字不易,转载请注明出处!android
教程代码:【Github传送门】 |
---|
本文主要介绍OpenGL相关的基础知识,包括坐标系、着色器、基本渲染流程等。git
提到OpenGL,想必不少人都会说,我知道这个东西,能够用来渲染2D画面和3D模型,同时又会说,OpenGL很难、很高级,不知道怎么用。github
一、为何OpenGL“感受很难”?编程
二、OpenGL ES是什么?小程序
为了解决以上问题,让OpenGL“学起来不是很难”,须要把其分解成一些简单的步骤,而后简单的东西串联起来,一切就水到渠成了。数组
首先,来看看什么是OpenGL。框架
在手机上,有两大元件,一个是CPU,一个是GPU。而手机上显示图形界面也有两种方式,一个是使用CPU来渲染,一个是使用GPU来渲染,能够说,GPU渲染实际上是一种硬件加速。编程语言
为何GPU能够大大提升渲染速度,由于GPU最擅长的是并行浮点运算,能够用来对许许多多的像素作并行运算。ide
OpenGL(Open Graphics Library)则是间接操做GPU的工具,是一组定义好的跨平台和跨语言的图形API,是可用于2D和3D画面渲染的底层图形库,是由各个硬件厂家具体实现的编程接口。
OpenGL ES 全称:OpenGL for Embedded Systems,是OpenGL 的子集,是针对手机 PAD等小型设备设计的,删减了没必要须的方法、数据类型、功能,减小了体积,优化了效率。
三、 OpenGL ES版本
目前主要版本有1.0/1.1/2.0/3.0/3.1
2.0 版本是 Android 目前支持最普遍的版本,后续主要以该版本为主,进行介绍和代码编写。
在音视频开发中,涉及到的坐标系主要有两个:世界坐标和纹理坐标。
因为基本不涉及3D贴图,因此只看x/y轴坐标,忽略z轴坐标,涉及到3D相关知识可自行Google,不在讨论范围内。
首先来看两个图:
经过名字就能够知道,这是OpenGL本身世界的坐标,是一个标准化坐标系,范围是 -1 ~ 1,原点在中间。
纹理坐标,其实就是屏幕坐标,标准的纹理坐标原点是在屏幕的左下方,而Android系统坐标系的原点是在左上方的。这是Android使用OpenGL须要注意的一个地方。
纹理坐标的范围是 0 ~ 1。
注:坐标系的xy轴方向很重要,决定了如何作顶点坐标和纹理坐标映射。
那么,这两个坐标系究竟有什么关系呢?
世界坐标,是用于显示的坐标,即像素点应该显示在哪一个位置由世界坐标决定。
纹理坐标,表示世界坐标指定的位置点想要显示的颜色,应该在纹理上的哪一个位置获取。即颜色所在的位置由纹理坐标决定。
二者之间须要作正确的映射,才能正常的显示一张画面。
在OpenGL 2.0之后,加入了新的可编程渲染管线,能够更加灵活的控制渲染。但也所以须要学习多一门针对GPU的编程语言,语法与C语言相似,名为GLSL。
在介绍GLSL以前,先来看两个比较陌生的名词:顶点着色器和片元着色器。
着色器,是一种可运行在GPU上的小程序,用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】,很是详尽。
OpenGL的渲染流程说实话是比较繁琐的,也是让不少人望而生畏的地方,可是,若是归结起来,其实整个渲染流程基本是固定的,只要把它按照固定的流程封装好,其实并无那么复杂。
接下来,就进入实战,一层一层扒开OpengGL的神秘面纱。
在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封装好的流程中,暴露出来的接口,留给给开发者实现初始化和渲染,而且这三个接口的回调都在同一个线程中。
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
}
复制代码
这里所说的绘制区域,是指OpenGL在GLSurfaceView中的绘制区域,通常都是所有铺满。
GLES20.glViewport(0, 0, width, height)
复制代码
先定义一个渲染接口类:
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_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则是须要定义另外的索引数组,来确认顶点的组合和绘制顺序。
经过以上步骤,就能够在屏幕上看到一个红色的三角形了。
可能有人就有疑问了:绘制三角形的时候只是直接设置了像素点的颜色值,并无用到纹理,纹理到底有什么用呢?
接下来,就用纹理来显示一张图片,看看纹理到底怎么使用。
建议先看清楚绘制三角形的流程,绘制图片就是基于以上流程,重复代码就再也不贴出。
如下只贴出和绘制三角形不同的部分代码,详细代码请看源码。
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中的限定符
各行代码解析以下:
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的时候,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。
环绕方式 | 描述 |
---|---|
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绘制流程:
以上基本是一个通用的流程,固然渲染图片和渲染视频稍有不一样,以及第5点,都将在下一篇说到。