我的视频课程:《Android C++ OpenGL 教程》

 

        在前一篇博客我们知道了Android中OpenGL ES是什么,然后知道了怎么搭建一个OpenGL ES的运行环境,现在我们就来开始绘制我们自己想要的图形了(绘制图片会在后面讲解,因为绘制图形是绘制图片的基础),我们最先开始绘制一个三角形,因为三角形是很多图形的基础。

一、顶点坐标系

在绘制之前,我们需要先了解Android中OpenGL ES的顶点坐标系是怎样的,如图:

android三角 android画三角形_OpenGL

其中:中心坐标(0,0)就是我们手机屏幕的中心,然后到最左边是(-1,0)、最右边是(1,0)、最上边是(0,1)、最下边是(0,-1)这样就把我们的手机屏幕分成了一个中心坐标为(0,0)上下左右长度分别为1的矩形。不管我们的手机(具体来讲是我们的GLSurfaceView)的大小是多少,都会映射到这个矩形中。这也是OpenGL中归一化的处理方式,什么都不管,反正都必须映射到这个范围内就对了。

二、设置绘制的三角形所需要的三个顶点

因为我们绘制的是三角形,所以我们需要三个顶点来确定我们的三角形的位置,比如我们要绘制如图的三角形:

android三角 android画三角形_顶点着色器_02

由图我们知道要绘制的三角形的三个顶点坐标分别为:(-1,0)、(0,1)和(1,0)

三、本地化三角形顶点

所谓本地化就是跳出java VM(Java虚拟机)的约束(垃圾回收)范围,使我们的顶点在程序运行时一直都有自己分配的内存地址,不会因为java的GC而把顶点内存地址给回收掉,导致顶点不存在,从而引起OpenGL找不到顶点位置等错误,所以在OpenGL中我们需要把顶点坐标给本地化。

这里我们就需要分2个步骤来完成顶点的本地化:

3.1、用float数组来存储我们的顶点坐标,因为顶点坐标范围是在(-1f~1f)之间的所有小数都可以,所以我们先创建顶点数组:

float[] vertexData = {
            -1.0f, 0.0f,//三角形左下角
            0.0f, 1.0f,//三角形右下角
            1.0f, 0.0f//三角形顶点
    };

3.2、然后根据顶点数组分配底层内存地址,因为需要本地化,所以就和c/c++一样需要我们手动分配内存地址,这里用到了ByteBuffer这个类:

FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)//分配内存空间(单位字节)
                .order(ByteOrder.nativeOrder())//内存bit的排序方式和本地机器一致
                .asFloatBuffer()//转换成float的buffer,因为我们是放float类型的顶点
                .put(vertexData);//把数据放入内存中
        vertexBuffer.position(0);//把索引指针指向开头位置

首先用allocateDirect分配内存大小,其大小为float数组长度乘以每一个float的大小,而float占4个字节,所以就是:vertexData.length * 4;然后设置其在内存中的对齐方式(分大端和小端对其)这里就和本地对齐方式一样:order(ByteOrder.nativeOrder());然后设置是存储float类型数据的内存空间:asFloatBuffer();最后再用float数组(vertexData)初始化内存中的数据:put(vertexData)。为了能从开头访问这块内存地址,还需要设置其position为0:vertexBuffer.position(0);。

这样我们的三角形的顶点内存地址就已经分配好了,并且做了本地持久化。

 

四、开始顶点着色器的编写(shader)

 

OpenGL的操作需要我们自己编写着色器(shader)程序给它,然后它会用GPU执行这个着色器程序,最终反馈执行结果给我们。我们用glsl语言来编写着色器程序,其语法方式和c语言类似,这里就不展开讲了,当学会了OpenGL编程后,可以自己学习glsl语法,然后就可以根据自己的能力编写“吊炸天”的效果了。

4.1、编写顶点着色器(vertex_shader.glsl),位置我们放在:res/raw/路径下

attribute vec4 av_Position;//用于在java代码中获取的属性
void main(){
    gl_Position = av_Position;//gl_Position是内置变量,opengl绘制顶点就是根据它的值绘制的,所以我们需要把我们自己的值赋值给它。
}

这段shader很短,但是足够说明OpenGL中顶点坐标的使用方法了:

首先解释一下attribute vec4 av_Position这句的意思:

attribute是表示顶点属性的,只能用在顶点坐标里面,然后在应用程序(java代码)中可以获取其变量,然后为其赋值。vec4是一个包含4个值(x,y,z,w)的向量,x和y表示2d平面,加上z就是3d的图像了,最后的w是摄像机的距离,因为我们绘制的是2d图形,所以最后z和w的值可以不用管,OpenGL会有默认值1。所以这句话的意思就是:声明了一个名字叫av_Position的包含4个向量的attribute类型的变量,用于我们在java代码中获取并把我们的顶点(FloatBuffer vertexBuffer)值赋值给它。这样OpenGL执行这段着色器代码(程序)时,就有了具体的顶点数据,就会在相应的顶点之间绘制图形(我们定义的三角形)了。

