阅读 44

OpenGL ES 2.0

OpenGL ES

图像处理和渲染就是在将要渲染到窗口上的像素上做许许多多的浮点运算。

通过有效的利用 GPU,可以成百倍甚至上千倍地提高手机上的图像渲染能力。如果不是基于 GPU 的处理,手机上实时高清视频滤镜是不现实,甚至不可能的。

着色器 (shader) 是我们利用这种能力的工具。着色器是用着色语言写的小的,基于 C 语言的程序。

在 OpenGL ES 中你必须创建两种着色器:顶点着色器 (vertex shaders) 和片段着色器 (fragment shaders)。 顶点着色器定义了在 2D 或者 3D 场景中几何图形是如何处理的。一个顶点指的是 2D 或者 3D 空间中的一个点。在图像处理中,有 4 个顶点:每一个顶点代表图像的一个角。顶点着色器设置顶点的位置,并且把位置和纹理坐标这样的参数发送到片段着色器。

然后 GPU 使用片段着色器在对象或者图片的每一个像素上进行计算,最终计算出每个像素的最终颜色。图片,归根结底,实际上仅仅是数据的集合。图片包含每一个像素的各个颜色分量和像素透明度的值。因为对每一个像素,算式是相同的,GPU 可以流水线作业这个过程,从而更加有效的进行处理。使用正确优化过的着色器,在 GPU 上进行处理,将使你获得百倍于在 CPU 上用同样的过程进行图像处理的效率。

GPU工作原理介绍

gpu图形处理,可以大致分成 5 个步骤,如下图箭头的部分。分别为 vertex shader、primitive processing、rasterisation、fragment shader、testing and blending。

第一步,vertex shader。是将三维空间中数个(x,y,z)顶点放进 gpu 中。在这一步骤中,电脑会在内部模拟出一个三维空间,并将这些顶点放置在这一空间内部。接着,投影在同一平面上,也是我们将看到的画面。同时,存下各点距离投影面的垂直距离,以便做后续的处理。

这个过程就像是本地球观看星星一般。地球的天空,就像是一个投影面,所有的星星,不管远近皆投影在同一面上。本地球的我们,抬起头来观看星星,分不出星星的远近,只能分辨出亮度。gpu 所投影出的结果,和这个情况类似。

从地球所看到的星空,星星就像是投影到一球面上,除非使用特别的仪器,不然分不出星星和地球的距离

第二步,primitive processing。是将相关的点链接在一起,以形成图形。在一开始输入数个顶点进入 gpu 时,程序会特别注记哪些点是需要组合在一起,以形成一线或面。就像是看星座的时候一样,将相关连的星星连起来,形成特定的图案。

第三步,rasterisation。因为电脑的屏幕是由一个又一个的像素组成,因此,需要将一条连续的直线,使用绘图的演算法,以方格绘出该直线。图形也是以此方式,先标出边线,再用方格填满整个平面。

第四步,fragment shader。将格点化后的图形着上颜色。所需着上的颜色也是于输入时便被注记。在游玩游戏时,这一步相当耗费 gpu 的计算资源,因为光影的效果、物体表面材质皆是在这一步进行,这些计算决定着游戏画面的精细程度。因此在游玩游戏时,调高游戏画面品质大幅增加这一步的计算负担,降低游戏品质。

将一个三角形,用方格呈现近似原始图案,并着上颜色。一块又一块的方格,就是显示器上的像素

最后一步,testing and blending。便是将第一步所获得的投影垂直距离取出,和第四步的结果一同做最后处理。在去除被会被其他较近距离的物体挡住的物体后,让剩下的图形放进 gpu 的输出内存。之后,结果便会被送到电脑屏幕显示

解析顶点着色器

 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;
 
 varying vec2 textureCoordinate;
 
 void main()
 {
     gl_Position = position;
     textureCoordinate = inputTextureCoordinate.xy;
 }
复制代码
attribute vec4 position;

我们创建了一系列顶点,我们为每个顶点提供的参数里的其中一个是顶点在画布中的位置。然后我们必须告诉我们的顶点着色器它需要接收这个参数,我们稍后会将它用在某些事情上。
复制代码
attribute vec4 inputTextureCoordinate;

纹理坐标是纹理映射的一部分。这意味着你想要对你的纹理进行某种滤镜操作的时候会用到它。左上角坐标是 (0,0)。右上角的坐标是 (1,0)。如果我们需要在图片内部而不是边缘选择一个纹理坐标,我们需要在我们的应用中设定的纹理坐标就会与此不同,像是 (.25, .25) 是在图片左上角向右向下各图片高宽1/4的位置。在我们当前的图像处理应用里,我们希望纹理坐标和顶点位置一致,因为我们想覆盖到图片的整个长度和宽度。有时候你或许会希望这些坐标是不同的,所以需要记住它们未必是相同的坐标。
复制代码
varying vec2 textureCoordinate;

因为顶点着色器负责和片段着色器交流,所以我们需要创建一个变量和它共享相关的信息。在图像处理中,片段着色器需要的唯一相关信息就是顶点着色器现在正在处理哪个像素。

如果你使用最新的opengl es版本, 这个关键字varying 已经取消了. 2008.8.11  OGL3.0发布,伴随GLSL1.30.10,  其中的attribute   varying等就已经改成了  in和out;   为了保持原文的一致性, 这里仍然使用varying关键字.
复制代码
gl_Position = position;

gl_Position 是一个内建的变量。GLSL 有一些内建的变量,在片段着色器的例子中我们将看到其中的一个。这些特殊的变量是可编程管道的一部分,API 会去寻找它们,并且知道如何和它们关联上。在这个例子中,我们指定了顶点的位置,并且把它从我们的程序中反馈给渲染管线。
复制代码
textureCoordinate = inputTextureCoordinate.xy;

我们取出这个顶点中纹理坐标的 X 和 Y 的位置。我们只关心 inputTextureCoordinate 中的前两个参数,X 和 Y。这个坐标最开始是通过 4 个属性存在顶点着色器里的,但我们只需要其中的两个。我们拿出需要的属性,然后赋值给一个将要和片段着色器通信的变量,而不是把更多的属性反馈给片段着色器。
复制代码

片段着色器

直通滤镜

varying highp vec2 textureCoordinate;
 
uniform sampler2D inputImageTexture;
 
void main()  
{
    gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}
复制代码

这个着色器实际上不会改变图像中的任何东西。它是一个直通着色器,意味着我们输入每一个像素,然后输出完全相同的像素。我们来一句句的看:

varying highp vec2 textureCoordinate;

因为片段着色器作用在每一个像素上,我们需要一个方法来确定我们当前在分析哪一个像素/片段。它需要存储像素的 X 和 Y 坐标。我们接收到的是当前在顶点着色器被设置好的纹理坐标。
复制代码
uniform sampler2D inputImageTexture;

为了处理图像,我们从应用中接收一个图片的引用,我们把它当做一个 2D 的纹理。这个数据类型被叫做 sampler2D ,这是因为我们要从这个 2D 纹理中采样出一个点来进行处理。
复制代码
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);

这是我们碰到的第一个 GLSL 特有的方法:texture2D,顾名思义,创建一个 2D 的纹理。它采用我们之前声明过的属性作为参数来决定被处理的像素的颜色。这个颜色然后被设置给另外一个内建变量,gl_FragColor。因为片段着色器的唯一目的就是确定一个像素的颜色,gl_FragColor 本质上就是我们片段着色器的返回语句。一旦这个片段的颜色被设置,接下来片段着色器就不需要再做其他任何事情了,所以你在这之后写任何的语句,都不会被执行。
复制代码

