Android OpenGl Es 学习(三):编译着色器

4,477 阅读10分钟

概述

这是一个新的系列,学习OpengGl Es,其实是《OpenGl Es 应用开发实践指南 Android卷》的学习笔记,感兴趣的可以直接看这本书,当然这个会记录自己的理解,以下只作为笔记,以防以后忘记

之后会对本书的前九章依次分析记录

Android OpenGl Es 学习(一):创建一个OpenGl es程序

Android OpenGl Es 学习(二):定义顶点和着色器

Android OpenGl Es 学习(三):编译着色器

Android OpenGl Es 学习(四):增填颜色

Android OpenGl Es 学习(五):调整宽高比

Android OpenGl Es 学习(六):进入三维

Android OpenGl Es 学习(七):使用纹理

Android OpenGl Es 学习(八):构建简单物体

Android OpenGl Es 学习(九):增添触摸反馈

最终是要实现一个曲棍球的简单游戏,类似这样的

加载着色器

还记得上篇文章我们已经编写顶点着色器和片段着色器,现在我们把它们加载到代码中,编写代码

public class ReadResouceText {

    public static String readResoucetText(Context context, int resouceId) {
        StringBuffer body = new StringBuffer();

        try {
            InputStream inputStream = context.getResources().openRawResource(resouceId);
            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) {
            e.printStackTrace();
        }

        return body.toString();
    }
}

我们新编写一个类用于在家raw里的glsl代码,然后就可以通过这个类加载着色器代码

 //读取着色器源码
        String fragment_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.fragment_shader);
        String vertex_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.vertex_shader);

编译着色器

我们先创建一个编译着色器的方法

 public static int compileShader(int type, String source) {
        //创建shader
        int shaderId = GLES20.glCreateShader(type);
        if (shaderId == 0) {
            Log.d("mmm", "创建shader失败");
            return 0;
        }
        //上传shader源码
        GLES20.glShaderSource(shaderId, source);
        //编译shader源代码
        GLES20.glCompileShader(shaderId);
        //取出编译结果
        int[] compileStatus = new int[1];
        //取出shaderId的编译状态并把他写入compileStatus的0索引
        GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
        Log.d("mmm编译状态", GLES20.glGetShaderInfoLog(shaderId));

        if (compileStatus[0] == 0) {
            GLES20.glDeleteShader(shaderId);
            Log.d("mmm", "创建shader失败");
            return 0;
        }

        return shaderId;
    }

调用

 //编译着色器源码
        int mVertexshader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
        int mFragmentshader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
方法描述
GLES20.glCreateShader(type)创建一个新的着色器对象,如果返回值是0,那么创建失败,参数type有俩种类型GLES20.GL_FRAGMENT_SHADER 片段着色器和GLES20.GL_VERTEX_SHADER 顶点着色器
GLES20.glShaderSource(shaderId, source)上传shader的源代码,并把它与现有的shaderid关联
GLES20.glCompileShader(shaderId);编译着色器,编译之前需要先上传源码
GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, compileStatus, 0)取出shaderId的编译状态并把他写入compileStatus的0索引,如果0索引处是0,那么就是编译失败
GLES20.glDeleteShader(shaderId)删除着色器
GLES20.glGetShaderInfoLog(shaderId)获取一个可读的消息,如果有着色器的有用内容,就会储存到着色器的信息日志中

把着色器链接到程序

opengl程序就是把一个顶点着色器和一个片段着色器链接在一起编程单个对象,顶点着色器和片段着色器总在一起工作,不能分开,但是并不意味之他们是一一配对的,我们也可以在多个程序使用同一个着色器

 public static int linkProgram(int mVertexshader, int mFragmentshader) {
        //创建程序对象
        int programId = GLES20.glCreateProgram();
        if (programId == 0) {
            Log.d("mmm", "创建program失败");
            return 0;
        }
        //依附着色器
        GLES20.glAttachShader(programId, mVertexshader);
        GLES20.glAttachShader(programId, mFragmentshader);
        //链接程序
        GLES20.glLinkProgram(programId);
        //检查链接状态
        int[] linkStatus = new int[1];
        GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
        Log.d("mmm", "链接程序" + GLES20.glGetProgramInfoLog(programId));
        if (linkStatus[0] == 0) {
            GLES20.glDeleteProgram(programId);
            Log.d("mmm", "链接program失败");
            return 0;
        }

        return programId;

    }

