从显示一张图片开始学习OpenGL ES

前言

网上很多介绍OpenGL ES的文章,但由于OpenGL ES内容太多,所以这些文章难免过于臃肿杂乱,很难抓住重点,对于初学者来说最后还是云里雾里。很多人(包括笔者本人)开始深入了解OpenGL ES是因为其涉及到实时滤镜的应用,通常都会参考开源框架GPUImage的实现。如果没有掌握基本的OpenGL Es的开发知识,很难弄懂其中代码缘由。

目前很流行的短视频特效处理也有涉及到OpenGL的应用,于是已经踩坑无数的笔者下决心让后来者少走弯路,以最实用的场景——显示一张图片开始学习OpenGL ES.

本文章适合初学Android OpenGL ES 2.0+,以及想要了解OpenGL实时滤镜实现原理的同学。

 

准备

在开始实现之前,先要讲一些基本的知识,也是OpenGL ES 2D\3D绘图的一些基本理论,这里我们只讲绘制一张图片后面需要用到的知识点。

 

坐标系

OpenGL拥有独立的坐标系,没有任何变换前的初始坐标系为三维坐标系,x y z 取值范围都是 [-1, 1]:

 

由于我们绘制的是2D图片,因此可以简化为二维坐标系(只包含xy轴),坐标系的原点在窗口中央,x 轴向右,y 轴向上:

这时就有疑问了,我们的屏幕或显示窗口长宽的比例不是1:1(即不是正方形),怎么跟OpenGL的初始世界坐标系对应呢?如果我们没有指定投影比例,那么世界坐标系则会填充整个显示窗口,这样就会导致拉伸变形,比如把上面的三角形投射在窗口时的显示如下:

如果要指定投影比例就得应用到投影和矩阵变换,这里我们仍使用初始的世界坐标系,比如为了上面的三角形显示正常,根据拉伸比例改变绘制的顶点坐标即可。

 

顶点坐标

在OpenGL ES中,支持三种类型的绘制:点、直线以及三角形;由这三种图形组成其他所有图形,比如我们看到的圆滑的球体也是由一个个三角形组成的,三角形越多看上去越圆滑:

在绘制图形时我们需要提供相应的顶点位置,然后指定绘制方式去绘制这些顶点,以此呈现出我们想要的图形。

后面我们显示一张图片的时候也需要绘制由两个三角形组成的矩形,通过GL_TRIANGLE_STRIP绘制方式(即每相邻三个顶点组成一个三角形,为一系列相接三角形构成)绘制:

 

纹理贴图(纹理映射)

我们需要显示的是一张图片,而上面一直说绘制图形。这就好比我们往墙上贴墙纸,首先得搭建好房子,然后决定墙纸的每个地方贴在墙上的哪个位置,这个过程在OpenGL的绘制过程中叫做纹理贴图,也叫纹理映射。

纹理贴图时涉及到UV坐标,所有的图像文件都是二维的一个平面,通过这个平面的UV坐标我们可以定位图象上的任意一个象素,在android的uv坐标的原点在左上角:

我们根据顶点的渲染顺序,定义每个顶点uv坐标,如下图是我们定义的四个顶点,绘制成一个矩形:

那么根据顶点的渲染顺序,定义每个顶点uv坐标:

指定好特定顶点对应的纹理坐标后,顶点与顶点间的其余部分会进行图像光滑插值处理,最后整张纹理就显示出来啦。

 

光栅化

光栅化就是把顶点数据转换为片元的过程。片元中的每一个元素对应于帧缓冲区中的一个像素。

把虚拟世界中的三维几何信息投影到二维屏幕上,由于目前的显示设备屏幕都是离散化的(有一个个的像素组成),因此需要把投影结果离散化,将其分解为一个个离散化的小单元,这些小单元称之为片元(片段,Fragment).

 

着色器

OpenGL ES2.0使用可编程渲染管线,既然是可编程,那就需要我们自己写着色器代码(GLSL),OpenGL中有顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。

顶点着色器主要用来处理图形中每个顶点的最终位置。顶点数据由我们传进着色器,由于绘制图片不需要变换顶点,所以顶点着色器里面我们不需要特殊处理每个顶点。而片元着色器主要处理每个片元的最终颜色,这里我们只要根据传进来的贴图数据,进行纹理采样即可。

 

开始动手实现!

在Android系统中使用OpenGL需要涉及到两个最基本的的类,GLSurfaceView和GLSurfaceView.Renderer。

  1. 1.GLSurfaceView继承了SurfaceView类,它是专门用来显示OpenGL渲染的图形。可以这么理解,GLSurfaceView就是前面我们说的用来显示OpenGL图形的窗口。
  2. GLSurfaceView.Renderer是GLSurfaceview的渲染器,通过GLSurfaceView.setRender()设置。
