0评论

OpenGL ES 着色器原理介绍及使用

文章来自https://blog.csdn.net/pangrui201/article/details/75208972 2019-03-15 55浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏开发行业精英群711501594

着色器原理:

我们之前多次介绍过OpenGL里面图形都是通过顶点着色器和片段着色器共同完成的,顶点着色器计算每个顶点在屏幕上的最终位置,OpenGL把这些顶点组装成点,直线,三角形并且分解成片段,会询问片段着色器每个片段的最终颜色,如果没有顶点着色器OpenGL就不知道在哪绘制图形,如果没有片段着色器就不知道要怎么绘制组成图形的点,直线,三角形的片段,所以他们总是一起工作的,最终一起合成屏幕上的一幅图像。

本文将具体讲解着色器simple_vertex_shader.glsl和simple_fragment_shader.glsl如何编译和使用。

加载着色器:

打开TextResourceReader类代码如下:
/***
 * Excerpted from "OpenGL ES for Android",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/kbogla for more book information.
***/
package opengl.timothy.net.openglesproject_lesson2.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.content.Context;
import android.content.res.Resources;
public class TextResourceReader {
    /**
     * Reads in text from a resource file and returns a String containing the
     * text.
     */
    public static String readTextFileFromResource(Context context,
        int resourceId) {
        StringBuilder body = new StringBuilder();
        try {
            InputStream inputStream = 
                context.getResources().openRawResource(resourceId);
            InputStreamReader inputStreamReader = 
                new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String nextLine;
            while ((nextLine = bufferedReader.readLine()) != null) {
                body.append(nextLine);
                body.append('\n');
            }
        } catch (IOException e) {
            throw new RuntimeException(
                "Could not open resource: " + resourceId, e);
        } catch (Resources.NotFoundException nfe) {
            throw new RuntimeException("Resource not found: " + resourceId, nfe);
        }
        return body.toString();
    }
}

readTextFileFromResource方法需要传入资源id,方法返回值为String类型,通过该方法就加载了raw里面定义的两种着色器代码。

编译着色器:

目的是通过加载着色器代码,返回一个代表着色器的对象。

在ShaderHelper类中
    /**
     * Compiles a shader, returning the OpenGL object ID.
     */
    private static int compileShader(int type, String shaderCode) {
        // Create a new shader object.
        final int shaderObjectId = glCreateShader(type);
        if (shaderObjectId == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not create new shader.");
            }
            return 0;
        }
        // Pass in the shader source.
        glShaderSource(shaderObjectId, shaderCode);
        // Compile the shader.
        glCompileShader(shaderObjectId);
        // Get the compilation status.
        final int[] compileStatus = new int[1];
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
        if (LoggerConfig.ON) {
            // Print the shader info log to the Android log output.
            Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:" 
                + glGetShaderInfoLog(shaderObjectId));
        }
        // Verify the compile status.
        if (compileStatus[0] == 0) {
            // If it failed, delete the shader object.
            glDeleteShader(shaderObjectId);
            if (LoggerConfig.ON) {
                Log.w(TAG, "Compilation of shader failed.");
            }
            return 0;
        }
        // Return the shader object ID.
        return shaderObjectId;
    }

第一个参数如果是顶点着色器则传入android.opengl.GLES20.GL_VERTEX_SHADER;片段着色器则是android.opengl.GLES20.GL_FRAGMENT_SHADER;第二个参数是之前的加载着色器返回的String。具体过程:
  1. 创建着色器对象GLES20.glCreateShader(type);
  2. 传入着色器代码 GLES20.glShaderSource(shaderObjectId, shaderCode);
  3. 编译着色器glCompileShader(shaderObjectId);
  4. 获取编译状态 // Get the compilation status.

final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
可以通过 glGetShaderInfoLog(shaderObjectId)打印日志信息

同时if (compileStatus[0] == 0) 则表示编译失败,则删除着色器并且返回0,即:
        if (compileStatus[0] == 0) {
            // If it failed, delete the shader object.
            glDeleteShader(shaderObjectId);
            if (LoggerConfig.ON) {
                Log.w(TAG, "Compilation of shader failed.");
            }
            return 0;
        }

成功则返回当前着色器的id即shaderObjectId.

在AirHockeyRenderer里面的onSurfaceCreated里面可以看到有对以上过程调用,最终得到

int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);

顶点着色器和片段着色器两个对象vertexShader 和fragmentShader 。

链接程序:

program = ShaderHelper.linkProgram(vertexShader, fragmentShader);

