OpenGL ES——着色器

前言

在App开发中,为了追求给CPU减负,我们经常会使用GPU来渲染我们想要显示的图片。如何控制GPU为我们工作?

渲染管线

GPU的工作流程是固定的:

image.png

上图就是OpenGL ES 2.0 的图形管线。

图中阴影部分的 Vertex Shader 和 Fragment Shader 是可编程管线。可编程管线就是说这个操作可以动态编程实现而不必固定写死在代码中。可动态编程实现这一功能一般都是脚本提供的,在OpenGL ES 中也一样,编写这样脚本的能力是由着色语言(Shader Language)提供的。

着色器

一个Shader就像一个函数,我们需要定义它的输入和输出。然后对输入和输出做一系列转换。OpenGL的优势就在于让这一系列转化在GPU上完成。

    private static final String VERTEX_SHADER =
            "attribute vec4 a_position;\n" +
                    "attribute vec2 a_texcoord;\n" +
                    "varying vec2 v_colorcoord;\n" +
                    "void main() {\n" +
                    "  gl_Position = vec4(a_position.x,a_position.y,a_position.z, a_position.w);\n" +
                    "  v_colorcoord = vec2(a_position.x,a_position.y);\n"+
                    "}\n";

    private static final String FRAGMENT_SHADER =
            "precision mediump float;\n" +
                    "varying vec2 v_colorcoord;\n" +
                    "void main() {\n" +
                    "  gl_FragColor = vec4(v_colorcoord.x,v_colorcoord.y,0.5,1.0);\n" +
                    "}\n";

我们先来看VERTEX_SHADER: attribute vec4是变量类型,表示宽度为4的输入向量。 varying vec2表示宽度为2的输出向量。其中varying专用于顶点着色器与片段着色器间的交互。 着色器逻辑从main函数开始执行,gl_Position表示了图形的顶点坐标。在上面的代码中,我们令

gl_Position = vec4(a_position.x, a_position.y ,a_position.z, a_position.w);

如果将代码修改为:

gl_Position = vec4(a_position.x, -a_position.y ,a_position.z, a_position.w);

图形则会上下倒置。

v_colorcoord = vec2(a_position.x,a_position.y);

将一个二维向量传给Fragment Shader。

在片段着色器中:

gl_FragColor = vec4(v_colorcoord.x,v_colorcoord.y,0.5,1.0);

我们将颜色设置为与坐标相关。

初始化

OpenGL的开发中,比较大的困难在于OpenGL的流程非常复杂。整个流程步骤繁多,且顺序不能颠倒。因此我们会在本文中实践一个正确的流程。通过反复的阅读和实践熟悉GPU绘制的步骤。

加载着色器

    public static int loadShader(int shaderType, String source) {
        int shader = GLES20.glCreateShader(shaderType);
        if (shader != 0) {
            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) {
                String info = GLES20.glGetShaderInfoLog(shader);
                GLES20.glDeleteShader(shader);
                shader = 0;
                throw new RuntimeException("Could not compile shader " +
                        shaderType + ":" + info);
            }
        }
        return shader;
    }

如果要加载顶点着色器: shaderType = GLES20.GL_VERTEX_SHADER source = VERTEX_SHADER 如果是片段着色器: shaderType = GLES20.GL_FRAGMENT_SHADER source = FRAGMENT_SHADER

加载着色器的步骤比较简单:

  • 生成一个指定类型的着色器
  • 加载着色器源码
  • 编译阶段使用glGetShaderiv获取编译情况
  • 如果失败,glGetShaderInfoLog获取编译错误,删除着色器

创建计划

    public static 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);
            checkGlError("glAttachShader");
            GLES20.glAttachShader(program, pixelShader);
            checkGlError("glAttachShader");
            GLES20.glLinkProgram(program);
            int[] linkStatus = new int[1];
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus,
                    0);
            if (linkStatus[0] != GLES20.GL_TRUE) {
                String info = GLES20.glGetProgramInfoLog(program);
                GLES20.glDeleteProgram(program);
                program = 0;
                throw new RuntimeException("Could not link program: " + info);
            }
        }
        return program;
    }

创建计划的步骤主要包括:

  • 创建计划
  • 计划绑定着色器
  • 链接计划,生成GPU可执行程序

初始化顶点数据

    public void init() {
        // Create program
        mProgram = GLToolbox.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);

        // Bind attributes
        mPosCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_position");

        // Setup coordinate buffers
        mPosVertices = ByteBuffer.allocateDirect(
                POS_VERTICES.length * FLOAT_SIZE_BYTES)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        mPosVertices.put(POS_VERTICES).position(0);
    }

初始化顶点数据步骤:

  • 创建计划
  • 获取计划中,"a_position"的句柄
  • 将顶点数据输入ByteBuffer

至此就完成了初始化的全部工作。接下来,就要开始渲染了。


绘制

宏观流程

    @Override
    public void onDrawFrame(GL10 gl) {
        if(!initialized){
            init();
            initialized = true;
        }

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        synchronized (mRunOnDraw) {
            while (!mRunOnDraw.isEmpty()) {
                mRunOnDraw.poll().run();
            }
        }

        renderResult();
    }

onDrawFrame会在绘制时调用。我们的流程中,onDrawFrame分为三部分。

  • 初始化
  • 执行mRunOnDraw任务
  • 渲染结果

初始化的阅读我们已经完成,我们来看一下mRunOnDraw任务:

