OpenGL.ES在Android上的简单实践:2-曲棍球(编译着色器及屏幕上绘图)

OpenGL.ES在Android上的简单实践:2-曲棍球(编译着色器及屏幕上绘图)

 

本文会继续上一篇开始的工作。我们首先加载并编译前面定义的着色器,然后把它们链接在一起放在OpenGL的一个程序里。我们接下来就可以用着色器程序在屏幕上绘制曲棍球桌子了。

1、加载着色器。

既然我们已经准备好了顶点着色器和片段着色器,我们就需要把它们从res/raw/shader.glsl 加载到内存。

我们在工程目录下添加一个utils的工具包类目录,并添加 TextResourceReader 的新类。在类中加入如下代码:

 

   /**
     * 从原生文件读入glsl
     * @param context
     * @param resourceId
     * @return
     */
    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 (Exception e) {
            e.printStackTrace();
        }
        return body.toString();
    }

我们在HockeyRenderer添加一个构造函数,并在构造中读入着色器的代码。

public HockeyRenderer(Context context) {

        String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader);
        String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader);
        ... ...
}

现在我们已经把着色器源代码从文件中读出来了,下一步就是编译每个着色器了。我们要创建一个新的辅助类,它可以创建新的OpenGL着色器对象、编译着色器代码并且返回代表那段着色器代码的着色器对象。一旦写出样板代码,在未来的项目中就可以重用了。

 

2、编译着色器

作为开始,创建一个名为ShaderHelper的新类,并在类中添加如下代码:

 

public class ShaderHelper {
    private static final String TAG = "ShaderHelper";

    public static int compileVertexShader(String shaderCode){
        return compileShader(GL_VERTEX_SHADER, shaderCode);
    }

    public static int compileFragmentShader(String shaderCode){
        return compileShader(GL_FRAGMENT_SHADER, shaderCode);
    }

    private static int compileShader(int type, String shaderCode){
        final int shaderObjectId = glCreateShader(type);
        if (shaderObjectId == 0){
            if(LoggerConfig.ON){
                Log.w(TAG,"Warning! Could not create new shader, glGetError:"+glGetError());
            }
            return 0;
        }
        glShaderSource(shaderObjectId, shaderCode);

        glCompileShader(shaderObjectId);

        final int[] compileStatus = new int[1];
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

        if(LoggerConfig.ON){
            Log.i(TAG, "Result of compiling source:"+"\n"+shaderCode+"\n"
                    + glGetShaderInfoLog(shaderObjectId));
        }

        if(compileStatus[0] == 0){
            glDeleteShader(shaderObjectId);
            if(LoggerConfig.ON){
                Log.w(TAG,"Warning! Compilation of shader failed, glGetError:"+glGetError());
            }
            return 0;
        }
        return shaderObjectId;
    }

    ... ...
}