interface GLSurfaceView.Renderer {
	//在Surface创建的时候回调,可以在这里进行一些初始化操作
	public void onSurfaceCreated(GL10 gl, EGLConfig config);
	//在Surface尺寸改变的的时候回调,可以在这里设置窗口的大小
	public void onSurfaceChanged(GL10 gl, int width, int height);
	//绘制每一帧的时候回调
	public void onDrawFrame(GL10 gl);
}

这里需要特别说明,Render渲染器的回调是在一个单独的线程上执行的,因此我们进行OpenGL的相关操作也需要切换到该GL环境下的线程上,可以通过GLSurfaceView.queueEvent(Runnable)把操作放入GL环境的队列中,也可以自己控制队列,等待Render回调时再执行队列的操作。

代码如下:

public class GLShowImageActivity extends Activity {
    // 绘制图片的原理:定义一组矩形区域的顶点,然后根据纹理坐标把图片作为纹理贴在该矩形区域内。

    // 原始的矩形区域的顶点坐标,因为后面使用了顶点法绘制顶点,所以不用定义绘制顶点的索引。无论窗口的大小为多少,在OpenGL二维坐标系中都是为下面表示的矩形区域
    static final float CUBE[] = { // 窗口中心为OpenGL二维坐标系的原点(0,0)
            -1.0f, -1.0f, // v1
            1.0f, -1.0f,  // v2
            -1.0f, 1.0f,  // v3
            1.0f, 1.0f,   // v4
    };
    // 纹理也有坐标系,称UV坐标,或者ST坐标。UV坐标定义为左上角(0,0),右下角(1,1),一张图片无论大小为多少,在UV坐标系中都是图片左上角为(0,0),右下角(1,1)
    // 纹理坐标,每个坐标的纹理采样对应上面顶点坐标。
    public static final float TEXTURE_NO_ROTATION[] = {
            0.0f, 1.0f, // v1
            1.0f, 1.0f, // v2
            0.0f, 0.0f, // v3
            1.0f, 0.0f, // v4
    };

    private GLSurfaceView mGLSurfaceView;
    private int mGLTextureId = OpenGlUtils.NO_TEXTURE; // 纹理id
    private GLImageHandler mGLImageHandler = new GLImageHandler();

    private FloatBuffer mGLCubeBuffer;
    private FloatBuffer mGLTextureBuffer;
    private int mOutputWidth, mOutputHeight; // 窗口大小
    private int mImageWidth, mImageHeight; // bitmap图片实际大小

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_01);
        mGLSurfaceView = findViewById(R.id.gl_surfaceview);
        mGLSurfaceView.setEGLContextClientVersion(2); // 创建OpenGL ES 2.0 的上下文环境

        mGLSurfaceView.setRenderer(new MyRender());
        mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 手动刷新
    }

    private class MyRender implements GLSurfaceView.Renderer {

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLES20.glClearColor(0, 0, 0, 1);
            GLES20.glDisable(GLES20.GL_DEPTH_TEST); // 当我们需要绘制透明图片时,就需要关闭它
            mGLImageHandler.init();

            // 需要显示的图片
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thelittleprince);
            mImageWidth = bitmap.getWidth();
            mImageHeight = bitmap.getHeight();
            // 把图片数据加载进GPU,生成对应的纹理id
            mGLTextureId = OpenGlUtils.loadTexture(bitmap, mGLTextureId, true); // 加载纹理

            // 顶点数组缓冲器
            mGLCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            mGLCubeBuffer.put(CUBE).position(0);

            // 纹理数组缓冲器
            mGLTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            mGLTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            mOutputWidth = width;
            mOutputHeight = height;
            GLES20.glViewport(0, 0, width, height); // 设置窗口大小
            adjustImageScaling(); // 调整图片显示大小。如果不调用该方法,则会导致图片整个拉伸到填充窗口显示区域
        }

        @Override
        public void onDrawFrame(GL10 gl) { // 绘制
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
            // 根据纹理id,顶点和纹理坐标数据绘制图片
            mGLImageHandler.onDraw(mGLTextureId, mGLCubeBuffer, mGLTextureBuffer);
        }

        // 调整图片显示大小为居中显示
        private void adjustImageScaling() {
            float outputWidth = mOutputWidth;
            float outputHeight = mOutputHeight;

            float ratio1 = outputWidth / mImageWidth;
            float ratio2 = outputHeight / mImageHeight;
            float ratioMax = Math.min(ratio1, ratio2);
            // 居中后图片显示的大小
            int imageWidthNew = Math.round(mImageWidth * ratioMax);
            int imageHeightNew = Math.round(mImageHeight * ratioMax);

            // 图片被拉伸的比例
            float ratioWidth = outputWidth / imageWidthNew;
            float ratioHeight = outputHeight / imageHeightNew;
            // 根据拉伸比例还原顶点
            float[] cube = new float[]{
                        CUBE[0] / ratioWidth, CUBE[1] / ratioHeight,
                        CUBE[2] / ratioWidth, CUBE[3] / ratioHeight,
                        CUBE[4] / ratioWidth, CUBE[5] / ratioHeight,
                        CUBE[6] / ratioWidth, CUBE[7] / ratioHeight,
                };

            mGLCubeBuffer.clear();
            mGLCubeBuffer.put(cube).position(0);
        }
    }
}