输入,输出,以及精度修饰 (Precision Qualifiers)

看一看我们的直通着色器,你会注意到有一个属性被标记为 “varying”,另一个属性被标记为 “uniform”。 这些变量是 GLSL 中的输入和输出。它允许从我们应用的输入,以及在顶点着色器和片段着色器之间进行交流。 在 GLSL 中,实际有三种标签可以赋值给我们的变量:

  • Uniforms
  • Attributes
  • Varyings

再一次声明, Varyings 已经作废, 请使用 in out关键字.

Uniforms 是一种外界和你的着色器交流的方式。Uniforms 是为在一个渲染循环里不变的输入值设计的。如果你正在应用茶色滤镜,并且你已经指定了滤镜的强度,那么这些就是在渲染过程中不需要改变的事情,你可以把它作为 Uniform 输入。 Uniform 在顶点着色器和片段着色器里都可以被访问到。

Attributes 仅仅可以在顶点着色器中被访问。Attribute 是在随着每一个顶点不同而会发生变动的输入值,例如顶点的位置和纹理坐标等。顶点着色器利用这些变量来计算位置,以它们为基础计算一些值,然后把这些值以 varyings 的方式传到片段着色器。

最后,但同样重要的,是 varyings 标签。Varying 在顶点着色器和片段着色器都会出现。Varying 是用来在顶点着色器和片段着色器传递信息的,并且在顶点着色器和片段着色器中必须有匹配的名字。数值在顶点着色器被写入到 varying ,然后在片段着色器被读出。被写入 varying 中的值,在片段着色器中会被以插值的形式插入到两个顶点直接的各个像素中去。

回看我们之前写的简单的着色器的例子,在顶点着色器和片段着色器中都用 varying 声明了 textureCoordinate。我们在顶点着色器中写入 varying 的值。然后我们把它传入片段着色器,并在片段着色器中读取和处理。

你会注意到纹理坐标有一个叫做 highp 的属性。这个属性负责设置你需要的变量精度。因为 OpenGL ES 被设计为在处理能力有限的系统中使用,精度限制被加入进来可以提高效率。

精度修饰存在于 OpenGL ES 中,因为它是被设计用在移动设备中的。但是,在老版本的桌面版的 OpenGL 中则没有。因为 OpenGL ES 实际上是 OpenGL 的子集,你几乎总是可以直接把 OpenGL ES 的项目移植到 OpenGL。如果你这样做,记住一定要在你的桌面版着色器中去掉精度修饰。这是很重要的一件事,尤其是当你计划在 iOS 和 OS X 之间移植项目时。

向量

在 GLSL 环境中,向量是一个类似数组的特殊的数据类型。每一种类型都有固定的可以保存的元素。深入研究一下,你甚至可以获得数组可以存储的数值的精确的类型。但是在大多数情况下,只要使用通用的向量类型就足够了。 有三种向量类型你会经常看到:

  • vec2
  • vec3
  • vec4

这些向量类型包含特定数量的浮点数:vec2 包含两个浮点数,vec3 包含三个浮点数,vec4 包含四个浮点数。

这些类型可以被用在着色器中可能被改变或者持有的多种数据类型中。在片段着色器中,很明显 X 和 Y 坐标是的你想保存的信息。 (X,Y) 存储在 vec2 中就很合适。

在图像处理过程中,另一个你可能想持续追踪的事情就是每个像素的 R,G,B,A 值。这些可以被存储在 vec4 中。

矩阵

现在我们已经了解了向量,接下来继续了解矩阵。矩阵和向量很相似,但是它们添加了额外一层的复杂度。矩阵是一个浮点数数组的数组,而不是单个的简单浮点数数组。 类似于向量,你将会经常处理的矩阵对象是:

  • mat2
  • mat3
  • mat4

vec2 保存两个浮点数,mat 保存相当于两个 vec2 对象的值。将向量对象传递到矩阵对象并不是必须的,只需要有足够填充矩阵的浮点数即可。在 mat2 中,你需要传入两个 vec2 或者四个浮点数。因为你可以给向量命名,而且相比于直接传浮点数,你只需要负责两个对象,而不是四个,所以非常推荐使用封装好的值来存储你的数字,这样更利于追踪。对于 mat4 会更复杂一些,因为你要负责 16 个数字,而不是 4 个。

在我们 mat2 的例子中,我们有两个 vec2 对象。每个 vec2 对象代表一行。每个 vec2 对象的第一个元素代表一列。构建你的矩阵对象的时候,确保每个值都放在了正确的行和列上是很重要的,否则使用它们进行运算肯定得不到正确的结果。

既然我们有了矩阵也有了填充矩阵的向量,问题来了:“我们要用它们做什么呢?“ 我们可以存储点和颜色或者其他的一些的信息,但是要如果通过修改它们来做一些很酷的事情呢?

GLSL 特有函数

step(): GPU 有一个局限性,它并不能很好的处理条件逻辑。GPU 喜欢做的事情是接受一系列的操作,并将它们作用在所有的东西上。分支会在片段着色器上导致明显的性能下降,在移动设备上尤其明显。step() 通过允许在不产生分支的前提下实现条件逻辑,从而在某种程度上可以缓解这种局限性。如果传进 step() 函数的值小于阈值,step() 会返回 0.0。如果大于或等于阈值,则会返回 1.0。通过把这个结果和你的着色器的值相乘,着色器的值就可以被使用或者忽略,而不用使用 if() 语句。

mix(): mix 函数将两个值 (例如颜色值) 混合为一个变量。如果我们有红和绿两个颜色,我们可以用 mix() 函数线性插值。这在图像处理中很常用,比如在应用程序中通过一组独特的设定来控制效果的强度等。

clamp(): GLSL 中一个比较一致的方面就是它喜欢使用归一化的坐标。它希望收到的颜色分量或者纹理坐标的值在 0.0 和 1.0 之间。为了保证我们的值不会超出这个非常窄的区域,我们可以使用 clamp() 函数。 clamp() 会检查并确保你的值在 0.0 和 1.0 之间。如果你的值小于 0.0,它会把值设为 0.0。这样做是为了防止一些常见的错误,例如当你进行计算时意外的传入了一个负数,或者其他的完全超出了算式范围的值。

更复杂的着色器的例子

饱和度调整

在这个着色器上,参照人类对颜色和亮度的感知过程,我们有一些优化可以使用。一般而言,人类对亮度要比对颜色敏感的多。这么多年来,压缩软件体积的一个优化方式就是减少存储颜色所用的内存。

人类不仅对亮度比颜色要敏感,同样亮度下,我们对某些特定的颜色反应也更加灵敏,尤其是绿色。这意味着,当你寻找压缩图片的方式,或者以某种方式改变它们的亮度和颜色的时候,多放一些注意力在绿色光谱上是很重要的,因为我们对它最为敏感。

varying highp vec2 textureCoordinate;
 
 uniform sampler2D inputImageTexture;
 uniform lowp float saturation;
 
 const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);
 
 void main()
 {
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
    lowp vec3 greyScaleColor = vec3(luminance);
    
    gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w);
     
 }
复制代码

我们一行行的看这个片段着色器的代码:

varying highp vec2 textureCoordinate;
 
uniform sampler2D inputImageTexture;  
uniform lowp float saturation;
复制代码

