文章目录
- 基本概念
- Vertex和Fragment
- 着色器程序
- 准备工作
- getUniformLocation/getAttribLocation
- glVertexAttribPointer
- 开始绘制
- Demo实现
OpenGL SE是一套适用于嵌入式设备的图形API,本文主要介绍如何通过OpenGL SE在Android设备上进行图形绘制,同时我会通过WebRTC视频帧绘制部分的源码让读者加深整个绘制流程的印象,最后修改WebRTC源码实现一个在视频预览画面随机绘制矩形边框的小demo。
基本概念
Vertex和Fragment
OpenGL最基本的两个概念就是:Vertex(顶点) 和 Fragment(片段),试想一下当我们在绘画时,都需要准备什么东西,首先是图案形状,然后是上色。
类比之下Vertex顶点就是用于描述形状,例如两点代表一条线、三点代表一个三角形。需要注意的是,在OpenGL中只有点、线、三角形3种图形,所有的复杂图形都是由这3个基本图形组成,那么如果要描述一个正方形,岂不是要定义例如[(0,0), (0,1), (1,0)]和[(0,1), (1,0),(1,1)]两组顶点,事实上OpenGL有对应的优化,只需定义4个顶点即可,这里后面我们会从代码中看到。
而Fragment片段就是用于描绘如何上色的,片段代表在这组顶点所围绕的范围内,每个像素对应的颜色。
着色器程序
着色器程序是用于定义Vertex和Fragment的,它是通过GLSL这门语言实现的,每个顶点都会执行一次顶点着色器,通过顶点着色器可以确定顶点的位置(gl_Position)。在顶点确定后,就会执行片段着色器,每一个像素都会执行一遍片段着色器以确定最终颜色(gl_FragColor)
以下是一个描绘三角形的着色器程序:
// 顶点着色器
attribute vec4 vPosition;
void main() {gl_Position = vPosition;
}// 片段着色器
precision mediump float; // 所有没有明确指定精度的float变量,默认使用 mediump(中等精度)。
void main() {gl_FragColor = vec4(0.5, 0, 0, 1);
}
所有的着色器程序都从main函数开始执行,其中gl_Position和gl_FragColor都类似于系统变量,他们代表顶点的最终位置和最终颜色。vec4代表定义一个4维向量,包括xyzw4个分量,常用于表示齐次坐标、RGB颜色等。attribute定义一个只用于顶点着色器的变量,该变量由用户输入,后面我们会看到这部分由用户输入的代码。类似的变量定义还有uniform也是由用户输入的变量,还有varying变量是用于顶点着色器和片段着色器共享之间传递的变量。
准备工作
我们可以通过WebRTC源码来看OpenGL ES是如何使用的:
private static int compileShader(int shaderType, String source) {// 1. 创建着色器程序final int shader = GLES20.glCreateShader(shaderType);// 2. 为着色器设置GLSL源代码,并加载GLES20.glShaderSource(shader, source);GLES20.glCompileShader(shader);// 检查是否设置成功int[] compileStatus = new int[] {GLES20.GL_FALSE};GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);if (compileStatus[0] != GLES20.GL_TRUE) {Logging.e(TAG, "Compile error " + GLES20.glGetShaderInfoLog(shader) + " in shader:\n" + source);throw new RuntimeException(GLES20.glGetShaderInfoLog(shader));}GlUtil.checkNoGLES2Error("compileShader");return shader;
}public GlShader(String vertexSource, String fragmentSource) {final int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource);final int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);// 3. 创建GLSL程序program = GLES20.glCreateProgram();if (program == 0) {throw new RuntimeException("glCreateProgram() failed. GLES20 error: " + GLES20.glGetError());}// 4. 为GLSL程序添加着色器GLES20.glAttachShader(program, vertexShader);GLES20.glAttachShader(program, fragmentShader);// 5. 连接程序GLES20.glLinkProgram(program);// 检查连接情况int[] linkStatus = new int[] {GLES20.GL_FALSE};GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);if (linkStatus[0] != GLES20.GL_TRUE) {Logging.e(TAG, "Could not link program: " + GLES20.glGetProgramInfoLog(program));throw new RuntimeException(GLES20.glGetProgramInfoLog(program));}GLES20.glDeleteShader(vertexShader);GLES20.glDeleteShader(fragmentShader);GlUtil.checkNoGLES2Error("Creating GlShader");
}private void prepareShader(ShaderType shaderType, float[] texMatrix, int frameWidth,int frameHeight, int viewportWidth, int viewportHeight) {final GlShader shader;if (shaderType.equals(currentShaderType)) {// Same shader type as before, reuse exising shader.shader = currentShader;} else {// Allocate new shader.currentShaderType = null;if (currentShader != null) {currentShader.release();currentShader = null;}shader = createShader(shaderType);currentShaderType = shaderType;currentShader = shader;// 6. 正式使用GLSL程序shader.useProgram();// Set input texture units.if (shaderType == ShaderType.YUV) {GLES20.glUniform1i(shader.getUniformLocation("y_tex"), 0);GLES20.glUniform1i(shader.getUniformLocation("u_tex"), 1);GLES20.glUniform1i(shader.getUniformLocation("v_tex"), 2);} else {GLES20.glUniform1i(shader.getUniformLocation("tex"), 0);}GlUtil.checkNoGLES2Error("Create shader");shaderCallbacks.onNewShader(shader);// 7. 获取uniform变量的索引,后续对uniform变量赋值通过这个索引texMatrixLocation = shader.getUniformLocation(TEXTURE_MATRIX_NAME);// 8. 获取attribute变量索引,后续对attribute变量赋值通过这个索引inPosLocation = shader.getAttribLocation(INPUT_VERTEX_COORDINATE_NAME);inTcLocation = shader.getAttribLocation(INPUT_TEXTURE_COORDINATE_NAME);// 9. 通过uniform变量的索引对其赋值GLES20.glUniform4f(shader.getUniformLocation("uBorderColor"), 1.0f, 0.0f,0.0f,1.0f);GLES20.glUniform1f(shader.getUniformLocation("uBorderWidth"), 0.005f);}shader.useProgram();// 10. attribute变量使用前需要先激活GLES20.glEnableVertexAttribArray(inPosLocation);// 11. 对attribute变量进行赋值GLES20.glVertexAttribPointer(inPosLocation, /* size= */ 2,/* type= */ GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0,FULL_RECTANGLE_BUFFER);// Upload the texture coordinates.GLES20.glEnableVertexAttribArray(inTcLocation);GLES20.glVertexAttribPointer(inTcLocation, /* size= */ 2,/* type= */ GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0,FULL_RECTANGLE_TEXTURE_BUFFER);// 9.1 通过uniform变量的索引对其赋值。但是uniform变量是4维向量类型(vec4)GLES20.glUniformMatrix4fv(texMatrixLocation, 1 /* count= */, false /* transpose= */, texMatrix, 0 /* offset= */);
}
以上是WebRTC在对视频帧渲染时初始化OpenGL部分的代码,重点部分都进行了注释,我们总结下整个调用流程:
- 创建着色器:
GLES20.glCreateShader、GLES20.glShaderSource、GLES20.glCompileShader
- 初始化GLSL程序并绑定着色器:
GLES20.glCreateProgram、GLES20.glAttachShader、GLES20.glLinkProgram、shader.useProgram()
- 对着色器GLSL源代码定义的变量进行赋值:对于Uniform变量使用
shader.getUniformLocation、GLES20.glUniform4f
。对于attribute变量使用shader.getAttribLocation、GLES20.glEnableVertexAttribArray、GLES20.glVertexAttribPointer
在对GLSL源代码定义变量进行赋值时,可以把通过getUniformLocation或者getAttribLocation得到的索引保存起来,这个是不会变的
接下来重点讲一下几个函数各个参数的含义:
getUniformLocation/getAttribLocation
此函数用于获取GLSL变量的索引,所以它需要传入的变量,必须和我们GLSL源码定义的变量是相同的,否则会找不到,以上面绘制三角形为例子,应该这个使用:
// 顶点着色器
attribute vec4 vPosition;
void main() {gl_Position = vPosition;
}
// 获取着色器变量
getAttribLocation("vPosition")
glVertexAttribPointer
此函数用于给attribute变量进行赋值,他的参数分别是:
- int index:变量索引
- int size:变量维度,例如vec2代表2个分量,所以要填2,同理vec4填4
- int type:变量类型:例如GLES20.GL_FLOAT代表每个分量都是float
- boolean normalized:是否将整数类型数据归一化到[0,1]或[-1,1]
- int stride:连续顶点之间的字节跨度(0表示紧密排列)
- java.nio.Buffer ptr:数据指针
开始绘制
同样的我们通过WebRTC源代码来学习:
@Override
public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight,int viewportX, int viewportY, int viewportWidth, int viewportHeight, boolean showTestRect) {// 这里是上面写的初始化操作prepareShader(ShaderType.OES, texMatrix, frameWidth, frameHeight, viewportWidth, viewportHeight);// 1.激活一个纹理单元,GL_TEXTURE0代表第一个GLES20.glActiveTexture(GLES20.GL_TEXTURE0);// 2.绑定外部纹理(OES)GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId);// 3.设置画布大小GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight);// 4.绘制顶点GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);// 5.解绑textureGLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}
通过GLES20.glViewport可以设置绘制画布的大小,GLES20.glDrawArrays用于绘制顶点,其第一个参数为绘制类型,GLES20.GL_TRIANGLE_STRIP代表指定顶点如何连接成三角形。它的作用是通过一组顶点绘制一系列相连的三角形,减少顶点数据的冗余,从而提高渲染效率。我们前文提到的如果要绘制矩形要定义6个顶点,通过这个类型,只需要定义4个即可。第二个参数为第一个顶点的index, 第三个参数为绘制顶点数量。
glActiveTexture、glBindTexture分别用户激活和绑定纹理,纹理(Texture) 是一张存储在显存中的图像数据,用于为3D模型或2D图形添加表面细节、颜色、光照效果等。简单来说,纹理就是“贴”在物体表面的图片,可以让简单的几何形状(如立方体、球体)呈现出更真实的视觉效果。GL_TEXTURE_EXTERNAL_OES代表外部纹理例如从相机或视频流中获取的纹理。在执行片段着色器的时候,就可以通过纹理获取每个片段应该渲染的颜色。
讲到这里,我们在看看WebRTC这里是如何定义着色器GLSL代码的,以下代码来自org.webrtc.GlGenericDrawer#DEFAULT_VERTEX_SHADER_STRING
和org.webrtc.GlGenericDrawer#createFragmentShaderString
我做了整合,方便大家观看
//顶点着色器如下:
varying vec2 tc;
attribute vec4 in_pos;
attribute vec4 in_tc;
uniform mat4 tex_mat;
void main() {// in_pos 由外部输入,代表顶点最终坐标gl_Position = in_pos;// in_tc由外部输入,为纹理坐标,通过tex_mat变化矩阵(例如缩放、裁剪等),也由外部输入// in_tc左乘tex_mat后取其XY向量赋值给tc,传递给片段着色器tc = (tex_mat * in_tc).xy;
}
//片段着色器如下:
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 tc;
// 纹理ID,由外部传入
uniform samplerExternalOES tex;
void main() {// texture2D系统方法,根据纹理ID和tc纹理坐标,计算该坐标应该渲染什么颜色,赋值给gl_FragColorgl_FragColor = texture2D(tex, tc);
}
Demo实现
想要实现在WebRTC预览画面随机绘制一个矩形,首先应该要把矩形的顶点、矩形边框的颜色、矩形的边框粗细传入到GLSL中,其次要在片段着色器中,对当前绘制的坐标进行判断,是否处于随机矩阵上,如果是则绘制矩形边框的颜色,如果不是则继续绘制纹理颜色,因此我们要修改着色器代码如下:
//顶点着色器如下:
varying vec2 tc;
attribute vec4 in_pos;
attribute vec4 in_tc;
uniform mat4 tex_mat;
// 输入矩阵顶点
attribute vec4 in_rect;
// 于片段着色器共享输入矩阵顶点
varying vec4 vRect;
void main() {gl_Position = in_pos;tc = (tex_mat * in_tc).xy;vRect = in_rect;
}
//片段着色器如下:
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 tc;
uniform samplerExternalOES tex;// 共享矩阵顶点
varying vec4 vRect;
// 定义矩阵边框颜色
uniform vec4 uBorderColor;
// 定义矩阵边框粗细
uniform float uBorderWidth;void main() {vec4 texColor = texture2D(tex, tc);// 判断当前xy是否在随机矩阵上vec2 pixelPos = tc - vRect.xy;vec2 rectSize = vRect.zw;float distLeft = pixelPos.x;float distRight = rectSize.x - pixelPos.x;float distTop = pixelPos.y;float distBottom = rectSize.y - pixelPos.y;float minDist = min(min(distLeft, distRight), min(distTop, distBottom));// 如果在就用矩阵边框的颜色if (minDist < uBorderWidth && pixelPos.x >= 0.0 && pixelPos.x <= rectSize.x &&pixelPos.y >= 0.0 && pixelPos.y <= rectSize.y) {gl_FragColor = uBorderColor;} else {// 不在就用原来纹理颜色gl_FragColor = texColor;}
}
修改完GLSL代码后,就需要改绘制代码, 首先是把GLSL需要传入的参数传递:
// uniform 变量赋值
GLES20.glUniform4f(shader.getUniformLocation("uBorderColor"), 1.0f, 0.0f,0.0f,1.0f);
GLES20.glUniform1f(shader.getUniformLocation("uBorderWidth"), 0.005f);// attribute 变量赋值
inRect = shader.getAttribLocation("in_rect");
GLES20.glEnableVertexAttribArray(inRect);float x = (float) Math.random();
float y = (float) Math.random();
FloatBuffer rect = GlUtil.createFloatBuffer(new float[] {x, y, 0.2f, 0.2f,x, y, 0.2f, 0.2f,x, y, 0.2f, 0.2f,x, y, 0.2f, 0.2f,
});
GLES20.glVertexAttribPointer(inRect,4, GLES20.GL_FLOAT, false, 0, rect);
定义的随机矩阵为(x, y, width, height)这里xy用的随机数,宽高固定0.2f,这里的顶点数据中重复定义了4次相同的矩形参数(vRect),是因为每个顶点需要独立携带完整的矩形信息,以便在片段着色器中正确计算边框。当然也可以不定义vRect直接使用uniform实现。