OpenGL ES应用开发实践指南(android 卷)笔记 第四章

第四章 增加颜色和着色

本章的开发计划:

首先,我们会学习如何把每个点上的颜色定义为一个顶点属性,而不是整个对象都使用一种单一的颜色。

然后,我们会学习如何在构成一个物体的不同顶点之间平滑的混合这些颜色。

1.平滑着色

在第2章中,我们了解到如何在一个uniform里用单一的颜色绘制物体。我们已经知道,我们只能画点、直线以及三角形,并且所有物体都以它们为基础构建。既然受限于这三个基本的图元,我们怎样用许多不同的颜色和着色表达一个复杂的场景呢?

我们能使用的一个方法是用上百万个小三角形,每个三角形都有一个不同的颜色。如果使用足够多的三角形,我们就能欺骗观察者,让他们看到一幅美丽的、复杂的、有丰富颜色变化的场景。尽管这在技术上是可行的,但是性能和内存的开销也是非常恐怖的。

不绘制大量的平面三角形,如果有一个方法可以在同一个三角形中混合不同的颜色,如何?如果在一个三角形的每个点上都有一个不同的颜色,并在三角形的表面上混合这些颜色,我们最终将得到一个平滑着色的三角形。

平滑着色是在顶点之间完成的

OpenGL给了我们一个方法,它可以平滑地混合一条直线或一个三角形的表面上的每个顶点的颜色。我们要使用这种类型的着色使桌子中心表现得更加明亮,而其边缘处显得比较暗淡,这就好像有一盏灯挂在桌子中间的上方一样。

2.引入三角形扇

一个三角形扇以一个中心顶点作为起始,使用相邻的两个顶点创建第一个三角形,接下来的每个顶点都会创建一个三角形,围绕起始的中心点按扇形展开。为了使这个扇形闭合,我们只需要在最后重复第二个点。


3.增加一个新的颜色属性

float[] tableVerticesWithTriangles= {
        0f,0f, 1f, 1f, 1f,

        -0.5f,-0.5f, 0.7f,0.7f, 0.7f,
        0.5f,-0.5f, 0.7f,0.7f, 0.7f,
        0.5f,0.5f, 0.7f,0.7f, 0.7f,
        -0.5f,0.5f, 0.7f,0.7f, 0.7f,
        -0.5f,-0.5f, 0.7f,0.7f, 0.7f,

        -0.5f,0f, 1f, 0f, 0f,
        0.5f,0f, 1f, 0f, 0f,

        0f,-0.25f, 0f, 0f, 1f,
        0f,0.25f, 1f, 0f, 0f
};

下一步就是从着色器中去掉uniform定义的颜色,并用一个属性替换它。接下来会更新java代码以体现这段新的着色器代码。

attribute vec4 a_Position;
attribute vec4 a_Color;

varying vec4 v_Color;

void main()
{
    v_Color = a_Color;

    gl_Position = a_Position;
    gl_PointSize = 10.0;
}

varying 是一个特殊的变量类型,它把给它的那些值进行混合,并把这些混合后的值发送给片段着色器。使用上面的直线作为一个例子,如果顶点0的a_Color是红色,且顶点1的a_Color是绿色,然后,通过把a_Color赋值给v_Color,来告诉OpenGL我们需要每个片段都接手一个混合后的颜色。接近顶点0的片段,混合后的颜色显得更红,而越接近顶点1的片段,颜色就会越绿。

关于这种混合是如何实现的,在讨论更多细节之前,我们把varying也加入片段着色器。

precision mediump float;

varying vec4 v_Color;

void main()
{
    gl_FragColor = v_Color;
}
我们用varying变量v_Color替换了原来代码中的uniform。如果那个片段属于一条直线,那么OpenGL就会用构建那条直线的两个顶点计算其混合后的颜色;如果那个片段属于一个三角形,那OpenGL就会用构成那个三角形的三个顶点计算其混合后的颜色。

既然已经更新了着色器,我们也需要更新Java代码,以便我们传递新的颜色属性给顶点着色器中的a_Color。在做这些之前,让我们花些时间多了解一下OpenGL怎样平滑地从一个点向另外一个点混合颜色。

4.一个varying如何生成每个片段上混合后的颜色