再一次,因为这是一个要和基础的顶点着色器通信的片段着色器,我们需要为输入纹理坐标和输入图片纹理声明一个 varyings 变量,这样才能接收到我们需要的信息,并进行过滤处理。这个例子中我们有一个新的 uniform 的变量需要处理,那就是饱和度。饱和度的数值是一个我们从用户界面设置的参数。我们需要知道用户需要多少饱和度,从而展示正确的颜色数量。

const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);
复制代码

这就是我们设置三个元素的向量,为我们的亮度来保存颜色比重的地方。这三个值加起来要为 1,这样我们才能把亮度计算为 0.0 – 1.0 之间的值。注意中间的值,就是表示绿色的值,用了 70% 的颜色比重,而蓝色只用了它的 10%。蓝色对我们的展示不是很好,把更多权重放在绿色上是很有意义的。

lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
复制代码

我们需要取样特定像素在我们图片/纹理中的具体坐标来获取颜色信息。我们将会改变它一点点,而不是想直通滤镜那样直接返回。

lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
复制代码

我们是在使用 GLSL 中的点乘运算。如果你记得在学校里曾用过点运算符来相乘两个数字的话,那么你就能明白是什么回事儿了。点乘计算以包含纹理颜色信息的vec4 为参数,舍弃 vec4 的最后一个不需要的元素,将它和相对应的亮度权重相乘。然后取出所有的三个值把它们加在一起,计算出这个像素综合的亮度值。

lowp vec3 greyScaleColor = vec3(luminance);
复制代码

为什么又声明了一个vec3变量, 是为了后面调用mix函数时必须提供一个vec3的变量.

mix(greyScaleColor, textureColor.rgb, saturation)
复制代码

关于mix()函数的签名: anyFLoat mix(anyFLoat x,anyFloat y,anyFloat a) 返回x和y的线性混合,a从0到1变化

最后,我们把所有的片段组合起来。为了确定每个新的颜色是什么,我们使用刚刚学过的很好用的 mix 函数。mix 函数会把我们刚刚计算的灰度值和初始的纹理颜色以及我们得到的饱和度的信息相结合。 这就是一个很棒的,好用的着色器,它让你用主函数里的四行代码就可以把图片从彩色变到灰色,或者从灰色变到彩色。

demo

什么是EGL, 有什么用? 怎么用? 为什么要用?

EGL实际上是OpenGL和设备(又或者叫操作系统)间的中间件,因为OpenGL是标准的,但设备是千奇百怪的,要对接就需要一个中间件做协调。也就是说一个设备要支持OpenGL,那么它需要开发一套相对应的EGL来对接。EGL主要负责初始化OpenGL的运行环境和设备间的交互,简单的说就是OpenGL负责绘图EGL负责和设备交互。

自带EGL环境的GLSurfaceView

我们知道SurfaceView实质是将底层显存Surface显示在界面上,而GLSurfaceView做的就是在这个基础上增加OpenGL绘制环境。

GLSurfaceView demoGlv = (GLSurfaceView) findViewById(R.id.glv_main_demo);
// 设置OpenGL版本(一定要设置)
demoGlv.setEGLContextClientVersion(2); 
// 设置渲染器(后面会着重讲这个渲染器的类)
demoGlv.setRenderer(new MyRenderer());
// 设置渲染模式为连续模式(会以60fps的速度刷新)
demoGlv.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
复制代码

现在画布有了,我们需要的就是一根画笔和想要画什么样的图形。在OpenGL中着色器shader就相当于画笔,而顶点vertices相当于图形。

着色器OpenGL中分成两个部分,一个用于绘制顶点的顶点着色器VerticesShader;一个用于给顶点连线后所包围的区域填充颜色的片元着色器FragmentShader,你可以简单的理解成windows中画图的填充工具。

public class MyRenderer implements GLSurfaceView.Renderer {
    private int program;
    private int vPosition;
    private int uColor;

    /**
     * 加载制定shader的方法
     * @param shaderType shader的类型  GLES20.GL_VERTEX_SHADER   GLES20.GL_FRAGMENT_SHADER
     * @param sourceCode shader的脚本
     * @return shader索引
     */
    private int loadShader(int shaderType,String sourceCode) {
        // 创建一个新shader
        int shader = GLES20.glCreateShader(shaderType);
        // 若创建成功则加载shader
        if (shader != 0) {
            // 加载shader的源代码
            GLES20.glShaderSource(shader, sourceCode);
            // 编译shader
            GLES20.glCompileShader(shader);
            // 存放编译成功shader数量的数组
            int[] compiled = new int[1];
            // 获取Shader的编译情况
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
            if (compiled[0] == 0) {//若编译失败则显示错误日志并删除此shader
                Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
                Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
                GLES20.glDeleteShader(shader);
                shader = 0;
            }
        }
        return shader;
    }

    /**
     * 创建shader程序的方法
     */
    private int createProgram(String vertexSource, String fragmentSource) {
        //加载顶点着色器
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) {
            return 0;
        }

        // 加载片元着色器
        int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

        // 创建程序
        int program = GLES20.glCreateProgram();
        // 若程序创建成功则向程序中加入顶点着色器与片元着色器
        if (program != 0) {
            // 向程序中加入顶点着色器
            GLES20.glAttachShader(program, vertexShader);
            // 向程序中加入片元着色器
            GLES20.glAttachShader(program, pixelShader);
            // 链接程序
            GLES20.glLinkProgram(program);
            // 存放链接成功program数量的数组
            int[] linkStatus = new int[1];
            // 获取program的链接情况
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
            // 若链接失败则报错并删除程序
            if (linkStatus[0] != GLES20.GL_TRUE) {
                Log.e("ES20_ERROR", "Could not link program: ");
                Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
                GLES20.glDeleteProgram(program);
                program = 0;
            }
        }
        return program;
    }

    /**
     * 获取图形的顶点
     * 特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
     * 转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
     *
     * @return 顶点Buffer
     */
    private FloatBuffer getVertices() {
        float vertices[] = {
                0.0f,   0.5f,
                -0.5f, -0.5f,
                0.5f,  -0.5f,
        };

        // 创建顶点坐标数据缓冲
        // vertices.length*4是因为一个float占四个字节
        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
        vbb.order(ByteOrder.nativeOrder());             //设置字节顺序
        FloatBuffer vertexBuf = vbb.asFloatBuffer();    //转换为Float型缓冲
        vertexBuf.put(vertices);                        //向缓冲区中放入顶点坐标数据
        vertexBuf.position(0);                          //设置缓冲区起始位置

        return vertexBuf;
    }

    /**
     * 当GLSurfaceView中的Surface被创建的时候(界面显示)回调此方法,一般在这里做一些初始化
     * @param gl10 1.0版本的OpenGL对象,这里用于兼容老版本,用处不大
     * @param eglConfig egl的配置信息(GLSurfaceView会自动创建egl,这里可以先忽略)
     */
    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        // 初始化着色器
        // 基于顶点着色器与片元着色器创建程序
        program = createProgram(verticesShader, fragmentShader);
        // 获取着色器中的属性引用id(传入的字符串就是我们着色器脚本中的属性名)
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        uColor = GLES20.glGetUniformLocation(program, "uColor");

        // 设置clear color颜色RGBA(这里仅仅是设置清屏时GLES20.glClear()用的颜色值而不是执行清屏)
        GLES20.glClearColor(1.0f, 0, 0, 1.0f);
    }

    /**
     * 当GLSurfaceView中的Surface被改变的时候回调此方法(一般是大小变化)
     * @param gl10 同onSurfaceCreated()
     * @param width Surface的宽度
     * @param height Surface的高度
     */
    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        // 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
        GLES20.glViewport(0,0,width,height);
    }

    /**
     * 当Surface需要绘制的时候回调此方法
     * 根据GLSurfaceView.setRenderMode()设置的渲染模式不同回调的策略也不同:
     * GLSurfaceView.RENDERMODE_CONTINUOUSLY : 固定一秒回调60次(60fps)
     * GLSurfaceView.RENDERMODE_WHEN_DIRTY   : 当调用GLSurfaceView.requestRender()之后回调一次
     * @param gl10 同onSurfaceCreated()
     */
    @Override
    public void onDrawFrame(GL10 gl10) {
        // 获取图形的顶点坐标
        FloatBuffer vertices = getVertices();

        // 清屏
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

        // 使用某套shader程序
        GLES20.glUseProgram(program);
        // 为画笔指定顶点位置数据(vPosition)
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertices);
        // 允许顶点位置数据数组
        GLES20.glEnableVertexAttribArray(vPosition);
        // 设置属性uColor(颜色 索引,R,G,B,A)
        GLES20.glUniform4f(uColor, 0.0f, 1.0f, 0.0f, 1.0f);
        // 绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);
    }

    // 顶点着色器的脚本
    private static final String verticesShader
            = "attribute vec2 vPosition;            \n" // 顶点位置属性vPosition
            + "void main(){                         \n"
            + "   gl_Position = vec4(vPosition,0,1);\n" // 确定顶点位置
            + "}";

    // 片元着色器的脚本
    private static final String fragmentShader
            = "precision mediump float;         \n" // 声明float类型的精度为中等(精度越高越耗资源)
            + "uniform vec4 uColor;             \n" // uniform的属性uColor
            + "void main(){                     \n"
            + "   gl_FragColor = uColor;        \n" // 给此片元的填充色
            + "}";
}
复制代码
GLES20.glVertexAttribPointer(属性索引,单顶点大小,数据类型,归一化,顶点间偏移量,顶点Buffer)
GLES20.glDrawArrays(绘制方式, 起始偏移, 顶点数量)
复制代码