调用

        int program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);

方法描述
GLES20.glCreateProgram()创建程序对象,如果返回值是0则创建失败
GLES20.glAttachShader(programId, shader)依附着色器,依次把顶点着色器和片段着色器添加到程序上
GLES20.glLinkProgram(programId);链接程序,把着色器和程序链接起来
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0)查看程序状态,会把结果存入linkStatus数组的0索引位置,如果为0则链接程序失败
GLES20.glDeleteProgram(programId)删除程序

验证openl程序对象

在使用opengl之前我们应该验证一下,看当前程序对opengl是否有效

 public static boolean volidateProgram(int program) {
        GLES20.glValidateProgram(program);
        int[] validateStatus = new int[1];
        GLES20.glGetProgramiv(program, GLES20.GL_VALIDATE_STATUS, validateStatus, 0);
        Log.d("mmm", "当前openl情况" + validateStatus[0] + "/" + GLES20.glGetProgramInfoLog(program));

        return validateStatus[0] != 0;
    }

调用

 		//验证opengl对象
        ShaderHelper.volidateProgram(program);
        //使用程序
        GLES20.glUseProgram(program);
方法描述
GLES20.glValidateProgram(program)验证程序
GLES20.glGetProgramiv(program, GLES20.GL_VALIDATE_STATUS, validateStatus, 0)获取验证结果
GLES20.glGetProgramInfoLog(program)如果opengl有信息打印,就会在这里显示
LES20.glUseProgram(program)告诉opengl在绘制任何东西到屏幕需要用这里定义的程序

获取一个uniform位置

还记得上篇文章我们定义的片段着色器

 precision mediump float;
 uniform vec4 u_Color;
   void main() {
        gl_FragColor = u_Color;
    }

我们在这个片段着色器中定义了一个uniform的变量,当opengl把着色器链接成一个程序的时候,他实际上使用一个位置编号,把片段找色器中定义的每一个uniform都关联起来,这些位置编号用来着色器发送数据,并且我们需要u_Color的位置,以便我们可以在要绘画的时候绘制颜色

上边的着色器中,我们定义了一个u_Coloruniform,并且在main方法把这个uniform赋值给gl_FragColor,我们要使用这个uniform设置将要绘制的颜色,现在我们在java代码中获取这个uniform的位置

int u_color = GLES20.glGetUniformLocation(program, "u_Color");
方法描述
GLES20.glGetUniformLocation(program, "u_Color")获取指定uniform的位置,并保存在返回值u_color变量中,方便之后使用

获取属性的位置

像uniform一样,在使用属性之前我们也要获取他们的位置,我们可以让opengl自动给这些属性分配位置编号,这样更容易管理

一旦着色器被链接到一起,我们只需要加入下面代码就可以获取属性的位置


     int a_position = GLES20.glGetAttribLocation(program, "a_Position");

方法描述
GLES20.glGetAttribLocation(program, "a_Position")获取属性的位置

关联属性和顶点数据

 //绑定a_position和verticeData顶点位置
        /**
         * 第一个参数,这个就是shader属性
         * 第二个参数,每个顶点有多少分量,我们这个只有来个分量
         * 第三个参数,数据类型
         * 第四个参数,只有整形才有意义,忽略
         * 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
         * 第六个参数,opengl从哪里读取数据
         */
         verticeData.position(0);
        GLES20.glVertexAttribPointer(a_position, 2, GLES20.GL_FLOAT,
                false, 0, verticeData);
         //使用顶点
       GLES20.glEnableVertexAttribArray(a_position);
   

verticeData是我们在上章创建的顶点数据,我们要确定要从头读取顶点,每个缓冲区都有一个内部指针,可以通过调用position(int)来移动,当opengl读取数据就会从此处读取数据,为了保证从头读取数据,我们调用verticeData.position(0),把数据移到开头处

然后我们调用GLES20.glVertexAttribPointer()方法告诉opengl,他可以在缓冲区 verticeData中找a_Position对应的数据,我们看下这个方法的具体参数意思

glVertexAttribPointer( int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr )

参数描述
int indx这个是属性的位置,传入之前获取的a_position
int size这个是每个属性的数据计数,对于这个属性有多少个分量与每一个顶点关联,我们上一节定义顶点用了俩个分量x,y,这就意味着每个顶点需要俩个分量,我们为顶点设置了俩个分量,但是a_Position定义为vec4,他有4个分量,如果没有有指定值,那么默认第三个分量为0,第四个分量为1
int type这个是数据类型,我们是浮点数所以设置为GLES20.GL_FLOAT
boolean normalized只有使用整形数据他才有意义,我们暂时忽略设为false
int stride当数组存储多个属性时他才有意义,本章只有一个属性,暂时忽略传0
java.nio.Buffer ptr告诉opengl在哪里读取数据,

最后在调用GLES20.glEnableVertexAttribArray(a_position)使用这个属性顶点,这样opengl就知道在哪里寻找数据了

方法描述
GLES20.glEnableVertexAttribArray(a_position)使用顶点

在屏幕上绘制

        //指定着色器u_color的颜色为白色
        GLES20.glUniform4f(u_color, 1.0f, 1.0f, 1.0f, 1.0f);
        /**
         * 第一个参数:绘制绘制三角形
         * 第二个参数:从顶点数组0索引开始读
         * 第三个参数:读入6个顶点
         *
         * 最终绘制俩个三角形,组成矩形
         */
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

        //绘制分割线

        GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);

        //绘制点
        GLES20.glUniform4f(u_color, 0.0f, 0.0f, 1.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);

        GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
方法描述
GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1.0f)更新着色器u_color的值,后面四个参数分别为,红,绿,蓝,透明度
GLES20.glDrawArrays(int mode,int first,int count)第一个参数:你想画什么,有三种模式GLES20.GL_TRIANGLES三角形,GLES20.GL_LINES线,GLES20.GL_POINTS点,第二个参数:从数组那个位置开始读,第三个参数:一共读取几个顶点

我们先通过GLES20.glUniform4f更新着色器代码中的u_color的值,与属性不同,uniform的分量是没有默认值的,因此如果一个uniform在着色器中定义vec4类型,我们需要提供四分量的值,接下来调用GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);第一个参数表示绘制三角形,第二个参数表示从数组的0索引开始,第三个参数表示数量是6个,这样就会绘制俩个三角形组成矩形,然后就是画线画点

把opengl坐标映射到屏幕

我们先看下之前定义的坐标

而opengl会把屏幕映射到【-1,1】的范围内,如图:

具体怎么映射的我们之后再说,现在我们把坐标映射一下

下面是映射后的坐标

 //逆时针绘制三角形
    float[] tableVertices = {
            //第一个三角
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            //第二个三角
            -0.5f,-0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,
            //线
            -0.5f, 0f,
            0.5f, 0f,
            //点
            0f, -0.25f,
            0f, 0.25f
    };

最后我们可以运行一下应用

这篇文章就完成了

完整代码

public class AirHockKeyRender implements GLSurfaceView.Renderer {

    private final FloatBuffer verticeData;
    private final int BYTES_PER_FLOAT = 4;
    private final Context mContext;
    //逆时针绘制三角形
    float[] tableVertices = {
            //第一个三角
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            //第二个三角
            -0.5f,-0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,
            //线
            -0.5f, 0f,
            0.5f, 0f,
            //点
            0f, -0.25f,
            0f, 0.25f
    };
    private int u_color;
    private int a_position;
    private int POSITION_COMPONENT_COUNT = 2;


    public AirHockKeyRender(Context context) {
        this.mContext=context;
        //把float加载到本地内存
        verticeData = ByteBuffer.allocateDirect(tableVertices.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(tableVertices);
        verticeData.position(0);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //当surface被创建时,GlsurfaceView会调用这个方法,这个发生在应用程序
        // 第一次运行的时候或者从其他Activity回来的时候也会调用

        //清空屏幕
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        //读取着色器源码
        String fragment_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.fragment_shader);
        String vertex_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.vertex_shader);