我们来分析一下这部分模板代码。
        首先我们调用glCreateShader()创建了一个新的着色器对象,并把这个对象的ID存入变量shaderObjectId。这个type可以是代表顶点着色器的GL_VERTEX_SHADER,或者是代表片段着色器的GL_FRAGMENT_SHADER。剩下代码也同样的方式。

 

        记住我们是如何创建对象并检查它是否有效的;这个模式将在OpenGL里广泛使用:

        1、首先使用一个如glCreateShader()一样的调用创建一个对象,这个调用会返回一个整型值,代表这个对象。

        2、这个整型值就是OpenGL对象的引用。无论后面什么时候想要引用这个对象,就要把这个整型值传回OpenGL。

        3、返回值0表示这个对象创建失败,它类似于Java代码中返回null值。

        如果对象创建失败,就给调用代码返回0。为什么返回0而不是抛出一个异常呢?这是因为OpenGL内部实际不会抛出任何异常;相反,我们会得到返回值0,并且OpenGL通过glGetError()告诉我们这个错误,这个方法可以让我们询问OpenGL是不是某个API调用导致了错误。这个惯例会一直遵从贯彻整个Android工程。

        然后我们上传和编译着色器源代码,一旦有了有效的着色器对象,就可以调用glShaderSource(shaderObjectId, shaderCode); 上传源代码了。这个调用告诉OpenGL读入字符串shaderCode定义的源代码,并把它与shaderObjectId所引用的着色器对象关联在一起。然后,可以调用 glCompileShader(shaderObjectId) 编译这个着色器。

        接着取出编译状态及着色器信息日志,为了检查编译是失败还是成功,我们可以调用glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0)。当我们获得编译状态的时候,OpenGL只给出一个简单的是或否的回答。难道没兴趣知道发生了什么错误以及哪里出问题了吗?事实证明,我们可以通过调用glGetShaderInfoLog(shaderObjectId) 获得一个可读的信息。

        验证编译状态并返回着色器对象ID,既然我们已经记录了着色器信息日志,就可以查看一下编译是否成功了。我们所需要做的就是检查上面那步的返回值,它是不是0。如果是0,编译就失败了。这种情况下,我们就不再需要着色器对象了,因此告诉OpenGL把它删除并返回0;如果编译成功,着色器对象就是有效了,我们就可以返回给调用代码使用。

        至此,我们完成了编译着色器。

 

3、链接着色器到OpenGL的程序

        既然我们已经加载并编译了 一个顶点着色器 和一个片段着色器,下一步就是把他们绑定到程序(program)里。简单来说,一个OpenGL程序(program)就是把一个顶点着色器和片段着色器链接在一起变成单个对象。顶点着色器和片段着色器总是一起工作的,没有片段着色器,OpenGL就不知道怎么绘制那些组成每个点、直线、三角形的片段;没有顶点着色器,OpenGL就不知道在哪里绘制这些片段。

        虽然顶点着色器和片段着色器总是要一起工作,但并不意味着它们必须是一对一匹配的,它们是可以共用的;同一顶点的着色器可以和不同的片段着色器工作,效果也不尽相同。

        让我们在ShaderHelper增加如下代码,并比较以上的模板代码,逐一分析之。

 

public static int linkProgram(int vertexShaderId, int fragmentShaderId){
        final int programObjectId = glCreateProgram();
        if (programObjectId == 0){
            if(LoggerConfig.ON){
                Log.w(TAG," Warning! Could not create new program, glGetError:"+glGetError());
            }
            return 0;
        }

        glAttachShader(programObjectId, vertexShaderId);
        glAttachShader(programObjectId, fragmentShaderId);

        glLinkProgram(programObjectId);

        final int[] linkStatus = new int[1];
        glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

        if(LoggerConfig.ON){
            Log.i(TAG, "Result of linking program:"
                    + glGetProgramInfoLog(programObjectId));
        }

        if(linkStatus[0] == 0){
            glDeleteProgram(programObjectId);
            if(LoggerConfig.ON){
                Log.w(TAG," Warning! Linking of program failed, glGetError:"+glGetError());
            }
            return 0;
        }

        return programObjectId;
    }

参照之前的模板代码,我想这段代码不能理解了。

 

        首先我们就是调用glCreateProgram() 新建程序对象,用programObjectId记录这个程序对象的ID。

        下一步就是使用glAttachShader附上顶点着色器 和 片段着色器。

        紧接着调用glLinkProgram把两个着色器链接起来,以及调用glGetProgramiv检查程序的信息日志。

        最后验证链接状态并返回程序对象ID到调用者。至此,着色器程序已经链接顶点和片段着色器了。
 

到最后,我们再写一个buildProgram 把以上模板代码组织起来。

 

public static int buildProgram(String vertexShaderSource, String fragmentShaderSource){
        int programObjectId;

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

        programObjectId = linkProgram(vertexShader,fragmentShader);

        if(LoggerConfig.ON){
            validateProgram(programObjectId);
        }

        return programObjectId;
    }