我们刚刚了解到,直线或三角形上的每个片段混合后的颜色可以用一个varying生成。我们不仅能混合颜色,还可以给varying传递任何值,OpenGL会选取属于那条直线的两个值,或者属于那个三角形的三个值,并平滑地在那个基本图元上混合这些值,每个片段都会有一个不同的值。这种混合是使用线性插值(linear interpolation)实现的。要了解它是怎么工作的,让我们首先以一条直线为开始讲解。

沿着一条直线做线性插值

假设有一条直线,它有一个红色顶点和一个绿色顶点,我们要从一个向另外一个混合颜色。

在这条直线的左边,每个片段的颜色更多地呈红色;随着向右边前进,那些片段的红色分量逐渐减少,在中间处,它们处于红色和绿色之间;随着与绿色顶点越来越近,片段也变得越来越绿了。我们可以看到每种颜色分量都随着直线长度线性缩放,因为这条线段的左侧顶点是红色,而右侧顶点是绿色,它的左端就是100%的红色,中间是50%的红色,而右端是0%的红色。

绿色的变化也是一样的。这就是线性插的基本解释。每种颜色的强度依赖于每个片段与包含那个颜色的顶点的距离。


在一个三角形表面上混合

当我们只处理两个点的时候,阐明线性插值是怎么工作的并不困难;我们知道,从某个颜色的一个顶点到另外一个顶点,其比例从100%到0%缩减,所有按比例缩减的颜色合在一起就得到了最后的颜色。

在一个三角形上的线性插值也是一样的工作原理,但是现在需要处理三个点和三种颜色。这个三角形与三种颜色有关联:顶端顶点是青色,左端顶点是品红色,右端顶点是黄色。就想那条直线一样,每个颜色在接近它的顶点处都是最强的,向其他顶点移动就会变暗。我们同样用比例确定每种颜色的相对权重,但这次要使用面积的比例,不是长度。

public class AirHockeyRenderer implements GLSurfaceView.Renderer{

    private static final int POSITION_COMPOMENT_COUNT = 2;

    private static final int BYTES_PER_FLOAT = 4;
    private final FloatBuffer vertexData;

    private static final String A_COLOR = "a_Color";
    private static final int COLOR_COMPONENT_COUNT = 3;

    private static final int STRIDE = (POSITION_COMPOMENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
    private int aColorLocation;

//    private static final String U_COLOR = "u_Color";
//    private int uColorLocation;

    private static final String A_POSITION = "a_Position";
    private int aPostionLocation;

    private Context mContext;
    private int program;

    public AirHockeyRenderer(Context context){
        this.mContext = context;

        float[] tableVerticesWithTriangles= {
                0f,0f, 1f, 1f, 1f,

                -0.5f,-0.5f, 0.7f,0.7f, 0.7f,
                0.5f,-0.5f, 0.7f,0.7f, 0.7f,
                0.5f,0.5f, 0.7f,0.7f, 0.7f,
                -0.5f,0.5f, 0.7f,0.7f, 0.7f,
                -0.5f,-0.5f, 0.7f,0.7f, 0.7f,

                -0.5f,0f, 1f, 0f, 0f,
                0.5f,0f, 1f, 0f, 0f,

                0f,-0.25f, 0f, 0f, 1f,
                0f,0.25f, 1f, 0f, 0f
        };



        vertexData = ByteBuffer.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        vertexData.put(tableVerticesWithTriangles);
    }
    /**
     * 当Surface被创建的时候,GLSurfaceView会调用这个方法;
     * 这发生在应用程序第一次运行的时候,并且,当设备被唤醒或者用户从其他activity切换回来时,这个方法也可能会被调用。
     * 在实践中,这意味着,当应用程序运行时,本方法可能会被调用多次。
     * @param gl10
     * @param eglConfig
     */
    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
//        //红绿蓝透明度,渲染后结果为红色
//        gl10.glClearColor(1.0f,0.0f,0.0f,0.0f);
        gl10.glClearColor(0.0f,0.0f,0.0f,0.0f);

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

        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);

        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);

        program = ShaderHelper.linkProgram(vertexShader, fragmentShader);

        if(LoggerConfig.ON){
            ShaderHelper.validateProgram(program);
        }

        //告诉OpenGL在绘制任何东西到屏幕上的时候要使用这里定义的程序
        glUseProgram(program);

        /**
         * 调用glGetUniformLocation()获取uniform的位置,并把这个位置存入uColorLocation;当我们稍后要更新这个uniform值的时候,我们会使用它。
         */