GLSurfaceView将OpenGL绑定到一起,也就是说GLSurfaceView一但销毁,伴随的OpenGL也一起销毁了,一个OpenGL只能渲染一个GLSurfaceView。(如果你的应用是基于实时显示,用不到保留状态或者后台渲染那这部分是不需要的)

为 SurfaceView 添加 EGL 环境

EGL既然做平台和OpenGL ES的中间件那EGL做的就肯定是和平台息息相关的事:

  • 创建绘图窗口

也就是所谓的FrameBuffer,FrameBuffer可以显示到屏幕上(SurfaceView)

  • 创建渲染环境(Context上下文)

渲染环境指OpenGL ES的所有项目运行需要的数据结构。如顶点、片段着色器、顶点数据矩阵。

OpenGL渲染一般流程

SurfaceView sv = (SurfaceView)findViewById(R.id.sv_main_demo);
glRenderer = new GLRenderer();
glRenderer.start();

sv.getHolder().addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
        glRenderer.render(surfaceHolder.getSurface(),width,height);  // 这里偷懒直接在主线程渲染了,大家切莫效仿
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

    }
});
复制代码

OpenGL的渲染是基于线程的,我们这里先创建一个GLRenderer类继承于HandlerThread:

public class GLRenderer extends HandlerThread {
    private static final String TAG = "GLThread";
    private EGLConfig eglConfig = null;
    private EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY;
    private EGLContext eglContext = EGL14.EGL_NO_CONTEXT;

    private int program;
    private int vPosition;
    private int uColor;

    public GLRenderer() {
        super("GLRenderer");
    }

    /**
     * 创建OpenGL环境
     */
    private void createGL(){
        // 获取显示设备(默认的显示设备)
        eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        // 初始化
        int []version = new int[2];
        if (!EGL14.eglInitialize(eglDisplay, version,0,version,1)) {
            throw new RuntimeException("EGL error "+EGL14.eglGetError());
        }
        // 获取FrameBuffer格式和能力
        int []configAttribs = {
                EGL14.EGL_BUFFER_SIZE, 32,
                EGL14.EGL_ALPHA_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
                EGL14.EGL_NONE
        };
        int []numConfigs = new int[1];
        EGLConfig[]configs = new EGLConfig[1];
        if (!EGL14.eglChooseConfig(eglDisplay, configAttribs,0, configs, 0,configs.length, numConfigs,0)) {
            throw new RuntimeException("EGL error "+EGL14.eglGetError());
        }
        eglConfig = configs[0];
        // 创建OpenGL上下文
        int []contextAttribs = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };
        eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribs,0);
        if(eglContext== EGL14.EGL_NO_CONTEXT) {
            throw new RuntimeException("EGL error "+EGL14.eglGetError());
        }
    }

    /**
     * 销毁OpenGL环境
     */
    private void destroyGL(){
        EGL14.eglDestroyContext(eglDisplay, eglContext);
        eglContext = EGL14.EGL_NO_CONTEXT;
        eglDisplay = EGL14.EGL_NO_DISPLAY;
    }

    @Override
    public synchronized void start() {
        super.start();

        new Handler(getLooper()).post(new Runnable() {
            @Override
            public void run() {
                createGL();
            }
        });
    }

    public void release(){
        new Handler(getLooper()).post(new Runnable() {
            @Override
            public void run() {
                destroyGL();
                quit();
            }
        });
    }

    /**
     * 加载制定shader的方法
     * @param shaderType shader的类型  GLES20.GL_VERTEX_SHADER   GLES20.GL_FRAGMENT_SHADER
     * @param sourceCode shader的脚本
     * @return shader索引
     */
    private int loadShader(int shaderType,String sourceCode) {
        // 创建一个新shader
        int shader = GLES20.glCreateShader(shaderType);
        // 若创建成功则加载shader
        if (shader != 0) {
            // 加载shader的源代码
            GLES20.glShaderSource(shader, sourceCode);
            // 编译shader
            GLES20.glCompileShader(shader);
            // 存放编译成功shader数量的数组
            int[] compiled = new int[1];
            // 获取Shader的编译情况
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
            if (compiled[0] == 0) {//若编译失败则显示错误日志并删除此shader
                Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
                Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
                GLES20.glDeleteShader(shader);
                shader = 0;
            }
        }
        return shader;
    }

    /**
     * 创建shader程序的方法
     */
    private int createProgram(String vertexSource, String fragmentSource) {
        //加载顶点着色器
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) {
            return 0;
        }

        // 加载片元着色器
        int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

        // 创建程序
        int program = GLES20.glCreateProgram();
        // 若程序创建成功则向程序中加入顶点着色器与片元着色器
        if (program != 0) {
            // 向程序中加入顶点着色器
            GLES20.glAttachShader(program, vertexShader);
            // 向程序中加入片元着色器
            GLES20.glAttachShader(program, pixelShader);
            // 链接程序
            GLES20.glLinkProgram(program);
            // 存放链接成功program数量的数组
            int[] linkStatus = new int[1];
            // 获取program的链接情况
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
            // 若链接失败则报错并删除程序
            if (linkStatus[0] != GLES20.GL_TRUE) {
                Log.e("ES20_ERROR", "Could not link program: ");
                Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
                GLES20.glDeleteProgram(program);
                program = 0;
            }
        }
        return program;
    }

    /**
     * 获取图形的顶点
     * 特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
     * 转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
     *
     * @return 顶点Buffer
     */
    private FloatBuffer getVertices() {
        float vertices[] = {
                0.0f,   0.5f,
                -0.5f, -0.5f,
                0.5f,  -0.5f,
        };

        // 创建顶点坐标数据缓冲
        // vertices.length*4是因为一个float占四个字节
        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
        vbb.order(ByteOrder.nativeOrder());             //设置字节顺序
        FloatBuffer vertexBuf = vbb.asFloatBuffer();    //转换为Float型缓冲
        vertexBuf.put(vertices);                        //向缓冲区中放入顶点坐标数据
        vertexBuf.position(0);                          //设置缓冲区起始位置

        return vertexBuf;
    }


    public void render(Surface surface, int width, int height){
        final int[] surfaceAttribs = { EGL14.EGL_NONE };
        EGLSurface eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface, surfaceAttribs, 0);
        EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);

        // 初始化着色器
        // 基于顶点着色器与片元着色器创建程序
        program = createProgram(verticesShader, fragmentShader);
        // 获取着色器中的属性引用id(传入的字符串就是我们着色器脚本中的属性名)
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        uColor = GLES20.glGetUniformLocation(program, "uColor");

        // 设置clear color颜色RGBA(这里仅仅是设置清屏时GLES20.glClear()用的颜色值而不是执行清屏)
        GLES20.glClearColor(1.0f, 0, 0, 1.0f);
        // 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
        GLES20.glViewport(0,0,width,height);
        // 获取图形的顶点坐标
        FloatBuffer vertices = getVertices();

        // 清屏
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        // 使用某套shader程序
        GLES20.glUseProgram(program);
        // 为画笔指定顶点位置数据(vPosition)
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertices);
        // 允许顶点位置数据数组
        GLES20.glEnableVertexAttribArray(vPosition);
        // 设置属性uColor(颜色 索引,R,G,B,A)
        GLES20.glUniform4f(uColor, 0.0f, 1.0f, 0.0f, 1.0f);
        // 绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);

        // 交换显存(将surface显存和显示器的显存交换)
        EGL14.eglSwapBuffers(eglDisplay, eglSurface);

        EGL14.eglDestroySurface(eglDisplay, eglSurface);
    }

    // 顶点着色器的脚本
    private static final String verticesShader
            = "attribute vec2 vPosition;            \n" // 顶点位置属性vPosition
            + "void main(){                         \n"
            + "   gl_Position = vec4(vPosition,0,1);\n" // 确定顶点位置
            + "}";

    // 片元着色器的脚本
    private static final String fragmentShader
            = "precision mediump float;         \n" // 声明float类型的精度为中等(精度越高越耗资源)
            + "uniform vec4 uColor;             \n" // uniform的属性uColor
            + "void main(){                     \n"
            + "   gl_FragColor = uColor;        \n" // 给此片元的填充色
            + "}";
}
复制代码

