Android OpenGL ES2.0(一):详细讲解如何绘制一个三角形

时间:2022-09-04 17:49:56

一、Android OpenGL ES2.0简介

1. 什么是OpenGL?

OpenGL(全写Open Graphics Library)是指定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。它用于三维图像(二维的亦可),是一个功能强大,调用方便的底层图形库。

OpenGL在不同的平台上有不同的实现,但是它定义好了专业的程序接口,不同的平台都是遵照该接口来进行实现的,思想完全相同,方法名也是一致的,所以使用时也基本一致,只需要根据不同的语言环境稍有不同而已。
2.什么是OpenGL ES

OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。

OpenGL ES相对于OpenGL来说,减少了许多不是必须的方法和数据类型,去掉了不必须的功能,对代价大的功能做了限制,比OpenGL更为轻量。在OpenGL ES的世界里,没有四边形、多边形,无论多复杂的图形都是由点、线和三角形组成的,也去除了glBegin/glEnd等方法

3.OpenGL ES 可以做什么?

OpenGL ES是手机、PDA和游戏主机等嵌入式设备三维(二维也包括)图形处理的API,当然是用来在嵌入式设备上的图形处理了,OpenGL ES 强大的渲染能力使其成为我们在嵌入式设备上进行图形处理的优良选择。我们经常使用的场景有:

  • 图片处理。比如图片色调转换、美颜等。

  • 摄像头预览效果处理。比如美颜相机、恶搞相机等。

  • 视频处理。摄像头预览效果处理可以,这个自然也不在话下了。

  • 3D游戏。比如神庙逃亡、都市赛车等。

4.OpenGL ES 2.0中基本概念

4.1 着色器

OpenGL ES 2.0中最重要的一个概念就是关于着色器:顶点着色器和片元着色器。

