内容简介:陈鸿宇:《理想三旬》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 渲染视频 》
◆ ◆ ◆
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Tales from Facebook
Daniel Miller / Polity Press / 2011-4-1 / GBP 55.00
Facebook is now used by nearly 500 million people throughout the world, many of whom spend several hours a day on this site. Once the preserve of youth, the largest increase in usage today is amongst ......一起来看看 《Tales from Facebook》 这本书的介绍吧!