createGL()方法获取了一个默认的显示设备(也就是手机屏幕),初始化并返回当前系统使用的OpenGL版本(主板本+子版本),然后通过配置(主要以键值对的方式配置,最后由EGL_NONE结尾)得到一个EGLConfig,最后创建一个EGLContext。 destroyGL()方法则是释放掉OpenGL的资源(主要就是EGLContext)。

render()方法中主要是渲染,这里为了方便把渲染的环境和渲染写在一起并只渲染一次(我们只画了一个三角形),前三行代码我们创建了一个EGLSurface并设置为当前的渲染对象,后面eglSwapBuffers()交换了显示器和EGLSurface的显存,也就是将我们渲染的东西放到显示器去显示,这样我们就看到我们绘制的三角形了,最后就是销毁我们创建的EGLSurface。

这里要注意,OpenGL是基于线程的,虽然有些方法可以在别的线程调用,但最好还是都放到OpenGL所在的线程调用,否则可能调用后无效果(不一定报错)。

EGLConfig 属性

着色器

电脑中CPU和显卡(GPU)的关系:

OpenGL主程序在CPU上执行,主程序(CPU)向显存输入顶点等数据,启动渲染,着色器程序开始在GPU上运行。 早期GPU使用固定管线(pipeline)就像CPU中的加法器、乘法器、浮点运算单元一般,每个模块固定处理特定事情,就像工厂的流水线一般,所以才叫管线吧。与CPU不同,GPU更多的是要求并行执行(百万级的线程),因为同一个基元(图形)的每个像素点都需要做同样的处理(输入不同),所以高并发是GPU的基本特点了。 随着技术的发展,固定管线逐渐被可编程管线取代,也就是着色器,着色器处理事情根据实际需要进行编程可大大提高处理单元的利用率(如果是固定管线,那么不做某些操作那部分处理单元就被闲置了)。

图形绘制过程

绘制图形一般是我们拥有一些图形的顶点,放置到坐标系中,使用绘制函数进行绘制(顶点着色器),分块填充颜色(片元着色器)。这样我们的图形就显示到屏幕上了。

可编程管线

固定管线部分仅作为理解用,现在已经很少用了(OpenGL4.0已经不支持固定管线),我们来看一下OpenGL ES 2.0的可编程管线:

染流程如下:顶点数据(Vertices) > 顶点着色器(Vertex Shader) > 图元装配(Assembly) > 几何着色器(Geometry Shader) > 光栅化(Rasterization) > 片元着色器(Fragment Shader) > 逐片元处理(Per-Fragment Operations) > 帧缓冲(FrameBuffer)。再经过双缓冲的交换(SwapBuffer),渲染内容就显示到了屏幕上。

图元装配:往土讲就是把图形放置到坐标系中。

光栅化:将图形投影到屏幕上,把图形栅格化成一个个的像素点。一个像素点也就是一个片元。

逐片元处理:填充颜色,在渲染的时候是基于像素分片处理的,会涉及到纹理和光影处理等。

着色器

着色器的代码是运行在GPU上面的,和我们一般运行在CPU上的程序肯定会有些不同(毕竟硬件架构不一样),GPU更注重不同的输入数据执行相同的运算。

着色器是分别编译后链接成一个program的,那么对于CPU这边两个着色器是一体的,输入有Attribute、Unifroms和采样器(纹理/Textures),输出就是Framebuff,但这里我们关注一下gl_Position(位置)和gl_fragColor(颜色),因为这两个是两个着色器的主要输出(在什么位置显示什么颜色)。

Attribute:顶点属性,也就是存储顶点的坐标信息(对于着色器只读)

Unifroms:常量,一般存放矩阵变换或者光照之类(对于着色器只读)

Varying:顶点着色器和片元着色器间的通信接口变量,在顶点着色器中写入值(如颜色、纹理坐标等)在片元着色器中读出(对于片元着色器只读)

gl_Position、gl_PointSize、gl_FrontFacing、gl_FragColor、gl_FragCoord、gl_FrontFacing和gl_PointCoord:这些都是着色器内置的特殊变量,编译器已经定义好的。

着色器代码处理的是一个片元,也就是一个像素点,千千万万个点通过同一个着色器处理后组成的才是我们的图像,而不是着色器代码执行一遍图像就出来了。

OpenGL ES着色器语言

着色器语言只是引入了向量和矩阵的概念以及一些细节上的改变。同样有变量、常量、运算、函数、预处理等。

数据类型

结构体

像C 语言一样,可以集合几种变量成为结构体。定义一个结构体类型后一个同名的构造函数也被定义,可以使用构造函数进行初始化,参数是每个成员变量。访问结构体中的元素和 C 语言相同。

// 定义结构体类型 FogStruct,和变量fogVar
struct FogStruct { 
      vec4 color;  
      float start;  
      float end;  
}fogVar;
// 初始化
fogVar = FogStruct(vec4(0.0, 1.0, 0.0, 0.0),        // color  
                   0.5,                             // start  
                   2.0);                            // end
