内容简介:陈鸿宇:《理想三旬》01 前言
陈鸿宇:《理想三旬》
01 前言
大家好,本文是 iOS/Android 音视频专题 的第五篇,该专题中 AVPlayer 项目代码将在 Github 进行托管,你可在微信公众号 ( GeekDev ) 后台回复 资料 获取项目地址 。
上篇文章 《使用 MediaExtractor 及 MediaCodec 解码音视频》 介绍过对音视频进行解码,但是我们并没有将解码后的数据在屏幕上展示,如果需要渲染到屏幕上我们就需要了解下 OpenGL 的相关知识。
目录:
-
OpenGL ES 基础概念
-
OpenGL ES GLSL 着色器
-
OpenGL ES Program
-
OpenGL ES 纹理
-
OpenGL ES 绘制纹理
-
结束语
02 OpenGL ES 基础概念
OpenGL ES 是 OpenGL 三维图像 API 的子集,是为手机,PAD和游戏机等嵌入式设备而设计。OpenGL ES 目前支持 iOS 、 Android 、 BlackBerry 、 bada 、 Linux 和 Windows。
由于 OpenGL API 相当复杂,并且在嵌入式设备上很多功能并没有什么卵用,Khronos 组织牵头对 OpenGL API 进行了删减,最终诞生了 OpenGL ES。
OpenGL ES 在移动设备上做了很多优化,例如,降低电源消耗,提高着色器性能,在着色器语言中引入精度限定符( highp、mediump、lowp )。
Context 是 OpenGL 中的一个重要概念,理解 Context 我们首先需要知道状态机,OpenGL 本身是一个巨大且复杂的状态机,当调用一个 GL 函数时,其实,就是在改变 OpenGL 当前的状态信息,比如:颜色 、纹理坐标、光照、混合、深度测试等。而这些状态信息都保存在 Context 上下中,因此渲染的时候,必须创建当前环境的 Context 。 在 Android 中 Context 使用 EGLContext 对象表示。
03 OpenGL ES 着色器
OpenGL ES 中相当重要的一部分是 GL Shader Language(GLSL),GLSL 是 OpenGL ES 开放给我们的可编程部分,通常,我们编写的代码运行在 CPU 中,但 GLSL 在 GPU 中运行。 GLSL 由顶点( vertex )着色器和片段( fragment )着色器构成, 可以在着色器中自定义我们自己的渲染逻辑,比如,滤镜、素描、马赛克特效等。
GLSL 的语法与 C 语言比较类似, GLSL 包括:
-
变量
-
变量类型
-
main 函数
-
结构体
-
数组
-
限定符
变量类型
void :用于函数无返回值或无参数列表声明
标量 : float、int 、bool 浮点、整型、布尔型
浮点向量 : float、vec2 、vec3、vec4 包含1、2、3、4个元素的浮点型向量
整数向量 : int、ivec2 、ivec3、ivec4 包含1、2、3、4个元素的整型向量
布尔 向量 : bool、bvec2 、bvec3、bvec4 包含1、2、3、4个元素的布尔型向量
矩阵 : mat2、mat3 、mat4 为 2x2、3x3、4x4 的浮点型矩阵
纹理句柄 : sampler2D、samplerCube 表示 2D、3D纹理句柄
获取向量分量时即可以通过 "." 符号也可以通数组下标的方法,由于向量在 GLSL 中常常用来表示颜色 、纹理坐标等, GLSL 提供了通过 {x, y, z, w}
, {r, g, b, a}
或 {s, t, r, q}
操作来获取向量分量,这种方式在编写 GLSL 代码时很容易可以断定该向量的意义。
GLSL 限定符
限定符是对变量的解释说明,并限定变量在 GLSL 中的使用场景,在 GLSL 中支持如下限定符:
attrib ute : 只能用在顶点着色器中,一般用于表示顶点数据。由程序通过
glGetAttribLocation 获取 attribute 地址,并通过 glEnableVertexAttriArray / glVertexAttribPointer 为 attribute 属性赋值。
varying :可用于顶点和片段着色器,一般用于在着色器之间做数据传递。通常,
varying 在顶点着色器中进行计算,片段着色器使用 varying 计算后的值。
uniform :可用于顶点和片段着色器, 由程序通过 glGetUniformLocation 获取地址 ,并通过 glUniforml 系列函数复制。
顶点着色器
在一个 OpenGL ES 程序中,顶点着色器和片元着色器是标准配置,顶点着色器用于定义绘制的形状,片元着色器为这个形状上色。
例如,我们如果想要绘制一个三角形,我们首先确定三角形的三个顶点坐标,并将顶点信息告知顶点着色器,顶点着色器根据顶点坐标绘制三角形,然后交由片元着色器为三角形粉刷颜色。通常,顶点着色器为每个顶点调用一次顶点着色器。
下面是一个非常简单的顶点着色器:
"attribute vec3 aPosition;" +
"void main(void) {" +
" gl_Position = vec4(aPosition,1.0);" +
"}";
片元着色器
"片元" 可以简单理解为像素,片元着色器也就意味着我们可以操作图像的像素,比如,颜色 、坐标、深度等。所以,片元着色器就是我们实现各种特效的地方。
片元着色器总是在顶点着色器之后执行,片元着色器会为每个 "片元" 执行一次片元着色器,这意味着顶点着色器和片元着色器的执行次数并不是相同的。你可能会产生疑问?? 如果不相同顶点着色器的顶点坐标如何传入片元着色器呢???
如果要搞清楚这个问题,我们就需要知道 OpenGL 的渲染管线,如下图:
渲染管线是指图形数据经过一系列处理过程,最终输出到屏幕上,这个过程就像一个输送管道,或者一个处理流水线,它有着固定的处理顺序。
从上图 管线, 我们可以看到在顶点着色器和片元着色器之间有图元装配 、 几何着色器、光栅化阶段。
图元装配 (Primitive Assembly):将 顶点着色器输出的所有顶点作为输入,根据指定类型 (GL_POINTS、GL_LINES、GL_TRIANGLES )装配图元形状。
光栅化 (Resterization Stage): 光栅化阶段会将图元形状映射为最终屏幕上显示的像素,然后生成供片元着色器使用的 "片元",然后将每个片元输入片元着色器。
下面是一个简单的片元着色器代码:
"precision mediump float;" +
"void main(void) {" +
" gl_FragColor = vec4(1.0,0.5,0.2,1.0);" +
"}"
下图是通过顶点着色器和片元着色器绘制的三角形,具体代码可以参考 AVPlayer 项目。
详见 DemoGLTriangleActivity
04 OpenGL ES Program
Program 是 OpenGL 另外一个重要的概念,一个完整的 GL 程序顶点着色器 、片元着色器 、Program 对象是必不可少的部分,缺一不可。
Program 通过链接顶点着色器和片元着色器,并将 Program 激活后,后续我们执行的绘制命令,会在 Program 链接的顶点着色器和片元着色器中执行。
创建一个 完整的 GL 程序的过程大致如下:
// step1:创建一个 Program 程序
int program = GLES20.glCreateProgram();
checkGlError("glCreateProgram");
// step2:编译顶点着色器
int vShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER );
// step3:编译片元着色器
int fShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_2D );
// step4:将 Program 与顶点着色器和片元着色器链接
if (!linkProgram( program ,vShader,fShader)) {
GLES20.glDeleteProgram( program );
GLES20.glDeleteShader(vShader);
GLES20.glDeleteShader(fShader);
}
/**
* 加载并编译着色器
*
* @param shaderType 着色器类型 GLES20.GL_VERTEX_SHADER / GLES20.GL_FRAGMENT_SHADER
* @param source 着色器源码
* @return 着色器句柄
*/
private static int loadShader(int shaderType, String source) {
int shader = GLES20.glCreateShader(shaderType);
checkGlError("glCreateShader type=" + shaderType);
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
AVLog.e("Could not compile shader " + shaderType + ":");
AVLog.e(GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
return shader;
}
/**
* 链接程序
*
* @param program 程序句柄
* @param vShader 顶点着色器句柄
* @param fShader 片元着色器句柄
* @return 是否链接成功
*/
private static boolean linkProgram(int program,int vShader,int fShader){
GLES20.glAttachShader(program,vShader);
checkGlError("glAttachShader vShader");
GLES20.glAttachShader(program,fShader);
checkGlError("glAttachShader fShader");
GLES20.glLinkProgram(program);
checkGlError("glLinkProgram");
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
AVLog.e("Could not link program: ");
AVLog.e(GLES20.glGetProgramInfoLog(program));
return false;
}
return true;
}
详见 AVPlayer 工程
05 OpenGL ES 纹理
纹理 、 贴图 、材质的概念都比较相似,大致关系是:材质( Material )> 贴图(Map)> 纹理(Texture) ( > 表示为包含关系) , 纹理是最小输入单位,贴图更多是用来做纹理映射,贴图包含纹理及纹理的 UV 坐标,材质不仅包含纹理和贴图,更主要的功能是提供了光照 、透明度 、折射 、质感等属性信息。
你可以把纹理想象成墙面上的壁纸,它可以为物体添加细节,有更强的视觉感受。如下图所示:
一张纹理图片
在 GLSL 中纹理类型使用 sampler2D (2D世界)表示,在 片元着色器中我们已经看到纹理变量的声明方式为:
uniform sampler2D sTexture;
我们知道 uniform 属性值由应用程序赋值,
/** 生成一个纹理id,texutes 用以接收纹理句柄id */
int[] texutes = new int[1];
GLES20.glGenTextures(1,textures,0);
/** Bitmap 与 纹理绑定 */
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test_img);
// GLUtils 可以直接将 bitmap 与 纹理 id 绑定
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
如果要把改纹理绘制到屏幕上,还需指定纹理的映射关系,通常我们需要指定顶点坐标,每个顶点坐标对应一个纹理坐标(Texture Coordiate),用来标明纹理图像的哪部分被采集片段颜色(采样)。
2D 纹理坐标(x,y)范围在 0 - 1 之间,它是一个归一化坐标,不依赖实际分辨率。 纹理坐标起始点为(0,0),(0,0) 在纹理图片的左下角,与 Android 屏幕坐标系 y 轴相反,终始于(1,1),即纹理图片的右上角。 使用纹理坐标获取纹理颜色的过程叫做纹理采样(Sampling)。
将上述纹理映射到三角形上
06 OpenGL 绘制纹理
现在我们已经有一个纹理图片了,现在我们就把这张图片绘制到屏幕上,对以上内容做个整合,首先,准备顶点和片元着色器代码:
顶点着色器:
private static final String VERTEX_SHADER =
"attribute vec4 aPosition;" +
"attribute vec4 aTextureCoord;" +
"varying vec2 vTextureCoord;" +
"void main() {" +
" gl_Position = aPosition;" +
" vTextureCoord = aTextureCoord.xy;" +
"}";
在顶点着色其中我们声明了一个 aPosition 属性, aPosition 用以确定在窗口中的绘制位置。另外,我们也声明了一个 aTextureCoord 属性,该属性用来确定纹理坐标。 vTextureCoord 会传递给片元着色器,片元着色器通该属性的插值结果对纹理进行采样。
片元着色器:
private static final String FRAGMENT_SHADER_2D =
"precision mediump float;" +
"varying vec2 vTextureCoord;" +
"uniform sampler2D sTexture;" +
"void main() {" +
" gl_FragColor = texture2D(sTexture, vTextureCoord);" +
"}";
在片元着色器中,我们通过 vTextureCoord 获取从顶点着色器传入的纹理坐标,通过定义 sampler2D 属性用来接收程序传入需要绘制的纹理,然后通过 texture2D 方法对纹理进行采样渲染。
紧接着,我们需要创建一个 Program ,并生产一个纹理 id,
// GPU2DTextureProgram 为 AVPlayer 封装的 2D 纹理绘制程序
m2DTextureProgram = new GPU2DTextureProgram();
// 创建一个纹理
mImageTexure = createTexture();
// 将图片与纹理进行绑定
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,mImageTexure);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test_img);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
然后,我们在 GLSurafaceView 的 Render 方法中进行绘制, GLSurafaceView 我们会在下篇文章进行讲解。
@Override
public void onDrawFrame(GL10 gl) {
m2DTextureProgram.draw(mImageTexure);
}
详见 DemoGLTextureActivity
该部分代码已经在 AVPlayer 项目中有详细说明,这里就不在做介绍。
最终效果如下:
DemoGLTextureActivity
07 结束语
现在, 你已经对 OpenGLES 有所了解,对接下来 GLSurafeView 的使用打下了基础,这部分内容我们将在下篇文章中进行讲解。 如果你想了解更多信息, 可关注微信公众号 ( GeekDev ) 并回复 资料 获取。
往期内容:
MediaCodec/OpenMAX/StageFright 介绍
使用 MediaExtractor 及 MediaCodec 解码音视频
下期预告:
《使用OpenGLES 及 Surface 渲染视频 》
◆ ◆ ◆
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Web Designer's Idea Book
Patrick Mcneil / How / 2008-10-6 / USD 25.00
The Web Designer's Idea Book includes more than 700 websites arranged thematically, so you can find inspiration for layout, color, style and more. Author Patrick McNeil has cataloged more than 5,000 s......一起来看看 《The Web Designer's Idea Book》 这本书的介绍吧!