然后void main(){}是程序中函数,和c中是一样的。

最后是gl_Position = av_Position,这里的gl_Position是glsl中内置的最终顶点变量,我们要绘制的顶点就是传递给它。这段代码就是把我们设置的顶点数据传递给gl_Position,然后OpenGL就知道在哪里绘制顶点了。

五、片元着色器程序编写(shader)

上面我们只是写了我们的三角形绘制顶点的着色器程序,而三角形是 “顶点+颜色(样式)” 组成的,所以我们就需要告诉OpenGL我们绘制的三角形是什么颜色的,这就需要片元着色器程序了。

 

  1. 编写片元着色器程序(fragment_shader.glsl)
precision mediump float;//声明用中等精度的float
uniform vec4 af_Color;//用于在java层传递颜色数据
void main(){
    gl_FragColor = af_Color;//gl_FragColor内置变量,opengl渲染的颜色就是获取的它的值,这里我们把我们自己的值赋值给它。
}

这里的precision mediump float 表明用中等精度的float类型来保存变量,其他还可以设置高精度和低精度,一般中等精度就可以了,精度不同,执行的效率也会有差别。

然后这里是用了uniform这个类型来声明变量,uniform是用于应用程序(java代码中)向顶点和片元着色器传递数据,和attribute的区别在于,attribute是只能用在顶点着色器程序中,并且它里面包含的是具体的顶点的数据,每次执行时都需要从顶点内存里面获取新的值,而uniform始终都是用同一个变量。vec4 af_Color也是声明一个4个分量的变量af_Color,这个里面保存的是颜色的值了(rgba四个分量)。

最后gl_FragColor也是glsl中内置的变量,用于最终渲染颜色的赋值,这里我们就把我们自己的颜色赋值给gl_FragColor就行,是操作每一个像素的rgba。

通过第四步和第五步,我们已经设置好了顶点和颜色的着色器程序,接下来就可以让OpenGL加载这2个着色器程序,然后执行里面的代码,最终绘制出我们想要的图形(三角形)了。

六、加载并编译着色器语言

6.1、通过GLES20.glCreateShader(shaderType)创建(顶点或片元)类型的代码程序,如:

创建顶点类型的着色器代码程序:

int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)

片元传入:GLES20.GL_FRAGMENT_SHADER。

6.2、加载shader源码并编译shader

GLES20.glShaderSource(shader, source);//这里更加我们创建的类型加载相应类型的着色器(如:顶点类型)
GLES20.glCompileShader(shader);//编译我们自己写的着色器代码程序

6.3、实际创建并返回一个渲染程序(program)

int program = GLES20.glCreateProgram();//创建一个program程序

6.4、将着色器程序添加到渲染程序中

GLES20.glAttachShader(program, vertexShader);//把顶点着色器加入program程序中
GLES20.glAttachShader(program, fragmentShader);//把片元着色器加入program程序中

6.5、链接源程序

GLES20.glLinkProgram(program);//最终链接顶点和片元着色器,后面在program中就可以访问顶点和片元着色器里面的属性了。

通过上面5个步骤,我们就将用glsl写的着色器程序变成了我们可以在应用程序(java代码)中可以获取里面的变量并操作变量的具体的程序(program)了。

七、接下来就是传递顶点坐标和颜色值给着色器程序:

7.1、获取顶点变量

int aPositionHandl  = GLES20.glGetAttribLocation(programId, "av_Position");//获取顶点属性,后面会给它赋值(即:把我们的顶点赋值给它)

这里的av_Position就是顶点着色器中的attribute变量,后续操作就可以用返回值aPositionHandl这个句柄了。

7.2、获取颜色变量

int afColor = GLES20.glGetUniformLocation(program, "af_Color");//获取片元变量,后面可以通过它设置片元要显示的颜色。

这里的af_Color就是片元着色器中的uniform变量。后面可以对它赋值来改变三角形的颜色。

7.3、开始执行着色器程序

GLES20.glUseProgram(programId);//开始绘制之前,先设置使用当前programId这个程序。

7.4、首先激活顶点属性

GLES20.glEnableVertexAttribArray(aPositionHandl);//激活顶点属性数组,激活后才能对它赋值

7.4、向顶点属性传递顶点数组的值

GLES20.glVertexAttribPointer(aPositionHandl, 2, GLES20.GL_FLOAT, false, 8,
	 vertexBuffer);//现在就是把我们的顶点vertexBuffer赋值给顶点着色器里面的变量。

第一参数就是我们的顶点属性的句柄

第二个参数是我们用的几个分量表示的一个点,这里用的(x,y)2个分量,所以就填入2

第三个参数表示顶点的数据类型,因为我们用的float类型,所以就填入GL_FLOAT类型

