OpenGL (一)OpenGL ES 绘制基础
OpenGL (二)GLSurface 视频录制
OpenGL (三)滤镜filter 应用
OpenGL (四)贴纸和磨皮理论
Open Graphics Library
图形领域的工业标准,是一套与硬件无关的跨编程语言、跨平台的、专业的图形编程(软件)接口。它用于二维、三维图像,是一个功能强大,调用方便的底层图形库。
OpenGL ES(OpenGL for Embedded Systems)
针对手机、PDA和游戏主机等嵌入式设备而设计的OpenGL API 子集。
在Android SDK与NDK中均有提供OpenGL ES的类库。所以我们可以借助Java、C/C++来使用OpenGL
和面向对象不同,OpenGL 是个状态机,面向过程编程。代码量比较多,难度较大。但是现在流 行的各种美颜相机、短视频APP实现各种图像渲染效果都需要OpenGL来完成。
图像处理流程一般的方式如下;
很多图形处理的书都有这样一个经典的图
抓了一张图,还是说明一下,OpenGL管线,通俗 的说所有的图形都是由三角行构成,只要给定定点就好。
把三角形细分成像素点,每个像素点放大,就是我们第三个Fragments。根据像素点的位置,用着色器上色。
最后精工Fragment处理,行程图像。
EGL
OpenGL ES只是图形API,不负责管理(显示)窗口,窗口的管理交由各个设备自己来完成。OpenGL ES调用用于渲染纹理多边形,而 EGL 调用用于将渲染放到屏幕上。
GLSurfaceView
调用任何 OpenGL 函数前,必须已经创建了 OpenGL 上下文。Android中GLSurfaceView中会为我们初始化OpenGL上下文。
内部启动一个子线程(GL线 程)来初始化ELG环境,并完成OpenGL的绘制。使用GLSurfaceView,我们只能在这个EGL线程调用 OpenGL函数。
继承至SurfaceView,它内嵌的Surface专门负责OpenGL渲染。
- 管理Surface与EGL;
- 允许自定义渲染器(render);
- 支持按需渲染和连续渲染;
GLThread定义在GLSurfaceView内部,我们怎么在这个线程中去执行我们编写的代码?
GLSurfaceView并不是一创建就会启动GL线程初始化环境。你可以把GLSurfaceView当成普通的 SurfaceView来使用。但是如果我们需要使用OpenGL ES在GLSurfaceView中绘制,那么我们需要调 用:
//使用OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
//設置渲染回調接口
renderer = new CameraRender(this);
setRenderer(renderer);
/**
* 刷新方式:注意必须在setRenderer 后面设置
* RENDERMODE_WHEN_DIRTY 手动刷新,調用requestRender()回调一次渲染器的onDraw方
* 法;
* RENDERMODE_CONTINUOUSLY 自动刷新,大概16ms自動回調一次渲染器的onDraw方法
*/
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
其中 CameraRender 是一个实现了 GLSurfaceView.Renderer 接口的类。这个接口定义为:
public interface Renderer {
//窗口画布准备完成
void onSurfaceCreated(GL10 gl, EGLConfig config);
//画布发生改变(如:横竖切换)
void onSurfaceChanged(GL10 gl, int width, int height);
//绘制
void onDrawFrame(GL10 gl);
}
所谓的刷新就是回调这个接口的 onDrawFrame 方法让我们进行OpenGL ES的绘制调用。其实 GLSurfaceView的setRenderer方法源码实现为:
public void setRenderer(Renderer renderer) {
checkRenderThreadState();
if (mEGLConfigChooser == null) {
mEGLConfigChooser = new SimpleEGLConfigChooser(true);
}
if (mEGLContextFactory == null) {
mEGLContextFactory = new DefaultContextFactory();
}
if (mEGLWindowSurfaceFactory == null) {
mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
}
mRenderer = renderer;
mGLThread = new GLThread(mThisWeakRef);
mGLThread.start();
}
可以看到,如果需要在GLSurfaceView中配置的ELG环境并使用OpenGL ES就必须设置渲染接口。这样 才会启动线程初始化OpenGL ES的环境。而渲染接口需要实现的方法就是在GLThread回调的。
GLThread定义在GLSurfaceView内部,我们怎么在这个线程中去执行我们编写的代码?
渲染接口Renderer的三个回调方法就是在该线程中回调的。
下面我们需要使用OpenGL ES在GLSurfaceView中显示摄像头的图像。 这里在之前的文章有说过
请转到 RTMP(四)交叉编译与CameraX 结尾有提到
Renderer渲染
前面我们说了,我们需要在GLThread中去调用OpenGL ES的方法来完成渲染。因此我们的主要工作就 是在实现了Renderer接口的类中我们需要实现的: onSurfaceCreated 、 onSurfaceChanged 与 onDrawFrame 三个方法中完成绘制。
onSurfaceCreated
在OpenGL ES环境准备完成之后,会在GLThread中回调 onSurfaceCreated 方法。在这个方法中我们 需要准备我们需要绘制的图像。
OpenGL纹理可以看成是能在OpenGL中使用的图像,在代码中我们通过id来操作纹理。
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//创建OpenGL 纹理 ,把摄像头的数据与这个纹理关联
textures = new int[1]; //当做能在opengl用的一个图片的ID
mCameraTexure.attachToGLContext(textures[0]);
// 当摄像头数据有更新回调 onFrameAvailable
mCameraTexure.setOnFrameAvailableListener(this);
screenFilter = new ScreenFilter(cameraView.getContext());
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// 请求执行一次 onDrawFrame
cameraView.requestRender();
}
让摄像头数据绑定到我们创建的纹理 textures[0] 之后,后续我们会将 textures[0] 交给OpenGL ES 的Sharder着色器去进行渲染,并且在渲染时也能去实现各种图像效果。
OpenGL Sharder(着色器)需要使用GLSL(着色器语言)编写
上述代码中的 mCameraTexure 就是摄像头预览数据绘制的窗口。 attachToGLContext 方法能够让摄像
头预览的窗口与我们的纹理绑定起来。
摄像头捕获
由于我们本次会使用CameraX来完成摄像机的使用,使用CameraX,将捕获的图像与OpenGL纹理关联 起来也非常简单。再来回顾下我们之前直接通过CameraX与TextureView显示预览的代码。
// 预览配置
private Preview getPreView() {
// 分辨率并不是最终的分辨率,CameraX会自动根据设备的支持情况,结合你的参数,设置一个最为接近的分辨率
PreviewConfig previewConfig = new PreviewConfig.Builder()
.setTargetResolution(new Size(640, 480))
.setLensFacing(currentFacing) //前置或者后置摄像头
.build();
Preview preview = new Preview(previewConfig);
preview.setOnPreviewOutputUpdateListener(listener);
return preview;
}
// 绑定声明周期
CameraX.bindToLifecycle(lifecycleOwner, getPreView());
@Override
public void onUpdated(Preview.PreviewOutput output) {
mCameraTexure = output.getSurfaceTexture();
// if (mCameraV.getSurfaceTexture() != surfaceTexture) {
// if (textureView.isAvailable()) {
// ViewGroup parent = (ViewGroup) displayer.getParent();
// parent.removeView(displayer);
// parent.addView(displayer, 0);
// parent.requestLayout(); }
// //设置布局中TextureView中的纹理画布完成预览
// textureView.setSurfaceTexture(mCameraTexure); }
// }
}
着色器
着色器(Shader)是运行在GPU上的小程序。
顶点着色器(vertex shader)
如何处理顶点、法线等数据的小程序。
#version 120
attribute vec4 vPosition;//变量 float[4] 一个顶点 java 传过来
attribute vec2 vCoord;//传给片元进行采样的纹理坐标
varying vec2 aCoord;//易变变量 和片元写的一模一样 会传给片元
void main() {
// gl_Position = vec4(vec3(0.0), 1.0);
// 内置变量:把坐标点赋值给gl_position就ok
gl_Position = vPosition;
aCoord = vCoord;
}
这段程序就是一个顶点着色器的代码。与Java或者C一样,程序入口就是 main 方法。在 main 方法中只 要我们把要画的物体形状的点坐标给到 gl_position 即可。 vPosition 就是我们在CPU中确定的物体 形状坐标点。现在我们需要绘制的是摄像头采集图像,也就是矩形,那就需要传递4个顶点坐标数据到 着色器中。
与我们熟悉的Java语法类似, vPosition 是变量名, vec4 是类型,而应用程序传递到顶点着色器中的 变量值需要使用 attribute 修饰。可以看成是 in 顶点着色器输入变量的修饰。
vec4 其实就是vector向量,4表示这个向量包含4个数据。需要注意的是vec4只能存储float数据, 如果需要存储整型则需要使用ivec4。
aCoord 定义为存储2个float的vec2类型,被varying修饰。如果说 attribute 是 in 输入变量的修饰, 那么 varying 就是 out 输出变量。我们会在片元着色器使用这个变量。
这个顶点着色器中没有任何逻辑,只进行了赋值的操作,那么作为 attribute 输入的值 vPosition 与 vCoord 就需要我们在Android中传递进来。这两个值分别为顶点坐标与纹理坐标。
我们需要根据OpenGL世界坐标系传递顶点坐标点来确定绘制的几何形状,需要在绘制的几何表面进行 贴图,就需要按照纹理坐标贴合几何顶点。顶点着色器执行4次, vPostion 接收的点坐标与 vCoord 分 别为:
-1.0,-1.0 与 0,0
1.0,-1.0 与 1.0,0
-1.0,1.0 与 0,1.0
1.0,1.0 与 1.0,1.0
和U3D 坐标系类似
片元着色器(fragment shader)
如何处理光、阴影、遮挡、环境等等对物体表面的影响,最终生成一副图像的小程序。
#version 120
precision mediump float;//数据精度
varying vec2 aCoord;
// uniform 片元着色器必须用这个
uniform samplerExternalOES vTexture;//samplerExternalOES 图片,采样器
void main() {
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = texure2D(vTexture,aCoord);//rgba
}
片元着色器中,使用内置函数 texture2D 采集对应坐标点的像素,赋值给 gl_FragColor 即可。
#extension GL_OES_EGL_image_external : require :Android摄像头只能用samplerExternalOES 类型的纹理去接收摄像头的画面,而使用samplerExternalOES需要开启 GL_OES_EGL_image_external功能。
uniform 变量是Android中需要传递给着色器的变量,不能被着色器修改。可以用于修饰共享变 量(在顶点和片元中声明方式完全一样)。
aCoord 的定义需要和顶点着色器一模一样,但是在片元着色器能读取之前会通过光栅化传递, 光栅化程序在三角形的三个顶点之间进行插值处理,会访问三个顶点之间每一个像素,然后对每 个像素点执行片元着色器。所以在片元着色器中 aCoord 的值可以看成几何中每一个像素点的坐 标。
texture2D(vTexture,aCoord) ,则是利用摄像头纹理的采样器 vTexture 获取 aCoord 坐标的 像素RGBA值并赋值给 gl_FragColor ,OpenGL就知道当前处理的片元是什么颜色从而绘制。
实际上,在Android中我们就是通过OpenGL接口给着色器传参就好啦。我们可以在渲染接口的 onDraw 中调用OpenGL动态的给着色器传递参数。而各种绘制效果的处理在着色器中来完成。
GLSL
OpenGL着色语言(OpenGL Shading Language)
attribute
属性变量,顶点着色器输入数据。如:顶点坐标、纹理坐标、颜色等。
uniforms
一致变量。在着色器执行期间一致变量的值是不变的。类似常量,但不同的是,这个值在编译时期是未知的,由着色器外部初始化。
varying
易变变量,顶点着色器输出数据。是从顶点着色器传递到片元着色器的数据变量