//        uColorLocation = glGetUniformLocation(program, U_COLOR);

        aColorLocation = glGetAttribLocation(program, A_COLOR);
        /**
         * 一旦着色器被链接在一起了,我们就只需要加入一些代码去获取属性位置。
         * 调用glGetAttribLocation()获取属性的位置。有了这个位置,就能告诉OpenGL到哪里去找到这个属性对应的数据了。
         */
        aPostionLocation = glGetAttribLocation(program, A_POSITION);

        /**
         * 下一步是要告诉OpenGL到哪里找到属性a_Position对应的数据
         * 在我们确保它会从开头处开始读取数据,而不是中间或者结尾处。每个缓冲区都有一个内部的指针,可以通过调用position(int)移动它
         * 并且当OpenGL从缓冲区读取时,它会从这个位置开始读取。为了保证它一定从开头出开始读取。我们调用position把位置设在数据的开头处。
         */
        vertexData.position(0);
        /**
         * 传递不正确的参数给glVertexAttribPointer()会导致奇怪的结果,甚至导致程序崩溃。
         * 这种崩溃还很难跟踪,因此,我不是言过其实,获得正确的参数是非常重要的。
         */
        glVertexAttribPointer(aPostionLocation,POSITION_COMPOMENT_COUNT,GL_FLOAT,false,STRIDE,vertexData);
        /**
         * 通过这最后一个调用,OpenGL现在就知道去哪里寻找它所需要的数据了。
         */
        glEnableVertexAttribArray(aPostionLocation);

        /**
         * 我们可以加入代码告诉OpenGL把顶点数据与着色器中的a_Color关联起来。
         */
        vertexData.position(POSITION_COMPOMENT_COUNT);
        glVertexAttribPointer(aColorLocation,COLOR_COMPONENT_COUNT,GL_FLOAT,false,STRIDE,vertexData);
        glEnableVertexAttribArray(aColorLocation);

    }

    /**
     * 在Surface被创建以后,每次Surface尺寸变化时,这个方法都会被GLSurfaceView调用到。在横屏、竖屏来回切换的时候,Surface尺寸会发生变化
     * @param gl10
     * @param width
     * @param height
     */
    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        gl10.glViewport(0, 0, width, height);
    }

    /**
     * 当绘制一帧时,这个方法会被GLSurfaceView调用。
     * 在这个方法中,我们一定要绘制一些东西,即使只是清空屏幕;
     * 因为,在这个方法返回后,渲染缓冲区会被交换并显示在屏幕上,如果什么都没画,可能会看到糟糕的闪烁效果
     * @param gl10
     */
    @Override
    public void onDrawFrame(GL10 gl10) {
        gl10.glClear(GL10.GL_COLOR_BUFFER_BIT);
        /**
         * 我们首先通过调用glUniform4f()更新着色器代码中的u_Color的值。
         * 与属性不同,uniform的分量没有默认值,因此,如果一个uniform在着色器中被定义为vec4类型,我们需要提供所有四个分量的值。
         * 我们想要以画一张白桌子作为开始,因此我们把红色、绿色和蓝色的值设置为代表完全亮度的值1.0f;阿尔法的值无关紧要,但是我们还是要指定它,因为一个颜色有四个分量。
         */
//        glUniform4f(uColorLocation, 1.0f, 1.0f,1.0f,1.0f);

        /**
         * 一旦制定了颜色,接下来就可以用glDrawArrays(GLES20.GL_TRIANGLES,0,6)绘制桌子了,第一个参数告诉OpenGL,我们想要画三角形。
         * 而要话三角形,我们需要给每个三角形传递进去至少三个顶点;
         * 第二个参数告诉OpenGL从顶点数组的开头处开始读顶点;
         * 而第三个参数是告诉OpenGL读入六个顶点。因为每个三角形有三个顶点,这个调用最终会画出两个三角形
         */
//        glDrawArrays(GL_TRIANGLES,0,6); //从0开始读,读6个
        glDrawArrays(GL_TRIANGLE_FAN,0,6);

//        glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
        glDrawArrays(GL_LINES, 6, 2); //从6开始读,读2个

//        glUniform4f(uColorLocation,0.0f, 0.0f, 1.0f, 1.0f);
        glDrawArrays(GL_POINTS,8,1);

//        glUniform4f(uColorLocation,0.0f,0.0f,1.0f,1.0f);
        glDrawArrays(GL_POINTS,9,1);

    }

}








你可能感兴趣的:(openGL)