着色器(Shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点。此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。

  • 顶点着色器

其功能是把每个顶点在虚拟空间中的三维坐标变换为可以在屏幕上显示的二维坐标,并带有用于z-buffer的深度信息。顶点着色器可以操作的属性有:位置、颜色、纹理坐标,但是不能创建新的顶点。

  • 片元着色器

片元着色器计算每个像素的颜色和其它属性。它通过应用光照值、凹凸贴图,阴影,镜面高光,半透明等处理来计算像素的颜色并输出。它也可改变像素的深度(z-buffering)或在多个渲染目标被激活的状态下输出多种颜色。

着色器语言(Shading Language)是一种高级的图形编程语言,仅适合于GPU编程,其源自应用广泛的C语言。对于顶点着色器和片元着色器的开发都需要用到着色器语言进行开发。它是面向过程的而非面向对象。具体可参照相关资料。因此,对于绘制不同的图形时,所编写的着色器语言不同,这个需要大家参照相关例子学习。

4.2坐标系
OpenGL ES 采用的是右手坐标,即向右为X正轴方向,向左为X负轴方向,向上为Y轴正轴方向,向下为Y轴负轴方向,屏幕面垂直向上为Z轴正轴方向,垂直向下为Z轴负轴方向。下图是对比右手和左手坐标系。
Android OpenGL ES2.0(一):详细讲解如何绘制一个三角形
由于实际的绘制是3D图形,投影到屏幕中显示的是2D效果,因此这里需要转换就是3D到2D图形的转换。如下图所示,实际运行是采用选取屏幕中心为原点,原点到屏幕边缘的长度为单位1,由于屏幕中心点到边缘的长度不同(屏幕一般都是长方形,而不是正方形),即:屏幕到宽的距离相对较小,因此从原点到(1,0,0)的距离和到(0,1,0)的距离在屏幕上展示的并不相同。
Android OpenGL ES2.0(一):详细讲解如何绘制一个三角形
在实际运行设置坐标时,就会出现一定的图形变换,例如绘制一个等边三角形,在实际投射到屏幕中时,显示的却是等边三角形,这个就是上面提到的屏幕中心点到屏幕边缘的长度是单位长度,在实际投射时,屏幕到宽和高的比例不同,出现如下图所示的展示效果。
Android OpenGL ES2.0(一):详细讲解如何绘制一个三角形

4.3 其他
除了上面提到的着色器和坐标外,还包括投影、光照和纹理映射等。

  • 投影:OpenGL ES中有两种投影方式:正交投影和透视投影。正交投影,物体不会随距离观测点的位置而大小发生变化。而透视投影,距离观测点越远,物体越小,距离观测点越近,物体越大。
  • 光照:在屏幕中很难直接的显示3D场景的效果,因为曲面相对平面更具有光照效果,因此在实际的绘制中,需要加入光照元素:环境光、镜面光和散射光。
  • 纹理映射:现实世界中的物体往往是绚丽多彩的,要模拟现实世界的绚丽多彩,绘制出更加真实、酷炫的3D物体,就需要用到纹理映射了。纹理映射是将2D的纹理映射到3D场景中的立体物体上。

    4.4 OpenGL ES 2.0运行过程
    读取顶点数据——执行顶点着色器——组装图元——光栅化图元——执行片元着色器——写入帧缓冲区——显示到屏幕上。

  • OpenGL作为本地库直接运行在硬件上,没有虚拟机,也没有垃圾回收或者内存压缩。在Java层定义图像的数据需要能被OpenGL存取,因此,需要把内存从Java堆复制到本地堆。

  • 顶点着色器是针对每个顶点都会执行的程序,是确定每个顶点的位置。同理,片元着色器是针对每个片元都会执行的程序,确定每个片元的颜色。
  • 着色器需要进行编译,然后链接到OpenGL程序中。一个OpenGL的程序就是把一个顶点着色器和一个片段着色器链接在一起变成单个对象。

二、绘制一个简单的三角形的步骤

绘制一个三角形的步骤,包括如下几点:

  • 在manifest中声明使用OpenGL ES 2.0 API
  • 构造GLSurfaceView.Renderer对象
  • 构造GLSurfaceView对象
  • 将Activity的ContentView设为GLSurfaceView

1、在manifest中声明使用OpenGL ES 2.0 API

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

0x00020000代表OpenGL ES 2.0版本,3.0版本是0x00030000,3.1版本是0x00030001

2、构造GLSurfaceView.Renderer对象
Renderer类提供三个回调方法供Android系统调用,用来计算在GLSurfaceView中绘制什么以及如何绘制。
onSurfaceCreated():仅调用一次,用于设置view的OpenGL ES环境(初始化)
onDrawFrame():每次重绘view时调用,我们自定义的绘制主要是在该方法中实现
onSurfaceChanged():当view的几何形状发生变化时调用,比如设备屏幕方向改变时

public class TriggerRender implements GLSurfaceView.Renderer {
    float triangleCoords[] = {
            0.5f, 0.5f, 0.0f, // top
            -0.5f, -0.5f, 0.0f, // bottom left
            0.5f, -0.5f, 0.0f  // bottom right
    };
    float color[] = {1.0f, 0f, 0f, 1.0f}; //red
    private FloatBuffer vertexBuffer;
    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                    "void main() {" +
                    " gl_Position = vPosition;" +
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    " gl_FragColor = vColor;" +
                    "}";
    private int mProgram;
    private int mPositionHandle;
    private int mColorHandle;
    static final int COORDS_PER_VERTEX = 3;
    //顶点个数
    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    //顶点之间的偏移量
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 每个顶点四个字节

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        //将背景设置为灰色
        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
        //将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
        vertexBuffer = BufferUtil.fBuffer(triangleCoords);

        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);

        //创建一个空的OpenGLES程序
        mProgram = GLES20.glCreateProgram();
        //将顶点着色器加入到程序
        GLES20.glAttachShader(mProgram, vertexShader);
        //将片元着色器加入到程序中
        GLES20.glAttachShader(mProgram, fragmentShader);
        //连接到着色器程序
        GLES20.glLinkProgram(mProgram);
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        //将程序加入到OpenGLES2.0环境(加载
        // )
        GLES20.glUseProgram(mProgram);

        //获取顶点着色器的vPosition成员句柄
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        //启用三角形顶点的句柄
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        //准备三角形的坐标数据
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false,
                vertexStride, vertexBuffer);
        //获取片元着色器的vColor成员的句柄
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
        //设置绘制三角形的颜色
        GLES20.glUniform4fv(mColorHandle, 1, color, 0);
        //绘制三角形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
        //禁止顶点数组的句柄
        GLES20.glDisableVertexAttribArray(mPositionHandle);
    }
    public int loadShader(int type, String shaderCode) {
        //根据type创建顶点着色器或者片元着色器
        int shader = GLES20.glCreateShader(type);
        //将资源加入到着色器中,并编译
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }
}

以上代码有完善的注释,不需额外讲解,有一点需要大家注意,在创建顶点坐标数组时,由于Java采用的是大端模式,OpenGL ES采用的是小端模式,需要大家手动转换一下,否则会报错误,详细分析请看Must use a native order direct Buffer
3、构造GLSurfaceView对象

public class MyGLSurfaceView extends GLSurfaceView {
    private final TriggerRender mRenderer;

    public MyGLSurfaceView(Context context) {
        super(context);
        // 创建OpenGL ES 2.0的上下文
        setEGLContextClientVersion(2);
        mRenderer = new TriggerRender();
        //设置Renderer用于绘图
        setRenderer(mRenderer);
        //只有在绘制数据改变时才绘制view,可以防止GLSurfaceView帧重绘
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
}

4、将Activity的ContentView设为GLSurfaceView

   private MyGLSurfaceView mGLSurfaceView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //实例化对象
        mGLSurfaceView = new MyGLSurfaceView(this);
        setContentView(mGLSurfaceView);
    }

这里需要注意,我们不需要额外的修改Activity的布局内容(不需要额外将自定义的View放置在layout中)。

三、小结

至此,通过OpenGL ES 来绘制一个简单的三角形,就完成了,总结一下本文重点:

  • 讲解OpenGL 和OpenGL ES是什么?,OpenGL ES可以做什么?
  • 讲解OpenGL ES的基本概念:着色器、坐标系和其他相关概念
  • 详细叙述OpenGL ES绘制一个三角形的步骤

有需要该Demo的可在GitHub中下载。