通过WebRTC源码入门OpenGL ES

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部分的代码,重点部分都进行了注释,我们总结下整个调用流程:

  1. 创建着色器:GLES20.glCreateShader、GLES20.glShaderSource、GLES20.glCompileShader
  2. 初始化GLSL程序并绑定着色器:GLES20.glCreateProgram、GLES20.glAttachShader、GLES20.glLinkProgram、shader.useProgram()
  3. 对着色器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变量进行赋值,他的参数分别是:

  1. int index:变量索引
  2. int size:变量维度,例如vec2代表2个分量,所以要填2,同理vec4填4
  3. int type:变量类型:例如GLES20.GL_FLOAT代表每个分量都是float
  4. boolean normalized:是否将整数类型数据归一化到[0,1]或[-1,1]
  5. int stride:连续顶点之间的字节跨度(0表示紧密排列)
  6. 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_STRINGorg.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实现。

相关推荐
顾林海23 分钟前
深度解析LinkedHashMap工作原理
android·java·面试
JasonYin25 分钟前
Git提交前缀
android
louisgeek1 小时前
Android 类加载机制
android
碎风,蹙颦1 小时前
Android开发过程中遇到的SELINUX权限问题
android·人工智能
HZW89701 小时前
鸿蒙应用开发—数据持久化之SQLite
android·前端·harmonyos
百锦再1 小时前
Android Studio 日志系统详解
android·java·ide·app·android studio·安卓·idea
fatiaozhang95272 小时前
晶晨线刷工具下载及易错点说明:Key文件配置错误/mac剩余数为0解决方法
android·电视盒子·魔百盒刷机
QING6186 小时前
详解:Kotlin 类的继承与方法重载
android·kotlin·app
QING6186 小时前
Kotlin 伴生对象(Companion Object)详解 —— 使用指南
android·kotlin·app
一一Null6 小时前
Android studio 动态布局
android·java·android studio