对于着色器的语法和相关使用,这里我不去赘述,我给的建议是,先了解顶点着色器和片元着色器的主要作用,然后在把这篇教程理解一遍后,对着色器感兴趣的话再去查找相关的资料。这里我们只是显示一张图片,使用的着色器代码很简单,都加了注释,不影响大家理解哈。

/**
 * 负责显示一张图片
 */
public class GLImageHandler {
    // 数据中有多少个顶点,管线就调用多少次顶点着色器
    public static final String NO_FILTER_VERTEX_SHADER = "" +
            "attribute vec4 position;\n" + // 顶点着色器的顶点坐标,由外部程序传入
            "attribute vec4 inputTextureCoordinate;\n" + // 传入的纹理坐标
            " \n" +
            "varying vec2 textureCoordinate;\n" +
            " \n" +
            "void main()\n" +
            "{\n" +
            "    gl_Position = position;\n" +
            "    textureCoordinate = inputTextureCoordinate.xy;\n" + // 最终顶点位置
            "}";

    // 光栅化后产生了多少个片段,就会插值计算出多少个varying变量,同时渲染管线就会调用多少次片段着色器
    public static final String NO_FILTER_FRAGMENT_SHADER = "" +
            "varying highp vec2 textureCoordinate;\n" + // 最终顶点位置,上面顶点着色器的varying变量会传递到这里
            " \n" +
            "uniform sampler2D inputImageTexture;\n" + // 外部传入的图片纹理 即代表整张图片的数据
            " \n" +
            "void main()\n" +
            "{\n" +
            "     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +  // 调用函数 进行纹理贴图
            "}";

    private final LinkedList<Runnable> mRunOnDraw;
    private final String mVertexShader;
    private final String mFragmentShader;
    protected int mGLProgId;
    protected int mGLAttribPosition;
    protected int mGLUniformTexture;
    protected int mGLAttribTextureCoordinate;

    public GLImageHandler() {
        this(NO_FILTER_VERTEX_SHADER, NO_FILTER_FRAGMENT_SHADER);
    }

    public GLImageHandler(final String vertexShader, final String fragmentShader) {
        mRunOnDraw = new LinkedList<Runnable>();
        mVertexShader = vertexShader;
        mFragmentShader = fragmentShader;
    }

    public final void init() {
        mGLProgId = OpenGlUtils.loadProgram(mVertexShader, mFragmentShader); // 编译链接着色器,创建着色器程序
        mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position"); // 顶点着色器的顶点坐标
        mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture"); // 传入的图片纹理
        mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId, "inputTextureCoordinate"); // 顶点着色器的纹理坐标
    }

    public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
                       final FloatBuffer textureBuffer) {
        GLES20.glUseProgram(mGLProgId);
        // 顶点着色器的顶点坐标
        cubeBuffer.position(0);
        GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribPosition);
        // 顶点着色器的纹理坐标
        textureBuffer.position(0);
        GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
        // 传入的图片纹理
        if (textureId != OpenGlUtils.NO_TEXTURE) {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
            GLES20.glUniform1i(mGLUniformTexture, 0);
        }

        // 绘制顶点 ,方式有顶点法和索引法
        // GLES20.GL_TRIANGLE_STRIP即每相邻三个顶点组成一个三角形,为一系列相接三角形构成
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 顶点法,按照传入渲染管线的顶点顺序及采用的绘制方式将顶点组成图元进行绘制

        GLES20.glDisableVertexAttribArray(mGLAttribPosition);
        GLES20.glDisableVertexAttribArray(mGLAttribTextureCoordinate);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }
}

上面的代码中使用的OpenGlUtils类是封装的一个工具类,主要负责加载纹理id,以及加载着色器代码,这里不详细贴出代码细节(都是一些模板代码),感兴趣的同学待会可以在该文章对应的项目代码查看哦。

最终我们的图片就显示出来啦。

注意事项

  • 需要在GLSurfaceView中设置OpenGL的版本:
setEGLContextClientVersion(2); // 2.0

否则会报类似错误 glDrawArrays is called with VERTEX_ARRAY client state disabled!

  • 操作跟GPU相关的接口时需要在GLSurfaceView渲染的线程里否则会报call to OpenGL ES API with no current context。比如获取纹理id不能在界面初始化时,需要在onSurfaceCreated之后

 

完整代码地址