两个String参数vertexShaderSource、fragmentShaderSource就是使用TextResourceReader.readTextFileFromResource从res/raw读取的shader源代码。   

 

上面还有一个validateProgram没有讲解,在开始使用OpenGL的程序之前,我们应该验证一下它,看看这个程序对于当前的OpenGL状态是不是有效的。

 

public static boolean validateProgram(int programObjectId){
        glValidateProgram(programObjectId);

        final int[] validateStatus = new int[1];
        glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);

        if(LoggerConfig.ON){
            Log.i(TAG, "Result of validating program:"+validateStatus[0]
                    +"\nLog:"+ glGetProgramInfoLog(programObjectId));
        }

        return validateStatus[0]!= GLES20.GL_FALSE ;
    }

我们调用glValidateProgram来验证这个程序,然后用GL_VALIDATE_STATUS作为参赛调用glGetProgramiv方法检查其结果。如果OpenGL有什么有用的信息要透露,这些信息会在程序日志里,因此我们使用glGetProgramInfoLog把日志信息打印出来。
 

 

 

4、在GLSurfaceview上使用

以上的代码我们要怎样组织起来呢?初学者可以秉持着一个原则,GLES20.*的接口必须只能在GLSurfaceView.Renderer的接口上调用。所以我们在 onSurfaceCreated 接口添加如下代码:

 

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
        GLES20.glUseProgram(programId);
    }

调用glUseProgram告诉OpenGL在绘制任何东西到屏幕上的时候要使用这里定义的程序对象。

 

好了,我们来总结一下:

        1、我们现在已经把顶点数据加载到本地内存了。

        2、我们也建立OpenGL管道了。

下一步我们要通过管道操作着色器链接数据了。

 

5、操作着色器链接数据

下一步是获得我们早前在着色器定义的attribute和uniform等属性。当OpenGL把着色器链接成一个程序的时候,它实际上为每个自定义的属性附加了一个位置编号。这些位置编号用来给着色器发送数据的时候使用的。现在我们回来看看之前定义的两个着色器:

// 顶点着色器
attribute vec4 a_Position;
void main()
{
    gl_Position = a_Position;
}

// 片段着色器
precision mediump float;
uniform vec4 u_Color;
void main()
{
    gl_FragColor = u_Color;
}

我们需要获取a_Position的位置来传送顶点数据,获取u_Color的位置传送颜色值。怎么获取呢?看看以下模板代码:

(记住GLES20.* 必须在Renderer的接口里面调用)

    private static final String U_COLOR = "u_Color";
    private int uColorLocation;
    private static final String A_POSITION = "a_Position";
    private int aPositionLocation;
    
    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
        GLES20.glUseProgram(programId);
        uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR);
        aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);
    }

我们为自定义attribute/uniform的属性名字创建一个常量和一个用来保存位置编号的变量。attribute/uniform的位置并不是事先指定的,因此,一旦程序链接成功了,我们就要查询这个位置。一个attribute/uniform的位置在一个程序对象中是唯一的,即使在两个不同的程序中使用了相同的attribute/uniform名字,也不意味着它们使用相同的位置。

我们调用GLES20.glGetAttribLocation / GLES20.glGetUniformLocation 分别获取attribute/uniform的位置,并把这个位置存入位置变量aPositionLocation / uColorLocation,接下来我们就可以使用它了。
 

还记得我们之前已经加载本地内存的桌子顶点数据吗?下一步是要告诉OpenGL到哪里找到属性a_Position对应的数据了。

(注意这句话的描述,不是传数据到a_Position,是让OpenGL找。) 我们在onSurfaceCreated中添加如下代码:

        vertexData.position(0);
        GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, 
                GLES20.GL_FLOAT, false, 0, vertexData);

我们调用GLES20.glVertexAttribPointer告诉OpenGL,它可以在缓冲区vertexData中找到a_Position对应的数据。这是一个很重要的函数,因此这里有必要仔细看一下每个参数的意义。

