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)
以下是一个描绘三角形的着色器程序:
java
// 顶点着色器
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是如何使用的:
java
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源码定义的变量是相同的,否则会找不到,以上面绘制三角形为例子,应该这个使用:
java
// 顶点着色器
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源代码来学习:
java
@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.解绑texture
GLES20.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
我做了整合,方便大家观看
java
//顶点着色器如下:
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_FragColor
gl_FragColor = texture2D(tex, tc);
}
Demo实现
想要实现在WebRTC预览画面随机绘制一个矩形,首先应该要把矩形的顶点、矩形边框的颜色、矩形的边框粗细传入到GLSL中,其次要在片段着色器中,对当前绘制的坐标进行判断,是否处于随机矩阵上,如果是则绘制矩形边框的颜色,如果不是则继续绘制纹理颜色,因此我们要修改着色器代码如下:
java
//顶点着色器如下:
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需要传入的参数传递:
java
// 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实现。