https://github.com/1993hzw/OpenGLESIntroduction

 

后话

OpenGL ES的初步介绍就到此为止了,虽然一直想尽量通俗简单地讲解,但整个写下来发现还是要涉及到很多东西,因此有不足的地方还望各位读者指正!其实上面讲的就是GPUImage这个开源库的核心原理,同时目前流行的短视频特效也是有不少涉及到OpenGL处理的,希望此文对大家学习OpenGL有些许帮助吧。

最后,谢谢大家的的支持!!!后面会根据这篇文章的反响,考虑是否需要继续写下一篇关于滤镜的实现(其实主要是通过编写着色器实现)。

  • 9
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Android OpenGL ES 3.0是一个强大的图形渲染API,用于开发Android平台上的高性能3D应用程序和游戏。从入门到精通OpenGL ES 3.0需要系统性的学习教程,以下是一个简要的学习路径: 1. 基础知识:首先需要了解OpenGL ES 3.0的基础知识,包括图形渲染管线、坐标系、顶点和片元着色器等。可以通过阅读相关的教程、书籍或者在线资源来获得这方面的知识。 2. 环境搭建:学习OpenGL ES 3.0之前,需要先搭建好学习环境。可以下载安装Android Studio和相关的开发工具,以及配置好OpenGL ES 3.0的开发环境。 3. 学习资源:寻找一些高质量的学习资源,如教程、书籍或者在线课程。可以选择一些经典的OpenGL ES 3.0教程,其中包括基础知识、实例代码和案例分析等。 4. 实践练习:学习OpenGL ES 3.0最重要的一点就是不断地进行实践练习。可以按照教程中的示例代码,逐步实现一些简单的图形渲染效果。通过实践来加深对OpenGL ES 3.0的理解,掌握各种绘制技术和渲染效果。 5. 深入研究:在掌握了基础知识和实践经验之后,可以进一步深入研究OpenGL ES 3.0的高级特性和扩展功能。包括纹理映射、着色器编程、光照和阴影效果等。可以参考一些专业书籍和高级教程来进一步提升自己的技术水平。 6. 项目实践:最后一步是通过实际项目的实践来巩固所学的知识。可以尝试开发一些简单的游戏或者应用程序,利用OpenGL ES 3.0来实现复杂的图形渲染效果。通过实际项目的经验,可以进一步提升自己的技术能力和解决问题的能力。 总之,学习Android OpenGL ES 3.0需要系统性的学习教程,并通过不断实践和项目实践来提升自己的技术水平。只有在不断学习和实践中,才能逐步精通OpenGL ES 3.0并运用到实际开发中。 ### 回答2: Android OpenGL ES 3.0 是一种强大的图形渲染技术,用于在Android设备上创建高性能的3D图形和特效。要系统地学习和掌握Android OpenGL ES 3.0,您可以按照以下步骤: 1. 学习基础知识:首先,您需要了解计算机图形学和OpenGL ES的基本概念。这包括了解3D图形渲染的原则、OpenGL ES的架构、状态机模型等。可以通过阅读相关的教材或者参考互联网上的优质教程来学习这些内容。 2. 编程环境设置:为了开始使用Android OpenGL ES 3.0,您需要配置开发环境。这包括安装和配置Android开发工具包(Android SDK)以及适当的集成开发环境(如Android Studio)。确保您的开发环境正确设置,并具备OpenGL ES 3.0的支持。 3. 学习OpenGL ES API:学习OpenGL ES 3.0的API是掌握该技术的关键。您需要理解OpenGL ES的基本绘图函数、顶点和片段着色器编程、纹理映射等概念。可以通过查阅OpenGL ES 3.0的官方文档或参考书籍来学习这些API。 4. 实践项目:通过实践项目来巩固所学的知识。您可以从最简单的项目开始,如画一个三角形,然后逐步扩展,添加更多的图形对象和特效。这样您可以深入了解OpenGL ES 3.0的使用和性能优化。 5. 学习高级主题:一旦掌握了基础知识,您可以进一步学习OpenGL ES 3.0的高级主题。这可能包括光照、阴影、投影、深度测试和其他高级特性。这些主题的学习可以通过参考更高级的教程、专业书籍或者参与相关论坛和社区来深入研究。 6. 性能优化:了解如何优化OpenGL ES 3.0的性能也是非常重要的。您可以学习如何使用缓冲区对象、顶点缓冲区对象(VBO)、纹理压缩和其他优化技术来提高应用程序的帧率和响应速度。 总而言之,要系统学习和掌握Android OpenGL ES 3.0,您需要深入理解计算机图形学和OpenGL ES的原理,学习API的使用和性能优化技术,并通过实践项目来强化您的理解和应用能力。这需要坚持不懈的学习和实践,但通过这样的系统学习,您将能够成为一名Android OpenGL ES 3.0的专家。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值