我们已经知道顶点着色器和片段着色器是需要一起使用的,使用需要连接成一个对象,具体ShaderHelper.linkProgram代码如下:
 /**
     * Links a vertex shader and a fragment shader together into an OpenGL
     * program. Returns the OpenGL program object ID, or 0 if linking failed.
     */
    public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
        // Create a new program object.
        final int programObjectId = glCreateProgram();
        if (programObjectId == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not create new program");
            }
            return 0;
        }
        // Attach the vertex shader to the program.
        glAttachShader(programObjectId, vertexShaderId);
        // Attach the fragment shader to the program.
        glAttachShader(programObjectId, fragmentShaderId);
        // Link the two shaders together into a program.
        glLinkProgram(programObjectId);
        // Get the link status.
        final int[] linkStatus = new int[1];
        glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);
        if (LoggerConfig.ON) {
            // Print the program info log to the Android log output.
            Log.v(TAG, "Results of linking program:\n" 
                + glGetProgramInfoLog(programObjectId));            
        }
        // Verify the link status.
        if (linkStatus[0] == 0) {
            // If it failed, delete the program object.
            glDeleteProgram(programObjectId);
            if (LoggerConfig.ON) {
                Log.w(TAG, "Linking of program failed.");
            }
            return 0;
        }
        // Return the program object ID.
        return programObjectId;
    }

首先创建程序对象 GLES20.glCreateProgram(),GLES20.glAttachShader方法将顶点着色器和片段着色器添加到程序对象上,最后 GLES20.glLinkProgram(programObjectId)连接对象,如果成功则返回programObjectId否则删除 GLES20.glDeleteProgram(programObjectId).

回答AirHockeyRenderer的onSurfaceCreated方法,在linkProgram之后 ShaderHelper.validateProgram(program);这个是来验证程序对象的,是否存在一些问题,打印日志。

最后, GLES20.glUseProgram(program);是告诉opengl任何时候绘制东西到屏幕都需要使用这里的程序对象。

取值:

在simple_fragment_shader.glsl和simple_vertex_shader.glsl中我们定义了类型为attribute(属性)的a_Position和类型为uniform的u_Color,通过 uColorLocation = GLES20.glGetUniformLocation(program, U_COLOR);

aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION);

这两个方法我们就可以告诉opengl在哪里读取这两个属性的值,相当于是告诉opengl这两个属性的值的位置,并且将位置信息存入uColorLocation和aPositionLocation。

关联属性和缓冲区:

vertexData.position(0);

glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,

false, 0, vertexData);

这个方法是告诉opengl从之前定义的顶点缓冲区vertexData里找到a_Position对应的数据,同时由于每个缓冲区是本地一块内存,为了保证是从内存起始位置读取,所以调用之前需要先将缓冲区指针移动到最初位置即vertexData.position(0)。glVertexAttribPointer参数含义如下表:

glVertexAttribPointer方法各个参数含义

glVertexAttribPointer参数不正确的话可能导致一些问题。同时有了该方法,opengl就知道去哪里读取属性a_Position的值了。最后调用 glEnableVertexAttribArray(aPositionLocation);使这一切有效。

截止目前,我们已经完成了顶点着色器的功能,找到了绘制点,直线,三角形的位置信息即顶点缓冲区的值。接下来具体怎么绘制?就是片段着色器的事了。

片段着色器绘制过程:

在AirHockeyRenderer的onDrawFrame方法里面, glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f); 方法是用来更新着色器里面的u_Color的值,同时uniform没有默认值,如果uniform是vec4类型的则需要提供所有4个分量的值即红,绿,蓝和透明度,本文指定的4个分量值为1.0f, 1.0f, 1.0f, 1.0f即白色不透明。

glDrawArrays(GL_TRIANGLES, 0, 6);就是具体绘制了,第一个参数GL_TRIANGLES指的是绘制三角形,第二个参数告诉opengl从浮点数组tableVerticesWithTriangles的开头位置读取顶点,第三个参数是读取6个顶点,一个三角形3顶点,读取6个的话就是2个三角形即一个长方形桌子。当我们在之前调用glVertexAttribPointer的时候第二个参数表示每个顶点的分量,比如是2维坐标就是2,三维坐标就是3等等,本例子POSITION_COMPONENT_COUNT值为2则一共是读取浮点数组tableVerticesWithTriangles的前12个值,即

// Triangle 1
0f, 0f,
9f, 14f,
0f, 14f,
// Triangle 2
0f, 0f,
9f, 0f,
9f, 14f

接下来就是 // Draw the center dividing line.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2); 就是花了一条红色的线,是从第6个顶点后在读入两个顶点画成一条线GL_LINES,即
// Line 1
0f, 7f,
9f, 7f,

同理后面代码是绘制出两个木槌:
        // Draw the first mallet blue.        
        glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);        
        glDrawArrays(GL_POINTS, 8, 1);
        // Draw the second mallet red.
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);        
        glDrawArrays(GL_POINTS, 9, 1);

即坐标 tableVerticesWithTriangles里面坐标值
// Mallets
4.5f, 2f,
4.5f, 12f。

在simple_vertex_shader.glsl里面有gl_PointSize = 10.0;其实就是给点(木槌)的绘制设置大小。下一讲我们继续深入颜色和着色。

本文代码地址:
https://github.com/pangrui201/OpenGlesProject/tree/master/OpenGlesProject_lesson2