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 条评论
登录 后参与评论

相关文章

来自专栏一棹烟波

OpenGL进行简单的通用计算实例

博主作为OpenGL新手,最近要用OpenGL进行并行的数据计算,突然发现这样的资料还是很少的,大部分资料和参考书都是讲用OpenGL进行渲染的。好不容易找到一...

2017
来自专栏落影的专栏

iOS开发-OpenGL ES入门教程4

教程 OpenGL ES入门教程1-Tutorial01-GLKit OpenGL ES入门教程2-Tutorial02-shader入门 OpenGL E...

3085
来自专栏向治洪

OpenGL ES简介

概述 在聊Android的View渲染流程中,通常会有一个比较核心的步骤:通过OpeGL ES接口调用GPU接口通知GPU绘制图形。其完整的流程:UI对象—->...

2105
来自专栏MelonTeam专栏

OpenGL ES读书笔记(一)—初始庐山真面目

1. OpenGL ES简介 OpenGL ES(OpenGL for Embedded Systems)是以手持和嵌入式设备为目标的高级3D图形应用程序编程接...

21710
来自专栏Flutter入门

Android OpenGL ES(一)-开始描绘一个平面三角形

今天的目标是做一个OpenGL ES学习的开端。就是画一个简单的三角形。暂时不考虑坐标系的矩阵变换和纹理等。只需要用顶点着色器简单的来进行描述。 这一节需要使...

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

随便聊聊水面效果的2D实现(二)

之前提到想要随便聊一聊RippleEffect的2D实现方法,近来又总算有了些许空余时间,于是便有了这篇东西~

1303
来自专栏点滴积累

PhiloGL学习(1)——场景创建及方块欲露还羞出水面

前言 上一篇文章中介绍了我认识PhiloGL框架的机缘以及初步的探讨(见JS前端三维地球渲染——中国各城市航空路线展示),在此文中仅仅对此框架进行了简单介绍并初...

3446
来自专栏清墨_iOS分享

OpenGLES绘制立体多边形加纹理

前面写了OpenGLES的入门篇,一些朋友觉得还不错,找到我问了一些知识,这次我有针对性的写下这篇文章,也为我OpenGLES进阶篇做个开始。 我已认证微信,感...

42312
来自专栏菩提树下的杨过

WritableBitmapEx 一瞥

今天在蓝色上看到一篇介绍WritableBitmapEx的贴子(是开源项目),项目地址:http://writeablebitmapex.codeplex.co...

1758
来自专栏拂晓风起

cocos2d-js Shader系列2:在cc.Sprite上使用Shader(黑白、灰度、造旧效果)

1194

扫码关注云+社区