        //编译着色器源码
        int mVertexshader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
        int mFragmentshader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
        //链接程序
        int program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);

        //验证opengl对象
        ShaderHelper.volidateProgram(program);
        //使用程序
        GLES20.glUseProgram(program);

        //获取shader属性
        u_color = GLES20.glGetUniformLocation(program, "u_Color");
        a_position = GLES20.glGetAttribLocation(program, "a_Position");

        //绑定a_position和verticeData顶点位置
        /**
         * 第一个参数,这个就是shader属性
         * 第二个参数,每个顶点有多少分量,我们这个只有来个分量
         * 第三个参数,数据类型
         * 第四个参数,只有整形才有意义,忽略
         * 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
         * 第六个参数,opengl从哪里读取数据
         */
        verticeData.position(0);
        GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
                false, 0, verticeData);
        //开启顶点
        GLES20.glEnableVertexAttribArray(a_position);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //在Surface创建以后,每次surface尺寸大小发生变化,这个方法会被调用到,比如横竖屏切换

        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        //当绘制每一帧数据的时候,会调用这个放方法,这个方法一定要绘制一些东西,即使只是清空屏幕
        //因为这个方法返回后,渲染区的数据会被交换并显示在屏幕上,如果什么都没有话,会看到闪烁效果

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        //绘制长方形
        //指定着色器u_color的颜色为白色
        GLES20.glUniform4f(u_color, 1.0f, 1.0f, 1.0f, 1.0f);
        /**
         * 第一个参数:绘制绘制三角形
         * 第二个参数:从顶点数组0索引开始读
         * 第三个参数:读入6个顶点
         *
         * 最终绘制俩个三角形,组成矩形
         */
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

        //绘制分割线

        GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);

        //绘制点
        GLES20.glUniform4f(u_color, 0.0f, 0.0f, 1.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);

        GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
    }
}
public class ReadResouceText {

    public static String readResoucetText(Context context, int resouceId) {
        StringBuffer body = new StringBuffer();

        try {
            InputStream inputStream = context.getResources().openRawResource(resouceId);
            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) {
            e.printStackTrace();
        }

        return body.toString();
    }
}
public class ShaderHelper {

    public static int compileShader(int type, String source) {
        //创建shader
        int shaderId = GLES20.glCreateShader(type);
        if (shaderId == 0) {
            Log.d("mmm", "创建shader失败");
            return 0;
        }
        //上传shader源码
        GLES20.glShaderSource(shaderId, source);
        //编译shader源代码
        GLES20.glCompileShader(shaderId);
        //取出编译结果
        int[] compileStatus = new int[1];
        //取出shaderId的编译状态并把他写入compileStatus的0索引
        GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
        Log.d("mmm编译状态", GLES20.glGetShaderInfoLog(shaderId));

        if (compileStatus[0] == 0) {
            GLES20.glDeleteShader(shaderId);
            Log.d("mmm", "创建shader失败");
            return 0;
        }

        return shaderId;
    }


    public static int linkProgram(int mVertexshader, int mFragmentshader) {
        //创建程序对象
        int programId = GLES20.glCreateProgram();
        if (programId == 0) {
            Log.d("mmm", "创建program失败");
            return 0;
        }
        //依附着色器
        GLES20.glAttachShader(programId, mVertexshader);
        GLES20.glAttachShader(programId, mFragmentshader);
        //链接程序
        GLES20.glLinkProgram(programId);
        //检查链接状态
        int[] linkStatus = new int[1];
        GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
        Log.d("mmm", "链接程序" + GLES20.glGetProgramInfoLog(programId));
        if (linkStatus[0] == 0) {
            GLES20.glDeleteProgram(programId);
            Log.d("mmm", "链接program失败");
            return 0;
        }

        return programId;

    }

    public static boolean volidateProgram(int program) {
        GLES20.glValidateProgram(program);
        int[] validateStatus = new int[1];
        GLES20.glGetProgramiv(program, GLES20.GL_VALIDATE_STATUS, validateStatus, 0);
        Log.d("mmm", "当前openl情况" + validateStatus[0] + "/" + GLES20.glGetProgramInfoLog(program));

        return validateStatus[0] != 0;
    }


    public static int buildProgram(String vertex_shader_source,String fragment_shader_source){
        //编译着色器源码
        int mVertexshader = compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
        int mFragmentshader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
        //链接程序
        int program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);
        //验证opengl对象
        ShaderHelper.volidateProgram(program);

        return program;
    }
}