前言
上面文章介绍了一下OpenGL基本使用,由于接触OpenGL时间不长,对它理解的不是很深,讲得不是很清楚,接下来用这篇文章,通过一个实际的开发例子,重新介绍一下OpenGL。
提示:话不多说,正文来了。
一、常规操作
上面文章已经介绍了OpenGL如何使用,下面直接上代码了。
glSurfaceView = findViewById(R.id.camera_glsurface_view)
glSurfaceView.setEGLContextClientVersion(2)
myRender = MyRender()
glSurfaceView.setRenderer(myRender)
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
简单介绍一下上面代码,首先是设置OpenGL版本号,创建了一个自定义Renderer,把Render设置给当前GLSurfaceView,最后一行代码强调一下,是指GLSurfaceView刷新方式,一般选择这个模式,就是有变化的主动刷新一下,如果不设置的话,每隔一段时间,自动刷新,挺消耗资源的,一般选择上面这个。
二、使用步骤
1.创建SurfaceTexture
定义一个SurfaceTexture来显示处理后的数据,并实现OnFrameAvailableListener接口回调来通知GlSurfaceview渲染新的帧数据:
private lateinit var mSurfaceTexture: SurfaceTexture
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
glSurfaceView.requestRender()
}
在说创建SurfaceTexture之前,我们先回顾一下,Renderer几个方法作用:
onSurfaceCreated():系统会在创建 GLSurfaceView 时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。 onDrawFrame():系统会在每次重新绘制 GLSurfaceView 时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。 onSurfaceChanged():系统会在GLSurfaceView 几何图形发生变化(包括 GLSurfaceView大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。使用此方法可响应GLSurfaceView 容器中的更改。
如上所述,创建方法写在:
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
mSurfaceTexture = SurfaceTexture(createOESTextureObject())
...
再看createOESTextureObject如何实现的:
fun createOESTextureObject(): Int {
val tex: IntArray = IntArray(1)
GLES20.glGenBuffers(1, tex, 0)
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0])
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST.toFloat()
);
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR.toFloat()
);
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE.toFloat()
);
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE.toFloat()
);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
return tex[0]
}
主要是生成了一个纹理id,设置了一系列参数,最后返回。
2.自定义Renderer
重头戏,可以说OpenGL最重要的就是Renderer类,而绘制形状最重要的是两个着色器:
- 顶点着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码。
- 片段着色程序-用于使用颜色或纹理渲染形状面的 OpenGL
ES 代码。 这是官方定义,其实简单来说,顶点着色器就是画形状,片段着色器就是上颜色,贴图。 再来张图片,看的更明白一些:
我们的形状都是由一个个三角形绘制而成,最后由片段着色器上色、贴图。 原理介绍完了,咋们看下代码:
private val mPosCoordinate = floatArrayOf(
-1f, -1f,
-1f, 1f,
1f, -1f,
1f, 1f)
private val mTexCoordinateBackRight = floatArrayOf(
1f, 1f,
0f, 1f,
1f, 0f,
0f, 0f)
上面定义了顶点着色器和片段着色器的坐标,最后将我们定义好的坐标传入我们的着色程序代码里面。上面文章,我们是直接写在String里面,直接引用,实际编写代码是不会这么做的,我们写好的着色程序,是放在asserts文件里面的,然后读取的:
val vertexSource = AssetsUtils.read(instance, "camera_vertexShader.glsl")
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource!!)
val fragmentSource = AssetsUtils.read(instance, "camera_fragmentShader.glsl");
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource!!)
...
再看看我们着色器代码如何写的(建议大家在AS安装一个GLSL Support插件,有惊喜) 顶点着色器:
uniform mat4 textureTransform;
attribute vec2 inputTextureCoordinate;
attribute vec4 position;
varying vec2 textureCoordinate;
void main() {
gl_Position = textureTransform * position;
textureCoordinate = inputTextureCoordinate;
}
上面代码,简单说下,vec2、vec4分别代表两位float和四位float,其中gl_Position自带变量,textureTransform变换矩阵。 片段着色器:
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES videoTex;
varying vec2 textureCoordinate;
void main() {
vec4 tc = texture2D(videoTex, textureCoordinate);
float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;
gl_FragColor = vec4(color,color,color,1.0);
}
gl_FragColor自带变量,上面代码实现了一个黑白滤镜。
3.坐标系
除了上面的着色器代码以外,OpenGL的坐标系尤其重要,因为我们了解它的坐标系,我们才知道形状如画绘制出来的。
我们看上面图片,主要有两种坐标,一种是绘制形状的,一种是贴图的纹理坐标,我们知道我们画图是有一个一个点连接起来的,OpenGL也是一样的,既然涉及到绘图,肯定有绘制顺序的,在OpenGL有专门的方法设置:
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate1.size / 2)
OpenGL通常是GLES20.GL_TRIANGLE_STRIP这种绘制顺序,对应的就是左下角,右下角,左上角,右上角,敲黑板,这个是重点,因为我们的形状就是按这个顺序绘制出来的,你可以试着按逆时针顺序,连起来,你会发现我们画出来的形状不是矩形,会少一块。 同理,我们形状绘制出来了,贴图也要照着这个顺序绘出来。
4.OpenGL和Camera相结合
前面说了很多OpenGL相关的东西,最后我们的效果要结合Camera预览显示的。 我们知道Android相机目前有3种API,Camera1、Camera2、CameraX,这里图方便,就使用Camera1。
try {
camera = Camera.open(0)
camera.setPreviewTexture(mSurfaceTexture)
camera.startPreview()
camera.autoFocus(object : Camera.AutoFocusCallback {
override fun onAutoFocus(success: Boolean, camera: Camera?) {
if (success) {
}
}
})
} catch (e: Exception) {
e.printStackTrace()
}
这里我们将OpenGL处理过的mSurfaceTexture,直接设置给camera显示。 最后我们再看下OpenGL处理的代码:
uPosHandle = GLES20.glGetAttribLocation(mProgram, "position");
aTexHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "textureTransform");
mPosBuffer = convertToFloatBuffer(mPosCoordinate1)
mTexBuffer = convertToFloatBuffer(mTexCoordinateBackRight1);
GLES20.glVertexAttribPointer(uPosHandle, 2, GLES20.GL_FLOAT, false, 0, mPosBuffer)
GLES20.glVertexAttribPointer(aTexHandle, 2, GLES20.GL_FLOAT, false, 0, mTexBuffer)
GLES20.glEnableVertexAttribArray(uPosHandle)
GLES20.glEnableVertexAttribArray(aTexHandle)
主要是把之前传入的参数,配置OpenGL ES环境中。 最后在onDrawFrame更新画面:
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
mSurfaceTexture.updateTexImage()
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0)
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate1.size / 2)
5.实际运行效果
看下代码运行的实际效果: 可以看到画面是颠倒的,前面我们说过OpenGL坐标系,我们试着将纹理坐标旋转90度试下,对应的坐标修改如下:
private val mTexCoordinateBackRight1 = floatArrayOf(
1f,0f,
1f,1f,
0f,0f,
0f,1f
)
这个坐标怎么来的呢,可以试着将纹理坐标图片旋转90度,然后按照之前的顺序,重新填入就可以了。 然后我们再看下效果:
我们可以看到字是对称的,根据前面的经验,我们把旋转系90度后图片,两边坐标换下不就可以了:
private val mTexCoordinateBackRight1 = floatArrayOf(
1f,1f,
1f,0f,
0f,1f,
0f,0f
)
修改后看下效果: 现在看是不是正常了,其实还有一种方法,不修改坐标,直接使用矩阵的方式(顶点着色器代码里面修改),大家可以试下。
6.分屏效果
现在画面正常了,还差最后一步分屏,分屏的效果其实很简单,我们知道分屏其实就是显示两个一模一样的画面,其实主要就是显示中间那部分画面,不属于中间的那部分,我们用中间的部分重复就行。 说的可能有点绕,看代码你们就明白了,由于显示画面,我们直接修改片段着色器代码:
vec4 tc = texture2D(videoTex, textureCoordinate);
float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;
float x = textureCoordinate.x;
if(x < 0.5) {
x+=0.25;
}else {
x-=0.25;
}
gl_FragColor = texture2D(videoTex, vec2(x, textureCoordinate.y));
}
代码很简单啊,就是把x<0.5和x>0.5坐标进行变换显示中间的画面,看下效果: 同理,三分屏呢:
if (x < 1.0/3.0) {
x+=1.0/3.0;
} else if (x > 2.0/3.0){
x-=1.0/3.0;
}
替换之前x坐标代码就可以了,看下效果: 我们上面实现的是横屏的分屏啊,竖屏的分屏,照葫芦画瓢就行,这里我就不写了。
7.项目地址
参考例子地址:GitHub地址,觉得不错的给个🌟。
总结
至此,我们把使用OpenGL实现抖音分屏效果全部讲完了,最后做个简单的总结吧。
- 我们实现这样的效果,我们首先要知道OpenGL的基础知识,了解一些基本概念,感兴趣的可以看下上面文章,Android
OpenGL开发学习(一)绘制简单图形,最重要的还是两个着色器。 - 其次就是要了解OpenGL坐标系,这对我们绘制形状至关重要。
- 最后,就是要了解一下编写着色器代码规则,我们甚至可以把别人的着色器代码反编译过来,自己使用。
Thanks: Android openGl开发详解(二)——通过SurfaceView,TextureView,GlSurfaceView显示相机预览 OpenGL ES官方API
创作不易,觉得不错的话,请点赞、评论鼓励,谢谢。
下面文章预告一下,前面我们写了一些OpenGL单一效果的例子,复合效果怎么实现呢,如果期待下篇文章,敬请点赞啊。
|