复制代码

数组

语法和C语言非常类似,索引以0开始,但索引只能是常量表达式不能是变量(可以是uniform变量,硬件限制)。不支持创建时初始化,所以也不支持const修饰。数组元素需要一对一的初始化。

变量的定义与赋值

矢量元素可以通过“.”运算符或者数组下标(vec3[1])获取。使用“.”运算符的可以选择{x, y, z, w}(顶点坐标),{r, g, b, a}(颜色),或{s, t, r, q}(贴图坐标)中的任意一组命名表表示。使用“.”运算符时可以重新排列一个向量。

float   myFloat;          // 定义一个浮点标量
bool    myBool = true;    // 定义时进行初始化
int     myInt = 0; 
vec3    myVec3;           // 定义一个三维矢量
vec4    myVec4;           // 定义一个四维矢量
mat4    myMat4;           // 定义一个4x4矩阵
float   myArray[4];       // 定义一个数组

myFloat = float(myBool);  // 使用构造函数构造一个浮点数值
myFloat = float(myInt);   // 使用构造函数构造一个浮点数值
myBool = bool(myInt);     // 使用构造函数构造一个布尔值

// 矢量赋值,矢量赋值是相当灵活的,如果只传一个标量,则赋值给矢量的所有成员
// 如果传多个标量或矢量,那从左到右依次赋值,多余部分被截断
myVec3 = vec3(1.0);          // myVec3 = {1.0, 1.0, 1.0}  
myVec3 = vec3(0.0,1.0,2.0);  // myVec3 = {0.0, 1.0, 2.0}
vec3 temp = vec3(myVec3);    // temp = myVec3 
vec2 myVec2 = vec2(myVec3);  // myVec2 = {myVec3.x, myVec3.y}  
myVec4 = vec4(myVec2, temp); // myVec4 = {myVec2.x, myVec2.y,temp.x, temp.y}
// 使用“.”运算符
temp = myVec3.xyz;            // temp = {0.0, 1.0, 2.0}
temp = myVec3.zyx;            // temp = {2.0, 1.0, 0.0}
temp = myVec3.xxx;            // temp = {0.0, 0.0, 0.0}  

// 矩阵赋值
mat3 myMat3 = mat3(1.0,2.0,3.0,  //  第一列 
                   4.0,5.0,6.0,  //  第二列 
                   7.0,8.0,9.0); //  第三列
myMat3 = mat3(1.0);              // myMat3 = {1.0,0.0,0.0,
                                 //           0.0,1.0,0.0,
                                 //           0.0,0.0,1.0}
myMat3 = mat3(myVec3,myVec3,myVec3); 
myMat3 = mat3(myVec2,1.0
              myVec2,2.0,
              myVec2,3.0);
// 从矩阵取值
myVec3 = myMat3[0];
myFloat = myMat3[1][1];
myFloat = myMat3[2].z;
复制代码

变量修饰符

和C语音类似,变量有修饰符,用来表示变量的类别,如attribute、uniform、varying和const。

// 顶点着色器
uniform mat4 viewMatrix;    // 从CPU传进来的常量属性,一般放矩阵(矩阵变换-平移、旋转、缩放...) 、光照参数或者颜色
attribute vec4 a_position;  // 顶点属性(位置),顶点着色器特有的(位置、法线、贴图坐标和颜色数据)
attribute vec2 a_texCoord0; // 顶点属性(贴图坐标)
varying vec2 v_texCoord;    // 变体(随便翻译的),顶点着色器和片元着色器公有,用于着色器间交互(如传递纹理坐标)
const float myFloat;        // 常量,和C语音一样,固定一个值不可变

void main(void) { 
      gl_Position = u_matViewProjection * a_position; 
      v_texCoord = a_texCoord0;  
} 

// 片元着色器
precision mediump float;       // 此句是声明float的默认精度是中等(片元着色器没默认值,所以需要声明,顶点着色器默认中等)
varying vec2 v_texCoord;       // 和顶点着色器做同样的声明
uniform sampler2D s_baseMap;   // 2D贴图采样器,基础贴图(暂不关心)
uniform sampler2D s_lightMap;  // 2D贴图采样器,光照贴图(暂不关心)                                              
void main(){                                                                                                       
      vec4 base Color;                                                                       
      vec4 lightColor;                                                                     
                                                                                                         
      baseColor = texture2D(s_baseMap, v_texCoord);       
      lightColor = texture2D(s_lightMap, v_texCoord);   
      gl_FragColor = baseColor * (lightColor + 0.25);       
}
复制代码

运算符

if语句

着色器支持if语句,条件表达式的结果必须是布尔值。

if(条件表达式){
    [语句组]
}else{
    [语句组]
}
复制代码

循环语句

理论上着色器是不支持循环的,这里的循环在编译的时候会被展开,所以会有很多限制。 使用循环时应该非常小心,基本的限制有:必须有循环迭代变量,它的值必须是增加或减小使用简单的表达式(i++, i -- , i+=constant, i-=constant),停止条件必须匹配循环索引,并且是常量表达式。在循环内部不能改变迭代的值。

// 错误范例
float myArr[4]; 
for(int i = 0; i < 3; i++) { 
      sum += myArr[i]; // 变量不能作为数组的下标
}

uniform int loopIter; 
// loopIter必须是常量(uniform变量也不行)
for(int i = 0; i < loopIter; i++) { 
      sum += i;   
} 
复制代码

函数

和C语言一样,函数使用前必须定义。在参数使用上,提供变量限定词,指示变量是否能够被函数修改。函数不能递归(硬件限制)。 支持内联函数

vec4 myFunc(inout float myFloat, out vec4 myVec4, mat4 myMat4){
    ...
}
复制代码

预处理(宏)

扩展

例如, 3D 贴图扩展不被支持,你想让预编译产生警告(如果被支持,着色器将正常进行)。你在你的着色器顶上加上:

#extension GL_OES_texture_3D : enable

不变性

在OpenGL ES着色器编程语言里,in variant 是被用于任何顶点着色器的变量输出的关键字。着色器被编译时可能进行优化,一些指令被重新整理。指令重新整理意味着两个着色器之间平等的计算不保证产生相同的结果。这是个问题在多通道着色器特殊情况下,依稀物体使用透明混合来绘制时。如果精度被使用来计算输出位置是不准确一样,精度的不同将导致 artifacts。这在Z fighting情况下经常发生。每个像素很小的 Z 精度不同引起了不同的反光。

顶点属性

顶点是OpenGL绘图的基础,比如说你要画一个三角形,那么你首先就需要三个顶点然后依次连接起来最后填充上颜色。那么一个顶点需要有什么样的属性呢? 顶点一般有位置(position)、法线(normal)、颜色(color)和纹理坐标(texture coordinates),但这些并非是必须的。顶点属性存储方式有两种:1)结构数组,指把每个顶点的属性放在一起,多个顶点组成数组;2)数组结构,指把每个顶点的同一属性抽出来组成数组,多个属性就有多个数组。这两种方式据说是第一种效率比较高,个人感觉是这种结构比较容易看懂调试,我们下面就来看看这种结构:

图中一个顶点包含一个位置坐标、一个法线向量和两个纹理坐标,也许你会问,为什么是两个贴图坐标?如果你做过3D游戏或者玩过游戏引擎你应该知道,一个模型建完后需要加贴图(也就是纹理,或者叫材质),除了基本的色彩贴图一般还有高光和漫反射多张贴图(这里写两个更多的是因为在OpenGL ES 1.1固定管线最多支持两个纹理坐标转换)。 贴图(纹理)不仅仅是颜色,可以说是一个存储数据的二维矩阵,贴图坐标(s、t)就是用来取这个矩阵中的值,这个值怎么用就看你着色器怎么写了。换句话说这里的坐标就是拿来和位置坐标对齐用的。毕竟一张图贴墙上你可以选正着贴倒着贴斜着贴是吧,这里也一样。 位置坐标相信我是不用多说了,那么法线是什么鬼?我们知道法线是垂直于一个面的,那一个点怎么会有法线?首先问一个问题,如果你要画一个弧面怎么画?做无限切割?如果你这么想那就too young too simple了(你真以为你传多少个顶点着色器就运行多少次?着色器运行的次数远比你想象的多),在光栅化阶段GPU会进行插补,在顶点之间分割成一小片一小片,然后每一片过一边片元着色器,此时输入的坐标就是插补得来的。换个方向想,一个面趋近于无穷小那是不是就是一个点?懂了吗,下面来看一下法线如何来插补:

上图是侧着看的,可以看到有三个顶点、三条法线和两个面(侧看为两条线),可以看出三条法线都不垂直于顶点构成的两个面,而是垂直于红色线条的曲面,红色线条则为实际插补后实际显示的曲面。

坐标系统

在OpenGL中有三种重要的坐标,即顶点的位置坐标、纹理坐标和屏幕坐标。 位置坐标标识了图元(绘制的物体)放置在哪里;纹理坐标是图元上如何贴图,比如说我们一张方形桌子(图元)上面要盖一张方形的台布(纹理),那么你是不是要把桌面四个角对应到台布的四个角,其中桌面四个角的坐标就是位置坐标而台布的四个角就是纹理坐标,四个角都对上了那台布就盖上去了,我们的贴图也就贴到了物体上面了;屏幕坐标就是这个物体映射到屏幕上的位置,因为屏幕显示是二维的,你可以理解成将桌子拍扁在底板上的样子。 这三个坐标系统的定义有些不同,位置坐标是中间为(0,0,0),往X(右)Y(上)Z(外)三个方向延伸,最大为1最小为-1的浮点坐标;纹理坐标为左下角为坐标原点(0,0),沿X(右)Y(上)方向延伸,最大为1的浮点坐标;屏幕坐标则为左上角为坐标原点(0,0),沿X(右)Y(下)方向延伸,最大无限制(看具体视口设置的大小,最大视口应该在10万像素以下,太大似乎显示异常)。

上图为一个贴图贴在一个二分之一坐标系大小的正方形上面在屏幕的显示效果(暂忽略坐标转置,相机之类的因素)。

设置OpenGL ES 2.0视图

Vertex

顶点(多个顶点)是3D模型的最小构建块。顶点是两个或多个边相交的点。在3D模型中,顶点可以在所有连接的边,步调和多边形之间共享。 顶点也可以表示相机或光源的位置。

为了在android上定义顶点,我们将它们定义为一个float数组,我们将它放入一个字节缓冲区以获得更好的性能。 查看右侧的图像和下面的代码,将图像上标记的顶点与代码相匹配。

private float vertices[] = {
      -1.0f,  1.0f, 0.0f,  // 0, Top Left
      -1.0f, -1.0f, 0.0f,  // 1, Bottom Left
       1.0f, -1.0f, 0.0f,  // 2, Bottom Right
       1.0f,  1.0f, 0.0f,  // 3, Top Right
};

// a float is 4 bytes, therefore we multiply the number if vertices with 4.
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder());
FloatBuffer vertexBuffer = vbb.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
复制代码

浮点数是4个字节,并将它与顶点数相乘以在分配的缓冲区上获得正确的大小。

当你告诉它渲染时,OpenGL ES有一个管道,其中包含要应用的函数。默认情况下,大多数这些功能都没有启用,因此您必须记住转动您想要使用的功能。您可能还需要告诉这些函数要使用的内容。因此,在我们的顶点的情况下,我们需要告诉OpenGL ES可以使用我们创建的顶点缓冲区,我们还需要告诉它它在哪里。

// Enabled the vertex buffer for writing and to be used during rendering.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// Specifies the location and data format of an array of vertex
// coordinates to use when rendering.
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
复制代码

完成缓冲后,不要忘记禁用它。

// Disable the vertices buffer.
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
复制代码

Edge

边是两个顶点之间的线。它们是面和多边形的边界线。在3D模型中,边缘可以在两个相邻面或多边形之间共享。 变换边会影响所有连接的顶点,面和多边形。在OpenGL ES中,您不定义边,而是通过为它们提供构建三条边的顶点来定义边。如果要修改边,可以更改构成边的两个顶点。

Face

面是三角形。面是三个角顶点和三个周围边缘之间的曲面。变换面会影响所有连接的顶点,边和多边形。

顺序很重要

在卷起面时,重要的是要在正确的方向上进行,因为方向定义了正面的哪一侧以及背面的哪一侧。为什么这很重要是因为为了获得性能,我们不想画两侧,所以我们关闭了背面。因此,在整个项目中使用相同的绕组是个好主意。可以使用glFrontFace更改定义正面的方向。

 gl.glFrontFace(GL10.GL_CCW);
复制代码

要使OpenGL跳过背对屏幕的面,可以使用称为背面剔除的东西。它是通过检查面部是否以正确的顺序结束来确定图形对象的多边形是否可见。

 gl.glEnable(GL10.GL_CULL_FACE); 
复制代码

它可以改变应该绘制的面部边缘。

 gl.glCullFace(GL10.GL_BACK);
复制代码

Polygon

记住我们已经决定逆时针方向使用默认的缠绕方式。看看右边的图像和下面的代码,看看如何绘制这个方块。

private short[] indices = { 0, 1, 2, 0, 2, 3 };
复制代码

为了获得一些性能,我们还将这些放在字节缓冲区中。

// short is 2 bytes, therefore we multiply the number if vertices with 2.
ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
ibb.order(ByteOrder.nativeOrder());
ShortBuffer indexBuffer = ibb.asShortBuffer();
indexBuffer.put(indices);
indexBuffer.position(0);
复制代码

不要忘记short是2个字节并将其乘以索引数以在分配的缓冲区上获得正确的大小。

渲染

是时候在屏幕上获取内容了,有两个功能用于绘制,我们必须决定使用哪个。

这两个功能是:

public abstract void glDrawArrays(int mode, int first, int count)
复制代码

glDrawArrays按照它们在verticesBuffer构造中指定的顺序绘制顶点。

public abstract void glDrawElements(int mode, int count, int type,
                                    Buffer indices) 
复制代码

glDrawElements需要更多才能绘制。它需要知道绘制顶点的顺序,它需要indicesBuffer。

要渲染的基元

GL_POINTS

在屏幕上绘制单个点。

GL_LINE_STRIP

GL_LINE_LOOP

GL_LINES

成对的顶点被解释为单独的线段。

GL_TRIANGLES

顶点三元组被解释为三角形。

GL_TRIANGLE_STRIP

使用顶点v0,v1,v2,然后是v2,v1,v3(注意顺序),然后是v2,v3,v4等绘制一系列三角形(三边形多边形)。 排序是为了确保三角形都以相同的方向绘制,以便条带可以正确地形成表面的一部分。

GL_TRIANGLE_FAN

