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

 

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

一、顶点坐标系

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

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

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

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

由图我们知道要绘制的三角形的三个顶点坐标分别为:(-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;//声明用中等精度的floatuniform 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中OpenGL ES的顶点坐标加载过程,在OpenGL ES中最复杂的图像就是三角形,其他任意图像都可以通过三角形来组合出来,这也为我们后续的功能打下了基础,务必好好理解里面的流程和逻辑。

GitHub:Android-OpenGL-ES

 

 

 

更多相关文章

  1. Android(安卓)吸入动画效果实现分解
  2. android下的内存泄漏
  3. Android(安卓)中使用OpenGL ES进行2D开发(绘制第一个三角形番外篇
  4. Android中一个Activty控制另一个Activity的函数及变量
  5. Android第一行代码踩坑qwq
  6. Android(安卓)OpenGL ES绘制三角形时角度的控制
  7. Android中Settings.System的使用
  8. Android(安卓)OpenGL ES 应用(一)
  9. Android(安卓)XML布局中关于资源的使用

随机推荐

  1. android 获取网络资源
  2. Android(安卓)MAT
  3. android 模拟器横竖切换
  4. Android(安卓)远程链接 daemon not runni
  5. Q版疯狂大炮游戏android源码下载
  6. Android(安卓)进化
  7. Android(安卓)Jetpack Navigation 的使用
  8. 【Android】记一次Ninja引发的血案
  9. Android为按钮添加相应事件的代码
  10. Android(安卓)AudioRecord录音实现