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";

如有问题,欢迎指正。

你可能感兴趣的:(OpenGL ES——着色器)