mRunOnDraw

    public void setTexture(final int width, final int height){
        runOnDraw(new Runnable() {
            @Override
            public void run() {
                loadTexture(width, height);
            }
        });
    }

    private void loadTexture(int width, int height){
        GLES20.glGenTextures(2, mTextures , 0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]);
        GLToolbox.initTexParams();
    }

    public static void initTexParams() {
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
                GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
                GLES20.GL_CLAMP_TO_EDGE);
    }
  • 获取纹理id
  • 绑定纹理
  • 设置纹理参数

渲染结果

经过上面的过程,我们加载了着色器,创建并连接了计划 ,初始化了顶点数据,获取了顶点着色器的输入句柄,获取了纹理地址,初始化了纹理参数,,,,终于要开始绘制了。

    private void renderResult() {
        renderTexture(mTextures[0]);
    }

    public void renderTexture(int texId) {
        GLES20.glUseProgram(mProgram);
        GLToolbox.checkGlError("glUseProgram");

        GLES20.glViewport(0, 0, mViewWidth, mViewHeight);
        GLToolbox.checkGlError("glViewport");

        GLES20.glDisable(GLES20.GL_BLEND);

        GLES20.glVertexAttribPointer(mPosCoordHandle, 2, GLES20.GL_FLOAT, false,
                0, mPosVertices);
        GLES20.glEnableVertexAttribArray(mPosCoordHandle);
        GLToolbox.checkGlError("PosCoor attribute setup");

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLToolbox.checkGlError("glActiveTexture");
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);//把已经处理好的Texture传到GL上面
        GLToolbox.checkGlError("glBindTexture");

        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }
  • 使用计划
  • 设置绘制区域
  • 关闭混合
  • 设置顶点坐标到着色器顶点输入句柄
  • 启动顶点着色器
  • 激活纹理
  • 绑定纹理id
  • 绘制图形

结果

为什么它的颜色是这样?

最初的着色器就是答案:

    private static final String VERTEX_SHADER =
            "attribute vec4 a_position;\n" +
                    "attribute vec2 a_texcoord;\n" +
                    "varying vec2 v_colorcoord;\n" +
                    "void main() {\n" +
                    "  gl_Position = vec4(a_position.x,a_position.y,a_position.z, a_position.w);\n" +
                    "  v_colorcoord = vec2(a_position.x,a_position.y);\n"+
                    "}\n";

    private static final String FRAGMENT_SHADER =
            "precision mediump float;\n" +
                    "varying vec2 v_colorcoord;\n" +
                    "void main() {\n" +
                    "  gl_FragColor = vec4(v_colorcoord.x,v_colorcoord.y,0.5,1.0);\n" +
                    "}\n";

如有问题,欢迎指正。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小詹同学

Leetcode打卡 | No.015 三数之和

欢迎和小詹一起定期刷leetcode,每周一和周五更新一题,每一题都吃透,欢迎一题多解,寻找最优解!这个记录帖哪怕只有一个读者,小詹也会坚持刷下去的!

1032
来自专栏落影的专栏

程序员进阶之算法练习(三十五)LeetCode专场

LeetCode上的题目是大公司面试常见的算法题,今天的目标是拿下5道算法题: 题目1是基于链表的大数加法,既考察基本数据结构的了解,又考察在处理加法过程中的边...

45016
来自专栏IT可乐

深入理解计算机系统(2.6)------整数的运算

  前面两篇博客我们详细讲解了计算机中整数的表示,包括有符号和无符号(补码编码)的详细介绍。那么这篇博客我们将对它们的运算有个详细的了解。   在讲解之前首先看...

2216
来自专栏Python爬虫与算法进阶

Python实现常见的回文字符串算法

Manacher 算法首先对字符串做一个预处理,使得所有的串都是奇数长度, 插入的是同样的符号且符号不存在与原串中,串的回文性不受影响

684
来自专栏java一日一条

经典数据结构和算法回顾

最近想回过头来看看以前写的一些代码,可叹为何刚进大学的时候不知道要养成写博客的好习惯。现在好多东西都没有做记录,后面也没再遇到相同的问题,忘的都差不多了。只能勉...

541
来自专栏五分钟学算法

每天一算:Odd Even Linked List

我们会在每天早上8点30分准时推送一条LeetCode上的算法题目,并给出该题目的动画解析以及参考答案,每篇文章阅读时长为五分钟左右。

823
来自专栏有趣的Python

算法与数据结构(三)高级排序算法:O(nlogn)的算法:归并排序&快速排序优化对比

O(nlogn)的算法 ? o(nlogn)比O(n^2)快多少 优化改进的算法要比笨的算法快太多。 归并排序:Merge Sort ? 把数组分成左右两半 ?...

4805
来自专栏专注研发

poj-1131-(大数)八进制转化成十进制

Fractions in octal (base 8) notation can be expressed exactly in decimal notatio...

1291
来自专栏python3

python3--类的组合,初始类的继承

圆环是由两个圆组成的,圆环的面积是外面圆的面积减去内部圆的面积。圆环的周长是内部圆的周长加上外部圆的周长

662
来自专栏tkokof 的技术,小趣及杂念

sizeof 知多少?

稍熟悉C/C++的朋友,对于sizeof肯定不陌生,通过他我们可以知晓某个类型或者实例的内存大小(以字节计),但是如果深入一下sizeof计算的细节,想来大部分...

580

扫码关注云+社区