第四个参数是是否做归一化处理,如果我们的坐标不在(-1,1)之间,就需要,由于我们的坐标是在(-1,1)之间,所以不需要,填入false

第五个参数是每个点所占空间大小,因为是(x,y)2个点,每个点是4个字节,所以一个点占8个空间大小,这个设置好后,OpenGL才知道8个字节表示一个点,就能按照这个规则,依次取出所有的点的值。

第六个参数就是OpenGL要从哪个内存中取出这些点的数据。

7.4、最后绘制这些顶点

GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);//绘制三角形,从我们的顶点数组里面第0个位置开始,绘制顶点的个数为3(因为三角形只有三个顶点)

这里是以顶点数组的方式来绘制图形

第一个参数表示绘制的方式:GLES20.GL_TRIANGLES,单个三角形的方式,还有其他方式,我们后面会讲解。

第二个参数表示从哪个位置开始绘制,因为顶点坐标里面只有3个坐标点,所以从0开始绘制。

第三个参数表示绘制多少个点,这里显然绘制三个点。

以上就是OpenGL的执行过程:

坐标点(顶点或纹理)->编写着色器程序->加载着色器程序并编译生成program->获取program中的变量->program变量赋值->最终绘制。

注:在加载着色器程序的时候还需要检查是否加载成功等结果,还有绘制图形时的清屏操作会在实例代码中给出完整的例子。

八、核心代码

8.1、加载着色器程序生成program(WlShaderUtil.java)

package com.ywl5320.opengldemo;

import android.content.Context;
import android.opengl.GLES20;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class WlShaderUtil {


    public static String readRawTxt(Context context, int rawId) {
        InputStream inputStream = context.getResources().openRawResource(rawId);
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        StringBuffer sb = new StringBuffer();
        String line;
        try
        {
            while((line = reader.readLine()) != null)
            {
                sb.append(line).append("\n");
            }
            reader.close();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return sb.toString();
    }

    public static int loadShader(int shaderType, String source)
    {
        int shader = GLES20.glCreateShader(shaderType);
        if(shader != 0)
        {
            GLES20.glShaderSource(shader, source);
            GLES20.glCompileShader(shader);
            int[] compile = new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compile, 0);
            if(compile[0] != GLES20.GL_TRUE)
            {
                Log.d("ywl5320", "shader compile error");
                GLES20.glDeleteShader(shader);
                shader = 0;
            }
        }
        return shader;
    }

    public static int createProgram(String vertexSource, String fragmentSource)
    {
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if(vertexShader == 0)
        {
            return 0;
        }
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if(fragmentShader == 0)
        {
            return 0;
        }
        int program = GLES20.glCreateProgram();
        if(program != 0)
        {
            GLES20.glAttachShader(program, vertexShader);
            GLES20.glAttachShader(program, fragmentShader);
            GLES20.glLinkProgram(program);
            int[] linsStatus = new int[1];
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linsStatus, 0);
            if(linsStatus[0] != GLES20.GL_TRUE)
            {
                Log.d("ywl5320", "link program error");
                GLES20.glDeleteProgram(program);
                program = 0;
            }
        }
        return  program;

    }

}

8.2、WlRender.java

package com.ywl5320.opengldemo;

import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class WlRender implements GLSurfaceView.Renderer{


    private Context context;

    private final float[] vertexData ={
            -1f, 0f,
            0f, 1f,
            1f, 0f
    };
    private FloatBuffer vertexBuffer;
    private int program;
    private int avPosition;
    private int afColor;



    public WlRender(Context context)
    {
        this.context = context;
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexData);
        vertexBuffer.position(0);
    }


    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

        String vertexSource = WlShaderUtil.readRawTxt(context, R.raw.vertex_shader);
        String fragmentSource = WlShaderUtil.readRawTxt(context, R.raw.fragment_shader);
        program = WlShaderUtil.createProgram(vertexSource, fragmentSource);
        if(program > 0)
        {
            avPosition = GLES20.glGetAttribLocation(program, "av_Position");
            afColor = GLES20.glGetUniformLocation(program, "af_Color");
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        GLES20.glUseProgram(program);
        GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);
        GLES20.glEnableVertexAttribArray(avPosition);
        GLES20.glVertexAttribPointer(avPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

    }
}

这里用到了通过uniform类型变量传值的方式:

GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);//分别设置片元变量的rgba四个值(前面的glUniform4f:表示这是uniform类型的变量的4个float类型的值)

给片元着色器中的颜色变量afColor设置argb的值为:(1f, 0f, 0f,1f)——红色

然后其他的代码和上一篇博客一样。

九、最终效果如下:

android三角 android画三角形_OpenGL_03

十、总结

通过本篇文章,我们了解了Android中OpenGL ES的顶点坐标加载过程,在OpenGL ES中最复杂的图像就是三角形,其他任意图像都可以通过三角形来组合出来,这也为我们后续的功能打下了基础,务必好好理解里面的流程和逻辑。

GitHub:Android-OpenGL-ES