调用了glVertexAttribPointer之后,OpenGL就知道在哪里读取属性a_Position的数据了。但是!尽管我们已经把数据属性链接起来了,在开始绘制之前,我们还需要调用glEnableVertexAttribArray使能顶点数据。完整代码如下:

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
        GLES20.glUseProgram(programId);
        uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR);
        aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);

        vertexData.position(0);
        GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
                GLES20.GL_FLOAT, false, 0, vertexData);
        GLES20.glEnableVertexAttribArray(aPositionLocation);
    }

好了,现在来小结

 

        1、我们现在已经把顶点数据加载到本地内存了。

        2、我们也建立OpenGL管道了。(建立程序,链接 顶点/片段着色器)

        3、通过管道,操作着色器,告诉对应数据在哪里。

剩下最后一步了,在屏幕上绘制(光栅化图元

 

 

 

6、在屏幕上绘制

随着完成以上最后的连接,我们现在算是准备好开始在屏幕上绘制了!我们先回顾之前定义的结构 点数据 和 示意图:

float[] tableVerticesWithTriangles = {
            // 第一个三角形
            0f, 0f,
            9f, 14f,
            0f, 14f,
            // 第二个三角形
            0f, 0f,
            9f, 0f,
            9f, 14f
            // 中间的分界线
            0f, 7f,
            9f, 7f,
            // 两个摇杆的质点位置
            4.5f, 2f,
            4.5f, 12f
    };


好了,我们在onDrawFrame处,添加如下代码:

 

      @Override
      public void onDrawFrame(GL10 gl10) {
          GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        
          GLES20.glUniform4f(uColorLocation, 1.0f,1.0f,1.0f,1.0f);
          GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
          ... ...
      }

我们首先通过调用glUniform4f更新着色器代码中的u_Color的值,与attribute属性不同,uniform的分量没有默认值,因此,如果一个uniform在着色器中被定义vec4类型,我们需要提供所有四个分量的值。我们想要一张白色桌子作为开始,所有把红、绿、蓝的色值设置为代表完全亮度的1.0f值,透明度阿尔法无关紧要,但是还是要指定值的。


一旦指定了颜色,接下来就可以用 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);   第一个参数告诉OpenGL我们要画三角形; 第二个参数0,告诉OpenGL从顶点数组(tableVerticesWithTriangles )的开头处开始读顶点;   第三个参数6,告诉OpenGL读入六个顶点。   因为每个三角形有三个顶点,所以调用最终会画出两个三角形。

 

之前我们调用glVertexAttribPointer方法,记得第二个参数告诉OpenGL每个顶点的位置包含2个浮点数分量。glDrawArrays调用让OpenGL使用前六个顶点绘制三角形, 第一个被绘制的三角形由点(0,0)(9,14)(0,14)围成,而第二个由(0,0)(9,0)(9,14)围成。

 

下一步是绘制跨越桌子中间的中心分割线。继续添加代码:

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

这次我们定义u_Color为红色,这次画的是线GLES20.GL_LINES,从顶点数据的第6组开始,每组也是2个元素。因此,这条线是从(0,7)到(9,7)画一条线。

接着就是画木槌位置的两个点了。添加代码:

        ... ...
        GLES20.glUniform4f(uColorLocation, 0.0f,0.0f,1.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);
        GLES20.glUniform4f(uColorLocation, 0.0f,1.0f,0.0f, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);

通过传递GL_POINTS给glDrawArrays方法,让OpenGL绘制点。对于第一个木槌,我们设置其颜色为蓝色,从偏移位置8开始,并用一个顶点绘制一个点,对于第二个木槌,设置其颜色为绿色,从偏移位置9开始,并用一个顶点绘制一个点。

 

 

 

好了,试着把程序跑起来吧。发生什么奇怪的问题不要怪我哦。再见

 

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值