与GL_TRIANGLE_STRIP相同,除了顶点是v0,v1,v2,然后是v0,v2,v3,然后是v0,v3,v4等等。

绘制方块

package se.jayway.opengl.tutorial;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import javax.microedition.khronos.opengles.GL10;

public class Square {
	// Our vertices.
	private float vertices[] = {
		      -1.0f,  1.0f, 0.0f,  // 0, Top Left
		      -1.0f, -1.0f, 0.0f,  // 1, Bottom Left
		       1.0f, -1.0f, 0.0f,  // 2, Bottom Right
		       1.0f,  1.0f, 0.0f,  // 3, Top Right
		};

	// The order we like to connect them.
	private short[] indices = { 0, 1, 2, 0, 2, 3 };

	// Our vertex buffer.
	private FloatBuffer vertexBuffer;

	// Our index buffer.
	private ShortBuffer indexBuffer;

	public Square() {
		// a float is 4 bytes, therefore we multiply the number if
		// vertices with 4.
		ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
		vbb.order(ByteOrder.nativeOrder());
		vertexBuffer = vbb.asFloatBuffer();
		vertexBuffer.put(vertices);
		vertexBuffer.position(0);

		// short is 2 bytes, therefore we multiply the number if
		// vertices with 2.
		ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
		ibb.order(ByteOrder.nativeOrder());
		indexBuffer = ibb.asShortBuffer();
		indexBuffer.put(indices);
		indexBuffer.position(0);
	}

	/**
	 * This function draws our square on screen.
	 * @param gl
	 */
	public void draw(GL10 gl) {
		// Counter-clockwise winding.
		gl.glFrontFace(GL10.GL_CCW);
		// Enable face culling.
		gl.glEnable(GL10.GL_CULL_FACE);
		// What faces to remove with the face culling.
		gl.glCullFace(GL10.GL_BACK);

		// Enabled the vertices buffer for writing and to be used during
		// rendering.
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
		// Specifies the location and data format of an array of vertex
		// coordinates to use when rendering.
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0,
                                 vertexBuffer);

		gl.glDrawElements(GL10.GL_TRIANGLES, indices.length,
				  GL10.GL_UNSIGNED_SHORT, indexBuffer);

		// Disable the vertices buffer.
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
		// Disable face culling.
		gl.glDisable(GL10.GL_CULL_FACE);
	}

}
复制代码

我们必须在OpenGLRenderer类中初始化我们的方块。

// Initialize our square.
Square square = new Square();
复制代码

并在绘制函数调用方形绘制。

public void onDrawFrame(GL10 gl) {
		// Clears the screen and depth buffer.
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
                           GL10.GL_DEPTH_BUFFER_BIT);

		// Draw our square.
		square.draw(gl); // ( NEW )
}
复制代码

OpenGL ES从当前位置的位置进行渲染,所以默认情况下是在点0,0,0处与视口相同的位置。并且OpenGL ES不会渲染太靠近视口的东西。解决方案是在绘制正方形之前将绘制位置移动几步到屏幕:

// Translates 4 units into the screen.
gl.glTranslatef(0, 0, -4);
复制代码

OpenGL ES不会重置您必须自己完成的帧之间的绘制点:

// Replace the current matrix with the identity matrix
gl.glLoadIdentity();
复制代码

Android gl版本

在onDrawFrame(GL10 gl10)生命周期回调方法中的参数是GL10 类型的, 而大部分示例, 包括谷歌官方的示例目前都是直接这么使用opengl的:

GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) 
复制代码

GL10 gl10参数基本没用了, 太老了. 只所以留下来, 可能是为了兼容老版本程序吧. 现在不只有GLES20, 还有GLES30, 甚至还有GLES31。

而关于实例调用和静态方法调用, 不管使用哪种调用方法, 效果基本是一样的, 据说静态方法调用更高效。

编写顶点着色器时, 如果加上#version 300 es , 会编译失败, 无法生成顶点着色器对象. 这个文件头不是标准的吗? in 和 out参数描述符也会导致失败, 不明白为什么 ---- 目前glsl的版本必须在3.3以上才能支持in out

安卓平台的系统UI绘制最终是如何跟opengl关联的

简单说, 是在SurfaceFlinger中与egl关联的, 当然应用程序也可以通过使用GLSurfaceView直接与opengl交互.

如果我实在受不了android的这套view框架, 那我可以通过自定义GlsurfaceView 来搭建自己的view系统. 这会我的measure, layout就不用在ui线程了.

如何对camera中的数据直接从后台进行处理, 而不是渲染到可视控件上.

这对于一些监控类的程序好像是个功能点, 目前还没搞清楚. 隐约感觉到需要使用egl创建一个pbuffer类型的输出, 然后从其中读取数据, 保存到文件. 但是怎么把camera数据导向到pbuffer类型的输出, 怎么处理YUV类型的数据 都还没想清楚.

GLSurfaceView queueEvent的作用

使用OpenGL的时候,我们需要一个工作线程来调用OpenGL方法。GLSurfaceView为此创建了一个独立的渲染线程,这也是所有实现GLSurfaceView.Renderer中的方法(这些方法包装了调用OpenGL的细节,简化了开发)的工作线程。所以我们不需要关心线程相关的东西,只需要编写需要实现的方法即可。

基于线程原因,我们不能简单的在UI线程中调用OpenGL方法,例如,事件分发的方法中我们直接调用Renderer中的方法。除此之外,我们还需要考虑线程安全问题,即同时被UI线程和OpenGL渲染线程读写的变量。

使用queueEvent(),则完全不必担心上述问题,因为最终所有方法都是在GLSUrfaceView.Renderer中的方法中调用的,也就是在渲染线程中使用的。

看看源码中该函数的调用流程:

/**
 * Queue a runnable to be run on the GL rendering thread. This can be used
 * to communicate with the Renderer on the rendering thread.
 * Must not be called before a renderer has been set.
 * @param r the runnable to be run on the GL rendering thread.
 */
public void queueEvent(Runnable r) {
    mGLThread.queueEvent(r);
}
复制代码

mGLThread即是渲染线程。从注释中即可知,这个方法是用来和渲染线程进行通信的。

/**
 * Queue an "event" to be run on the GL rendering thread.
 * @param r the runnable to be run on the GL rendering thread.
 */
public void queueEvent(Runnable r) {

    synchronized(sGLThreadManager) {
        mEventQueue.add(r);
        sGLThreadManager.notifyAll();
    }

}
复制代码

可见,当渲染线程空闲的时候,这个方法可以与渲染线程交互并唤醒渲染线程。

private ArrayList<Runnable> mEventQueue = new ArrayList<Runnable>();

private void guardedRun() throws InterruptedException {

    Runnable event = null;

    while (true) {
        synchronized (sGLThreadManager) {
            while (true) {
                if (mShouldExit) {
                    return;
                }

                if (! mEventQueue.isEmpty()) {
                    event = mEventQueue.remove(0);
                    break;
                }
                
                ...
                
                if (event != null) {
                    event.run();
                    event = null;
                    continue;
                }    

                ...
                
                view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);                
                
                ...
                
                view.mRenderer.onSurfaceChanged(gl, w, h);
                
                ...
                
                view.mRenderer.onDrawFrame(gl);
复制代码

guardedRun即是渲染线程的工作渲染函数的入口,从上面的函数选摘可知,会在渲染线程的mRenderer方法调用之前调用mEventQueue方法。

queueEvent的典型用法是根据用户输入改变mRenderer中的状态,然后在mRenderer.onDrawFrame中使用新的状态。

关注下面的标签,发现更多相似文章
评论